feat(sql): add validateForeignKeyDefinitions
This commit is contained in:
1 parent
c51129b8b1
commit
f477f22e5b
1 file changed
+173
@@ -961,3 +961,176 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deliberately duplicated from [kestrel.engine.Executor.validateForeignKeyDefinitions] (same
|
||||||
|
* reasoning as [coerceForColumn]): [columns] is already normalized by [applyCompositePrimaryKey],
|
||||||
|
* matching the in-memory engine's own call order. A self-reference (`referencedTable` equal to
|
||||||
|
* the table being created) is checked against a [TableSchema] built from [columns] rather than
|
||||||
|
* the catalog, because the table doesn't exist there yet while it's being created.
|
||||||
|
*/
|
||||||
|
private fun validateForeignKeyDefinitions(statement: CreateTableStatement, columns: List<Column>) {
|
||||||
|
for (fk in statement.foreignKeys) {
|
||||||
|
if (fk.columns.size != fk.referencedColumns.size) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"FOREIGN KEY (${fk.columns.joinToString(", ")}) column count does not match " +
|
||||||
|
"REFERENCES ${fk.referencedTable}(${fk.referencedColumns.joinToString(", ")})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (columnName in fk.columns) {
|
||||||
|
if (columns.none { it.name.equals(columnName, ignoreCase = true) }) {
|
||||||
|
throw DiskExecutorException("FOREIGN KEY refers to unknown column: $columnName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val selfReference = fk.referencedTable.equals(statement.tableName, ignoreCase = true)
|
||||||
|
val hasEnforcedUniqueness = if (selfReference) {
|
||||||
|
for (columnName in fk.referencedColumns) {
|
||||||
|
if (columns.none { it.name.equals(columnName, ignoreCase = true) }) {
|
||||||
|
throw DiskExecutorException("FOREIGN KEY references unknown column: ${fk.referencedTable}.$columnName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val selfSchema = TableSchema(
|
||||||
|
statement.tableName, columns,
|
||||||
|
uniqueConstraints = statement.uniqueConstraints,
|
||||||
|
compositePrimaryKey = statement.compositePrimaryKey
|
||||||
|
)
|
||||||
|
selfSchema.hasEnforcedUniqueness(fk.referencedColumns)
|
||||||
|
} else {
|
||||||
|
val referencedTable = catalog.tableOrNull(fk.referencedTable)
|
||||||
|
?: throw DiskExecutorException("FOREIGN KEY references unknown table: ${fk.referencedTable}")
|
||||||
|
for (columnName in fk.referencedColumns) {
|
||||||
|
if (referencedTable.schema.column(columnName) == null) {
|
||||||
|
throw DiskExecutorException("FOREIGN KEY references unknown column: ${fk.referencedTable}.$columnName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
referencedTable.schema.hasEnforcedUniqueness(fk.referencedColumns)
|
||||||
|
}
|
||||||
|
if (!hasEnforcedUniqueness) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"FOREIGN KEY must reference a PRIMARY KEY or UNIQUE column(s), but " +
|
||||||
|
"${fk.referencedTable}(${fk.referencedColumns.joinToString(", ")}) is not one"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deliberately duplicated from [kestrel.engine.Executor.validateCheckDefinitions] (same
|
||||||
|
* reasoning as [coerceForColumn]): every CHECK expression must only reference a real column of
|
||||||
|
* the table being created and must contain no subquery, aggregate, or window function - there is
|
||||||
|
* no row yet to run one against, and this executor's [Evaluator] has no aggregation pass of its
|
||||||
|
* own outside [GroupAggregation].
|
||||||
|
*/
|
||||||
|
private fun validateCheckDefinitions(statement: CreateTableStatement) {
|
||||||
|
val schema = TableSchema(statement.tableName, statement.columns)
|
||||||
|
for (check in statement.checks) {
|
||||||
|
checkColumns(check.expression, schema)
|
||||||
|
requireNoSubquery(check.expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ported 1:1 from [kestrel.engine.Executor.checkColumns] - see [validateCheckDefinitions]. */
|
||||||
|
private fun checkColumns(expression: Expression, schema: TableSchema) {
|
||||||
|
when (expression) {
|
||||||
|
is ColumnExpression -> if (schema.columnIndex(expression.name) == null) {
|
||||||
|
throw DiskExecutorException("no such column: ${expression.name}")
|
||||||
|
}
|
||||||
|
is BinaryExpression -> {
|
||||||
|
checkColumns(expression.left, schema)
|
||||||
|
checkColumns(expression.right, schema)
|
||||||
|
}
|
||||||
|
is UnaryExpression -> checkColumns(expression.operand, schema)
|
||||||
|
is FunctionExpression -> throw DiskExecutorException("aggregate functions are not allowed here")
|
||||||
|
is WindowExpression -> throw DiskExecutorException("window functions are not allowed here")
|
||||||
|
is LiteralExpression -> Unit
|
||||||
|
is CallExpression -> expression.arguments.forEach { checkColumns(it, schema) }
|
||||||
|
is IsNullExpression -> checkColumns(expression.operand, schema)
|
||||||
|
is InExpression -> {
|
||||||
|
checkColumns(expression.operand, schema)
|
||||||
|
expression.values.forEach { checkColumns(it, schema) }
|
||||||
|
}
|
||||||
|
is BetweenExpression -> {
|
||||||
|
checkColumns(expression.operand, schema)
|
||||||
|
checkColumns(expression.low, schema)
|
||||||
|
checkColumns(expression.high, schema)
|
||||||
|
}
|
||||||
|
is LikeExpression -> {
|
||||||
|
checkColumns(expression.operand, schema)
|
||||||
|
checkColumns(expression.pattern, schema)
|
||||||
|
}
|
||||||
|
is CaseExpression -> {
|
||||||
|
expression.operand?.let { checkColumns(it, schema) }
|
||||||
|
expression.branches.forEach {
|
||||||
|
checkColumns(it.condition, schema)
|
||||||
|
checkColumns(it.result, schema)
|
||||||
|
}
|
||||||
|
expression.elseExpression?.let { checkColumns(it, schema) }
|
||||||
|
}
|
||||||
|
is ScalarSubqueryExpression -> Unit
|
||||||
|
is ExistsExpression -> Unit
|
||||||
|
is InSubqueryExpression -> checkColumns(expression.operand, schema)
|
||||||
|
is CastExpression -> checkColumns(expression.operand, schema)
|
||||||
|
is CollateExpression -> checkColumns(expression.operand, schema)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ported 1:1 from [kestrel.engine.Executor.checkColumns] (the general-purpose variant used for
|
||||||
|
* WHERE/SET/RETURNING) - NOT [checkColumns] above, which is CHECK-constraint specific and
|
||||||
|
* rejects NEXTVAL/CURRVAL outright. Used only by [evaluateReturning]: validates every column
|
||||||
|
* reference in a RETURNING projection item exists on [schema] and that no aggregate/window
|
||||||
|
* function sneaks in.
|
||||||
|
*/
|
||||||
|
private fun checkReturningColumns(expression: Expression, schema: TableSchema) {
|
||||||
|
when (expression) {
|
||||||
|
is ColumnExpression -> if (schema.columnIndex(expression.name) == null) {
|
||||||
|
throw DiskExecutorException("no such column: ${expression.name}")
|
||||||
|
}
|
||||||
|
is BinaryExpression -> {
|
||||||
|
checkReturningColumns(expression.left, schema)
|
||||||
|
checkReturningColumns(expression.right, schema)
|
||||||
|
}
|
||||||
|
is UnaryExpression -> checkReturningColumns(expression.operand, schema)
|
||||||
|
is FunctionExpression -> throw DiskExecutorException("aggregate functions are not allowed here")
|
||||||
|
is WindowExpression -> throw DiskExecutorException("window functions are not allowed here")
|
||||||
|
is LiteralExpression -> Unit
|
||||||
|
is CallExpression -> expression.arguments.forEach { checkReturningColumns(it, schema) }
|
||||||
|
is IsNullExpression -> checkReturningColumns(expression.operand, schema)
|
||||||
|
is InExpression -> {
|
||||||
|
checkReturningColumns(expression.operand, schema)
|
||||||
|
expression.values.forEach { checkReturningColumns(it, schema) }
|
||||||
|
}
|
||||||
|
is BetweenExpression -> {
|
||||||
|
checkReturningColumns(expression.operand, schema)
|
||||||
|
checkReturningColumns(expression.low, schema)
|
||||||
|
checkReturningColumns(expression.high, schema)
|
||||||
|
}
|
||||||
|
is LikeExpression -> {
|
||||||
|
checkReturningColumns(expression.operand, schema)
|
||||||
|
checkReturningColumns(expression.pattern, schema)
|
||||||
|
}
|
||||||
|
is CaseExpression -> {
|
||||||
|
expression.operand?.let { checkReturningColumns(it, schema) }
|
||||||
|
expression.branches.forEach {
|
||||||
|
checkReturningColumns(it.condition, schema)
|
||||||
|
checkReturningColumns(it.result, schema)
|
||||||
|
}
|
||||||
|
expression.elseExpression?.let { checkReturningColumns(it, schema) }
|
||||||
|
}
|
||||||
|
is ScalarSubqueryExpression -> Unit
|
||||||
|
is ExistsExpression -> Unit
|
||||||
|
is InSubqueryExpression -> checkReturningColumns(expression.operand, schema)
|
||||||
|
is CastExpression -> checkReturningColumns(expression.operand, schema)
|
||||||
|
is CollateExpression -> checkReturningColumns(expression.operand, schema)
|
||||||
|
// Structurally harmless (no column reference to check) - whether MERGE_ACTION() is
|
||||||
|
// actually legal at this particular call site (MERGE's own RETURNING: yes; plain INSERT/
|
||||||
|
// UPDATE/DELETE's RETURNING: no) is enforced by whether the Evaluator this feeds into was
|
||||||
|
// actually given a mergeAction, exactly like NEXTVAL/CURRVAL is enforced by SequenceAccess -
|
||||||
|
// see evaluateMergeReturning, the one call site that binds one.
|
||||||
|
is NextValueExpression, is CurrentValueExpression, is MergeActionExpression, is ParameterExpression -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user