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)
|
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 table = catalog.table(statement.tableName)
|
||||||
val schema = table.schema
|
val schema = table.schema
|
||||||
val targetIndices = resolveInsertTargets(schema, statement.columns)
|
val targetIndices = resolveInsertTargets(schema, statement.columns)
|
||||||
|
|||||||
Reference in new issue
Block a user