feat(sql): add validateTriggerBodyStatementTarget and rejectReturning
This commit is contained in:
1 parent
daf6ce803f
commit
c51129b8b1
1 file changed
+162
@@ -799,3 +799,165 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
||||
return CreateTriggerResult(statement.triggerName)
|
||||
}
|
||||
|
||||
private fun validateTriggerBodyStatementTarget(statement: Statement) {
|
||||
when (statement) {
|
||||
is InsertStatement -> { catalog.table(statement.tableName); rejectReturning(statement.returning) }
|
||||
is UpdateStatement -> { catalog.table(statement.tableName); rejectReturning(statement.returning) }
|
||||
is DeleteStatement -> { catalog.table(statement.tableName); rejectReturning(statement.returning) }
|
||||
else -> throw DiskExecutorException("trigger body only supports INSERT, UPDATE, or DELETE statements")
|
||||
}
|
||||
}
|
||||
|
||||
/** Ported 1:1 from [kestrel.engine.Executor.rejectReturning]: a trigger body statement's own [runTriggerBody] discards whatever [StatementResult] it returns, so a RETURNING clause there would compute output nobody could ever see. */
|
||||
private fun rejectReturning(returning: Projection?) {
|
||||
if (returning != null) {
|
||||
throw DiskExecutorException("RETURNING is not allowed in a trigger body statement - its result would never be visible to anything")
|
||||
}
|
||||
}
|
||||
|
||||
/** Ported 1:1 from [kestrel.engine.Executor.validateTriggerRowReferences]. */
|
||||
private fun validateTriggerRowReferences(expression: Expression, event: TriggerEvent, schema: TableSchema) {
|
||||
when (expression) {
|
||||
is ColumnExpression -> {
|
||||
val qualifier = expression.qualifier
|
||||
?: throw DiskExecutorException("column '${expression.name}' in trigger WHEN clause must be qualified with NEW or OLD")
|
||||
val isNew = qualifier.equals("new", ignoreCase = true)
|
||||
val isOld = qualifier.equals("old", ignoreCase = true)
|
||||
if (!isNew && !isOld) throw DiskExecutorException("no such table or alias: $qualifier")
|
||||
val availableForEvent = when (event) {
|
||||
TriggerEvent.Insert -> isNew
|
||||
TriggerEvent.Delete -> isOld
|
||||
TriggerEvent.Update -> true
|
||||
}
|
||||
if (!availableForEvent) {
|
||||
throw DiskExecutorException("${if (isNew) "NEW" else "OLD"} is not available in a trigger ON ${event.name.uppercase()}")
|
||||
}
|
||||
if (schema.columnIndex(expression.name) == null) {
|
||||
throw DiskExecutorException("no such column: ${expression.name}")
|
||||
}
|
||||
}
|
||||
is BinaryExpression -> {
|
||||
validateTriggerRowReferences(expression.left, event, schema)
|
||||
validateTriggerRowReferences(expression.right, event, schema)
|
||||
}
|
||||
is UnaryExpression -> validateTriggerRowReferences(expression.operand, event, schema)
|
||||
is CallExpression -> expression.arguments.forEach { validateTriggerRowReferences(it, event, schema) }
|
||||
is IsNullExpression -> validateTriggerRowReferences(expression.operand, event, schema)
|
||||
is InExpression -> {
|
||||
validateTriggerRowReferences(expression.operand, event, schema)
|
||||
expression.values.forEach { validateTriggerRowReferences(it, event, schema) }
|
||||
}
|
||||
is BetweenExpression -> {
|
||||
validateTriggerRowReferences(expression.operand, event, schema)
|
||||
validateTriggerRowReferences(expression.low, event, schema)
|
||||
validateTriggerRowReferences(expression.high, event, schema)
|
||||
}
|
||||
is LikeExpression -> {
|
||||
validateTriggerRowReferences(expression.operand, event, schema)
|
||||
validateTriggerRowReferences(expression.pattern, event, schema)
|
||||
}
|
||||
is CaseExpression -> {
|
||||
expression.operand?.let { validateTriggerRowReferences(it, event, schema) }
|
||||
expression.branches.forEach {
|
||||
validateTriggerRowReferences(it.condition, event, schema)
|
||||
validateTriggerRowReferences(it.result, event, schema)
|
||||
}
|
||||
expression.elseExpression?.let { validateTriggerRowReferences(it, event, schema) }
|
||||
}
|
||||
is CastExpression -> validateTriggerRowReferences(expression.operand, event, schema)
|
||||
is CollateExpression -> validateTriggerRowReferences(expression.operand, event, schema)
|
||||
is LiteralExpression -> Unit
|
||||
is FunctionExpression, is WindowExpression, is ScalarSubqueryExpression,
|
||||
is ExistsExpression, is InSubqueryExpression,
|
||||
is NextValueExpression, is CurrentValueExpression, is MergeActionExpression, is ParameterExpression ->
|
||||
error("unreachable: banned by requireNoSubquery")
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeDropTrigger(statement: DropTriggerStatement): StatementResult {
|
||||
catalog.dropTrigger(statement.triggerName)
|
||||
return DropTriggerResult(statement.triggerName)
|
||||
}
|
||||
|
||||
private fun executeCreateTable(statement: CreateTableStatement): StatementResult {
|
||||
if (statement.temporary) return executeCreateTemporaryTable(statement)
|
||||
if (statement.columns.count { it.primaryKey } > 1 ||
|
||||
(statement.compositePrimaryKey.isNotEmpty() && statement.columns.any { it.primaryKey })
|
||||
) {
|
||||
throw DiskExecutorException("table ${statement.tableName} declares more than one primary key")
|
||||
}
|
||||
validateUniqueDefinitions(statement)
|
||||
validateCheckDefinitions(statement)
|
||||
val columns = applyCompositePrimaryKey(statement)
|
||||
validateForeignKeyDefinitions(statement, columns)
|
||||
for (column in columns) {
|
||||
when (val default = column.default) {
|
||||
is NextValueExpression -> validateNextValueDefault(default, column)
|
||||
is CurrentValueExpression -> throw DiskExecutorException("DEFAULT expression cannot contain CURRVAL")
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
validateGeneratedDefinitions(columns)
|
||||
catalog.createTable(
|
||||
statement.tableName, columns, statement.compositePrimaryKey, statement.uniqueConstraints,
|
||||
statement.checks, statement.foreignKeys
|
||||
)
|
||||
return CreateTableResult(statement.tableName)
|
||||
}
|
||||
|
||||
/**
|
||||
* `CREATE TEMP TABLE`/`CREATE TEMPORARY TABLE` - the disk-backed executor's counterpart of
|
||||
* [executeCreateTable], but building a [kestrel.storage.Table] via [DiskCatalog.createTemporaryTable]
|
||||
* instead of a [DiskTable]: a real, live table for the rest of this process's lifetime (INSERT/
|
||||
* SELECT/UPDATE/DELETE, WHERE/JOIN/ORDER BY/subqueries, NOT NULL/PRIMARY KEY/UNIQUE/CHECK/DEFAULT/
|
||||
* GENERATED all enforced exactly like a permanent table - see [executeTemporaryInsert]/
|
||||
* [executeTemporaryUpdate]/[executeTemporaryDelete] and [resolveFromSource]), simply never
|
||||
* touching the pager/WAL and never surviving a reopen. FOREIGN KEY is rejected outright rather
|
||||
* than silently ignored: [DiskForeignKeyEnforcer] and its cascade bookkeeping are wired to
|
||||
* [DiskTable]/[DiskCatalog]'s directory, which a temp table deliberately has no entry in, so
|
||||
* honoring the constraint would need a parallel enforcement path this session scoped out - a
|
||||
* named, documented scope-cut rather than a constraint that looks declared but is never checked.
|
||||
*/
|
||||
private fun executeCreateTemporaryTable(statement: CreateTableStatement): StatementResult {
|
||||
if (statement.foreignKeys.isNotEmpty()) {
|
||||
throw DiskExecutorException("FOREIGN KEY on a temporary table is not supported by the disk-backed executor yet")
|
||||
}
|
||||
if (statement.columns.count { it.primaryKey } > 1 ||
|
||||
(statement.compositePrimaryKey.isNotEmpty() && statement.columns.any { it.primaryKey })
|
||||
) {
|
||||
throw DiskExecutorException("table ${statement.tableName} declares more than one primary key")
|
||||
}
|
||||
validateUniqueDefinitions(statement)
|
||||
validateCheckDefinitions(statement)
|
||||
val columns = applyCompositePrimaryKey(statement)
|
||||
for (column in columns) {
|
||||
when (val default = column.default) {
|
||||
is NextValueExpression -> validateNextValueDefault(default, column)
|
||||
is CurrentValueExpression -> throw DiskExecutorException("DEFAULT expression cannot contain CURRVAL")
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
validateGeneratedDefinitions(columns)
|
||||
val schema = TableSchema(
|
||||
statement.tableName, columns,
|
||||
checks = statement.checks,
|
||||
uniqueConstraints = statement.uniqueConstraints,
|
||||
compositePrimaryKey = statement.compositePrimaryKey,
|
||||
temporary = true
|
||||
)
|
||||
catalog.createTemporaryTable(schema)
|
||||
return CreateTableResult(statement.tableName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ported 1:1 from [kestrel.engine.Executor.validateNextValueDefault]: checked statically rather
|
||||
* than by evaluating the DEFAULT for real, since a bare `CREATE TABLE` must not itself consume a
|
||||
* sequence value before a single row exists.
|
||||
*/
|
||||
private fun validateNextValueDefault(default: NextValueExpression, column: Column) {
|
||||
catalog.sequence(default.sequenceName)
|
||||
if (column.type != ColumnType.Integer) {
|
||||
throw DiskExecutorException("NEXTVAL can only be used as a DEFAULT for an INTEGER column, but ${column.name} is ${column.type.displayName}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user