feat(sql): add runTriggerBody and buildAssignments to DiskExecutor
This commit is contained in:
1 parent
dff226d846
commit
daf6ce803f
1 file changed
+157
@@ -642,3 +642,160 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a fired trigger's body statements in order, directly against [txn] - NOT through
|
||||||
|
* [executeInsert]/[executeUpdate]/[executeDelete] (which would re-select a transaction via
|
||||||
|
* [withStatementTransaction] and re-take a trigger-atomicity savepoint via
|
||||||
|
* [withTriggerAtomicity]) - so the whole cascade shares exactly one disk transaction and exactly
|
||||||
|
* one savepoint, taken once by the outermost caller. Mirrors [kestrel.engine.Executor.
|
||||||
|
* runTriggerBody], including [MAX_TRIGGER_DEPTH].
|
||||||
|
*/
|
||||||
|
private fun runTriggerBody(trigger: TriggerDefinition, context: TriggerRowContext, txn: Long) {
|
||||||
|
if (triggerDepth >= MAX_TRIGGER_DEPTH) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"trigger recursion limit ($MAX_TRIGGER_DEPTH) exceeded while firing '${trigger.name}' - " +
|
||||||
|
"check for a trigger whose body ends up re-triggering itself"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
triggerDepth++
|
||||||
|
try {
|
||||||
|
for (bodyStatement in trigger.body) {
|
||||||
|
when (bodyStatement) {
|
||||||
|
is InsertStatement -> {
|
||||||
|
val table = catalog.table(bodyStatement.tableName)
|
||||||
|
executeInsertInTxn(bodyStatement, table, table.schema, txn, context)
|
||||||
|
}
|
||||||
|
is UpdateStatement -> {
|
||||||
|
val table = catalog.table(bodyStatement.tableName)
|
||||||
|
executeUpdateInTxn(bodyStatement, buildAssignments(table.schema, bodyStatement), table, table.schema, txn, context)
|
||||||
|
}
|
||||||
|
is DeleteStatement -> {
|
||||||
|
val table = catalog.table(bodyStatement.tableName)
|
||||||
|
executeDeleteInTxn(bodyStatement, table, table.schema, txn, context)
|
||||||
|
}
|
||||||
|
else -> error("unreachable: trigger bodies only ever contain INSERT/UPDATE/DELETE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
triggerDepth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAssignments(schema: TableSchema, statement: UpdateStatement): List<Pair<Int, Expression?>> =
|
||||||
|
statement.assignments.map { assignment ->
|
||||||
|
val index = schema.columnIndex(assignment.columnName)
|
||||||
|
?: throw DiskExecutorException("no such column: ${assignment.columnName} in table ${schema.name}")
|
||||||
|
rejectGeneratedAssignment(schema.columns[index], explicitValueGiven = assignment.value != null)
|
||||||
|
index to assignment.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeCreateIndex(statement: CreateIndexStatement): StatementResult {
|
||||||
|
catalog.createIndex(statement.tableName, statement.indexName, statement.columnNames, statement.unique)
|
||||||
|
return CreateIndexResult(statement.indexName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeDropIndex(statement: DropIndexStatement): StatementResult {
|
||||||
|
catalog.dropIndex(statement.indexName)
|
||||||
|
return DropIndexResult(statement.indexName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ported 1:1 from [kestrel.engine.Executor.executeCreateSequence] - see that method's own doc
|
||||||
|
* for the ascending/descending default resolution and the one thing [SequenceDefinition]'s own
|
||||||
|
* `init` cannot check itself (START WITH falling inside [MINVALUE, MAXVALUE]).
|
||||||
|
*/
|
||||||
|
private fun executeCreateSequence(statement: CreateSequenceStatement): StatementResult {
|
||||||
|
val incrementBy = statement.incrementBy ?: 1L
|
||||||
|
if (incrementBy == 0L) throw DiskExecutorException("sequence '${statement.sequenceName}' INCREMENT BY cannot be zero")
|
||||||
|
val ascending = incrementBy > 0
|
||||||
|
val minValue = statement.minValue ?: if (ascending) 1L else Long.MIN_VALUE
|
||||||
|
val maxValue = statement.maxValue ?: if (ascending) Long.MAX_VALUE else -1L
|
||||||
|
if (minValue > maxValue) {
|
||||||
|
throw DiskExecutorException("sequence '${statement.sequenceName}' MINVALUE ($minValue) must not exceed MAXVALUE ($maxValue)")
|
||||||
|
}
|
||||||
|
val startValue = statement.startValue ?: if (ascending) minValue else maxValue
|
||||||
|
if (startValue < minValue || startValue > maxValue) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"START WITH ($startValue) must be between MINVALUE ($minValue) and MAXVALUE ($maxValue)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catalog.createSequence(SequenceDefinition(statement.sequenceName, incrementBy, minValue, maxValue, startValue, statement.cycle))
|
||||||
|
return CreateSequenceResult(statement.sequenceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeDropSequence(statement: DropSequenceStatement): StatementResult {
|
||||||
|
catalog.dropSequence(statement.sequenceName)
|
||||||
|
return DropSequenceResult(statement.sequenceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ported 1:1 from [kestrel.engine.Executor.executeAlterSequence] - see that method's own doc. */
|
||||||
|
private fun executeAlterSequence(statement: AlterSequenceStatement): StatementResult {
|
||||||
|
val existing = catalog.sequence(statement.sequenceName).definition
|
||||||
|
val incrementBy = statement.incrementBy ?: existing.incrementBy
|
||||||
|
if (incrementBy == 0L) throw DiskExecutorException("sequence '${statement.sequenceName}' INCREMENT BY cannot be zero")
|
||||||
|
val ascending = incrementBy > 0
|
||||||
|
val minValue = when {
|
||||||
|
statement.clearMinValue -> if (ascending) 1L else Long.MIN_VALUE
|
||||||
|
statement.minValue != null -> statement.minValue
|
||||||
|
else -> existing.minValue
|
||||||
|
}
|
||||||
|
val maxValue = when {
|
||||||
|
statement.clearMaxValue -> if (ascending) Long.MAX_VALUE else -1L
|
||||||
|
statement.maxValue != null -> statement.maxValue
|
||||||
|
else -> existing.maxValue
|
||||||
|
}
|
||||||
|
if (minValue > maxValue) {
|
||||||
|
throw DiskExecutorException("sequence '${statement.sequenceName}' MINVALUE ($minValue) must not exceed MAXVALUE ($maxValue)")
|
||||||
|
}
|
||||||
|
val cycle = statement.cycle ?: existing.cycle
|
||||||
|
val newDefinition = SequenceDefinition(statement.sequenceName, incrementBy, minValue, maxValue, existing.startValue, cycle)
|
||||||
|
val restartAt = if (statement.restart) (statement.restartWith ?: existing.startValue) else null
|
||||||
|
if (restartAt != null && (restartAt < minValue || restartAt > maxValue)) {
|
||||||
|
throw DiskExecutorException("RESTART value ($restartAt) must be between MINVALUE ($minValue) and MAXVALUE ($maxValue)")
|
||||||
|
}
|
||||||
|
catalog.alterSequence(statement.sequenceName, newDefinition, restartAt)
|
||||||
|
return AlterSequenceResult(statement.sequenceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs [CreateViewStatement.query] once, discarding the result, purely to fail fast on a bad
|
||||||
|
* column/table reference before anything is persisted - mirrors [kestrel.engine.Executor.
|
||||||
|
* executeCreateView] calling `selectRunner.explain` for the same reason, just via a real
|
||||||
|
* execution rather than a plan-only walk (this executor has no `explain`).
|
||||||
|
*/
|
||||||
|
private fun executeCreateView(statement: CreateViewStatement): StatementResult {
|
||||||
|
val txn = catalog.beginTransaction()
|
||||||
|
runInTransaction(txn) { runQuery(statement.query, txn, outer = null) }
|
||||||
|
catalog.createView(statement.viewName, statement.query, statement.orReplace)
|
||||||
|
return CreateViewResult(statement.viewName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeDropView(statement: DropViewStatement): StatementResult {
|
||||||
|
catalog.dropView(statement.viewName, cascade = statement.mode == DropMode.Cascade)
|
||||||
|
return DropViewResult(statement.viewName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ported 1:1 from [kestrel.engine.Executor.executeCreateTrigger]: body statements are
|
||||||
|
* deliberately NOT deep-validated here (no eager check that every column referenced in a body
|
||||||
|
* statement's WHERE/SET/VALUES actually exists) - a trigger body can target a different table per
|
||||||
|
* statement, and this executor already defers exactly this kind of check to the moment the
|
||||||
|
* trigger actually fires, same as SQLite. What IS checked eagerly: the trigger's own table and
|
||||||
|
* every body statement's target table both exist right now, and the WHEN clause - evaluated
|
||||||
|
* directly by this executor rather than through a nested statement run, so nothing else will ever
|
||||||
|
* validate it - contains no subquery/aggregate and only ever references NEW/OLD columns that both
|
||||||
|
* exist on this table and are actually available for this trigger's event.
|
||||||
|
*/
|
||||||
|
private fun executeCreateTrigger(statement: CreateTriggerStatement): StatementResult {
|
||||||
|
val schema = catalog.table(statement.tableName).schema
|
||||||
|
statement.whenClause?.let { whenClause ->
|
||||||
|
requireNoSubquery(whenClause)
|
||||||
|
validateTriggerRowReferences(whenClause, statement.event, schema)
|
||||||
|
}
|
||||||
|
for (bodyStatement in statement.body) validateTriggerBodyStatementTarget(bodyStatement)
|
||||||
|
catalog.createTrigger(
|
||||||
|
TriggerDefinition(statement.triggerName, statement.timing, statement.event, statement.tableName, statement.whenClause, statement.body)
|
||||||
|
)
|
||||||
|
return CreateTriggerResult(statement.triggerName)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user