feat(engine): extend CollationEquality with resolveCollation
This commit is contained in:
1 parent
71d994698a
commit
e42f3ed09b
2 files changed
+175
-5
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
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.time.LocalDate
|
||||
import kestrel.KestrelException
|
||||
import kestrel.catalog.TableSchema
|
||||
import kestrel.engine.function.ScalarFunctions
|
||||
import kestrel.sql.ast.BetweenExpression
|
||||
import kestrel.sql.ast.BinaryExpression
|
||||
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.ExistsExpression
|
||||
import kestrel.sql.ast.Expression
|
||||
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.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.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.Value
|
||||
import kestrel.storage.typeName
|
||||
|
||||
fun interface ColumnResolver {
|
||||
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) {
|
||||
constructor(schema: TableSchema?, row: Row?) : this(schema?.let { active ->
|
||||
ColumnResolver { expression ->
|
||||
fun interface SubqueryRunner {
|
||||
fun run(select: SelectStatement, outer: ColumnResolver?): SelectResult
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}")
|
||||
row!!.values[index]
|
||||
return row!!.values[index]
|
||||
}
|
||||
})
|
||||
|
||||
override fun declaredCollation(expression: ColumnExpression): Collation? =
|
||||
active.column(expression.name)?.collation
|
||||
}
|
||||
},
|
||||
subqueries,
|
||||
sequences
|
||||
)
|
||||
|
||||
fun evaluate(expression: Expression): Value = when (expression) {
|
||||
is LiteralExpression -> expression.value
|
||||
is ColumnExpression -> evaluateColumn(expression)
|
||||
is UnaryExpression -> evaluateUnary(expression)
|
||||
is CollateExpression -> evaluate(expression.operand)
|
||||
is BinaryExpression -> evaluateBinary(expression)
|
||||
is FunctionExpression -> throw KestrelException(
|
||||
"aggregate function ${expression.name} is not allowed here"
|
||||
|
||||
Reference in new issue
Block a user