feat: add compositeCursorForMatch and valueTupleEql to query

This commit is contained in:
dualfroz committed 2026-09-08 21:32:41 +02:00
1 parent 56a7b603a9
commit 4fa334072e
1 file changed
+159
+159
View File
@@ -1275,3 +1275,162 @@ pub const Table = struct {
return null;
}
fn compositeCursorForMatch(self: *Table, match: CompositeMatch) !IndexCursor {
const spec = self.composite.specs.items[match.specIdx];
const eqValues = match.values[0..match.matchedLen];
if (match.rangeLow != null or match.rangeHigh != null) {
return .{ .compositeRange = try idx.scanCompositeRangeCursor(self.db, self.allocator, self.name, spec.name, eqValues, match.rangeLow, match.rangeHigh) };
}
return .{ .compositeEq = try idx.scanCompositeEqCursor(self.db, self.allocator, self.name, spec.name, eqValues) };
}
};
fn valueTupleEql(a: []const Value, b: []const Value) bool {
if (a.len != b.len) return false;
for (a, b) |av, bv| {
if (!Value.eql(av, bv)) return false;
}
return true;
}
pub fn freeRows(allocator: Allocator, rows: []Row) void {
for (rows) |*r| r.deinit();
allocator.free(rows);
}
/// Stable in-place sort of `rows` by `order`, using `rowmod.compare`'s
/// total order across value types (see `OrderBy`'s doc comment for how a
/// missing field sorts). Columns are compared left to right: the first
/// entry decides unless it ties (`.eq`), in which case the next entry
/// breaks the tie, and so on - a plain lexicographic comparator chain, the
/// same rule SQL's `ORDER BY a, b, c` follows. `std.sort.block` rather than
/// the faster unstable `std.sort.pdq` specifically so ties that survive
/// every column preserve scan order, matching what `OrderBy`'s doc comment
/// promises callers.
fn sortRowsBy(rows: []Row, order: []const OrderBy) void {
const Ctx = struct {
order: []const OrderBy,
fn lessThan(ctx: @This(), a: Row, b: Row) bool {
for (ctx.order) |ob| {
const av = a.get(ob.field) orelse Value{ .none = {} };
const bv = b.get(ob.field) orelse Value{ .none = {} };
const cmp = rowmod.compare(av, bv);
if (cmp == .eq) continue;
return if (ob.descending) cmp == .gt else cmp == .lt;
}
return false;
}
};
std.sort.block(Row, rows, Ctx{ .order = order }, Ctx.lessThan);
}
/// Slices an already-materialized, unpaged match set (as `selectAllMatches`
/// returns) down to `page`'s offset/limit window, freeing every row that
/// falls outside it and the original backing allocation - the ordered
/// counterpart of the early-stop windowing `selectByIndexPage`/
/// `scanFilteredPage` do inline while scanning. `rows` must not be used
/// after this call regardless of error (see `errdefer` at the call site).
fn applyRowPage(allocator: Allocator, rows: []Row, page: Page) ![]Row {
const start = @min(page.offset, rows.len);
const end = if (page.limit == 0) rows.len else @min(rows.len, start + page.limit);
for (rows[0..start]) |*r| r.deinit();
for (rows[end..]) |*r| r.deinit();
const kept = rows[start..end];
const out = try allocator.alloc(Row, kept.len);
@memcpy(out, kept);
allocator.free(rows);
return out;
}
fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator) !Db {
const dbPath = try tmp.dir.realpathAlloc(allocator, ".");
defer allocator.free(dbPath);
return Db.open(allocator, dbPath, .{});
}
test "table put get delete round trip" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try table.put(.{ .int = 1 }, &.{
.{ .name = "name", .value = .{ .text = "ada" } },
.{ .name = "age", .value = .{ .int = 36 } },
});
var got = (try table.get(.{ .int = 1 })).?;
defer got.deinit();
try std.testing.expectEqualStrings("ada", got.get("name").?.text);
try std.testing.expectEqual(@as(i64, 36), got.get("age").?.int);
try std.testing.expectEqual(@as(i64, 1), got.get(pkField).?.int);
try table.delete(.{ .int = 1 });
try std.testing.expect((try table.get(.{ .int = 1 })) == null);
}
test "table put replaces whole row" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try table.put(.{ .int = 1 }, &.{
.{ .name = "name", .value = .{ .text = "ada" } },
.{ .name = "age", .value = .{ .int = 36 } },
});
try table.put(.{ .int = 1 }, &.{
.{ .name = "name", .value = .{ .text = "ada" } },
});
var got = (try table.get(.{ .int = 1 })).?;
defer got.deinit();
try std.testing.expect(got.get("age") == null);
try std.testing.expectEqualStrings("ada", got.get("name").?.text);
}
test "createIndex backfills then eq select uses index" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }});
try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "krakow" } }});
try table.put(.{ .int = 3 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }});
try table.createIndex("city");
try std.testing.expect(table.hasIndex("city"));
const results = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } });
defer freeRows(allocator, results);
try std.testing.expectEqual(@as(usize, 2), results.len);
}
test "like predicate filters by pattern and never uses an index on that field" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
// Index the very field the like-predicate targets: `like` must still
// fall back to a full scan (`Value.compare`-based index entries have no
// notion of substring matching), never mistake this for an eq lookup.
try table.createIndex("name");