diff --git a/src/main/kotlin/kestrel/store/sql/DiskExecutor.kt b/src/main/kotlin/kestrel/store/sql/DiskExecutor.kt index b40824e..8bd64d7 100644 --- a/src/main/kotlin/kestrel/store/sql/DiskExecutor.kt +++ b/src/main/kotlin/kestrel/store/sql/DiskExecutor.kt @@ -1134,3 +1134,154 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess { } } + /** Ported 1:1 from [kestrel.engine.Executor.requireNoSubquery] - see [validateCheckDefinitions]. */ + private fun requireNoSubquery(expression: Expression) { + when (expression) { + is ScalarSubqueryExpression, is ExistsExpression, is InSubqueryExpression -> + throw DiskExecutorException("CHECK constraints and GENERATED column expressions cannot contain subqueries") + is FunctionExpression -> throw DiskExecutorException("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 DiskExecutorException("window functions are not allowed in CHECK constraints") + is NextValueExpression, is CurrentValueExpression -> + throw DiskExecutorException("NEXTVAL/CURRVAL are not allowed in a CHECK constraint or GENERATED column expression") + is MergeActionExpression -> throw DiskExecutorException("MERGE_ACTION() is not allowed in a CHECK constraint or GENERATED column expression") + is ParameterExpression -> throw DiskExecutorException("a parameter reference is not allowed in a CHECK constraint or GENERATED column expression") + is ColumnExpression, is LiteralExpression -> Unit + } + } + + /** Ported 1:1 from [kestrel.engine.Executor.validateGeneratedDefinitions] - see that method's own doc. */ + private fun validateGeneratedDefinitions(columns: List) { + val schema = TableSchema("", columns) + val generatedNames = columns.filter { it.generated != null }.map { it.name.lowercase() }.toSet() + for (column in columns) { + val generated = column.generated ?: continue + if (column.default != null) { + throw DiskExecutorException("column ${column.name} cannot declare both DEFAULT and GENERATED ALWAYS AS") + } + checkColumns(generated, schema) + requireNoSubquery(generated) + requireNoGeneratedColumnReference(generated, generatedNames, column.name) + } + } + + /** Ported 1:1 from [kestrel.engine.Executor.requireNoGeneratedColumnReference]. */ + private fun requireNoGeneratedColumnReference(expression: Expression, generatedNames: Set, ownerColumn: String) { + when (expression) { + is ColumnExpression -> if (expression.name.lowercase() in generatedNames) { + throw DiskExecutorException( + "GENERATED column $ownerColumn cannot reference another GENERATED column: ${expression.name}" + ) + } + is LiteralExpression -> Unit + is BinaryExpression -> { + requireNoGeneratedColumnReference(expression.left, generatedNames, ownerColumn) + requireNoGeneratedColumnReference(expression.right, generatedNames, ownerColumn) + } + is UnaryExpression -> requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) + is CallExpression -> expression.arguments.forEach { requireNoGeneratedColumnReference(it, generatedNames, ownerColumn) } + is IsNullExpression -> requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) + is InExpression -> { + requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) + expression.values.forEach { requireNoGeneratedColumnReference(it, generatedNames, ownerColumn) } + } + is BetweenExpression -> { + requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) + requireNoGeneratedColumnReference(expression.low, generatedNames, ownerColumn) + requireNoGeneratedColumnReference(expression.high, generatedNames, ownerColumn) + } + is LikeExpression -> { + requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) + requireNoGeneratedColumnReference(expression.pattern, generatedNames, ownerColumn) + } + is CaseExpression -> { + expression.operand?.let { requireNoGeneratedColumnReference(it, generatedNames, ownerColumn) } + expression.branches.forEach { + requireNoGeneratedColumnReference(it.condition, generatedNames, ownerColumn) + requireNoGeneratedColumnReference(it.result, generatedNames, ownerColumn) + } + expression.elseExpression?.let { requireNoGeneratedColumnReference(it, generatedNames, ownerColumn) } + } + is CastExpression -> requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) + is CollateExpression -> requireNoGeneratedColumnReference(expression.operand, generatedNames, ownerColumn) + is FunctionExpression, is WindowExpression, is ScalarSubqueryExpression, is ExistsExpression, + is InSubqueryExpression, is NextValueExpression, is CurrentValueExpression, is MergeActionExpression, + is ParameterExpression -> Unit + } + } + + /** Ported 1:1 from [kestrel.engine.Executor.applyGeneratedColumns]. */ + private fun applyGeneratedColumns(schema: TableSchema, values: MutableList, subqueries: SubqueryRunner) { + if (schema.columns.none { it.generated != null }) return + val evaluator = Evaluator(schema, Row(values), subqueries) + for ((index, column) in schema.columns.withIndex()) { + val generated = column.generated ?: continue + values[index] = coerceForColumn(evaluator.evaluate(generated), column) + } + } + + /** Ported 1:1 from [kestrel.engine.Executor.rejectGeneratedAssignment]. */ + private fun rejectGeneratedAssignment(column: Column, explicitValueGiven: Boolean) { + if (explicitValueGiven && column.generated != null) { + throw DiskExecutorException("cannot assign an explicit value to GENERATED column ${column.name}") + } + } + + /** + * Ported 1:1 from [kestrel.engine.Executor.enforceChecks]: a CHECK expression that evaluates to + * SQL `FALSE` rejects the row; `TRUE` or `NULL` (three-valued "unknown", e.g. a comparison against + * a NULL column) both accept it, per standard SQL. + */ + private fun enforceChecks(schema: TableSchema, row: Row, subqueries: SubqueryRunner) { + for (check in schema.checks) { + val result = Evaluator(schema, row, subqueries).evaluate(check.expression) + if (result is Value.BooleanValue && !result.value) { + val label = check.name?.let { " $it" } ?: "" + throw DiskExecutorException("CHECK constraint$label failed: ${schema.name}") + } + } + } + + /** + * NOT NULL enforcement for a temp table row - [DiskTable.insert]/[DiskTable.update] get this for + * free from their own private `requireArity`, but a temp table's plain [kestrel.storage.Table] + * has no such gate, so [executeTemporaryInsert]/[executeTemporaryUpdate] call this directly. + * Ported 1:1 from [kestrel.engine.Executor.validateNotNull]. + */ + private fun validateTemporaryNotNull(schema: TableSchema, values: List) { + for ((index, column) in schema.columns.withIndex()) { + if (column.notNull && values[index] is Value.NullValue) { + throw DiskExecutorException("NOT NULL constraint failed: ${schema.name}.${column.name}") + } + } + } +