From cc86659511fa082e665b04456f443746cd07091b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 22:05:26 +0200 Subject: [PATCH] feat: extend query --- src/query.zig | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/query.zig b/src/query.zig index a295ab4..3c9e605 100644 --- a/src/query.zig +++ b/src/query.zig @@ -1434,3 +1434,164 @@ test "like predicate filters by pattern and never uses an index on that field" { // notion of substring matching), never mistake this for an eq lookup. try table.createIndex("name"); + try table.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "Ada Lovelace" } }}); + try table.put(.{ .int = 2 }, &.{.{ .name = "name", .value = .{ .text = "Alan Turing" } }}); + try table.put(.{ .int = 3 }, &.{.{ .name = "name", .value = .{ .text = "Grace Hopper" } }}); + + { + const results = try table.select(.{ .like = .{ .field = "name", .value = .{ .text = "A%" } } }); + defer freeRows(allocator, results); + try std.testing.expectEqual(@as(usize, 2), results.len); + } + { + const results = try table.select(.{ .like = .{ .field = "name", .value = .{ .text = "%ing" } } }); + defer freeRows(allocator, results); + try std.testing.expectEqual(@as(usize, 1), results.len); + try std.testing.expectEqualStrings("Alan Turing", results[0].get("name").?.text); + } + { + const results = try table.select(.{ .like = .{ .field = "name", .value = .{ .text = "%z%" } } }); + defer freeRows(allocator, results); + try std.testing.expectEqual(@as(usize, 0), results.len); + } + + var plan = try table.explain(allocator, .{ .like = .{ .field = "name", .value = .{ .text = "A%" } } }); + defer plan.deinit(allocator); + try std.testing.expectEqual(PlanKind.full_scan, plan.kind); +} + +test "like combines with an indexed eq leaf inside and_ without breaking the index plan" { + 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"); + + try table.put(.{ .int = 1 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "name", .value = .{ .text = "Ada" } } }); + try table.put(.{ .int = 2 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "name", .value = .{ .text = "Boris" } } }); + try table.put(.{ .int = 3 }, &.{ .{ .name = "city", .value = .{ .text = "krakow" } }, .{ .name = "name", .value = .{ .text = "Ada" } } }); + + const preds = [_]Predicate{ + .{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }, + .{ .like = .{ .field = "name", .value = .{ .text = "A%" } } }, + }; + const pred = Predicate{ .and_ = &preds }; + + // findIndexableLeaf must still pick the `eq` leaf on `city` for the + // index scan even though `like` sits right next to it in the same and_; + // selectByIndexPage then re-evaluates the whole predicate (including + // `like`) against every index candidate. + var plan = try table.explain(allocator, pred); + defer plan.deinit(allocator); + try std.testing.expectEqual(PlanKind.index_eq, plan.kind); + try std.testing.expectEqualStrings("city", plan.indexName.?); + + const results = try table.select(pred); + defer freeRows(allocator, results); + try std.testing.expectEqual(@as(usize, 1), results.len); + try std.testing.expectEqual(@as(i64, 1), results[0].get(pkField).?.int); +} + +test "index stays in sync across update and delete" { + 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"); + + try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + { + const warsaw = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer freeRows(allocator, warsaw); + try std.testing.expectEqual(@as(usize, 1), warsaw.len); + } + + // Move the row to a different city: the old index entry must disappear. + try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "krakow" } }}); + { + const warsaw = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer freeRows(allocator, warsaw); + try std.testing.expectEqual(@as(usize, 0), warsaw.len); + const krakow = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "krakow" } } }); + defer freeRows(allocator, krakow); + try std.testing.expectEqual(@as(usize, 1), krakow.len); + } + + try table.delete(.{ .int = 1 }); + { + const krakow = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "krakow" } } }); + defer freeRows(allocator, krakow); + try std.testing.expectEqual(@as(usize, 0), krakow.len); + } +} + +test "dropIndex removes entries and falls back to 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.createIndex("city"); + try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + + try table.dropIndex("city"); + try std.testing.expect(!table.hasIndex("city")); + + // Still correct without the index, just via full scan. + const results = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer freeRows(allocator, results); + try std.testing.expectEqual(@as(usize, 1), results.len); +} + +test "range predicate uses index and respects bounds" { + 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, "events"); + defer table.deinit(); + try table.createIndex("score"); + + var i: i64 = 0; + while (i < 10) : (i += 1) { + try table.put(.{ .int = i }, &.{.{ .name = "score", .value = .{ .int = i } }}); + } + + const results = try table.select(.{ .gte = .{ .field = "score", .value = .{ .int = 7 } } }); + defer freeRows(allocator, results); + try std.testing.expectEqual(@as(usize, 3), results.len); +} + +test "compound and predicate narrows via one indexed conjunct" { + 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"); + + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 20 } }, + }); + try table.put(.{ .int = 2 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 40 } }, + }); +