feat(engine): extend Executor with executeUpdate and executeUpdateInner
This commit is contained in:
1 parent
66cc015636
commit
1542846ef3
1 file changed
+62
-24
@@ -1542,53 +1542,91 @@ class Executor(private val catalog: Catalog) : SequenceAccess {
|
|||||||
private fun executeDeleteInner(statement: DeleteStatement, triggerContext: TriggerRowContext?): StatementResult {
|
private fun executeDeleteInner(statement: DeleteStatement, triggerContext: TriggerRowContext?): StatementResult {
|
||||||
val table = catalog.table(statement.tableName)
|
val table = catalog.table(statement.tableName)
|
||||||
val schema = table.schema
|
val schema = table.schema
|
||||||
if (statement.where == null) {
|
val deletedRows = if (statement.where == null) {
|
||||||
val removed = table.rows.size
|
table.rows.toList()
|
||||||
table.rows.clear()
|
|
||||||
catalog.reindexTable(schema.name)
|
|
||||||
return DeleteResult(removed)
|
|
||||||
}
|
|
||||||
checkColumns(statement.where, schema)
|
|
||||||
val kept = mutableListOf<Row>()
|
|
||||||
var deleted = 0
|
|
||||||
for (row in table.rows) {
|
|
||||||
if (isTrue(Evaluator(schema, row).evaluate(statement.where))) {
|
|
||||||
deleted++
|
|
||||||
} else {
|
} else {
|
||||||
kept.add(row)
|
checkColumns(statement.where, schema, triggerContext)
|
||||||
|
table.rows.filter {
|
||||||
|
isTrue(Evaluator(composeResolver(rowResolver(schema, it), triggerContext), subqueryRunner).evaluate(statement.where))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
table.rows.clear()
|
fireRowTriggers(schema, TriggerTiming.Before, TriggerEvent.Delete, deletedRows.map { it to null })
|
||||||
table.rows.addAll(kept)
|
ForeignKeyEnforcer.cascadeDelete(catalog, schema.name, deletedRows)
|
||||||
|
table.rows.removeAll(deletedRows)
|
||||||
catalog.reindexTable(schema.name)
|
catalog.reindexTable(schema.name)
|
||||||
return DeleteResult(deleted)
|
fireRowTriggers(schema, TriggerTiming.After, TriggerEvent.Delete, deletedRows.map { it to null })
|
||||||
|
val returning = statement.returning?.let { evaluateReturning(schema, deletedRows, it) }
|
||||||
|
return DeleteResult(deletedRows.size, returning)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun executeUpdate(statement: UpdateStatement): StatementResult {
|
private fun executeUpdate(statement: UpdateStatement, triggerContext: TriggerRowContext? = null): StatementResult =
|
||||||
|
withStatementAtomicity(statement.tableName, triggerContext) { executeUpdateInner(statement, triggerContext) }
|
||||||
|
|
||||||
|
private fun executeUpdateInner(statement: UpdateStatement, triggerContext: TriggerRowContext?): StatementResult {
|
||||||
val table = catalog.table(statement.tableName)
|
val table = catalog.table(statement.tableName)
|
||||||
val schema = table.schema
|
val schema = table.schema
|
||||||
val assignments = statement.assignments.map { assignment ->
|
val assignments = statement.assignments.map { assignment ->
|
||||||
val index = schema.columnIndex(assignment.columnName)
|
val index = schema.columnIndex(assignment.columnName)
|
||||||
?: throw KestrelException("no such column: ${assignment.columnName}")
|
?: throw KestrelException("no such column: ${assignment.columnName}")
|
||||||
checkColumns(assignment.value, schema)
|
assignment.value?.let { checkColumns(it, schema, triggerContext) }
|
||||||
|
rejectGeneratedAssignment(schema.columns[index], explicitValueGiven = assignment.value != null)
|
||||||
index to assignment.value
|
index to assignment.value
|
||||||
}
|
}
|
||||||
statement.where?.let { checkColumns(it, schema) }
|
statement.where?.let { checkColumns(it, schema, triggerContext) }
|
||||||
|
|
||||||
var updated = 0
|
var updated = 0
|
||||||
val newRows = table.rows.map { row ->
|
val oldRows = table.rows.toList()
|
||||||
val matches = statement.where == null || isTrue(Evaluator(schema, row).evaluate(statement.where))
|
// Every row actually matched by WHERE, paired old-to-new - fires an UPDATE trigger for
|
||||||
|
// every row WHERE selects, regardless of whether any assigned value actually differs from
|
||||||
|
// what was already there (matching standard SQL: WHERE selects the affected rows, not the
|
||||||
|
// changed ones).
|
||||||
|
val touchedRows = mutableListOf<Pair<Row, Row>>()
|
||||||
|
val newRows = oldRows.map { row ->
|
||||||
|
val matches = statement.where == null ||
|
||||||
|
isTrue(Evaluator(composeResolver(rowResolver(schema, row), triggerContext), subqueryRunner).evaluate(statement.where))
|
||||||
if (!matches) return@map row
|
if (!matches) return@map row
|
||||||
val values = row.values.toMutableList()
|
val values = row.values.toMutableList()
|
||||||
val evaluator = Evaluator(schema, row)
|
// NEXTVAL/CURRVAL are allowed in SET's assignment values (below), not in WHERE (above) -
|
||||||
|
// a separate Evaluator per clause, same reasoning as executeInsertWithConflictHandling.
|
||||||
|
val evaluator = Evaluator(composeResolver(rowResolver(schema, row), triggerContext), subqueryRunner, this)
|
||||||
for ((index, expression) in assignments) {
|
for ((index, expression) in assignments) {
|
||||||
values[index] = coerceForColumn(evaluator.evaluate(expression), schema.columns[index])
|
val column = schema.columns[index]
|
||||||
|
values[index] = if (expression != null) {
|
||||||
|
coerceForColumn(evaluator.evaluate(expression), column)
|
||||||
|
} else {
|
||||||
|
column.default?.let { coerceForColumn(evaluator.evaluate(it), column) } ?: Value.NullValue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
applyGeneratedColumns(schema, values)
|
||||||
validateNotNull(schema, values)
|
validateNotNull(schema, values)
|
||||||
updated++
|
updated++
|
||||||
Row(values)
|
val newRow = Row(values)
|
||||||
|
touchedRows.add(row to newRow)
|
||||||
|
newRow
|
||||||
|
}
|
||||||
|
// Fires before any constraint below is checked, same as executeInsert's BEFORE trigger -
|
||||||
|
// a BEFORE UPDATE trigger's body can reject the whole statement before this engine's own
|
||||||
|
// validation ever runs.
|
||||||
|
fireRowTriggers(schema, TriggerTiming.Before, TriggerEvent.Update, touchedRows)
|
||||||
|
// Same whole-batch validation as executeInsert: a self-referencing FK must see every row's
|
||||||
|
// final value for this statement, not just rows already committed to the table.
|
||||||
|
for (row in newRows) {
|
||||||
|
ForeignKeyEnforcer.validateRow(catalog, schema, row.values, uncommittedSiblings = newRows)
|
||||||
|
enforceChecks(schema, row)
|
||||||
}
|
}
|
||||||
enforcePrimaryKeyUniqueness(schema, newRows)
|
enforcePrimaryKeyUniqueness(schema, newRows)
|
||||||
|
enforceUniqueConstraints(schema, newRows)
|
||||||
|
enforceUniqueIndexes(schema, newRows)
|
||||||
|
// Every enforced-unique key group (the PRIMARY KEY, single-column or composite, plus each
|
||||||
|
// UNIQUE constraint) is a valid FOREIGN KEY target, so each group needs its own
|
||||||
|
// cascade/RESTRICT pass on UPDATE - not just the PRIMARY KEY.
|
||||||
|
for (group in schema.enforcedUniqueKeyGroups()) {
|
||||||
|
val columnIndices = group.map { schema.columnIndex(it)!! }
|
||||||
|
val changedPairs = oldRows.indices.map { i ->
|
||||||
|
columnIndices.map { oldRows[i].values[it] } to columnIndices.map { newRows[i].values[it] }
|
||||||
|
}
|
||||||
|
ForeignKeyEnforcer.cascadeUpdate(catalog, schema.name, group, changedPairs)
|
||||||
|
}
|
||||||
table.rows.clear()
|
table.rows.clear()
|
||||||
table.rows.addAll(newRows)
|
table.rows.addAll(newRows)
|
||||||
catalog.reindexTable(schema.name)
|
catalog.reindexTable(schema.name)
|
||||||
|
|||||||
Reference in new issue
Block a user