feat: build out txn_table

This commit is contained in:
dualfroz committed 2026-09-12 22:52:19 +02:00
1 parent 8cfd0fb7aa
commit 697e8c6461
1 file changed
+161
+161
View File
@@ -789,3 +789,164 @@ test "TableTxn `txn <script>` shape: a CHECK violation on the second put leaves
try setup.createCheckConstraint("qty_positive", "qty gt i:0");
}
var txn = Transaction.begin(&db);
defer txn.deinit();
var t = try TableTxn.open(&txn, allocator, "orders");
defer t.deinit();
try t.put(.{ .int = 1 }, &.{.{ .name = "qty", .value = .{ .int = 5 } }});
try std.testing.expectError(error.CheckViolation, t.put(.{ .int = 2 }, &.{
.{ .name = "qty", .value = .{ .int = -5 } },
}));
// A real `txn <script>` dispatch (main.zig's `runTxn`/server.zig's
// `handleTxnOp`) aborts the whole script on the first line that fails
// instead of continuing - `t.deinit()`/`txn.deinit()` above discard the
// transaction without ever calling `commit()`, so even row 1 (which was
// individually valid) must never become visible.
var plain = try querymod.Table.open(&db, allocator, "orders");
defer plain.deinit();
try std.testing.expect((try plain.get(.{ .int = 1 })) == null);
}
test "TableTxn delete enforces ON DELETE RESTRICT against a committed child row" {
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);
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
}
var txn = Transaction.begin(&db);
defer txn.deinit();
var t = try TableTxn.open(&txn, allocator, "customers");
defer t.deinit();
try std.testing.expectError(error.ForeignKeyInUse, t.delete(.{ .int = 1 }));
try txn.commit();
var plain = try querymod.Table.open(&db, allocator, "customers");
defer plain.deinit();
var stillThere = (try plain.get(.{ .int = 1 })).?;
stillThere.deinit();
}
test "TableTxn delete RESTRICT sees a child row 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();
{
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();
// The order referencing customer 1 is only staged in this SAME
// transaction's write set (via a sibling TableTxn on "orders"), never
// committed - `Transaction.scanPrefix` (not a plain Db scan, which
// would see nothing under the orders row prefix yet) is what makes
// this count as a live reference before the transaction ever commits.
var ordersTxn = try TableTxn.open(&txn, allocator, "orders");
defer ordersTxn.deinit();
try ordersTxn.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
var customersTxn = try TableTxn.open(&txn, allocator, "customers");
defer customersTxn.deinit();
try std.testing.expectError(error.ForeignKeyInUse, customersTxn.delete(.{ .int = 1 }));
}
test "TableTxn delete RESTRICT does not block deleting a parent once its only child is deleted in the same transaction" {
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);
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
}
var txn = Transaction.begin(&db);
defer txn.deinit();
var ordersTxn = try TableTxn.open(&txn, allocator, "orders");
defer ordersTxn.deinit();
try ordersTxn.delete(.{ .int = 100 });
var customersTxn = try TableTxn.open(&txn, allocator, "customers");
defer customersTxn.deinit();
try customersTxn.delete(.{ .int = 1 });
try txn.commit();
var plain = try querymod.Table.open(&db, allocator, "customers");
defer plain.deinit();
try std.testing.expect((try plain.get(.{ .int = 1 })) == null);
}
test "TableTxn delete RESTRICT excludes the row being deleted from its own self-referencing check" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
{
var employees = try querymod.Table.open(&db, allocator, "employees");
defer employees.deinit();
try employees.put(.{ .int = 1 }, &.{});
try employees.createForeignKey("manager_id", "employees", pkField, .restrict);
try employees.put(.{ .int = 2 }, &.{.{ .name = "manager_id", .value = .{ .int = 1 } }});
}
var txn = Transaction.begin(&db);
defer txn.deinit();
var t = try TableTxn.open(&txn, allocator, "employees");
defer t.deinit();
// The manager (pk 1) has a report (pk 2) -> restricted.
try std.testing.expectError(error.ForeignKeyInUse, t.delete(.{ .int = 1 }));
// The report itself has no reports of its own -> deletes cleanly,
// scanning the same table it belongs to without restricting itself.
try t.delete(.{ .int = 2 });
try txn.commit();
var plain = try querymod.Table.open(&db, allocator, "employees");
defer plain.deinit();
try std.testing.expect((try plain.get(.{ .int = 2 })) == null);
}
test "TableTxn put is restricted from changing a UNIQUE foreign key target a committed child still references" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();