diff --git a/src/txn_table.zig b/src/txn_table.zig index 8bb6499..3478457 100644 --- a/src/txn_table.zig +++ b/src/txn_table.zig @@ -632,3 +632,160 @@ test "TableTxn put FOREIGN KEY on pkField sees a parent staged earlier in the sa try employees.createForeignKey("manager_id", "employees", pkField, .restrict); } + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "employees"); + defer t.deinit(); + + // Neither row is committed yet. The manager (pk=1) is only staged in + // this same transaction's write set, but the FK check on the report + // (pk=2) must see it via `Transaction.get`'s read-your-writes, not + // just via the underlying Db. + try t.put(.{ .int = 1 }, &.{}); + try t.put(.{ .int = 2 }, &.{.{ .name = "manager_id", .value = .{ .int = 1 } }}); + + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "employees"); + defer plain.deinit(); + var report = (try plain.get(.{ .int = 2 })).?; + defer report.deinit(); + try std.testing.expectEqual(@as(i64, 1), report.get("manager_id").?.int); +} + +test "TableTxn put FOREIGN KEY on a non-pk unique field sees a target 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"); + + 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(); + + // Stage a brand-new product in the same transaction as the line item + // that references it. `foreignKeyTargetExists`'s non-pk branch now + // goes through `Transaction.scanPrefix`, which sees the product's + // staged index-entry write even though neither row is committed yet - + // same read-your-writes guarantee the pkField branch above already had. + var products = try TableTxn.open(&txn, allocator, "products"); + defer products.deinit(); + try products.put(.{ .int = 1 }, &.{.{ .name = "sku", .value = .{ .text = "WIDGET-1" } }}); + + var lineItems = try TableTxn.open(&txn, allocator, "line_items"); + defer lineItems.deinit(); + try lineItems.put(.{ .int = 1 }, &.{ + .{ .name = "product_sku", .value = .{ .text = "WIDGET-1" } }, + }); + + // A dangling reference in the same transaction is still rejected. + try std.testing.expectError(error.ForeignKeyViolation, lineItems.put(.{ .int = 2 }, &.{ + .{ .name = "product_sku", .value = .{ .text = "NO-SUCH-SKU" } }, + })); + + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "line_items"); + defer plain.deinit(); + var row = (try plain.get(.{ .int = 1 })).?; + defer row.deinit(); + try std.testing.expectEqualStrings("WIDGET-1", row.get("product_sku").?.text); +} + +test "TableTxn put UNIQUE constraint catches a conflict 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 setup = try querymod.Table.open(&db, allocator, "users"); + defer setup.deinit(); + try setup.createUniqueIndex("email"); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "users"); + defer t.deinit(); + + // First row claims the email; nothing committed yet. + try t.put(.{ .int = 1 }, &.{.{ .name = "email", .value = .{ .text = "a@example.com" } }}); + + // A second, different row in the SAME still-open transaction tries to + // claim the same email. Before `Transaction.scanPrefix`, the UNIQUE + // check ran `idx.scanEq` straight against `Db` and never saw the first + // row's staged index entry, so this put would have wrongly succeeded + // and only failed later as a generic OCC conflict at commit (or not at + // all, since the two rows touch disjoint keys and would not conflict). + try std.testing.expectError(error.UniqueViolation, t.put(.{ .int = 2 }, &.{ + .{ .name = "email", .value = .{ .text = "a@example.com" } }, + })); + + 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); + var row1 = (try plain.get(.{ .int = 1 })).?; + defer row1.deinit(); + try std.testing.expectEqualStrings("a@example.com", row1.get("email").?.text); +} + +test "TableTxn put enforces a CHECK constraint installed through query.zig's Table" { + 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, "orders"); + defer setup.deinit(); + 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 std.testing.expect(t.hasCheckConstraint("qty_positive")); + + try t.put(.{ .int = 1 }, &.{.{ .name = "qty", .value = .{ .int = 5 } }}); + try std.testing.expectError(error.CheckViolation, t.put(.{ .int = 2 }, &.{ + .{ .name = "qty", .value = .{ .int = 0 } }, + })); + + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "orders"); + defer plain.deinit(); + var row1 = (try plain.get(.{ .int = 1 })).?; + defer row1.deinit(); + try std.testing.expect((try plain.get(.{ .int = 2 })) == null); +} + +test "TableTxn `txn