feat(sql): add checkMergeExpressionColumns and recurse to DiskExecutor
This commit is contained in:
1 parent
f617dcc77e
commit
0cd7aef8fe
1 file changed
+127
@@ -4302,3 +4302,130 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fifth near-parallel recursive [Expression] walker in this file (alongside [requireNoSubquery]/[checkColumns]/[checkReturningColumns]/[validateTriggerRowReferences]) - same deliberate non-abstraction as [kestrel.engine.Executor.checkMergeExpressionColumns], since this one needs two relations plus ambiguity detection none of the others do. */
|
||||||
|
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 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 { 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)
|
||||||
|
}
|
||||||
|
is LikeExpression -> {
|
||||||
|
recurse(expression.operand)
|
||||||
|
recurse(expression.pattern)
|
||||||
|
}
|
||||||
|
is CaseExpression -> {
|
||||||
|
expression.operand?.let { recurse(it) }
|
||||||
|
expression.branches.forEach { recurse(it.condition); recurse(it.result) }
|
||||||
|
expression.elseExpression?.let { recurse(it) }
|
||||||
|
}
|
||||||
|
is ScalarSubqueryExpression -> Unit
|
||||||
|
is ExistsExpression -> Unit
|
||||||
|
is InSubqueryExpression -> recurse(expression.operand)
|
||||||
|
is CastExpression -> recurse(expression.operand)
|
||||||
|
is CollateExpression -> recurse(expression.operand)
|
||||||
|
// Structurally harmless here too - see checkReturningColumns's own MERGE_ACTION() branch.
|
||||||
|
is NextValueExpression, is CurrentValueExpression, is MergeActionExpression, is ParameterExpression -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkMergeColumnReference(
|
||||||
|
expression: ColumnExpression,
|
||||||
|
targetSchema: TableSchema,
|
||||||
|
targetLabel: String,
|
||||||
|
sourceSchema: TableSchema,
|
||||||
|
sourceLabel: String,
|
||||||
|
allowTarget: Boolean
|
||||||
|
) {
|
||||||
|
val qualifier = expression.qualifier
|
||||||
|
if (qualifier != null) {
|
||||||
|
val matchesSource = qualifier.equals(sourceLabel, ignoreCase = true)
|
||||||
|
val matchesTargetLabel = qualifier.equals(targetLabel, ignoreCase = true)
|
||||||
|
if (matchesTargetLabel && !allowTarget) {
|
||||||
|
throw DiskExecutorException("no target row is available in a WHEN NOT MATCHED clause: $targetLabel.${expression.name}")
|
||||||
|
}
|
||||||
|
if (!matchesSource && !matchesTargetLabel) {
|
||||||
|
throw DiskExecutorException("no such table or alias: $qualifier")
|
||||||
|
}
|
||||||
|
val schema = if (matchesSource) sourceSchema else targetSchema
|
||||||
|
if (schema.columnIndex(expression.name) == null) throw DiskExecutorException("no such column: ${expression.name}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val inTarget = allowTarget && targetSchema.columnIndex(expression.name) != null
|
||||||
|
val inSource = sourceSchema.columnIndex(expression.name) != null
|
||||||
|
if (inTarget && inSource) {
|
||||||
|
throw DiskExecutorException("column reference '${expression.name}' is ambiguous between $targetLabel and $sourceLabel")
|
||||||
|
}
|
||||||
|
if (!inTarget && !inSource) throw DiskExecutorException("no such column: ${expression.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Commits [txnId] if [block] returns normally, aborts it (then rethrows) if [block] throws. */
|
||||||
|
private inline fun <T> runInTransaction(txnId: Long, block: () -> T): T {
|
||||||
|
val result = try {
|
||||||
|
block()
|
||||||
|
} catch (failure: Exception) {
|
||||||
|
catalog.abort(txnId)
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
catalog.commit(txnId)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isTrue(value: Value): Boolean = value is Value.BooleanValue && value.value
|
||||||
|
|
||||||
|
private fun coerceForColumn(value: Value, column: Column): Value {
|
||||||
|
if (value is Value.NullValue) return value
|
||||||
|
return when (column.type) {
|
||||||
|
ColumnType.Integer -> value as? Value.IntegerValue ?: throw typeMismatch(column, value)
|
||||||
|
ColumnType.Text -> value as? Value.TextValue ?: throw typeMismatch(column, value)
|
||||||
|
ColumnType.Boolean -> value as? Value.BooleanValue ?: throw typeMismatch(column, value)
|
||||||
|
ColumnType.Real -> when (value) {
|
||||||
|
is Value.RealValue -> value
|
||||||
|
is Value.IntegerValue -> Value.RealValue(value.value.toDouble())
|
||||||
|
else -> throw typeMismatch(column, value)
|
||||||
|
}
|
||||||
|
ColumnType.Decimal -> {
|
||||||
|
val raw = when (value) {
|
||||||
|
is Value.DecimalValue -> value.value
|
||||||
|
is Value.IntegerValue -> BigDecimal(value.value)
|
||||||
|
is Value.RealValue -> BigDecimal.valueOf(value.value)
|
||||||
|
else -> throw typeMismatch(column, value)
|
||||||
|
}
|
||||||
|
Value.DecimalValue(DecimalColumns.normalize(raw, column.precision, column.scale, "column ${column.name}"))
|
||||||
|
}
|
||||||
|
ColumnType.Date -> value as? Value.DateValue ?: throw typeMismatch(column, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typeMismatch(column: Column, value: Value): DiskExecutorException =
|
||||||
|
DiskExecutorException("type mismatch: column ${column.name} expects ${column.type.displayName} but got ${value.typeName}")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_RECURSION_ITERATIONS = 10_000
|
||||||
|
private const val MAX_TRIGGER_DEPTH = 100
|
||||||
|
|
||||||
|
/** Kinds whose result actually depends on which rows fall inside the frame. */
|
||||||
|
private val FRAME_CAPABLE_KINDS = setOf(WindowFunctionKind.Aggregate, WindowFunctionKind.FirstValue, WindowFunctionKind.LastValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user