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