From 5c72201c45ad70b783fa7b861179592524c095b5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 22:05:12 +0200 Subject: [PATCH] feat: expand index --- src/index.zig | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/index.zig b/src/index.zig index 81c8238..5dae488 100644 --- a/src/index.zig +++ b/src/index.zig @@ -1423,3 +1423,157 @@ test "ForeignKeySet add remove save load round trip" { try std.testing.expect(!third.has("customer_id")); } +test "ForeignKeySet.load rejects a corrupted onDelete byte instead of accepting garbage" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const dbPath = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(dbPath); + + var db = try Db.open(allocator, dbPath, .{}); + defer db.close(); + + var set = try ForeignKeySet.load(allocator, &db, "orders"); + defer set.deinit(); + try set.add("customer_id", "customers", "__pk", .restrict); + try set.save(&db, "orders"); + + const key = try foreignKeyMetaKey(allocator, "orders"); + defer allocator.free(key); + const raw = try db.get(allocator, key) orelse return error.TestUnexpectedResult; + defer allocator.free(raw); + + // Flip the trailing onDelete byte to a value neither `restrict` (0) + // nor `cascade` (1) can ever legitimately be - same "malformed input + // is rejected, not silently coerced" contract every other decoder in + // this file already gives length-prefixed strings. + const corrupted = try allocator.dupe(u8, raw); + defer allocator.free(corrupted); + corrupted[corrupted.len - 1] = 0xFF; + try db.put(key, corrupted); + + try std.testing.expectError(error.Malformed, ForeignKeySet.load(allocator, &db, "orders")); +} + +test "ReferencedBySet add replaces an existing (childTable, childField) pair instead of duplicating it" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const dbPath = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(dbPath); + + var db = try Db.open(allocator, dbPath, .{}); + defer db.close(); + + var set = try ReferencedBySet.load(allocator, &db, "customers"); + defer set.deinit(); + try std.testing.expectEqual(@as(usize, 0), set.specs.items.len); + + try set.add("orders", "customer_id", "__pk", .restrict); + try set.add("orders", "customer_id", "__pk", .restrict); + try std.testing.expectEqual(@as(usize, 1), set.specs.items.len); + try set.add("line_items", "customer_id", "__pk", .cascade); + try std.testing.expectEqual(@as(usize, 2), set.specs.items.len); + try set.save(&db, "customers"); + + var reloaded = try ReferencedBySet.load(allocator, &db, "customers"); + defer reloaded.deinit(); + try std.testing.expectEqual(@as(usize, 2), reloaded.specs.items.len); + for (reloaded.specs.items) |spec| { + if (std.mem.eql(u8, spec.childTable, "line_items")) { + try std.testing.expectEqual(OnDelete.cascade, spec.onDelete); + } else { + try std.testing.expectEqual(OnDelete.restrict, spec.onDelete); + } + } + + // Re-adding the same pair under a different refField AND onDelete mode + // replaces both in place rather than accumulating a second, now-stale + // entry. + try reloaded.add("orders", "customer_id", "email", .cascade); + try std.testing.expectEqual(@as(usize, 2), reloaded.specs.items.len); + var sawOrders = false; + for (reloaded.specs.items) |spec| { + if (std.mem.eql(u8, spec.childTable, "orders")) { + sawOrders = true; + try std.testing.expectEqualStrings("email", spec.refField); + try std.testing.expectEqual(OnDelete.cascade, spec.onDelete); + } + } + try std.testing.expect(sawOrders); + + try std.testing.expect(reloaded.remove("orders", "customer_id")); + try std.testing.expect(!reloaded.remove("orders", "customer_id")); + try reloaded.save(&db, "customers"); + + var third = try ReferencedBySet.load(allocator, &db, "customers"); + defer third.deinit(); + try std.testing.expectEqual(@as(usize, 1), third.specs.items.len); + try std.testing.expectEqualStrings("line_items", third.specs.items[0].childTable); +} + +test "CheckConstraintSet add remove save load round trip" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const dbPath = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(dbPath); + + var db = try Db.open(allocator, dbPath, .{}); + defer db.close(); + + var set = try CheckConstraintSet.load(allocator, &db, "orders"); + defer set.deinit(); + try std.testing.expectEqual(@as(usize, 0), set.specs.items.len); + + try set.add("qty_positive", "qty gt 0"); + try set.add("qty_positive", "qty gt 100"); + try std.testing.expectEqual(@as(usize, 1), set.specs.items.len); + try set.save(&db, "orders"); + + var reloaded = try CheckConstraintSet.load(allocator, &db, "orders"); + defer reloaded.deinit(); + try std.testing.expect(reloaded.has("qty_positive")); + try std.testing.expect(!reloaded.has("missing")); + const spec = reloaded.find("qty_positive").?; + try std.testing.expectEqualStrings("qty gt 0", spec.source); + + try std.testing.expect(reloaded.remove("qty_positive")); + try std.testing.expect(!reloaded.remove("qty_positive")); + try reloaded.save(&db, "orders"); + + var third = try CheckConstraintSet.load(allocator, &db, "orders"); + defer third.deinit(); + try std.testing.expect(!third.has("qty_positive")); +} + +test "ForeignKeySet, UniqueSet, and IndexSet on the same table never collide" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const dbPath = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(dbPath); + + var db = try Db.open(allocator, dbPath, .{}); + defer db.close(); + + var indexes = try IndexSet.load(allocator, &db, "orders"); + defer indexes.deinit(); + try indexes.add("customer_id"); + try indexes.save(&db, "orders"); + + var unique = try UniqueSet.load(allocator, &db, "orders"); + defer unique.deinit(); + try unique.add("order_number"); + try unique.save(&db, "orders"); + + var fkeys = try ForeignKeySet.load(allocator, &db, "orders"); + defer fkeys.deinit(); + try fkeys.add("customer_id", "customers", "__pk", .restrict); + try fkeys.save(&db, "orders"); + + var checks = try CheckConstraintSet.load(allocator, &db, "orders"); + defer checks.deinit(); + try checks.add("qty_positive", "qty gt 0"); + try checks.save(&db, "orders"); +