feat(sql): add requireNoSubquery and validateGeneratedDefinitions

This commit is contained in:
dualfroz committed 2026-09-06 21:41:54 +02:00
1 parent f477f22e5b
commit 775d8dcb2b
1 file changed
+151
@@ -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<Column>) {
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<String>, 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<Value>, 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<Value>) {
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}")
}
}
}