diff --git a/src/main/kotlin/kestrel/engine/CollationEquality.kt b/src/main/kotlin/kestrel/engine/CollationEquality.kt new file mode 100644 index 0000000..293df86 --- /dev/null +++ b/src/main/kotlin/kestrel/engine/CollationEquality.kt @@ -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, + expressions: List, + declaredCollationOf: (ColumnExpression) -> Collation? +): List = 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, values: List): List = + 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.distinctByCollation(collation: Collation): List = + 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 +} diff --git a/src/main/kotlin/kestrel/engine/Evaluator.kt b/src/main/kotlin/kestrel/engine/Evaluator.kt index 72902d7..6b1099a 100644 --- a/src/main/kotlin/kestrel/engine/Evaluator.kt +++ b/src/main/kotlin/kestrel/engine/Evaluator.kt @@ -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 -> - val index = active.columnIndex(expression.name) - ?: throw KestrelException("no such column: ${expression.name}") - row!!.values[index] - } - }) +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}") + 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"