feat: build out txn_table
This commit is contained in:
1 parent
8cfd0fb7aa
commit
697e8c6461
1 file changed
+161
@@ -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");
|
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();
|
||||||
|
|
||||||
Reference in new issue
Block a user