feat: add scanAll and select to query

This commit is contained in:
dualfroz committed 2026-09-08 21:04:35 +02:00
1 parent da6a9cb289
commit a394f9e80f
1 file changed
+167
+167
View File
@@ -945,3 +945,170 @@ pub const Table = struct {
const matches = try self.selectAllMatches(pred);
defer freeRows(self.allocator, matches);
for (matches) |*row| {
const pk = row.get(pkField) orelse continue;
try self.delete(pk);
}
return matches.len;
}
/// Full table scan decoding every row, oldest key order. Callers own the
/// returned slice and each Row in it (see freeRows).
pub fn scanAll(self: *Table) ![]Row {
const prefix = try idx.rowPrefix(self.allocator, self.name);
defer self.allocator.free(prefix);
var results = std.ArrayList(Row).init(self.allocator);
errdefer {
for (results.items) |*r| r.deinit();
results.deinit();
}
var it = try self.db.scanPrefix(self.allocator, prefix);
defer it.deinit();
while (try it.next()) |entry| {
const decoded = try Row.decode(self.allocator, entry.value);
try results.append(decoded);
}
return results.toOwnedSlice();
}
/// Selects rows matching `pred` (or every row, if null). When `pred` is
/// a single indexed condition, or an And containing one, the matching
/// index is used to build a narrowed candidate set before the full
/// predicate is re-checked against each candidate row - so results are
/// always correct even though the optimization only ever inspects one
/// conjunct's index.
pub fn select(self: *Table, pred: ?Predicate) ![]Row {
return self.selectPage(pred, .{});
}
/// Same as `select`, but skips the first `page.offset` matches and stops
/// once `page.limit` matches have been collected (0 = unlimited, the
/// same convention `protocol.ScanArgs` uses). Both the full-scan and
/// index-narrowed paths stop iterating as soon as the limit is met
/// instead of materializing the whole match set first - a real
/// efficiency win for `LIMIT`-bounded queries against large tables, not
/// just a post-hoc slice of an already-full result.
pub fn selectPage(self: *Table, pred: ?Predicate, page: Page) ![]Row {
if (pred) |p| {
if (self.findCompositeMatch(p)) |match| {
const cursor = try self.compositeCursorForMatch(match);
return self.selectByIndexPage(p, cursor, page);
}
if (self.findIndexableLeaf(p)) |leaf| {
const cursor = try self.cursorForLeaf(leaf);
return self.selectByIndexPage(p, cursor, page);
}
}
return self.scanFilteredPage(pred, page);
}
/// Same as `selectPage`, but sorts the matches by `orderBy` (when
/// non-empty, one or more columns applied left to right - see
/// `OrderBy`'s doc comment) before applying `page`'s offset/limit
/// window. `selectPage`'s early-stop optimization (dropping a full scan
/// or index cursor the moment `page.limit` matches are collected) is
/// only correct when results come out in the strategy's natural order -
/// sorting requires the full match set first, so an `orderBy` request
/// materializes every match via `selectAllMatches` (still using
/// whichever index `explain` would report, just without the early
/// stop), sorts it, then slices the requested window with
/// `applyRowPage`. An empty `orderBy` falls straight through to
/// `selectPage`, keeping the early-stop behavior unchanged for the
/// common unordered case.
pub fn selectPageOrdered(self: *Table, pred: ?Predicate, page: Page, orderBy: []const OrderBy) ![]Row {
if (orderBy.len == 0) return self.selectPage(pred, page);
const all = try self.selectAllMatches(pred);
errdefer freeRows(self.allocator, all);
sortRowsBy(all, orderBy);
return applyRowPage(self.allocator, all, page);
}
/// Collects every row matching `pred`, unpaged - the same
/// index-vs-full-scan strategy choice `selectPage`/`explain` make,
/// just always run to completion instead of stopping at a page limit.
fn selectAllMatches(self: *Table, pred: ?Predicate) ![]Row {
if (pred) |p| {
if (self.findCompositeMatch(p)) |match| {
const cursor = try self.compositeCursorForMatch(match);
return self.selectByIndexPage(p, cursor, .{});
}
if (self.findIndexableLeaf(p)) |leaf| {
const cursor = try self.cursorForLeaf(leaf);
return self.selectByIndexPage(p, cursor, .{});
}
}
return self.scanFilteredPage(pred, .{});
}
/// Reports which strategy `selectPage` would pick for `pred` without
/// running it - the exact same decision ladder (composite match first,
/// then a single-field indexable leaf, then a full scan), just stopping
/// one step short of building a cursor. Exists so a caller (the CLI's
/// `table explain`, its network/repl mirrors) can answer "would this
/// query use an index" without materializing rows or mutating anything,
/// which is why this takes `*const Table` unlike `selectPage`.
pub fn explain(self: *const Table, allocator: Allocator, pred: ?Predicate) !Plan {
if (pred) |p| {
if (self.findCompositeMatch(p)) |match| {
const spec = self.composite.specs.items[match.specIdx];
const isRange = match.rangeLow != null or match.rangeHigh != null;
return .{
.kind = if (isRange) .composite_range else .composite_eq,
.indexName = try allocator.dupe(u8, spec.name),
.matchedFields = match.matchedLen,
.totalFields = spec.fields.len,
};
}
if (self.findIndexableLeaf(p)) |leaf| {
const field = switch (leaf) {
.eq, .lt, .lte, .gt, .gte => |c| c.field,
else => unreachable,
};
const isRange = switch (leaf) {
.lt, .lte, .gt, .gte => true,
else => false,
};
return .{
.kind = if (isRange) .index_range else .index_eq,
.indexName = try allocator.dupe(u8, field),
.matchedFields = 1,
.totalFields = 1,
};
}
}
return .{ .kind = .full_scan };
}
fn scanFilteredPage(self: *Table, pred: ?Predicate, page: Page) ![]Row {
const prefix = try idx.rowPrefix(self.allocator, self.name);
defer self.allocator.free(prefix);
var results = std.ArrayList(Row).init(self.allocator);
errdefer {
for (results.items) |*r| r.deinit();
results.deinit();
}
var skipped: usize = 0;
var it = try self.db.scanPrefix(self.allocator, prefix);
defer it.deinit();
while (try it.next()) |entry| {
var decoded = try Row.decode(self.allocator, entry.value);
const matches = if (pred) |p| p.evaluate(&decoded) else true;
if (!matches) {
decoded.deinit();
continue;
}
if (skipped < page.offset) {
skipped += 1;
decoded.deinit();
continue;
}
try results.append(decoded);
if (page.limit != 0 and results.items.len >= page.limit) break;
}
return results.toOwnedSlice();
}