feat: extend query

This commit is contained in:
dualfroz committed 2026-09-08 22:05:26 +02:00
1 parent 4fa334072e
commit cc86659511
1 file changed
+161
+161
View File
@@ -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. // notion of substring matching), never mistake this for an eq lookup.
try table.createIndex("name"); 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 } },
});