feat: add compositeCursorForMatch and valueTupleEql to query
This commit is contained in:
1 parent
56a7b603a9
commit
4fa334072e
1 file changed
+159
+159
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user