feat: expand query

This commit is contained in:
dualfroz committed 2026-09-08 22:19:19 +02:00
1 parent cc86659511
commit 38fb06ec6b
1 file changed
+157
+157
View File
@@ -1595,3 +1595,160 @@ test "compound and predicate narrows via one indexed conjunct" {
.{ .name = "age", .value = .{ .int = 40 } },
});
const preds = [_]Predicate{
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
.{ .gte = .{ .field = "age", .value = .{ .int = 30 } } },
};
const results = try table.select(.{ .and_ = &preds });
defer freeRows(allocator, results);
try std.testing.expectEqual(@as(usize, 1), results.len);
try std.testing.expectEqual(@as(i64, 2), results[0].get(pkField).?.int);
}
test "not and or predicates evaluate via full scan" {
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 = "age", .value = .{ .int = 10 } }});
try table.put(.{ .int = 2 }, &.{.{ .name = "age", .value = .{ .int = 20 } }});
try table.put(.{ .int = 3 }, &.{.{ .name = "age", .value = .{ .int = 30 } }});
const orPreds = [_]Predicate{
.{ .eq = .{ .field = "age", .value = .{ .int = 10 } } },
.{ .eq = .{ .field = "age", .value = .{ .int = 30 } } },
};
const orResults = try table.select(.{ .or_ = &orPreds });
defer freeRows(allocator, orResults);
try std.testing.expectEqual(@as(usize, 2), orResults.len);
const inner = Predicate{ .eq = .{ .field = "age", .value = .{ .int = 20 } } };
const notResult = try table.select(.{ .not_ = &inner });
defer freeRows(allocator, notResult);
try std.testing.expectEqual(@as(usize, 2), notResult.len);
}
test "is_null/is_not_null evaluate against missing fields, explicit none, and present values" {
const allocator = std.testing.allocator;
var sparse = Row.init(allocator);
defer sparse.deinit();
try sparse.set("age", .{ .int = 10 });
var withNone = Row.init(allocator);
defer withNone.deinit();
try withNone.set("city", .{ .none = {} });
var withCity = Row.init(allocator);
defer withCity.deinit();
try withCity.set("city", .{ .text = "warsaw" });
const isNull = Predicate{ .is_null = "city" };
const isNotNull = Predicate{ .is_not_null = "city" };
try std.testing.expect(isNull.evaluate(&sparse));
try std.testing.expect(!isNotNull.evaluate(&sparse));
try std.testing.expect(isNull.evaluate(&withNone));
try std.testing.expect(!isNotNull.evaluate(&withNone));
try std.testing.expect(!isNull.evaluate(&withCity));
try std.testing.expect(isNotNull.evaluate(&withCity));
}
test "is_null selects only sparse rows missing the field, over a full scan" {
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, "people");
defer table.deinit();
try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }});
try table.put(.{ .int = 2 }, &.{.{ .name = "age", .value = .{ .int = 40 } }});
try table.put(.{ .int = 3 }, &.{.{ .name = "age", .value = .{ .int = 50 } }});
var plan = try table.explain(allocator, .{ .is_null = "city" });
defer plan.deinit(allocator);
try std.testing.expectEqual(PlanKind.full_scan, plan.kind);
const missing = try table.select(.{ .is_null = "city" });
defer freeRows(allocator, missing);
try std.testing.expectEqual(@as(usize, 2), missing.len);
const present = try table.select(.{ .is_not_null = "city" });
defer freeRows(allocator, present);
try std.testing.expectEqual(@as(usize, 1), present.len);
// is_null on an indexed field still falls back to full_scan (not
// indexable), but the result is unaffected by the index's presence.
try table.createIndex("age");
const stillMissing = try table.select(.{ .is_null = "city" });
defer freeRows(allocator, stillMissing);
try std.testing.expectEqual(@as(usize, 2), stillMissing.len);
}
test "invalid identifiers are rejected" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try std.testing.expectError(error.InvalidIdentifier, Table.open(&db, allocator, ""));
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try std.testing.expectError(error.InvalidIdentifier, table.createIndex("bad\x00field"));
}
test "selectPage skips offset and stops at limit on a full scan" {
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();
// Row keys are the pk's order-preserving encoding, so full-scan order
// is ascending pk regardless of insertion order.
var pk: i64 = 10;
while (pk >= 1) : (pk -= 1) {
try table.put(.{ .int = pk }, &.{});
}
const page = try table.selectPage(null, .{ .offset = 3, .limit = 4 });
defer freeRows(allocator, page);
try std.testing.expectEqual(@as(usize, 4), page.len);
var expected: i64 = 4;
for (page) |r| {
try std.testing.expectEqual(expected, r.get(pkField).?.int);
expected += 1;
}
}
test "selectPage with limit 0 matches unlimited select" {
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 <= 5) : (pk += 1) try table.put(.{ .int = pk }, &.{});
const all = try table.select(null);
defer freeRows(allocator, all);
const page = try table.selectPage(null, .{});
defer freeRows(allocator, page);
try std.testing.expectEqual(all.len, page.len);
}