feat(sql): add requireNoSubquery and validateGeneratedDefinitions
This commit is contained in:
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user