feat: extend query
This commit is contained in:
1 parent
bda37d1ecf
commit
7fb27cc9dc
1 file changed
+209
+209
@@ -3657,3 +3657,212 @@ test "ON DELETE CASCADE keeps secondary indexes on the cascaded rows in sync" {
|
||||
.{ .name = "status", .value = .{ .text = "open" } },
|
||||
});
|
||||
|
||||
const before = try idx.scanEq(&db, allocator, "orders", "status", .{ .text = "open" });
|
||||
defer allocator.free(before);
|
||||
try std.testing.expectEqual(@as(usize, 1), before.len);
|
||||
|
||||
try customers.delete(.{ .int = 1 });
|
||||
|
||||
// The cascaded delete went through `executeDelete`, which maintains
|
||||
// secondary indexes exactly like a direct `orders.delete` would - a
|
||||
// stale index entry pointing at a now-gone row would otherwise poison
|
||||
// every future `status = "open"` scan.
|
||||
const after = try idx.scanEq(&db, allocator, "orders", "status", .{ .text = "open" });
|
||||
defer allocator.free(after);
|
||||
try std.testing.expectEqual(@as(usize, 0), after.len);
|
||||
}
|
||||
|
||||
test "ON DELETE CASCADE recurses transitively across more than one hop" {
|
||||
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 Table.open(&db, allocator, "customers");
|
||||
defer customers.deinit();
|
||||
try customers.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "Ada" } }});
|
||||
|
||||
var orders = try Table.open(&db, allocator, "orders");
|
||||
defer orders.deinit();
|
||||
try orders.createForeignKey("customer_id", "customers", pkField, .cascade);
|
||||
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
|
||||
|
||||
var lineItems = try Table.open(&db, allocator, "line_items");
|
||||
defer lineItems.deinit();
|
||||
try lineItems.createForeignKey("order_id", "orders", pkField, .cascade);
|
||||
try lineItems.put(.{ .int = 1000 }, &.{.{ .name = "order_id", .value = .{ .int = 100 } }});
|
||||
try lineItems.put(.{ .int = 1001 }, &.{.{ .name = "order_id", .value = .{ .int = 100 } }});
|
||||
|
||||
// Deleting the customer must cascade into orders, and from there into
|
||||
// line_items - two hops, not just one.
|
||||
try customers.delete(.{ .int = 1 });
|
||||
|
||||
try std.testing.expect((try customers.get(.{ .int = 1 })) == null);
|
||||
try std.testing.expect((try orders.get(.{ .int = 100 })) == null);
|
||||
try std.testing.expect((try lineItems.get(.{ .int = 1000 })) == null);
|
||||
try std.testing.expect((try lineItems.get(.{ .int = 1001 })) == null);
|
||||
}
|
||||
|
||||
test "ON DELETE CASCADE is all-or-nothing: a RESTRICT hit anywhere in the fan-out aborts with nothing deleted" {
|
||||
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 Table.open(&db, allocator, "customers");
|
||||
defer customers.deinit();
|
||||
try customers.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "Ada" } }});
|
||||
|
||||
var orders = try Table.open(&db, allocator, "orders");
|
||||
defer orders.deinit();
|
||||
try orders.createForeignKey("customer_id", "customers", pkField, .cascade);
|
||||
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
|
||||
|
||||
// A shipment row RESTRICTs deleting the order it points at - deleting
|
||||
// the customer would need to cascade into this order, but the order
|
||||
// itself cannot legally go while its own restrict-mode reference is
|
||||
// still live.
|
||||
var shipments = try Table.open(&db, allocator, "shipments");
|
||||
defer shipments.deinit();
|
||||
try shipments.createForeignKey("order_id", "orders", pkField, .restrict);
|
||||
try shipments.put(.{ .int = 9000 }, &.{.{ .name = "order_id", .value = .{ .int = 100 } }});
|
||||
|
||||
try std.testing.expectError(error.ForeignKeyInUse, customers.delete(.{ .int = 1 }));
|
||||
|
||||
// Two-phase plan-then-execute means the customer AND the order (which
|
||||
// had already been planned for cascade removal before the restrict
|
||||
// hit further down was discovered) are both still there - not just
|
||||
// the customer.
|
||||
var stillCustomer = (try customers.get(.{ .int = 1 })).?;
|
||||
stillCustomer.deinit();
|
||||
var stillOrder = (try orders.get(.{ .int = 100 })).?;
|
||||
stillOrder.deinit();
|
||||
var stillShipment = (try shipments.get(.{ .int = 9000 })).?;
|
||||
stillShipment.deinit();
|
||||
}
|
||||
|
||||
test "ON DELETE CASCADE terminates on a two-table delete cycle instead of recursing forever" {
|
||||
const allocator = std.testing.allocator;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTemp(&tmp, allocator);
|
||||
defer db.close();
|
||||
|
||||
// `a` and `b` each carry a nullable FK at the other, both ON DELETE
|
||||
// CASCADE - a delete starting from either side must not infinite-loop
|
||||
// bouncing back and forth between the two tables.
|
||||
var a = try Table.open(&db, allocator, "a");
|
||||
defer a.deinit();
|
||||
var b = try Table.open(&db, allocator, "b");
|
||||
defer b.deinit();
|
||||
|
||||
try a.put(.{ .int = 1 }, &.{});
|
||||
try b.put(.{ .int = 1 }, &.{.{ .name = "a_id", .value = .{ .int = 1 } }});
|
||||
try a.createForeignKey("b_id", "b", pkField, .cascade);
|
||||
try a.put(.{ .int = 1 }, &.{.{ .name = "b_id", .value = .{ .int = 1 } }});
|
||||
try b.createForeignKey("a_id", "a", pkField, .cascade);
|
||||
|
||||
try a.delete(.{ .int = 1 });
|
||||
|
||||
try std.testing.expect((try a.get(.{ .int = 1 })) == null);
|
||||
try std.testing.expect((try b.get(.{ .int = 1 })) == null);
|
||||
}
|
||||
|
||||
test "ON DELETE CASCADE on a self-referencing foreign key removes an entire subtree" {
|
||||
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 Table.open(&db, allocator, "employees");
|
||||
defer employees.deinit();
|
||||
try employees.createForeignKey("manager_id", "employees", pkField, .cascade);
|
||||
|
||||
// 1 (CEO) -> 2 (VP) -> 3 and 4 (reports of the VP).
|
||||
try employees.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "CEO" } }});
|
||||
try employees.put(.{ .int = 2 }, &.{
|
||||
.{ .name = "name", .value = .{ .text = "VP" } },
|
||||
.{ .name = "manager_id", .value = .{ .int = 1 } },
|
||||
});
|
||||
try employees.put(.{ .int = 3 }, &.{
|
||||
.{ .name = "name", .value = .{ .text = "IC-A" } },
|
||||
.{ .name = "manager_id", .value = .{ .int = 2 } },
|
||||
});
|
||||
try employees.put(.{ .int = 4 }, &.{
|
||||
.{ .name = "name", .value = .{ .text = "IC-B" } },
|
||||
.{ .name = "manager_id", .value = .{ .int = 2 } },
|
||||
});
|
||||
|
||||
// Deleting the VP must cascade to both reports, but must not touch the
|
||||
// CEO (an ancestor, not a dependent).
|
||||
try employees.delete(.{ .int = 2 });
|
||||
|
||||
var ceo = (try employees.get(.{ .int = 1 })).?;
|
||||
ceo.deinit();
|
||||
try std.testing.expect((try employees.get(.{ .int = 2 })) == null);
|
||||
try std.testing.expect((try employees.get(.{ .int = 3 })) == null);
|
||||
try std.testing.expect((try employees.get(.{ .int = 4 })) == null);
|
||||
}
|
||||
|
||||
test "deleteWhere cascades every match, not just the first" {
|
||||
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 Table.open(&db, allocator, "customers");
|
||||
defer customers.deinit();
|
||||
try customers.put(.{ .int = 1 }, &.{.{ .name = "region", .value = .{ .text = "eu" } }});
|
||||
try customers.put(.{ .int = 2 }, &.{.{ .name = "region", .value = .{ .text = "eu" } }});
|
||||
try customers.put(.{ .int = 3 }, &.{.{ .name = "region", .value = .{ .text = "us" } }});
|
||||
|
||||
var orders = try Table.open(&db, allocator, "orders");
|
||||
defer orders.deinit();
|
||||
try orders.createForeignKey("customer_id", "customers", pkField, .cascade);
|
||||
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
|
||||
try orders.put(.{ .int = 200 }, &.{.{ .name = "customer_id", .value = .{ .int = 2 } }});
|
||||
try orders.put(.{ .int = 300 }, &.{.{ .name = "customer_id", .value = .{ .int = 3 } }});
|
||||
|
||||
const deleted = try customers.deleteWhere(.{ .eq = .{ .field = "region", .value = .{ .text = "eu" } } });
|
||||
try std.testing.expectEqual(@as(usize, 2), deleted);
|
||||
|
||||
try std.testing.expect((try customers.get(.{ .int = 1 })) == null);
|
||||
try std.testing.expect((try customers.get(.{ .int = 2 })) == null);
|
||||
try std.testing.expect((try orders.get(.{ .int = 100 })) == null);
|
||||
try std.testing.expect((try orders.get(.{ .int = 200 })) == null);
|
||||
|
||||
var stillCustomer = (try customers.get(.{ .int = 3 })).?;
|
||||
stillCustomer.deinit();
|
||||
var stillOrder = (try orders.get(.{ .int = 300 })).?;
|
||||
stillOrder.deinit();
|
||||
}
|
||||
|
||||
test "dropForeignKey lifts a CASCADE constraint the same as a RESTRICT one" {
|
||||
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 Table.open(&db, allocator, "customers");
|
||||
defer customers.deinit();
|
||||
try customers.put(.{ .int = 1 }, &.{});
|
||||
|
||||
var orders = try Table.open(&db, allocator, "orders");
|
||||
defer orders.deinit();
|
||||
try orders.createForeignKey("customer_id", "customers", pkField, .cascade);
|
||||
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
|
||||
|
||||
try orders.dropForeignKey("customer_id");
|
||||
try customers.delete(.{ .int = 1 });
|
||||
|
||||
try std.testing.expect((try customers.get(.{ .int = 1 })) == null);
|
||||
// No FK left at all, so the order is untouched by the parent's delete -
|
||||
// not restricted, not cascaded, simply irrelevant now.
|
||||
var stillOrder = (try orders.get(.{ .int = 100 })).?;
|
||||
stillOrder.deinit();
|
||||
}
|
||||
Reference in new issue
Block a user