From 1a76c40f4ddc3b6a6bc6e6b9de08a8e81b3e41a4 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 16:50:31 +0200 Subject: [PATCH] feat: flesh out query --- src/query.zig | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/query.zig b/src/query.zig index 978cd99..b7cc886 100644 --- a/src/query.zig +++ b/src/query.zig @@ -3174,3 +3174,164 @@ test "deleting a parent row is restricted while a child still references it via 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 } }}); + + try std.testing.expectError(error.ForeignKeyInUse, customers.delete(.{ .int = 1 })); + // Restricted, not silently ignored: the row is still there afterwards. + var stillThere = (try customers.get(.{ .int = 1 })).?; + stillThere.deinit(); + + // Once the referencing child row is gone, the parent can be deleted. + try orders.delete(.{ .int = 100 }); + try customers.delete(.{ .int = 1 }); + try std.testing.expect((try customers.get(.{ .int = 1 })) == null); +} + +test "deleteWhere on a parent table stops at the first row a 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(); + + var customers = try Table.open(&db, allocator, "customers"); + defer customers.deinit(); + try customers.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "Ada" } }}); + try customers.put(.{ .int = 2 }, &.{.{ .name = "name", .value = .{ .text = "Boris" } }}); + + 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 = 2 } }}); + + try std.testing.expectError(error.ForeignKeyInUse, customers.deleteWhere(null)); + // customer 1 had no children and was already removed by the time + // deleteWhere hit customer 2's violation - deleteWhere has no + // per-batch atomicity (see its own doc comment), so a partial effect + // here is expected, not a bug this test is trying to hide. + try std.testing.expect((try customers.get(.{ .int = 1 })) == null); + var stillThere = (try customers.get(.{ .int = 2 })).?; + stillThere.deinit(); +} + +test "dropForeignKey lifts the delete restriction on the table it pointed at" { + 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 } }}); + try std.testing.expectError(error.ForeignKeyInUse, customers.delete(.{ .int = 1 })); + + try orders.dropForeignKey("customer_id"); + try customers.delete(.{ .int = 1 }); + try std.testing.expect((try customers.get(.{ .int = 1 })) == null); +} + +test "delete restriction works through a UNIQUE non-pk foreign key target too" { + 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.delete(.{ .int = 1 })); + + try lineItems.delete(.{ .int = 1 }); + try products.delete(.{ .int = 1 }); + try std.testing.expect((try products.get(.{ .int = 1 })) == null); +} + +test "a self-referencing foreign key restricts deleting a manager with reports, but not a childless leaf" { + 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.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "CEO" } }}); + try employees.createForeignKey("manager_id", "employees", pkField, .restrict); + try employees.put(.{ .int = 2 }, &.{ + .{ .name = "name", .value = .{ .text = "VP" } }, + .{ .name = "manager_id", .value = .{ .int = 1 } }, + }); + + try std.testing.expectError(error.ForeignKeyInUse, employees.delete(.{ .int = 1 })); + // The leaf (no reports) deletes cleanly - and does not restrict its + // own deletion against itself despite scanning the same table. + try employees.delete(.{ .int = 2 }); + try employees.delete(.{ .int = 1 }); + try std.testing.expect((try employees.get(.{ .int = 1 })) == null); +} + +test "foreign key constraint survives a table reopen" { + 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(); + var orders = try Table.open(&db, allocator, "orders"); + defer orders.deinit(); + try orders.createForeignKey("customer_id", "customers", pkField, .restrict); + } + + var reopened = try Table.open(&db, allocator, "orders"); + defer reopened.deinit(); + try std.testing.expect(reopened.hasForeignKey("customer_id")); + try std.testing.expectError(error.ForeignKeyViolation, reopened.put(.{ .int = 1 }, &.{ + .{ .name = "customer_id", .value = .{ .int = 7 } }, + })); +} + +test "createForeignKey rejects bad identifiers for field, refTable, and refField" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var orders = try Table.open(&db, allocator, "orders"); + defer orders.deinit(); + try std.testing.expectError(error.InvalidIdentifier, orders.createForeignKey("bad\x00field", "customers", pkField, .restrict)); + try std.testing.expectError(error.InvalidIdentifier, orders.createForeignKey("customer_id", "bad\x00table", pkField, .restrict)); + try std.testing.expectError(error.InvalidIdentifier, orders.createForeignKey("customer_id", "customers", "bad\x00field", .restrict)); +} + +test "a self-referencing foreign key enforces a tree structure against the table's own primary key" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); +