From 5b47228e4e9a99a39addb43eed315c1dbcfbfa58 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 15:02:01 +0200 Subject: [PATCH] feat: build out query --- src/query.zig | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/query.zig b/src/query.zig index 23d9a79..a49496f 100644 --- a/src/query.zig +++ b/src/query.zig @@ -2856,3 +2856,164 @@ test "deleteWhere removes stale composite index entries across matched equality try std.testing.expectEqual(@as(usize, 1), rows.len); try std.testing.expectEqual(exp.pk, rows[0].get(pkField).?.int); + const constTable: *const Table = &table; + var plan = try constTable.explain(allocator, .{ .and_ = &survivorPreds }); + defer plan.deinit(allocator); + try std.testing.expectEqual(PlanKind.composite_eq, plan.kind); + } +} + +test "createUniqueIndex backfills the plain index and rejects a colliding insert" { + 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 = "email", .value = .{ .text = "a@x.com" } }}); + + try table.createUniqueIndex("email"); + try std.testing.expect(table.hasUniqueIndex("email")); + try std.testing.expect(table.hasIndex("email")); + + try std.testing.expectError(error.UniqueViolation, table.put(.{ .int = 2 }, &.{ + .{ .name = "email", .value = .{ .text = "a@x.com" } }, + })); + + try table.put(.{ .int = 2 }, &.{.{ .name = "email", .value = .{ .text = "b@x.com" } }}); + const rows = try table.scanAll(); + defer freeRows(allocator, rows); + try std.testing.expectEqual(@as(usize, 2), rows.len); +} + +test "createUniqueIndex refuses to install over data that already has a duplicate" { + 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 = "email", .value = .{ .text = "dup@x.com" } }}); + try table.put(.{ .int = 2 }, &.{.{ .name = "email", .value = .{ .text = "dup@x.com" } }}); + + try std.testing.expectError(error.UniqueViolation, table.createUniqueIndex("email")); + try std.testing.expect(!table.hasUniqueIndex("email")); + // Refused install must not have left a half-installed plain index either. + try std.testing.expect(!table.hasIndex("email")); +} + +test "createUniqueIndex over an already-indexed field reuses the index instead of duplicating entries" { + 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("email"); + try table.put(.{ .int = 1 }, &.{.{ .name = "email", .value = .{ .text = "a@x.com" } }}); + + try table.createUniqueIndex("email"); + try std.testing.expect(table.hasUniqueIndex("email")); + + try std.testing.expectError(error.UniqueViolation, table.put(.{ .int = 2 }, &.{ + .{ .name = "email", .value = .{ .text = "a@x.com" } }, + })); + + const preds = Predicate{ .eq = .{ .field = "email", .value = .{ .text = "a@x.com" } } }; + const rows = try table.select(preds); + defer freeRows(allocator, rows); + try std.testing.expectEqual(@as(usize, 1), rows.len); +} + +test "unique constraint does not block updating a row's own value or an unrelated 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 = "email", .value = .{ .text = "a@x.com" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + }); + try table.createUniqueIndex("email"); + + // Re-writing the same email on the same row must not conflict with itself. + try table.put(.{ .int = 1 }, &.{ + .{ .name = "email", .value = .{ .text = "a@x.com" } }, + .{ .name = "age", .value = .{ .int = 31 } }, + }); + + var row = (try table.get(.{ .int = 1 })).?; + defer row.deinit(); + try std.testing.expectEqual(@as(i64, 31), row.get("age").?.int); +} + +test "unique constraint frees a value for reuse once the owning row is deleted" { + 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.createUniqueIndex("email"); + try table.put(.{ .int = 1 }, &.{.{ .name = "email", .value = .{ .text = "a@x.com" } }}); + try std.testing.expectError(error.UniqueViolation, table.put(.{ .int = 2 }, &.{ + .{ .name = "email", .value = .{ .text = "a@x.com" } }, + })); + + try table.delete(.{ .int = 1 }); + try table.put(.{ .int = 2 }, &.{.{ .name = "email", .value = .{ .text = "a@x.com" } }}); + + var row = (try table.get(.{ .int = 2 })).?; + defer row.deinit(); + try std.testing.expectEqualStrings("a@x.com", row.get("email").?.text); +} + +test "dropIndex on a unique field also drops the unique constraint" { + 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.createUniqueIndex("email"); + try table.put(.{ .int = 1 }, &.{.{ .name = "email", .value = .{ .text = "a@x.com" } }}); + + try table.dropIndex("email"); + try std.testing.expect(!table.hasUniqueIndex("email")); + try std.testing.expect(!table.hasIndex("email")); + + // With no constraint left, a colliding value must now be accepted. + try table.put(.{ .int = 2 }, &.{.{ .name = "email", .value = .{ .text = "a@x.com" } }}); + const rows = try table.scanAll(); + defer freeRows(allocator, rows); + try std.testing.expectEqual(@as(usize, 2), rows.len); +} + +test "unique constraint 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.createUniqueIndex("email"); + try table.put(.{ .int = 1 }, &.{.{ .name = "email", .value = .{ .text = "a@x.com" } }}); + } +