From 0631d1673ddbfa83d55d04889a74cc02887a2951 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 22:56:36 +0200 Subject: [PATCH] feat: grow txn_table --- src/txn_table.zig | 77 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/txn_table.zig b/src/txn_table.zig index 3c3d762..4fdbd54 100644 --- a/src/txn_table.zig +++ b/src/txn_table.zig @@ -950,3 +950,80 @@ test "TableTxn put is restricted from changing a UNIQUE foreign key target a com var db = try openTemp(&tmp, allocator); defer db.close(); + { + var products = try querymod.Table.open(&db, allocator, "products"); + defer products.deinit(); + try products.createUniqueIndex("sku"); + try products.put(.{ .int = 1 }, &.{.{ .name = "sku", .value = .{ .text = "WIDGET-1" } }}); + + var lineItems = try querymod.Table.open(&db, allocator, "line_items"); + defer lineItems.deinit(); + try lineItems.createForeignKey("product_sku", "products", "sku", .restrict); + try lineItems.put(.{ .int = 1 }, &.{.{ .name = "product_sku", .value = .{ .text = "WIDGET-1" } }}); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "products"); + defer t.deinit(); + + try std.testing.expectError(error.ForeignKeyInUse, t.put(.{ .int = 1 }, &.{ + .{ .name = "sku", .value = .{ .text = "WIDGET-2" } }, + })); + // Unchanged is always exempt. + try t.put(.{ .int = 1 }, &.{.{ .name = "sku", .value = .{ .text = "WIDGET-1" } }}); + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "products"); + defer plain.deinit(); + var stillThere = (try plain.get(.{ .int = 1 })).?; + defer stillThere.deinit(); + try std.testing.expectEqualStrings("WIDGET-1", stillThere.get("sku").?.text); +} + +test "TableTxn put update-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 products = try querymod.Table.open(&db, allocator, "products"); + defer products.deinit(); + try products.createUniqueIndex("sku"); + try products.put(.{ .int = 1 }, &.{.{ .name = "sku", .value = .{ .text = "WIDGET-1" } }}); + + var lineItems = try querymod.Table.open(&db, allocator, "line_items"); + defer lineItems.deinit(); + try lineItems.createForeignKey("product_sku", "products", "sku", .restrict); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + + // The line item referencing "WIDGET-1" is only staged in this SAME + // transaction's write set, never committed - `Transaction.scanPrefix` + // is what makes this count as a live reference before commit. + var lineItemsTxn = try TableTxn.open(&txn, allocator, "line_items"); + defer lineItemsTxn.deinit(); + try lineItemsTxn.put(.{ .int = 1 }, &.{.{ .name = "product_sku", .value = .{ .text = "WIDGET-1" } }}); + + var productsTxn = try TableTxn.open(&txn, allocator, "products"); + defer productsTxn.deinit(); + try std.testing.expectError(error.ForeignKeyInUse, productsTxn.put(.{ .int = 1 }, &.{ + .{ .name = "sku", .value = .{ .text = "WIDGET-2" } }, + })); + + // Once the referencing line item is removed in the same transaction, + // the rename goes through. + try lineItemsTxn.delete(.{ .int = 1 }); + try productsTxn.put(.{ .int = 1 }, &.{.{ .name = "sku", .value = .{ .text = "WIDGET-2" } }}); + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "products"); + defer plain.deinit(); + var renamed = (try plain.get(.{ .int = 1 })).?; + defer renamed.deinit(); + try std.testing.expectEqualStrings("WIDGET-2", renamed.get("sku").?.text); +}