feat(sql): add snapshotTemporaryTables and restoreTemporaryTables
This commit is contained in:
1 parent
f0dee2aee0
commit
73672c0184
1 file changed
+159
@@ -314,3 +314,162 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
|||||||
|
|
||||||
val preparedStatementNames: List<String> get() = preparedStatements.keys.toList()
|
val preparedStatementNames: List<String> get() = preparedStatements.keys.toList()
|
||||||
|
|
||||||
|
/** The disk transaction opened by a `BEGIN` still awaiting `COMMIT`/`ROLLBACK`, or null outside one. */
|
||||||
|
private var explicitTxn: Long? = null
|
||||||
|
|
||||||
|
/** Whether an explicit `BEGIN` is currently open - mirrors [kestrel.engine.Executor.inTransaction]. */
|
||||||
|
val inTransaction: Boolean get() = explicitTxn != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Named savepoints established inside the currently open [explicitTxn], oldest first, each paired
|
||||||
|
* with the storage engine LSN [DiskCatalog.savepoint] captured for it - mirrors
|
||||||
|
* [kestrel.engine.Executor]'s own `savepoints` stack (catalog snapshots there, WAL positions here)
|
||||||
|
* including reuse-shadows-the-older-one-of-the-same-name semantics via [indexOfLastSavepoint].
|
||||||
|
*/
|
||||||
|
private val savepoints = mutableListOf<Pair<String, Long>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A temp table's rows live in a plain [Table.rows] list outside [DiskCatalog]'s own WAL/2PL
|
||||||
|
* transaction (see [DiskCatalog]'s KDoc on [DiskCatalog.temporaryTableValues]), so `ROLLBACK`/
|
||||||
|
* `ROLLBACK TO SAVEPOINT` cannot undo a temp table's INSERT/UPDATE/DELETE the way they undo a
|
||||||
|
* real [DiskTable]'s writes just by aborting/rewinding [explicitTxn] - this stack is what makes
|
||||||
|
* them do it anyway. Index 0 is the snapshot taken at `BEGIN`; index `i + 1` is the snapshot
|
||||||
|
* taken when `savepoints[i]` was established, so `ROLLBACK` restores index 0 and `ROLLBACK TO
|
||||||
|
* SAVEPOINT` restores `temporaryTableSnapshots[index + 1]` - kept in lockstep with [savepoints]
|
||||||
|
* by every method below. Only *row contents* ever need restoring, never which temp tables exist:
|
||||||
|
* `CREATE`/`DROP TEMPORARY TABLE` are DDL, already rejected inside an explicit transaction (see
|
||||||
|
* [isDdl]), so the set of temp tables is fixed for the whole lifetime of one [explicitTxn].
|
||||||
|
*/
|
||||||
|
private val temporaryTableSnapshots = mutableListOf<Map<Table, List<Row>>>()
|
||||||
|
|
||||||
|
private fun snapshotTemporaryTables(): Map<Table, List<Row>> =
|
||||||
|
catalog.temporaryTableValues.associateWith { it.rows.toList() }
|
||||||
|
|
||||||
|
private fun restoreTemporaryTables(snapshot: Map<Table, List<Row>>) {
|
||||||
|
for ((table, rows) in snapshot) {
|
||||||
|
table.rows.clear()
|
||||||
|
table.rows.addAll(rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun execute(statement: Statement): StatementResult {
|
||||||
|
if (explicitTxn != null && isDdl(statement)) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"${statement::class.simpleName} cannot run inside an explicit transaction on the " +
|
||||||
|
"disk-backed executor - COMMIT or ROLLBACK first. Unlike the in-memory engine's " +
|
||||||
|
"whole-catalog snapshot/restore, an explicit transaction here is the storage " +
|
||||||
|
"engine's own WAL/2PL transaction (see BEGIN/COMMIT/ROLLBACK below), which has no " +
|
||||||
|
"way to undo a schema change recorded by DiskCatalog outside of it."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return when (statement) {
|
||||||
|
is CreateTableStatement -> executeCreateTable(statement)
|
||||||
|
is DropTableStatement -> executeDropTable(statement)
|
||||||
|
is TruncateTableStatement -> executeTruncate(statement)
|
||||||
|
is AlterTableStatement -> executeAlterTable(statement)
|
||||||
|
is CreateIndexStatement -> executeCreateIndex(statement)
|
||||||
|
is DropIndexStatement -> executeDropIndex(statement)
|
||||||
|
is CreateViewStatement -> executeCreateView(statement)
|
||||||
|
is DropViewStatement -> executeDropView(statement)
|
||||||
|
is CreateSequenceStatement -> executeCreateSequence(statement)
|
||||||
|
is DropSequenceStatement -> executeDropSequence(statement)
|
||||||
|
is AlterSequenceStatement -> executeAlterSequence(statement)
|
||||||
|
is CreateTriggerStatement -> executeCreateTrigger(statement)
|
||||||
|
is DropTriggerStatement -> executeDropTrigger(statement)
|
||||||
|
is InsertStatement -> executeInsert(statement)
|
||||||
|
is MergeStatement -> executeMerge(statement)
|
||||||
|
is QueryStatement -> executeQuery(statement)
|
||||||
|
is ExplainStatement -> executeExplain(statement)
|
||||||
|
is UpdateStatement -> executeUpdate(statement)
|
||||||
|
is DeleteStatement -> executeDelete(statement)
|
||||||
|
is BeginStatement -> executeBegin()
|
||||||
|
is CommitStatement -> executeCommit()
|
||||||
|
is RollbackStatement -> executeRollback()
|
||||||
|
is SavepointStatement -> executeSavepoint(statement)
|
||||||
|
is ReleaseSavepointStatement -> executeReleaseSavepoint(statement)
|
||||||
|
is RollbackToSavepointStatement -> executeRollbackToSavepoint(statement)
|
||||||
|
is PrepareStatement -> executePrepare(statement)
|
||||||
|
is ExecuteStatement -> execute(resolveExecute(statement))
|
||||||
|
is DeallocateStatement -> executeDeallocate(statement)
|
||||||
|
else -> throw DiskExecutorException(
|
||||||
|
"${statement::class.simpleName} is not supported by the disk-backed executor yet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDdl(statement: Statement): Boolean = when (statement) {
|
||||||
|
is CreateTableStatement, is DropTableStatement, is AlterTableStatement,
|
||||||
|
is CreateIndexStatement, is DropIndexStatement, is CreateViewStatement, is DropViewStatement,
|
||||||
|
is CreateSequenceStatement, is DropSequenceStatement, is AlterSequenceStatement,
|
||||||
|
is CreateTriggerStatement, is DropTriggerStatement -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executePrepare(statement: PrepareStatement): StatementResult {
|
||||||
|
val key = statement.name.lowercase()
|
||||||
|
if (preparedStatements.containsKey(key)) {
|
||||||
|
throw DiskExecutorException("prepared statement '${statement.name}' already exists")
|
||||||
|
}
|
||||||
|
preparedStatements[key] = statement.target
|
||||||
|
return PrepareResult(statement.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ported 1:1 from [kestrel.engine.Executor.resolveExecute] - looks [ExecuteStatement.name] up,
|
||||||
|
* evaluates every argument as a CONSTANT (a bare [Evaluator] with no resolver/subquery/sequence
|
||||||
|
* access, exactly like a DEFAULT expression), and substitutes the results for every `$1`, `$2`,
|
||||||
|
* ... via the same [ParameterSubstitution] both engines share. The result is always a concrete
|
||||||
|
* [PreparableStatement] - never [ExecuteStatement] itself.
|
||||||
|
*/
|
||||||
|
fun resolveExecute(statement: ExecuteStatement): Statement {
|
||||||
|
val target = preparedStatements[statement.name.lowercase()]
|
||||||
|
?: throw DiskExecutorException("no such prepared statement: ${statement.name}")
|
||||||
|
val required = ParameterSubstitution.parameterCount(target)
|
||||||
|
if (statement.arguments.size != required) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"prepared statement '${statement.name}' expects $required parameter(s) but got ${statement.arguments.size}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val values = statement.arguments.map { Evaluator().evaluate(it) }
|
||||||
|
return ParameterSubstitution.substitute(target, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeDeallocate(statement: DeallocateStatement): StatementResult {
|
||||||
|
if (statement.all) {
|
||||||
|
preparedStatements.clear()
|
||||||
|
return DeallocateResult(null, all = true)
|
||||||
|
}
|
||||||
|
val name = statement.name!!
|
||||||
|
if (preparedStatements.remove(name.lowercase()) == null) {
|
||||||
|
throw DiskExecutorException("no such prepared statement: $name")
|
||||||
|
}
|
||||||
|
return DeallocateResult(name, all = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeBegin(): StatementResult {
|
||||||
|
if (explicitTxn != null) throw DiskExecutorException("a transaction is already in progress")
|
||||||
|
explicitTxn = catalog.beginTransaction()
|
||||||
|
temporaryTableSnapshots.clear()
|
||||||
|
temporaryTableSnapshots.add(snapshotTemporaryTables())
|
||||||
|
return BeginResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeCommit(): StatementResult {
|
||||||
|
val txn = explicitTxn ?: throw DiskExecutorException("no transaction is in progress")
|
||||||
|
catalog.commit(txn)
|
||||||
|
explicitTxn = null
|
||||||
|
savepoints.clear()
|
||||||
|
temporaryTableSnapshots.clear()
|
||||||
|
return CommitResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeRollback(): StatementResult {
|
||||||
|
val txn = explicitTxn ?: throw DiskExecutorException("no transaction is in progress")
|
||||||
|
catalog.abort(txn)
|
||||||
|
explicitTxn = null
|
||||||
|
savepoints.clear()
|
||||||
|
restoreTemporaryTables(temporaryTableSnapshots[0])
|
||||||
|
temporaryTableSnapshots.clear()
|
||||||
|
return RollbackResult
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user