feat: continue work on query
This commit is contained in:
1 parent
1a76c40f4d
commit
2a8227b64d
1 file changed
+163
+163
@@ -3335,3 +3335,166 @@ test "a self-referencing foreign key enforces a tree structure against the table
|
|||||||
var db = try openTemp(&tmp, allocator);
|
var db = try openTemp(&tmp, allocator);
|
||||||
defer db.close();
|
defer db.close();
|
||||||
|
|
||||||
|
var employees = try Table.open(&db, allocator, "employees");
|
||||||
|
defer employees.deinit();
|
||||||
|
try employees.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "CEO" } }});
|
||||||
|
try employees.createForeignKey("manager_id", "employees", pkField, .restrict);
|
||||||
|
|
||||||
|
// A row with no manager (the root of the tree) is exempt.
|
||||||
|
try employees.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "CEO" } }});
|
||||||
|
// A report naming an existing manager is fine.
|
||||||
|
try employees.put(.{ .int = 2 }, &.{
|
||||||
|
.{ .name = "name", .value = .{ .text = "VP" } },
|
||||||
|
.{ .name = "manager_id", .value = .{ .int = 1 } },
|
||||||
|
});
|
||||||
|
try std.testing.expectError(error.ForeignKeyViolation, employees.put(.{ .int = 3 }, &.{
|
||||||
|
.{ .name = "name", .value = .{ .text = "Ghost report" } },
|
||||||
|
.{ .name = "manager_id", .value = .{ .int = 999 } },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "deleting a referenced parent is blocked before it can leave a child's foreign key dangling" {
|
||||||
|
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, .restrict);
|
||||||
|
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }});
|
||||||
|
|
||||||
|
// This used to be the one gap `updateWhere`'s own per-row FK re-check
|
||||||
|
// existed to catch after the fact: a bare `customers.delete` leaving
|
||||||
|
// orders#100's reference dangling even though it was valid when first
|
||||||
|
// written (see createForeignKey's OLD doc comment). `Table.delete`'s
|
||||||
|
// ON DELETE RESTRICT check (backed by `ReferencedBySet`) now closes it
|
||||||
|
// at the source instead - the dangling state this test used to
|
||||||
|
// construct is no longer reachable through delete at all.
|
||||||
|
try std.testing.expectError(error.ForeignKeyInUse, customers.delete(.{ .int = 1 }));
|
||||||
|
|
||||||
|
// updateWhere re-writes the row through put with every field carried
|
||||||
|
// over unchanged, including "customer_id" - it has nothing to catch,
|
||||||
|
// because the reference was never allowed to go dangling in the first
|
||||||
|
// place.
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), try orders.updateWhere(null, &.{
|
||||||
|
.{ .name = "note", .value = .{ .text = "touch" } },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "put is restricted from changing a UNIQUE foreign key target still referenced by a child" {
|
||||||
|
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 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 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" } }});
|
||||||
|
|
||||||
|
// Renaming the sku out from under the still-live line item must not
|
||||||
|
// silently strand it - the same RESTRICT a delete would get.
|
||||||
|
try std.testing.expectError(error.ForeignKeyInUse, products.put(.{ .int = 1 }, &.{
|
||||||
|
.{ .name = "sku", .value = .{ .text = "WIDGET-2" } },
|
||||||
|
}));
|
||||||
|
var stillThere = (try products.get(.{ .int = 1 })).?;
|
||||||
|
defer stillThere.deinit();
|
||||||
|
try std.testing.expectEqualStrings("WIDGET-1", stillThere.get("sku").?.text);
|
||||||
|
|
||||||
|
// Rewriting the same value back (or any other unrelated field) is not
|
||||||
|
// a change and is always exempt, mirroring the UNIQUE check's own
|
||||||
|
// "unchanged is safe" exemption.
|
||||||
|
try products.put(.{ .int = 1 }, &.{.{ .name = "sku", .value = .{ .text = "WIDGET-1" } }});
|
||||||
|
|
||||||
|
// Once the reference is gone, the rename goes through.
|
||||||
|
try lineItems.delete(.{ .int = 1 });
|
||||||
|
try products.put(.{ .int = 1 }, &.{.{ .name = "sku", .value = .{ .text = "WIDGET-2" } }});
|
||||||
|
var renamed = (try products.get(.{ .int = 1 })).?;
|
||||||
|
defer renamed.deinit();
|
||||||
|
try std.testing.expectEqualStrings("WIDGET-2", renamed.get("sku").?.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "updateWhere is restricted from pulling a UNIQUE foreign key target out from under a child" {
|
||||||
|
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 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 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" } }});
|
||||||
|
|
||||||
|
try std.testing.expectError(error.ForeignKeyInUse, products.updateWhere(null, &.{
|
||||||
|
.{ .name = "sku", .value = .{ .text = "WIDGET-2" } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// A bulk update that leaves the referenced field untouched has
|
||||||
|
// nothing to restrict.
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), try products.updateWhere(null, &.{
|
||||||
|
.{ .name = "note", .value = .{ .text = "touch" } },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a self-referencing foreign key restricts renaming a UNIQUE field still targeted by a report, but not by itself" {
|
||||||
|
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.createUniqueIndex("badge");
|
||||||
|
try employees.createForeignKey("manager_badge", "employees", "badge", .restrict);
|
||||||
|
try employees.put(.{ .int = 1 }, &.{.{ .name = "badge", .value = .{ .text = "B1" } }});
|
||||||
|
try employees.put(.{ .int = 2 }, &.{
|
||||||
|
.{ .name = "badge", .value = .{ .text = "B2" } },
|
||||||
|
.{ .name = "manager_badge", .value = .{ .text = "B1" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Employee 2 still points at "B1" - renaming employee 1's own badge
|
||||||
|
// must be restricted.
|
||||||
|
try std.testing.expectError(error.ForeignKeyInUse, employees.put(.{ .int = 1 }, &.{
|
||||||
|
.{ .name = "badge", .value = .{ .text = "B1-NEW" } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Employee 2 renaming its OWN badge (not the field being referenced by
|
||||||
|
// anyone) is unaffected - and does not restrict against itself despite
|
||||||
|
// the scan covering the same table.
|
||||||
|
try employees.put(.{ .int = 2 }, &.{
|
||||||
|
.{ .name = "badge", .value = .{ .text = "B2-NEW" } },
|
||||||
|
.{ .name = "manager_badge", .value = .{ .text = "B1" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Once the report's manager_badge field is dropped (a row with no
|
||||||
|
// value for the FK field is exempt from the constraint, so this is a
|
||||||
|
// valid way to stop referencing anything), the manager's badge can be
|
||||||
|
// renamed.
|
||||||
|
try employees.put(.{ .int = 2 }, &.{
|
||||||
|
.{ .name = "badge", .value = .{ .text = "B2-NEW" } },
|
||||||
|
});
|
||||||
|
try employees.put(.{ .int = 1 }, &.{.{ .name = "badge", .value = .{ .text = "B1-NEW" } }});
|
||||||
|
var renamed = (try employees.get(.{ .int = 1 })).?;
|
||||||
|
defer renamed.deinit();
|
||||||
|
try std.testing.expectEqualStrings("B1-NEW", renamed.get("badge").?.text);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user