feat(engine): extend Executor with executeCreateView and executeDropView
This commit is contained in:
1 parent
ebfc2d4d67
commit
b18f7c6346
1 file changed
+173
-1
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user