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);
|
const matches = try self.selectAllMatches(pred);
|
||||||
defer freeRows(self.allocator, matches);
|
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