feat(engine): extend Executor with applyGeneratedColumns

This commit is contained in:
dualfroz committed 2026-09-01 17:47:05 +02:00
1 parent 5ee00ad3c2
commit 5b69c50fdf
1 file changed
+154 -1
+154 -1
View File
@@ -473,8 +473,161 @@ class Executor(private val catalog: Catalog) : SequenceAccess {
is CollateExpression -> requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) is CollateExpression -> requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn)
// Already rejected earlier in validateGeneratedDefinitions by checkColumns/requireNoSubquery - // Already rejected earlier in validateGeneratedDefinitions by checkColumns/requireNoSubquery -
// kept here only so this `when` stays exhaustive. // kept here only so this `when` stays exhaustive.
is FunctionExpression, is WindowExpression, is ScalarSubqueryExpression, is ExistsExpression,
is InSubqueryExpression, is NextValueExpression, is CurrentValueExpression, is MergeActionExpression,
is ParameterExpression -> Unit
}
}
/**
* Recomputes every GENERATED column in [values] from its expression, in place - safe to call
* any time after every non-GENERATED column has its final value for this row (explicit
* assignment, DEFAULT, or otherwise), because [validateGeneratedDefinitions] guarantees a
* GENERATED expression only ever reads a non-GENERATED column of the same row, never another
* GENERATED one - so evaluation order among multiple GENERATED columns never matters.
*/
private fun applyGeneratedColumns(schema: TableSchema, values: MutableList<Value>) {
if (schema.columns.none { it.generated != null }) return
val evaluator = Evaluator(schema, Row(values), subqueryRunner)
for ((index, column) in schema.columns.withIndex()) {
val generated = column.generated ?: continue
values[index] = coerceForColumn(evaluator.evaluate(generated), column)
}
}
/** Standard SQL forbids assigning a GENERATED column's value directly - it is always derived, never set. */
private fun rejectGeneratedAssignment(column: Column, explicitValueGiven: Boolean) {
if (explicitValueGiven && column.generated != null) {
throw KestrelException("cannot assign an explicit value to GENERATED column ${column.name}")
}
}
/**
* [NextValueExpression] skips the generic "evaluate once to see what type comes back" check
* [validateDefaultDefinitions] otherwise uses, because evaluating it for real would mean a bare
* `CREATE TABLE` actually consumes a sequence value before a single row exists - instead this
* checks the two things that actually matter statically: the sequence is real, and the column it
* defaults can hold what NEXTVAL returns.
*/
private fun validateNextValueDefault(default: NextValueExpression, column: Column) {
catalog.sequence(default.sequenceName)
if (column.type != ColumnType.Integer) {
throw KestrelException("NEXTVAL can only be used as a DEFAULT for an INTEGER column, but ${column.name} is ${column.type.displayName}")
}
}
/**
* A DEFAULT must be a constant expression: no column reference (there is no row to read one
* from when a value is missing at INSERT time, or none at all yet when backfilling ALTER
* TABLE ADD COLUMN), no subquery, and no aggregate. [NextValueExpression] is the one exception -
* see [validateNextValueDefault] - because unlike everything else here it names an external,
* mutable counter rather than something computable from the expression alone.
* [CurrentValueExpression] is rejected outright: CURRVAL only ever means something after a prior
* NEXTVAL in the same session, which can never be true yet at CREATE TABLE/ADD COLUMN time.
*/
private fun validateConstantDefault(expression: Expression) {
when (expression) {
is ColumnExpression -> throw KestrelException("DEFAULT expression cannot reference a column")
is ScalarSubqueryExpression, is ExistsExpression, is InSubqueryExpression ->
throw KestrelException("DEFAULT expression cannot contain a subquery")
is FunctionExpression -> throw KestrelException("DEFAULT expression cannot contain an aggregate function")
is BinaryExpression -> {
validateConstantDefault(expression.left)
validateConstantDefault(expression.right)
}
is UnaryExpression -> validateConstantDefault(expression.operand)
is CallExpression -> expression.arguments.forEach { validateConstantDefault(it) }
is IsNullExpression -> validateConstantDefault(expression.operand)
is InExpression -> {
validateConstantDefault(expression.operand)
expression.values.forEach { validateConstantDefault(it) }
}
is BetweenExpression -> {
validateConstantDefault(expression.operand)
validateConstantDefault(expression.low)
validateConstantDefault(expression.high)
}
is LikeExpression -> {
validateConstantDefault(expression.operand)
validateConstantDefault(expression.pattern)
}
is CaseExpression -> {
expression.operand?.let { validateConstantDefault(it) }
expression.branches.forEach {
validateConstantDefault(it.condition)
validateConstantDefault(it.result)
}
expression.elseExpression?.let { validateConstantDefault(it) }
}
is CastExpression -> validateConstantDefault(expression.operand)
is CollateExpression -> validateConstantDefault(expression.operand)
is WindowExpression -> throw KestrelException("DEFAULT expression cannot contain a window function")
is NextValueExpression -> Unit
is CurrentValueExpression -> throw KestrelException("DEFAULT expression cannot contain CURRVAL")
is MergeActionExpression -> throw KestrelException("DEFAULT expression cannot contain MERGE_ACTION()")
is ParameterExpression -> throw KestrelException("DEFAULT expression cannot contain a parameter reference")
is LiteralExpression -> Unit
}
}
private fun requireNoSubquery(expression: Expression) {
when (expression) {
is ScalarSubqueryExpression, is ExistsExpression, is InSubqueryExpression ->
throw KestrelException("CHECK constraints and GENERATED column expressions cannot contain subqueries")
is FunctionExpression -> throw KestrelException("aggregate functions are not allowed in CHECK constraints")
is BinaryExpression -> {
requireNoSubquery(expression.left)
requireNoSubquery(expression.right)
}
is UnaryExpression -> requireNoSubquery(expression.operand)
is CallExpression -> expression.arguments.forEach { requireNoSubquery(it) }
is IsNullExpression -> requireNoSubquery(expression.operand)
is InExpression -> {
requireNoSubquery(expression.operand)
expression.values.forEach { requireNoSubquery(it) }
}
is BetweenExpression -> {
requireNoSubquery(expression.operand)
requireNoSubquery(expression.low)
requireNoSubquery(expression.high)
}
is LikeExpression -> {
requireNoSubquery(expression.operand)
requireNoSubquery(expression.pattern)
}
is CaseExpression -> {
expression.operand?.let { requireNoSubquery(it) }
expression.branches.forEach {
requireNoSubquery(it.condition)
requireNoSubquery(it.result)
}
expression.elseExpression?.let { requireNoSubquery(it) }
}
is CastExpression -> requireNoSubquery(expression.operand)
is CollateExpression -> requireNoSubquery(expression.operand)
is WindowExpression -> throw KestrelException("window functions are not allowed in CHECK constraints")
is NextValueExpression, is CurrentValueExpression ->
throw KestrelException("NEXTVAL/CURRVAL are not allowed in a CHECK constraint, trigger WHEN clause, or GENERATED column expression")
is MergeActionExpression ->
throw KestrelException("MERGE_ACTION() is not allowed in a CHECK constraint, trigger WHEN clause, or GENERATED column expression")
is ParameterExpression ->
throw KestrelException("a parameter reference is not allowed in a CHECK constraint, trigger WHEN clause, or GENERATED column expression")
is ColumnExpression, is LiteralExpression -> Unit
}
}
private fun enforceChecks(schema: TableSchema, row: Row) {
for (check in schema.checks) {
val result = Evaluator(schema, row, subqueryRunner).evaluate(check.expression)
if (result is Value.BooleanValue && !result.value) {
val label = check.name?.let { " $it" } ?: ""
throw KestrelException("CHECK constraint$label failed: ${schema.name}")
}
}
}
private fun executeDropTable(statement: DropTableStatement): StatementResult { private fun executeDropTable(statement: DropTableStatement): StatementResult {
catalog.dropTable(statement.tableName) catalog.dropTable(statement.tableName, cascade = statement.mode == DropMode.Cascade)
return DropTableResult(statement.tableName) return DropTableResult(statement.tableName)
} }