From abc713e5fc954d9b82ca429b8b0ee6896d6b7ca1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 13:49:39 +0200 Subject: [PATCH] feat: continue work on query --- src/query.zig | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/query.zig b/src/query.zig index b39858f..fc98432 100644 --- a/src/query.zig +++ b/src/query.zig @@ -2224,3 +2224,160 @@ test "composite index stays in sync across update and delete" { try std.testing.expectEqual(@as(usize, 0), stale.len); } + // A row missing one of the composite columns entirely must not produce + // a (partial, ill-formed) entry: the put must still succeed, and the + // row must never surface as a false match on any age value including + // its own current (missing) one. + try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + { + const wholeTable = try table.select(null); + defer freeRows(allocator, wholeTable); + try std.testing.expectEqual(@as(usize, 2), wholeTable.len); + + const ageless = [_]Predicate{ + .{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }, + .{ .eq = .{ .field = "age", .value = .{ .int = 31 } } }, + }; + const noMatch = try table.select(.{ .and_ = &ageless }); + defer freeRows(allocator, noMatch); + try std.testing.expectEqual(@as(usize, 1), noMatch.len); + try std.testing.expectEqual(@as(i64, 1), noMatch[0].get(pkField).?.int); + } + + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + }); + try table.delete(.{ .int = 1 }); + { + const afterDelete = try table.select(pred); + defer freeRows(allocator, afterDelete); + try std.testing.expectEqual(@as(usize, 0), afterDelete.len); + } +} + +test "dropCompositeIndex 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.createCompositeIndex("city_age", &.{ "city", "age" }); + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + }); + + try table.dropCompositeIndex("city_age"); + try std.testing.expect(!table.hasCompositeIndex("city_age")); + + const preds = [_]Predicate{ + .{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }, + .{ .eq = .{ .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); +} + +test "createCompositeIndex rejects bad field counts and identifiers" { + 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 std.testing.expectError(error.InvalidCompositeIndex, table.createCompositeIndex("solo", &.{"city"})); + try std.testing.expectError(error.InvalidIdentifier, table.createCompositeIndex("bad\x00name", &.{ "city", "age" })); + try std.testing.expectError(error.InvalidIdentifier, table.createCompositeIndex("ok", &.{ "city", "bad\x00field" })); +} + +test "composite index survives a table reopen (persisted across process restart)" { + 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.createCompositeIndex("city_age", &.{ "city", "age" }); + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + }); + } + + var reopened = try Table.open(&db, allocator, "users"); + defer reopened.deinit(); + try std.testing.expect(reopened.hasCompositeIndex("city_age")); + + const preds = [_]Predicate{ + .{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }, + .{ .eq = .{ .field = "age", .value = .{ .int = 30 } } }, + }; + const results = try reopened.select(.{ .and_ = &preds }); + defer freeRows(allocator, results); + try std.testing.expectEqual(@as(usize, 1), results.len); +} + +test "explain reports full_scan for a null predicate and an unindexed 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(); + try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + + var noPred = try table.explain(allocator, null); + defer noPred.deinit(allocator); + try std.testing.expectEqual(PlanKind.full_scan, noPred.kind); + try std.testing.expect(noPred.indexName == null); + + var unindexed = try table.explain(allocator, .{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer unindexed.deinit(allocator); + try std.testing.expectEqual(PlanKind.full_scan, unindexed.kind); +} + +test "explain reports index_eq and index_range for a single-field 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, "events"); + defer table.deinit(); + try table.createIndex("score"); + try table.put(.{ .int = 1 }, &.{.{ .name = "score", .value = .{ .int = 5 } }}); + + var eqPlan = try table.explain(allocator, .{ .eq = .{ .field = "score", .value = .{ .int = 5 } } }); + defer eqPlan.deinit(allocator); + try std.testing.expectEqual(PlanKind.index_eq, eqPlan.kind); + try std.testing.expectEqualStrings("score", eqPlan.indexName.?); + try std.testing.expectEqual(@as(usize, 1), eqPlan.matchedFields); + try std.testing.expectEqual(@as(usize, 1), eqPlan.totalFields); + + var rangePlan = try table.explain(allocator, .{ .gte = .{ .field = "score", .value = .{ .int = 5 } } }); + defer rangePlan.deinit(allocator); + try std.testing.expectEqual(PlanKind.index_range, rangePlan.kind); + try std.testing.expectEqualStrings("score", rangePlan.indexName.?); +} + +test "explain reports composite_eq and composite_range with matched/total field counts" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); +