diff --git a/src/query.zig b/src/query.zig new file mode 100644 index 0000000..b76cab2 --- /dev/null +++ b/src/query.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const dbmod = @import("db.zig"); +const rowmod = @import("row.zig"); +const idx = @import("index.zig"); +const exprmod = @import("expr.zig"); + +const Allocator = std.mem.Allocator; +const Db = dbmod.Db; +const Value = rowmod.Value; +const Row = rowmod.Row; +const Field = rowmod.Field; +const IndexSet = idx.IndexSet; +const CompositeIndexSet = idx.CompositeIndexSet; +const CompositeIndexSpec = idx.CompositeIndexSpec; +const UniqueSet = idx.UniqueSet; +const ForeignKeySet = idx.ForeignKeySet; +const CheckConstraintSet = idx.CheckConstraintSet; +const ReferencedBySet = idx.ReferencedBySet; + +/// Reserved field name a Table stores the primary key under, so a decoded +/// Row is self-sufficient for callers that only have the Row (e.g. results +/// of a full table scan) without needing to separately decode it from the +/// row key's escaped ordered bytes. +pub const pkField = "__pk"; + +pub const Condition = struct { + field: []const u8, + value: Value, +}; + +/// Paging window for `Table.selectPage`: skip the first `offset` matches, +/// return at most `limit` of them (0 = unlimited). +pub const Page = struct { + offset: usize = 0, + limit: usize = 0, +}; + +/// One column of a `Table.selectPageOrdered` sort key: order matched rows +/// by `field` (via `rowmod.compare`'s total order across value types). A +/// row missing `field` entirely sorts as `Value.none`, which `compare` +/// places before every other tag - so rows lacking the sort column +/// consistently group at the front (ascending) or back (descending) +/// instead of erroring. `selectPageOrdered` takes a `[]const OrderBy` +/// (empty = unsorted): columns after the first only break ties left by +/// the ones before them, the same lexicographic rule SQL's `ORDER BY a, b` +/// follows - the sort is stable, so a `[]const OrderBy` with a single +/// entry that leaves ties unresolved behaves exactly as before this type +/// gained multi-column support (rows keep their relative scan order: +/// ascending pk for a full scan, index order for an indexed lookup). +pub const OrderBy = struct { + field: []const u8, + descending: bool = false, +}; + +/// Which access strategy `Table.selectPage` would take for a given +/// predicate, as reported by `Table.explain`. Mirrors the three real +/// branches of `selectPage`'s decision ladder: an unindexed full scan, a +/// single-field index (`eq` narrows to one key, `lt`/`lte`/`gt`/`gte` +/// narrow to a range), or a composite index matched on a leftmost prefix +/// of its columns (`_eq` when the prefix is the whole match, `_range` when +/// the column right after the prefix also had a range bound). +pub const PlanKind = enum(u8) { + full_scan, + index_eq, + index_range, + composite_eq, + composite_range, +}; + +/// `indexName` is an owned copy the caller must free with `Plan.deinit` - +/// null (and `matchedFields`/`totalFields` both 0) for `.full_scan`, which +/// names no index. For `.index_eq`/`.index_range` it is the single indexed +/// field name and `matchedFields == totalFields == 1`. For +/// `.composite_eq`/`.composite_range` it is the composite index's name and +/// `matchedFields` counts how many of the index's `totalFields` leading +/// columns the query actually pinned down (a strict prefix match still +/// narrows the scan, same leftmost-prefix rule a real multi-column B-tree +/// index gives you). +pub const Plan = struct { + kind: PlanKind, + indexName: ?[]const u8 = null, + matchedFields: usize = 0, + totalFields: usize = 0, + + pub fn deinit(self: *Plan, allocator: Allocator) void { + if (self.indexName) |n| allocator.free(n); + self.* = undefined; + } +}; + +/// Predicate AST evaluated against a decoded Row. Leaves compare a named +/// field to a literal Value; And/Or/Not compose leaves and other predicates. +/// Predicate does not own the memory of any Value or field-name slice it +/// references - callers must keep those alive for as long as the predicate +/// tree is used. +pub const Predicate = union(enum) { + eq: Condition, + ne: Condition, + lt: Condition, + lte: Condition, + gt: Condition, + gte: Condition, + like: Condition, + /// True when `field` is absent from the row entirely, or present but + /// holding an explicit `Value.none` - a sparse row's "never set" and a + /// row that set the field to none are indistinguishable to a caller + /// asking "does this row have a real value here", same as SQL NULL. + is_null: []const u8, + is_not_null: []const u8, + and_: []const Predicate, + or_: []const Predicate, + not_: *const Predicate, + + fn isNullish(v: ?Value) bool { + const val = v orelse return true; + return val == .none; + } + + pub fn evaluate(self: Predicate, row: *const Row) bool { + return switch (self) { + .eq => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) == .eq else false, + .ne => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) != .eq else true, + .lt => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) == .lt else false, + .lte => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) != .gt else false, + .gt => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) == .gt else false, + .gte => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) != .lt else false, + .like => |c| blk: { + const rowVal = row.get(c.field) orelse break :blk false; + if (rowVal != .text or c.value != .text) break :blk false; + break :blk matchLike(c.value.text, rowVal.text); + }, + .is_null => |field| isNullish(row.get(field)), + .is_not_null => |field| !isNullish(row.get(field)), + .and_ => |preds| blk: { + for (preds) |p| { + if (!p.evaluate(row)) break :blk false; + } + break :blk true; + }, + .or_ => |preds| blk: { + for (preds) |p| { + if (p.evaluate(row)) break :blk true; + } + break :blk false; + }, + .not_ => |p| !p.evaluate(row), + }; + } +}; + +/// SQL-style LIKE matching: `%` matches any run of characters (including +/// none), `_` matches exactly one character, every other byte in `pattern` +/// must match `text` literally. No escape character - a pattern that needs a +/// literal `%`/`_` cannot express it, same simplification `expr.zig`'s other +/// operators already make (no escaping in the predicate language at all). +/// Iterative two-pointer wildcard matcher (not recursive backtracking) so a +/// pattern like "a%a%a%a%a%a%b" against a long non-matching text stays +/// linear-ish instead of exponential: on mismatch after a `%`, only the +/// text pointer rewinds-and-retries, the pattern never re-walks from +/// scratch. +fn matchLike(pattern: []const u8, text: []const u8) bool { + var pIdx: usize = 0; + var tIdx: usize = 0; + var starIdx: ?usize = null; + var matchIdx: usize = 0; +