feat: build out query
This commit is contained in:
1 parent
38fb06ec6b
commit
3efd09f0da
1 file changed
+157
+157
@@ -1752,3 +1752,160 @@ test "selectPage with limit 0 matches unlimited select" {
|
||||
try std.testing.expectEqual(all.len, page.len);
|
||||
}
|
||||
|
||||
test "selectPage offset past the end returns nothing" {
|
||||
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 }, &.{});
|
||||
try table.put(.{ .int = 2 }, &.{});
|
||||
|
||||
const page = try table.selectPage(null, .{ .offset = 50 });
|
||||
defer freeRows(allocator, page);
|
||||
try std.testing.expectEqual(@as(usize, 0), page.len);
|
||||
}
|
||||
|
||||
test "selectPage paginates through an index-narrowed predicate in pk order" {
|
||||
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.createIndex("city");
|
||||
|
||||
var pk: i64 = 1;
|
||||
while (pk <= 6) : (pk += 1) {
|
||||
try table.put(.{ .int = pk }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }});
|
||||
}
|
||||
// Noise row indexed under a different value - must never leak into the
|
||||
// paginated "warsaw" result regardless of offset/limit.
|
||||
try table.put(.{ .int = 99 }, &.{.{ .name = "city", .value = .{ .text = "krakow" } }});
|
||||
|
||||
const pred = Predicate{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } };
|
||||
|
||||
const first = try table.selectPage(pred, .{ .limit = 2 });
|
||||
defer freeRows(allocator, first);
|
||||
try std.testing.expectEqual(@as(usize, 2), first.len);
|
||||
try std.testing.expectEqual(@as(i64, 1), first[0].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 2), first[1].get(pkField).?.int);
|
||||
|
||||
const second = try table.selectPage(pred, .{ .offset = 2, .limit = 2 });
|
||||
defer freeRows(allocator, second);
|
||||
try std.testing.expectEqual(@as(usize, 2), second.len);
|
||||
try std.testing.expectEqual(@as(i64, 3), second[0].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 4), second[1].get(pkField).?.int);
|
||||
|
||||
const rest = try table.selectPage(pred, .{ .offset = 4 });
|
||||
defer freeRows(allocator, rest);
|
||||
try std.testing.expectEqual(@as(usize, 2), rest.len);
|
||||
try std.testing.expectEqual(@as(i64, 5), rest[0].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 6), rest[1].get(pkField).?.int);
|
||||
}
|
||||
|
||||
test "selectPage against a large indexed bucket stays correct with a small limit" {
|
||||
// Exercises the streaming `cursorForLeaf` path (see index.zig's
|
||||
// EqCursor) against a bucket far larger than the requested page: a
|
||||
// regression that only materialized the first `limit` matches would
|
||||
// silently pass this test too, but a regression that went back to
|
||||
// materializing the whole 500-row match set first would still pass on
|
||||
// correctness alone - the point of the streaming rewrite is that this
|
||||
// scan touches roughly `offset + limit` index entries, not 500, which is
|
||||
// asserted structurally in index.zig's cursor tests via early deinit.
|
||||
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.createIndex("city");
|
||||
|
||||
var pk: i64 = 0;
|
||||
while (pk < 500) : (pk += 1) {
|
||||
try table.put(.{ .int = pk }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }});
|
||||
}
|
||||
|
||||
const pred = Predicate{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } };
|
||||
|
||||
const page = try table.selectPage(pred, .{ .offset = 497, .limit = 10 });
|
||||
defer freeRows(allocator, page);
|
||||
try std.testing.expectEqual(@as(usize, 3), page.len);
|
||||
try std.testing.expectEqual(@as(i64, 497), page[0].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 498), page[1].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 499), page[2].get(pkField).?.int);
|
||||
}
|
||||
|
||||
test "selectPageOrdered sorts a full scan ascending and descending by an int 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();
|
||||
// Insert pks out of score order so ascending pk (the natural full-scan
|
||||
// order) is NOT already sorted by `score` - a regression that forgot to
|
||||
// sort and just returned scan order would fail both assertions below.
|
||||
const scores = [_]i64{ 30, 10, 50, 20, 40 };
|
||||
for (scores, 1..) |score, pk| {
|
||||
try table.put(.{ .int = @intCast(pk) }, &.{.{ .name = "score", .value = .{ .int = score } }});
|
||||
}
|
||||
|
||||
const asc = try table.selectPageOrdered(null, .{}, &.{.{ .field = "score" }});
|
||||
defer freeRows(allocator, asc);
|
||||
var expected = [_]i64{ 10, 20, 30, 40, 50 };
|
||||
try std.testing.expectEqual(expected.len, asc.len);
|
||||
for (asc, 0..) |r, i| try std.testing.expectEqual(expected[i], r.get("score").?.int);
|
||||
|
||||
const desc = try table.selectPageOrdered(null, .{}, &.{.{ .field = "score", .descending = true }});
|
||||
defer freeRows(allocator, desc);
|
||||
std.mem.reverse(i64, &expected);
|
||||
for (desc, 0..) |r, i| try std.testing.expectEqual(expected[i], r.get("score").?.int);
|
||||
}
|
||||
|
||||
test "selectPageOrdered applies offset/limit after sorting, not before" {
|
||||
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();
|
||||
var pk: i64 = 1;
|
||||
while (pk <= 10) : (pk += 1) {
|
||||
// Descending score as pk increases, so "top 3 by score desc" is the
|
||||
// FIRST 3 pks, not the last 3 - a page-then-sort bug (paging the
|
||||
// natural scan order before sorting) would return the wrong rows.
|
||||
try table.put(.{ .int = pk }, &.{.{ .name = "score", .value = .{ .int = 100 - pk } }});
|
||||
}
|
||||
|
||||
const top3 = try table.selectPageOrdered(null, .{ .limit = 3 }, &.{.{ .field = "score", .descending = true }});
|
||||
defer freeRows(allocator, top3);
|
||||
try std.testing.expectEqual(@as(usize, 3), top3.len);
|
||||
try std.testing.expectEqual(@as(i64, 1), top3[0].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 2), top3[1].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 3), top3[2].get(pkField).?.int);
|
||||
|
||||
const nextPage = try table.selectPageOrdered(null, .{ .offset = 3, .limit = 3 }, &.{.{ .field = "score", .descending = true }});
|
||||
defer freeRows(allocator, nextPage);
|
||||
try std.testing.expectEqual(@as(i64, 4), nextPage[0].get(pkField).?.int);
|
||||
try std.testing.expectEqual(@as(i64, 6), nextPage[2].get(pkField).?.int);
|
||||
}
|
||||
|
||||
test "selectPageOrdered sorts text fields lexically and is stable on ties" {
|
||||
const allocator = std.testing.allocator;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTemp(&tmp, allocator);
|
||||
defer db.close();
|
||||
|
||||
Reference in new issue
Block a user