feat(engine): extend Executor with executeCreateView and executeDropView

This commit is contained in:
dualfroz committed 2026-09-01 18:32:53 +02:00
1 parent ebfc2d4d67
commit b18f7c6346
1 file changed
+173 -1
+173 -1
View File
@@ -955,7 +955,179 @@ class Executor(private val catalog: Catalog) : SequenceAccess {
return DropIndexResult(statement.indexName)
}
private fun executeInsert(statement: InsertStatement): StatementResult {
private fun executeCreateView(statement: CreateViewStatement): StatementResult {
selectRunner.explain(statement.query)
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)
}
/**
* 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) - unlike CREATE VIEW's
* eager `selectRunner.explain`, a trigger body can target a different table per statement and
* SQLite itself defers exactly this kind of check to the moment the trigger actually fires.
* What IS checked eagerly, matching this codebase's usual "catch it before it's the user's
* runtime problem" bar for constraint-shaped input: the trigger's own table and every body
* statement's target table both exist right now, and the WHEN clause - evaluated directly by
* this method rather than through a nested statement executor, 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 (NEW is not
* available on DELETE, OLD is not available on INSERT).
*/
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)
}
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 KestrelException("trigger body only supports INSERT, UPDATE, or DELETE statements")
}
}
/** A trigger body statement's own [runTriggerBody] discards whatever [StatementResult] it returns, so a RETURNING clause there would compute output nobody could ever see - rejected eagerly, matching [validateTriggerRowReferences]'s "structurally without sense" bar. */
private fun rejectReturning(returning: Projection?) {
if (returning != null) {
throw KestrelException("RETURNING is not allowed in a trigger body statement - its result would never be visible to anything")
}
}
private fun validateTriggerRowReferences(expression: Expression, event: TriggerEvent, schema: TableSchema) {
when (expression) {
is ColumnExpression -> {
val qualifier = expression.qualifier
?: throw KestrelException("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 KestrelException("no such table or alias: $qualifier")
val availableForEvent = when (event) {
TriggerEvent.Insert -> isNew
TriggerEvent.Delete -> isOld
TriggerEvent.Update -> true
}
if (!availableForEvent) {
throw KestrelException("${if (isNew) "NEW" else "OLD"} is not available in a trigger ON ${event.name.uppercase()}")
}
if (schema.columnIndex(expression.name) == null) {
throw KestrelException("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)
}
/**
* Resolves the parser's "not written" `null`s into Postgres' own ascending/descending
* defaults - MINVALUE 1/MAXVALUE `Long.MAX_VALUE`/START WITH MINVALUE for a positive
* INCREMENT BY (1 if not given either), the mirror image for a negative one - then validates
* the combination as a whole via [SequenceDefinition]'s own `init` plus the one check it can't
* make itself: START WITH must actually fall within [MINVALUE, MAXVALUE].
*/
private fun executeCreateSequence(statement: CreateSequenceStatement): StatementResult {
val incrementBy = statement.incrementBy ?: 1L
if (incrementBy == 0L) throw KestrelException("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 KestrelException("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 KestrelException(
"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)
}
/**
* Unlike [executeCreateSequence], an option left `null`/`false` here means "keep whatever this
* sequence already has" rather than "apply a default" - only [AlterSequenceStatement.clearMinValue]/
* [AlterSequenceStatement.clearMaxValue] (Postgres' `NO MINVALUE`/`NO MAXVALUE`) recompute a bound
* from [SequenceDefinition.incrementBy]'s sign the way a fresh CREATE SEQUENCE would.
* [SequenceDefinition.startValue] is never touched by this method (see its own doc) regardless
* of what RESTART is asked for.
*/
private fun executeAlterSequence(statement: AlterSequenceStatement): StatementResult {
val existing = catalog.sequence(statement.sequenceName).definition
val incrementBy = statement.incrementBy ?: existing.incrementBy
if (incrementBy == 0L) throw KestrelException("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) {
val table = catalog.table(statement.tableName)
val schema = table.schema
val targetIndices = resolveInsertTargets(schema, statement.columns)