feat(sql): add requireUniqueConstraintName and executeAddColumn
This commit is contained in:
1 parent
faad477a32
commit
d5bb145d4c
1 file changed
+144
@@ -1616,3 +1616,147 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
|||||||
schema.checks, schema.uniqueConstraints, schema.compositePrimaryKey
|
schema.checks, schema.uniqueConstraints, schema.compositePrimaryKey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val using = action.using ?: ColumnExpression(action.columnName)
|
||||||
|
catalog.changeColumnType(tableName, action.columnName, newColumn) { row ->
|
||||||
|
val value = coerceForColumn(Evaluator(schema, row).evaluate(using), newColumn)
|
||||||
|
if (newColumn.notNull && value is Value.NullValue) {
|
||||||
|
throw DiskExecutorException("NOT NULL constraint failed: $tableName.${action.columnName}")
|
||||||
|
}
|
||||||
|
val newRow = Row(row.values.toMutableList().also { it[columnIndex] = value })
|
||||||
|
for (check in newSchema.checks) {
|
||||||
|
val result = Evaluator(newSchema, newRow).evaluate(check.expression)
|
||||||
|
if (result is Value.BooleanValue && !result.value) {
|
||||||
|
val label = check.name?.let { " $it" } ?: ""
|
||||||
|
throw DiskExecutorException("CHECK constraint$label failed: $tableName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirrors [kestrel.engine.Executor.requireUniqueConstraintName] - see that method's own doc. */
|
||||||
|
private fun requireUniqueConstraintName(schema: TableSchema, name: String) {
|
||||||
|
val taken = schema.foreignKeys.any { it.name?.equals(name, ignoreCase = true) == true } ||
|
||||||
|
schema.checks.any { it.name?.equals(name, ignoreCase = true) == true } ||
|
||||||
|
schema.uniqueConstraints.any { it.name?.equals(name, ignoreCase = true) == true }
|
||||||
|
if (taken) throw DiskExecutorException("constraint '$name' already exists on table ${schema.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A NOT NULL column added to a table that already has rows must carry a DEFAULT (there is no
|
||||||
|
* value to backfill existing rows with otherwise) - an empty table has no such requirement.
|
||||||
|
* Mirrors [kestrel.engine.Executor.executeAddColumn] (same reasoning as [coerceForColumn]):
|
||||||
|
* duplicated rather than shared, since this executor's [DiskTable] has no equivalent of the
|
||||||
|
* in-memory engine's `Table.rows` to check emptiness against without a transaction of its own.
|
||||||
|
* GENERATED ALWAYS AS sidesteps the NOT NULL/DEFAULT requirement entirely - see
|
||||||
|
* [addGeneratedColumnValue].
|
||||||
|
*/
|
||||||
|
private fun executeAddColumn(tableName: String, column: Column) {
|
||||||
|
val table = catalog.table(tableName)
|
||||||
|
if (column.generated != null) {
|
||||||
|
validateAddGeneratedColumn(table.schema, column)
|
||||||
|
catalog.addColumn(tableName, column) { row -> addGeneratedColumnValue(table.schema, column, row) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
column.default?.let { validateConstantDefault(it) }
|
||||||
|
val probeTxn = catalog.beginTransaction()
|
||||||
|
val hasRows = runInTransaction(probeTxn) { table.scan(probeTxn).isNotEmpty() }
|
||||||
|
if (column.notNull && column.default == null && hasRows) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"cannot add NOT NULL column ${column.name} to table $tableName with existing rows without a DEFAULT value"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val defaultValue = column.default?.let { coerceForColumn(Evaluator().evaluate(it), column) } ?: Value.NullValue
|
||||||
|
catalog.addColumn(tableName, column) { defaultValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirrors [kestrel.engine.Executor.validateAddGeneratedColumn] - see that method's own doc. */
|
||||||
|
private fun validateAddGeneratedColumn(schema: TableSchema, column: Column) {
|
||||||
|
val generated = column.generated!!
|
||||||
|
checkColumns(generated, schema)
|
||||||
|
requireNoSubquery(generated)
|
||||||
|
val generatedNames = schema.columns.filter { it.generated != null }.map { it.name.lowercase() }.toSet()
|
||||||
|
requireNoGeneratedColumnReference(generated, generatedNames, column.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirrors [kestrel.engine.Executor.addGeneratedColumnValue] - see that method's own doc. */
|
||||||
|
private fun addGeneratedColumnValue(schema: TableSchema, column: Column, row: Row): Value {
|
||||||
|
val value = coerceForColumn(Evaluator(schema, row).evaluate(column.generated!!), column)
|
||||||
|
if (column.notNull && value is Value.NullValue) {
|
||||||
|
throw DiskExecutorException("NOT NULL constraint failed: ${schema.name}.${column.name}")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DEFAULT must be a constant expression: no column reference (there is no row to read one
|
||||||
|
* from when backfilling `ALTER TABLE ... ADD COLUMN`), no subquery, and no aggregate. Ported
|
||||||
|
* 1:1 from [kestrel.engine.Executor.validateConstantDefault] - see [checkColumns] for why this
|
||||||
|
* executor duplicates rather than shares this kind of validation.
|
||||||
|
*/
|
||||||
|
private fun validateConstantDefault(expression: Expression) {
|
||||||
|
when (expression) {
|
||||||
|
is ColumnExpression -> throw DiskExecutorException("DEFAULT expression cannot reference a column")
|
||||||
|
is ScalarSubqueryExpression, is ExistsExpression, is InSubqueryExpression ->
|
||||||
|
throw DiskExecutorException("DEFAULT expression cannot contain a subquery")
|
||||||
|
is FunctionExpression -> throw DiskExecutorException("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 DiskExecutorException("DEFAULT expression cannot contain a window function")
|
||||||
|
is NextValueExpression ->
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"NEXTVAL cannot be used as an ADD COLUMN DEFAULT - it would give every existing row " +
|
||||||
|
"the exact same value instead of a growing sequence, silently defeating the one thing NEXTVAL is for"
|
||||||
|
)
|
||||||
|
is CurrentValueExpression -> throw DiskExecutorException("DEFAULT expression cannot contain CURRVAL")
|
||||||
|
is MergeActionExpression -> throw DiskExecutorException("DEFAULT expression cannot contain MERGE_ACTION()")
|
||||||
|
is ParameterExpression -> throw DiskExecutorException("DEFAULT expression cannot contain a parameter reference")
|
||||||
|
is LiteralExpression -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds every row's final values first (in [newRows], none written yet) and only then validates
|
||||||
|
* and inserts them one at a time - not inline in a single pass - so that a self-referencing FK
|
||||||
|
* can see sibling rows from the same multi-row INSERT regardless of their order in the statement,
|
||||||
|
* exactly like [kestrel.engine.Executor.executeInsert]'s own `uncommittedSiblings = newRows`.
|
||||||
|
*/
|
||||||
|
private fun executeInsert(statement: InsertStatement, triggerContext: TriggerRowContext? = null): StatementResult {
|
||||||
|
catalog.temporaryTableOrNull(statement.tableName)?.let { return executeTemporaryInsert(statement, it) }
|
||||||
|
val table = catalog.table(statement.tableName)
|
||||||
|
val schema = table.schema
|
||||||
|
return withStatementTransaction { txn ->
|
||||||
|
withTriggerAtomicity(schema.name, txn, triggerContext) {
|
||||||
|
executeInsertInTxn(statement, table, schema, txn, triggerContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user