feat: add scanAll and select to query
This commit is contained in:
1 parent
da6a9cb289
commit
a394f9e80f
1 file changed
+167
+167
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user