From bda37d1ecfc558c6f5f6e60eb0fe9e14f85b51aa Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 17:47:51 +0200 Subject: [PATCH] feat: fill in query --- src/query.zig | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/query.zig b/src/query.zig index 1e87434..70446cd 100644 --- a/src/query.zig +++ b/src/query.zig @@ -3498,3 +3498,162 @@ test "a self-referencing foreign key restricts renaming a UNIQUE field still tar try std.testing.expectEqualStrings("B1-NEW", renamed.get("badge").?.text); } +test "createCheckConstraint rejects a row that violates it and accepts one that satisfies it" { + 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 orders.createCheckConstraint("qty_positive", "qty gt i:0"); + try std.testing.expect(orders.hasCheckConstraint("qty_positive")); + + try orders.put(.{ .int = 1 }, &.{.{ .name = "qty", .value = .{ .int = 5 } }}); + try std.testing.expectError(error.CheckViolation, orders.put(.{ .int = 2 }, &.{ + .{ .name = "qty", .value = .{ .int = 0 } }, + })); + try std.testing.expectError(error.CheckViolation, orders.put(.{ .int = 3 }, &.{ + .{ .name = "qty", .value = .{ .int = -1 } }, + })); +} + +test "createCheckConstraint refuses to install over existing rows that already fail it" { + 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 orders.put(.{ .int = 1 }, &.{.{ .name = "qty", .value = .{ .int = -5 } }}); + + try std.testing.expectError(error.CheckViolation, orders.createCheckConstraint("qty_positive", "qty gt i:0")); + try std.testing.expect(!orders.hasCheckConstraint("qty_positive")); +} + +test "createCheckConstraint rejects a malformed expression before persisting anything" { + 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.InvalidCheckExpression, orders.createCheckConstraint("bad", "qty gt")); + try std.testing.expect(!orders.hasCheckConstraint("bad")); +} + +test "dropCheckConstraint lifts the constraint, and a check 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 orders = try Table.open(&db, allocator, "orders"); + defer orders.deinit(); + try orders.createCheckConstraint("qty_positive", "qty gt i:0"); + } + + var reopened = try Table.open(&db, allocator, "orders"); + defer reopened.deinit(); + try std.testing.expect(reopened.hasCheckConstraint("qty_positive")); + try std.testing.expectError(error.CheckViolation, reopened.put(.{ .int = 1 }, &.{ + .{ .name = "qty", .value = .{ .int = 0 } }, + })); + + try reopened.dropCheckConstraint("qty_positive"); + try std.testing.expect(!reopened.hasCheckConstraint("qty_positive")); + try reopened.put(.{ .int = 1 }, &.{.{ .name = "qty", .value = .{ .int = 0 } }}); +} + +test "a row missing the checked field entirely is exempt (like is_null), but present-and-none is still checked" { + 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 orders.createCheckConstraint("qty_known", "qty is_not_null"); + + try std.testing.expectError(error.CheckViolation, orders.put(.{ .int = 1 }, &.{})); +} + +test "multiple CHECK constraints on the same table all must pass" { + 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 orders.createCheckConstraint("qty_positive", "qty gt i:0"); + try orders.createCheckConstraint("qty_capped", "qty lt i:1000"); + + try orders.put(.{ .int = 1 }, &.{.{ .name = "qty", .value = .{ .int = 500 } }}); + try std.testing.expectError(error.CheckViolation, orders.put(.{ .int = 2 }, &.{ + .{ .name = "qty", .value = .{ .int = 1000 } }, + })); +} + +test "ON DELETE CASCADE removes referencing rows instead of restricting the parent delete" { + 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, .cascade); + try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }}); + try orders.put(.{ .int = 101 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }}); + try orders.put(.{ .int = 200 }, &.{.{ .name = "customer_id", .value = .{ .int = 2 } }}); + + 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 orders.get(.{ .int = 101 })) == null); + + // Untouched: customer 2 and its own order were never in the cascade's + // path. + var otherCustomer = (try customers.get(.{ .int = 2 })).?; + otherCustomer.deinit(); + var otherOrder = (try orders.get(.{ .int = 200 })).?; + otherOrder.deinit(); +} + +test "ON DELETE CASCADE keeps secondary indexes on the cascaded rows in sync" { + 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.createIndex("status"); + try orders.createForeignKey("customer_id", "customers", pkField, .cascade); + try orders.put(.{ .int = 100 }, &.{ + .{ .name = "customer_id", .value = .{ .int = 1 } }, + .{ .name = "status", .value = .{ .text = "open" } }, + }); +