From 5b69c50fdfebe283f0c050bbc7a99b083edbf60c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 17:47:05 +0200 Subject: [PATCH] feat(engine): extend Executor with applyGeneratedColumns --- src/main/kotlin/kestrel/engine/Executor.kt | 155 ++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/kestrel/engine/Executor.kt b/src/main/kotlin/kestrel/engine/Executor.kt index 91df54a..05228b2 100644 --- a/src/main/kotlin/kestrel/engine/Executor.kt +++ b/src/main/kotlin/kestrel/engine/Executor.kt @@ -473,8 +473,161 @@ class Executor(private val catalog: Catalog) : SequenceAccess { is CollateExpression -> requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) // Already rejected earlier in validateGeneratedDefinitions by checkColumns/requireNoSubquery - // 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) { + 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 { - catalog.dropTable(statement.tableName) + catalog.dropTable(statement.tableName, cascade = statement.mode == DropMode.Cascade) return DropTableResult(statement.tableName) }