From 71737debca3c90bb8617cf7e348a177342a551a1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 22:14:02 +0200 Subject: [PATCH] feat: extend txn_table --- src/txn_table.zig | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/src/txn_table.zig b/src/txn_table.zig index f4f3524..8bb6499 100644 --- a/src/txn_table.zig +++ b/src/txn_table.zig @@ -470,3 +470,165 @@ test "TableTxn delete removes row and index entries only on commit" { stillThere.deinit(); } + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "users"); + defer plain.deinit(); + try std.testing.expect((try plain.get(.{ .int = 1 })) == null); + const warsaw = try plain.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer querymod.freeRows(allocator, warsaw); + try std.testing.expectEqual(@as(usize, 0), warsaw.len); +} + +test "TableTxn rejects invalid table identifier" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var txn = Transaction.begin(&db); + defer txn.deinit(); + try std.testing.expectError(error.InvalidIdentifier, TableTxn.open(&txn, allocator, "")); +} + +test "TableTxn read-your-writes sees staged rows before commit" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "users"); + defer t.deinit(); + + try t.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "ada" } }}); + var seen = (try t.get(.{ .int = 1 })).?; + defer seen.deinit(); + try std.testing.expectEqualStrings("ada", seen.get("name").?.text); + + try t.delete(.{ .int = 1 }); + try std.testing.expect((try t.get(.{ .int = 1 })) == null); +} + +test "TableTxn put enforces UNIQUE constraint against committed data" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + { + var setup = try querymod.Table.open(&db, allocator, "users"); + defer setup.deinit(); + try setup.createUniqueIndex("email"); + try setup.put(.{ .int = 1 }, &.{.{ .name = "email", .value = .{ .text = "a@example.com" } }}); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "users"); + defer t.deinit(); + try std.testing.expect(t.hasUniqueIndex("email")); + + try std.testing.expectError(error.UniqueViolation, t.put(.{ .int = 2 }, &.{ + .{ .name = "email", .value = .{ .text = "a@example.com" } }, + })); + + // The rejected put staged nothing: committing now applies zero rows. + try txn.commit(); + var plain = try querymod.Table.open(&db, allocator, "users"); + defer plain.deinit(); + try std.testing.expect((try plain.get(.{ .int = 2 })) == null); +} + +test "TableTxn put UNIQUE check exempts a row keeping its own previous value" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + { + var setup = try querymod.Table.open(&db, allocator, "users"); + defer setup.deinit(); + try setup.createUniqueIndex("email"); + try setup.put(.{ .int = 1 }, &.{.{ .name = "email", .value = .{ .text = "a@example.com" } }}); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "users"); + defer t.deinit(); + + // Updating an unrelated field on row 1 keeps its own email unchanged - + // must not spuriously conflict with itself. + try t.put(.{ .int = 1 }, &.{ + .{ .name = "email", .value = .{ .text = "a@example.com" } }, + .{ .name = "name", .value = .{ .text = "ada" } }, + }); + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "users"); + defer plain.deinit(); + var row = (try plain.get(.{ .int = 1 })).?; + defer row.deinit(); + try std.testing.expectEqualStrings("ada", row.get("name").?.text); +} + +test "TableTxn put enforces FOREIGN KEY against committed data and rejects a dangling reference" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + { + var customers = try querymod.Table.open(&db, allocator, "customers"); + defer customers.deinit(); + try customers.put(.{ .int = 1 }, &.{}); + + var orders = try querymod.Table.open(&db, allocator, "orders"); + defer orders.deinit(); + try orders.createForeignKey("customer_id", "customers", pkField, .restrict); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "orders"); + defer t.deinit(); + try std.testing.expect(t.hasForeignKey("customer_id")); + + // customer 1 exists (committed) -> allowed. + try t.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }}); + // customer 999 does not exist anywhere -> rejected. + try std.testing.expectError(error.ForeignKeyViolation, t.put(.{ .int = 101 }, &.{ + .{ .name = "customer_id", .value = .{ .int = 999 } }, + })); + + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "orders"); + defer plain.deinit(); + var row = (try plain.get(.{ .int = 100 })).?; + defer row.deinit(); + try std.testing.expect((try plain.get(.{ .int = 101 })) == null); +} + +test "TableTxn put FOREIGN KEY on pkField sees a parent staged earlier in the same uncommitted transaction" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + { + // Self-referencing table (an org chart: manager_id -> employees.pk), + // installed while empty so no existing-row check gets in the way. + var employees = try querymod.Table.open(&db, allocator, "employees"); + defer employees.deinit(); + try employees.createForeignKey("manager_id", "employees", pkField, .restrict); + } +