feat: expand txn_table

This commit is contained in:
dualfroz committed 2026-09-12 22:21:55 +02:00
1 parent 71737debca
commit 8cfd0fb7aa
1 file changed
+157
+157
View File
@@ -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); 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 <script>` shape: a CHECK violation on the second put leaves the whole transaction uncommitted" {
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");
}