diff --git a/src/query.zig b/src/query.zig index 09c385d..8c14c1f 100644 --- a/src/query.zig +++ b/src/query.zig @@ -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(); + } +