feat(engine): extend CollationEquality with resolveCollation
This commit is contained in:
1 parent
71d994698a
commit
e42f3ed09b
2 files changed
+178
-8
No files matched your search
@@ -0,0 +1,94 @@
|
|||||||
|
package kestrel.engine
|
||||||
|
|
||||||
|
import kestrel.catalog.TableSchema
|
||||||
|
import kestrel.sql.ast.CollateExpression
|
||||||
|
import kestrel.sql.ast.ColumnExpression
|
||||||
|
import kestrel.sql.ast.Expression
|
||||||
|
import kestrel.storage.Collation
|
||||||
|
import kestrel.storage.Value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves which [Collation] governs equality/ordering for [expression]'s own value, with the same
|
||||||
|
* two-tier precedence [compareOrderKeyValues] already uses for `ORDER BY`: an explicit `COLLATE` on
|
||||||
|
* [expression] itself wins; failing that, a bare [ColumnExpression]'s own declared `COLLATE` (see
|
||||||
|
* [kestrel.catalog.Column.collation]), reported by [declaredCollationOf]; [Collation.Binary]
|
||||||
|
* otherwise. Shared by [compareOrderKeyValues] and every equality-based dedup below so both sides of
|
||||||
|
* the COLLATE story (ordering and equality) agree on which collation a given expression uses.
|
||||||
|
*/
|
||||||
|
fun resolveCollation(expression: Expression, declaredCollationOf: (ColumnExpression) -> Collation?): Collation =
|
||||||
|
(expression as? CollateExpression)?.collation
|
||||||
|
?: (expression as? ColumnExpression)?.let(declaredCollationOf)
|
||||||
|
?: Collation.Binary
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Folds [values] (one grouping/`DISTINCT` key, whose entries line up positionally with
|
||||||
|
* [expressions]) into a key usable as a [Map]/[Set] bucket or a `distinctBy` selector: a
|
||||||
|
* [Value.TextValue] under a collation resolved (via [resolveCollation]) to anything but
|
||||||
|
* [Collation.Binary] is rewritten through [Collation.canonicalize] first, so e.g. `'Bob'` and
|
||||||
|
* `'bob'` land in the same bucket under a `NOCASE`-declared column, while every other value keeps
|
||||||
|
* its own `equals`/`hashCode` - a call site with no declared collation anywhere behaves exactly as
|
||||||
|
* it did before this function existed, since [Collation.Binary]'s [Collation.canonicalize] is the
|
||||||
|
* identity.
|
||||||
|
*/
|
||||||
|
fun collationFoldedKey(
|
||||||
|
values: List<Value>,
|
||||||
|
expressions: List<Expression>,
|
||||||
|
declaredCollationOf: (ColumnExpression) -> Collation?
|
||||||
|
): List<Value> = values.mapIndexed { index, value ->
|
||||||
|
if (value !is Value.TextValue) value
|
||||||
|
else Value.TextValue(resolveCollation(expressions[index], declaredCollationOf).canonicalize(value.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [collationFoldedKey]'s counterpart for a PRIMARY KEY / `UNIQUE` constraint / `UNIQUE` index
|
||||||
|
* uniqueness check, where the "expression" naming each key column is always just that column
|
||||||
|
* itself - a bare index into [schema], not an arbitrary [Expression] tree - so the collation to
|
||||||
|
* apply is always [kestrel.catalog.Column.collation] directly rather than something resolved
|
||||||
|
* through [resolveCollation]'s explicit-`COLLATE`-wins rule (a `PRIMARY KEY`/`UNIQUE` column list
|
||||||
|
* has no expressions to attach an explicit `COLLATE` to in the first place). Used by
|
||||||
|
* [kestrel.engine.Executor]'s in-memory `enforcePrimaryKeyUniqueness`/`enforceUniqueConstraints`/
|
||||||
|
* `enforceUniqueIndexes`, [kestrel.store.sql.DiskTable]'s heap-scanned
|
||||||
|
* `rejectDuplicateUniqueConstraints`/`backfillSecondaryIndex`, and (via
|
||||||
|
* [kestrel.store.sql.DiskTable]'s `encodePrimaryKey`/`compositeKey`/`equalRangeFor`) the disk
|
||||||
|
* engine's own B+Tree-backed PRIMARY KEY and `CREATE UNIQUE INDEX` key encoding itself - folding
|
||||||
|
* only ever changes what bytes a [kestrel.store.codec.KeyCodec] index key encodes to, never the
|
||||||
|
* row values a [kestrel.store.heap.RecordId] resolves to, so it is safe for lookup/ordering
|
||||||
|
* without corrupting any stored row.
|
||||||
|
*/
|
||||||
|
fun collationFoldedUniquenessKey(schema: TableSchema, columnIndices: List<Int>, values: List<Value>): List<Value> =
|
||||||
|
values.mapIndexed { position, value ->
|
||||||
|
if (value !is Value.TextValue) value
|
||||||
|
else Value.TextValue(schema.columns[columnIndices[position]].collation.canonicalize(value.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single-column counterpart of [collationFoldedKey], for an aggregate's own `DISTINCT` dedup
|
||||||
|
* (`COUNT`/`SUM`/`AVG`/`MIN`/`MAX(DISTINCT ...)`), which has already reduced its argument down to a
|
||||||
|
* flat list of values rather than whole rows.
|
||||||
|
*/
|
||||||
|
fun List<Value>.distinctByCollation(collation: Collation): List<Value> =
|
||||||
|
distinctBy { value -> if (value is Value.TextValue) collation.canonicalize(value.value) else value }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite's own two-tier rule for a binary comparison's collating sequence, in decreasing
|
||||||
|
* precedence: (1) an explicit `COLLATE` on either operand, left first; (2) either operand's own
|
||||||
|
* declared column `COLLATE` (via [declaredCollationOf], with [Collation.Binary] treated the same
|
||||||
|
* as "no declaration" so it never out-ranks the *other* side purely by appearing first - see
|
||||||
|
* [resolveCollation]'s own doc for the identical reasoning), left first; (3) [Collation.Binary].
|
||||||
|
* Kept as two separate passes (not one combined per-side [resolveCollation] check) so an explicit
|
||||||
|
* `COLLATE` on the *right* operand still outranks the *left* operand's own declared column
|
||||||
|
* collation. Shared by [kestrel.engine.Evaluator]'s `=`/`<>`/`BETWEEN`/`CASE` comparisons and the
|
||||||
|
* hash-join fast path in [kestrel.engine.SelectRunner] (`hashJoinKeys`/`executeJoin`), so a
|
||||||
|
* `JOIN ... ON a.x = b.x` bucket lookup agrees with what evaluating the exact same condition
|
||||||
|
* row-by-row through [kestrel.engine.Evaluator] would have decided.
|
||||||
|
*/
|
||||||
|
fun pairCollation(
|
||||||
|
left: Expression,
|
||||||
|
right: Expression,
|
||||||
|
declaredCollationOf: (ColumnExpression) -> Collation?
|
||||||
|
): Collation {
|
||||||
|
fun explicitOf(expression: Expression) = (expression as? CollateExpression)?.collation
|
||||||
|
fun declaredOf(expression: Expression) =
|
||||||
|
(expression as? ColumnExpression)?.let(declaredCollationOf)?.takeIf { it != Collation.Binary }
|
||||||
|
return explicitOf(left) ?: explicitOf(right) ?: declaredOf(left) ?: declaredOf(right) ?: Collation.Binary
|
||||||
|
}
|
||||||
@@ -1,36 +1,112 @@
|
|||||||
package kestrel.engine
|
package kestrel.engine
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.RoundingMode
|
||||||
|
import java.time.LocalDate
|
||||||
import kestrel.KestrelException
|
import kestrel.KestrelException
|
||||||
import kestrel.catalog.TableSchema
|
import kestrel.catalog.TableSchema
|
||||||
|
import kestrel.engine.function.ScalarFunctions
|
||||||
|
import kestrel.sql.ast.BetweenExpression
|
||||||
import kestrel.sql.ast.BinaryExpression
|
import kestrel.sql.ast.BinaryExpression
|
||||||
import kestrel.sql.ast.BinaryOperator
|
import kestrel.sql.ast.BinaryOperator
|
||||||
|
import kestrel.sql.ast.CallExpression
|
||||||
|
import kestrel.sql.ast.CaseExpression
|
||||||
|
import kestrel.sql.ast.CastExpression
|
||||||
|
import kestrel.sql.ast.CollateExpression
|
||||||
import kestrel.sql.ast.ColumnExpression
|
import kestrel.sql.ast.ColumnExpression
|
||||||
|
import kestrel.sql.ast.ExistsExpression
|
||||||
import kestrel.sql.ast.Expression
|
import kestrel.sql.ast.Expression
|
||||||
import kestrel.sql.ast.FunctionExpression
|
import kestrel.sql.ast.FunctionExpression
|
||||||
|
import kestrel.sql.ast.InExpression
|
||||||
|
import kestrel.sql.ast.InSubqueryExpression
|
||||||
|
import kestrel.sql.ast.IsNullExpression
|
||||||
|
import kestrel.sql.ast.LikeExpression
|
||||||
|
import kestrel.sql.ast.CurrentValueExpression
|
||||||
import kestrel.sql.ast.LiteralExpression
|
import kestrel.sql.ast.LiteralExpression
|
||||||
|
import kestrel.sql.ast.MergeActionExpression
|
||||||
|
import kestrel.sql.ast.NextValueExpression
|
||||||
|
import kestrel.sql.ast.ParameterExpression
|
||||||
|
import kestrel.sql.ast.ScalarSubqueryExpression
|
||||||
|
import kestrel.sql.ast.SelectStatement
|
||||||
import kestrel.sql.ast.UnaryExpression
|
import kestrel.sql.ast.UnaryExpression
|
||||||
import kestrel.sql.ast.UnaryOperator
|
import kestrel.sql.ast.UnaryOperator
|
||||||
|
import kestrel.sql.ast.WindowExpression
|
||||||
|
import kestrel.storage.Collation
|
||||||
|
import kestrel.storage.ColumnType
|
||||||
|
import kestrel.storage.DecimalColumns
|
||||||
import kestrel.storage.Row
|
import kestrel.storage.Row
|
||||||
import kestrel.storage.Value
|
import kestrel.storage.Value
|
||||||
import kestrel.storage.typeName
|
import kestrel.storage.typeName
|
||||||
|
|
||||||
fun interface ColumnResolver {
|
fun interface ColumnResolver {
|
||||||
fun resolve(expression: ColumnExpression): Value
|
fun resolve(expression: ColumnExpression): Value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Collation] [expression] was declared with in its `CREATE TABLE ... COLLATE name` (its
|
||||||
|
* [kestrel.catalog.Column.collation]), or `null` when this resolver has no such column to
|
||||||
|
* report one for - a computed/derived row (a set operation's output, a LATERAL subquery's own
|
||||||
|
* result), an ambiguous reference across more than one source, or simply a resolver that never
|
||||||
|
* bothered to override this default. [Evaluator.collationOf] only ever calls this for a bare
|
||||||
|
* [ColumnExpression] with no [kestrel.sql.ast.CollateExpression] of its own - an explicit
|
||||||
|
* `COLLATE` in the query always wins over whatever this returns. A default method (not the SAM
|
||||||
|
* member) so every existing lambda-shaped `ColumnResolver { expression -> ... }` construction
|
||||||
|
* keeps compiling unchanged and simply reports "no declared collation" - only the resolvers
|
||||||
|
* built directly over a [kestrel.catalog.TableSchema] bother to override it.
|
||||||
|
*/
|
||||||
|
fun declaredCollation(expression: ColumnExpression): Collation? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
class Evaluator(private val resolver: ColumnResolver? = null) {
|
fun interface SubqueryRunner {
|
||||||
constructor(schema: TableSchema?, row: Row?) : this(schema?.let { active ->
|
fun run(select: SelectStatement, outer: ColumnResolver?): SelectResult
|
||||||
ColumnResolver { expression ->
|
}
|
||||||
val index = active.columnIndex(expression.name)
|
|
||||||
?: throw KestrelException("no such column: ${expression.name}")
|
/**
|
||||||
row!!.values[index]
|
* Bridges NEXTVAL/CURRVAL evaluation to whichever [kestrel.catalog.Catalog] and per-session state
|
||||||
}
|
* actually own sequence counters - kept as its own interface (not a `fun interface` like
|
||||||
})
|
* [ColumnResolver]/[SubqueryRunner]) because it needs two independent operations, not one.
|
||||||
|
* [kestrel.engine.Executor] is the only implementation; every other [Evaluator] construction site
|
||||||
|
* (SELECT projections, WHERE clauses read-side, CHECK constraints, trigger `WHEN` clauses) omits it,
|
||||||
|
* so [evaluateNextValue]/[evaluateCurrentValue] throw there exactly like a missing [SubqueryRunner]
|
||||||
|
* already does for a subquery - deliberately: a volatile, side-effecting call being evaluated an
|
||||||
|
* engine-implementation-defined number of times (once for filtering, again for projecting the same
|
||||||
|
* row, ...) is exactly the ambiguity real databases warn about, so this engine only ever allows it
|
||||||
|
* where a row's assignment is evaluated exactly once by construction (see
|
||||||
|
* [kestrel.engine.Executor.executeInsertInner]/`executeUpdateInner`).
|
||||||
|
*/
|
||||||
|
interface SequenceAccess {
|
||||||
|
fun nextValue(sequenceName: String): Long
|
||||||
|
fun currentValue(sequenceName: String): Long
|
||||||
|
}
|
||||||
|
|
||||||
|
class Evaluator(
|
||||||
|
private val resolver: ColumnResolver? = null,
|
||||||
|
private val subqueries: SubqueryRunner? = null,
|
||||||
|
private val sequences: SequenceAccess? = null,
|
||||||
|
/** Bound only by [kestrel.engine.Executor.evaluateMergeReturning]'s per-row [Evaluator] - see [MergeActionExpression]'s own doc for why every other call site leaves this null and lets MERGE_ACTION() throw. */
|
||||||
|
private val mergeAction: String? = null
|
||||||
|
) {
|
||||||
|
constructor(schema: TableSchema?, row: Row?, subqueries: SubqueryRunner? = null, sequences: SequenceAccess? = null) : this(
|
||||||
|
schema?.let { active ->
|
||||||
|
object : ColumnResolver {
|
||||||
|
override fun resolve(expression: ColumnExpression): Value {
|
||||||
|
val index = active.columnIndex(expression.name)
|
||||||
|
?: throw KestrelException("no such column: ${expression.name}")
|
||||||
|
return row!!.values[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun declaredCollation(expression: ColumnExpression): Collation? =
|
||||||
|
active.column(expression.name)?.collation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subqueries,
|
||||||
|
sequences
|
||||||
|
)
|
||||||
|
|
||||||
fun evaluate(expression: Expression): Value = when (expression) {
|
fun evaluate(expression: Expression): Value = when (expression) {
|
||||||
is LiteralExpression -> expression.value
|
is LiteralExpression -> expression.value
|
||||||
is ColumnExpression -> evaluateColumn(expression)
|
is ColumnExpression -> evaluateColumn(expression)
|
||||||
is UnaryExpression -> evaluateUnary(expression)
|
is UnaryExpression -> evaluateUnary(expression)
|
||||||
|
is CollateExpression -> evaluate(expression.operand)
|
||||||
is BinaryExpression -> evaluateBinary(expression)
|
is BinaryExpression -> evaluateBinary(expression)
|
||||||
is FunctionExpression -> throw KestrelException(
|
is FunctionExpression -> throw KestrelException(
|
||||||
"aggregate function ${expression.name} is not allowed here"
|
"aggregate function ${expression.name} is not allowed here"
|
||||||
|
|||||||
Reference in new issue
Block a user