feat: add Condition and Page
This commit is contained in:
1 parent
a91ac62468
commit
fd91d6174c
1 file changed
+166
+166
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user