feat: continue work on query

This commit is contained in:
dualfroz committed 2026-09-09 17:37:37 +02:00
1 parent 1a76c40f4d
commit 2a8227b64d
1 file changed
+163
+163
View File
@@ -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);
}