feat(engine): extend Executor with applyMergeDelete and applyMergeInsert
This commit is contained in:
1 parent
68f82b7815
commit
e015633f9f
1 file changed
+173
@@ -1805,6 +1805,179 @@ class Executor(private val catalog: Catalog) : SequenceAccess {
|
|||||||
return updatedRow
|
return updatedRow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns [targetRow] itself - the pre-deletion image, matching plain DELETE's own RETURNING (see [evaluateReturning]'s doc). */
|
||||||
|
private fun applyMergeDelete(schema: TableSchema, table: Table, targetRow: Row, hasTriggers: Boolean): Row {
|
||||||
|
fireRowTriggers(schema, TriggerTiming.Before, TriggerEvent.Delete, listOf(targetRow to null))
|
||||||
|
ForeignKeyEnforcer.cascadeDelete(catalog, schema.name, listOf(targetRow))
|
||||||
|
table.rows.removeAll { it === targetRow }
|
||||||
|
if (hasTriggers) catalog.reindexTable(schema.name)
|
||||||
|
fireRowTriggers(schema, TriggerTiming.After, TriggerEvent.Delete, listOf(targetRow to null))
|
||||||
|
return targetRow
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyMergeInsert(
|
||||||
|
schema: TableSchema,
|
||||||
|
table: Table,
|
||||||
|
clause: MergeNotMatchedInsert,
|
||||||
|
resolver: ColumnResolver,
|
||||||
|
hasTriggers: Boolean
|
||||||
|
): Row {
|
||||||
|
val targetIndices = resolveInsertTargets(schema, clause.columns)
|
||||||
|
val evaluator = Evaluator(resolver, subqueryRunner, this)
|
||||||
|
val row = buildInsertRow(schema, targetIndices, clause.values, evaluator)
|
||||||
|
fireRowTriggers(schema, TriggerTiming.Before, TriggerEvent.Insert, listOf(null to row))
|
||||||
|
ForeignKeyEnforcer.validateRow(catalog, schema, row.values, uncommittedSiblings = emptyList())
|
||||||
|
enforceChecks(schema, row)
|
||||||
|
enforcePrimaryKeyUniqueness(schema, table.rows + row)
|
||||||
|
enforceUniqueConstraints(schema, table.rows + row)
|
||||||
|
enforceUniqueIndexes(schema, table.rows + row)
|
||||||
|
table.rows.add(row)
|
||||||
|
if (hasTriggers) catalog.reindexTable(schema.name)
|
||||||
|
fireRowTriggers(schema, TriggerTiming.After, TriggerEvent.Insert, listOf(null to row))
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a MERGE `ON`/`WHEN MATCHED` column reference against whichever of [targetRow]/
|
||||||
|
* [sourceRow] its qualifier names ([targetLabel]/[sourceLabel] - alias if given, bare name
|
||||||
|
* otherwise); an unqualified reference must exist on exactly one side, matching standard SQL's
|
||||||
|
* "ambiguous column" rule for a two-relation scope.
|
||||||
|
*/
|
||||||
|
private fun mergeResolver(
|
||||||
|
targetSchema: TableSchema,
|
||||||
|
targetLabel: String,
|
||||||
|
targetRow: Row,
|
||||||
|
sourceSchema: TableSchema,
|
||||||
|
sourceLabel: String,
|
||||||
|
sourceRow: Row
|
||||||
|
): ColumnResolver = object : ColumnResolver {
|
||||||
|
override fun resolve(expression: ColumnExpression): Value {
|
||||||
|
val qualifier = expression.qualifier
|
||||||
|
if (qualifier != null) {
|
||||||
|
return when {
|
||||||
|
qualifier.equals(sourceLabel, ignoreCase = true) ->
|
||||||
|
sourceRow.values[sourceSchema.columnIndex(expression.name) ?: throw KestrelException("no such column: ${expression.name}")]
|
||||||
|
qualifier.equals(targetLabel, ignoreCase = true) ->
|
||||||
|
targetRow.values[targetSchema.columnIndex(expression.name) ?: throw KestrelException("no such column: ${expression.name}")]
|
||||||
|
else -> throw KestrelException("no such table or alias: $qualifier")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val targetIndex = targetSchema.columnIndex(expression.name)
|
||||||
|
val sourceIndex = sourceSchema.columnIndex(expression.name)
|
||||||
|
if (targetIndex != null && sourceIndex != null) {
|
||||||
|
throw KestrelException("column reference '${expression.name}' is ambiguous between $targetLabel and $sourceLabel")
|
||||||
|
}
|
||||||
|
targetIndex?.let { return targetRow.values[it] }
|
||||||
|
sourceIndex?.let { return sourceRow.values[it] }
|
||||||
|
throw KestrelException("no such column: ${expression.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun declaredCollation(expression: ColumnExpression): Collation? {
|
||||||
|
val qualifier = expression.qualifier
|
||||||
|
if (qualifier != null) {
|
||||||
|
return when {
|
||||||
|
qualifier.equals(sourceLabel, ignoreCase = true) -> sourceSchema.column(expression.name)?.collation
|
||||||
|
qualifier.equals(targetLabel, ignoreCase = true) -> targetSchema.column(expression.name)?.collation
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val targetColumn = targetSchema.column(expression.name)
|
||||||
|
val sourceColumn = sourceSchema.column(expression.name)
|
||||||
|
return if (targetColumn != null && sourceColumn == null) targetColumn.collation
|
||||||
|
else if (sourceColumn != null && targetColumn == null) sourceColumn.collation
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A `WHEN NOT MATCHED` clause has no target row to read - a qualifier naming the target throws rather than silently resolving to nothing. */
|
||||||
|
private fun notMatchedResolver(
|
||||||
|
sourceSchema: TableSchema,
|
||||||
|
sourceLabel: String,
|
||||||
|
sourceRow: Row,
|
||||||
|
targetLabel: String
|
||||||
|
): ColumnResolver = object : ColumnResolver {
|
||||||
|
override fun resolve(expression: ColumnExpression): Value {
|
||||||
|
val qualifier = expression.qualifier
|
||||||
|
if (qualifier != null && qualifier.equals(targetLabel, ignoreCase = true)) {
|
||||||
|
throw KestrelException("no target row is available in a WHEN NOT MATCHED clause: $targetLabel.${expression.name}")
|
||||||
|
}
|
||||||
|
if (qualifier != null && !qualifier.equals(sourceLabel, ignoreCase = true)) {
|
||||||
|
throw KestrelException("no such table or alias: $qualifier")
|
||||||
|
}
|
||||||
|
val index = sourceSchema.columnIndex(expression.name) ?: throw KestrelException("no such column: ${expression.name}")
|
||||||
|
return sourceRow.values[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun declaredCollation(expression: ColumnExpression): Collation? = sourceSchema.column(expression.name)?.collation
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upfront validation for every clause of a MERGE statement, run even when the source produces zero rows - matching [evaluateReturning]'s own reasoning for why RETURNING is checked before knowing how many rows will actually match. */
|
||||||
|
private fun checkMergeColumns(
|
||||||
|
statement: MergeStatement,
|
||||||
|
targetSchema: TableSchema,
|
||||||
|
targetLabel: String,
|
||||||
|
sourceSchema: TableSchema,
|
||||||
|
sourceLabel: String
|
||||||
|
) {
|
||||||
|
checkMergeExpressionColumns(statement.onCondition, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget = true)
|
||||||
|
for (clause in statement.whenClauses) {
|
||||||
|
when (clause) {
|
||||||
|
is MergeMatchedUpdate -> {
|
||||||
|
clause.condition?.let { checkMergeExpressionColumns(it, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget = true) }
|
||||||
|
for (assignment in clause.assignments) {
|
||||||
|
if (targetSchema.columnIndex(assignment.columnName) == null) {
|
||||||
|
throw KestrelException("no such column: ${assignment.columnName}")
|
||||||
|
}
|
||||||
|
assignment.value?.let { checkMergeExpressionColumns(it, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MergeMatchedDelete ->
|
||||||
|
clause.condition?.let { checkMergeExpressionColumns(it, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget = true) }
|
||||||
|
is MergeNotMatchedInsert -> {
|
||||||
|
clause.condition?.let { checkMergeExpressionColumns(it, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget = false) }
|
||||||
|
val targetIndices = resolveInsertTargets(targetSchema, clause.columns)
|
||||||
|
if (clause.values.size != targetIndices.size) {
|
||||||
|
throw KestrelException(
|
||||||
|
"table ${targetSchema.name} expects ${targetIndices.size} values but ${clause.values.size} were supplied"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
clause.values.forEach { value ->
|
||||||
|
value?.let { checkMergeExpressionColumns(it, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkMergeExpressionColumns(
|
||||||
|
expression: Expression,
|
||||||
|
targetSchema: TableSchema,
|
||||||
|
targetLabel: String,
|
||||||
|
sourceSchema: TableSchema,
|
||||||
|
sourceLabel: String,
|
||||||
|
allowTarget: Boolean
|
||||||
|
) {
|
||||||
|
fun recurse(inner: Expression) =
|
||||||
|
checkMergeExpressionColumns(inner, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget)
|
||||||
|
when (expression) {
|
||||||
|
is ColumnExpression -> checkMergeColumnReference(expression, targetSchema, targetLabel, sourceSchema, sourceLabel, allowTarget)
|
||||||
|
is BinaryExpression -> { recurse(expression.left); recurse(expression.right) }
|
||||||
|
is UnaryExpression -> recurse(expression.operand)
|
||||||
|
is FunctionExpression -> throw KestrelException("aggregate functions are not allowed here")
|
||||||
|
is WindowExpression -> throw KestrelException("window functions are not allowed here")
|
||||||
|
is LiteralExpression -> Unit
|
||||||
|
is CallExpression -> expression.arguments.forEach { recurse(it) }
|
||||||
|
is IsNullExpression -> recurse(expression.operand)
|
||||||
|
is InExpression -> {
|
||||||
|
recurse(expression.operand)
|
||||||
|
expression.values.forEach { recurse(it) }
|
||||||
|
}
|
||||||
|
is BetweenExpression -> {
|
||||||
|
recurse(expression.operand)
|
||||||
|
recurse(expression.low)
|
||||||
|
recurse(expression.high)
|
||||||
|
}
|
||||||
|
|
||||||
private fun coerceForColumn(value: Value, column: Column): Value {
|
private fun coerceForColumn(value: Value, column: Column): Value {
|
||||||
if (value is Value.NullValue) return value
|
if (value is Value.NullValue) return value
|
||||||
return when (column.type) {
|
return when (column.type) {
|
||||||
|
|||||||
Reference in new issue
Block a user