From 9f207fd0360ecd6b9d58170ad933b577f4234156 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 21:37:58 +0200 Subject: [PATCH] feat: extend index --- src/index.zig | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/src/index.zig b/src/index.zig index a8100b2..81c8238 100644 --- a/src/index.zig +++ b/src/index.zig @@ -1268,3 +1268,158 @@ test "rowKey and indexEntryKey stay within their own namespace" { try std.testing.expectEqual(@as(u8, 'I'), ik[0]); } +test "prefixUpperBound bounds a prefix scan" { + const allocator = std.testing.allocator; + const upper = (try prefixUpperBound(allocator, "abc")).?; + defer allocator.free(upper); + try std.testing.expect(std.mem.order(u8, "abc", upper) == .lt); + try std.testing.expect(std.mem.order(u8, "abd", upper) != .lt); +} + +test "IndexSet 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 IndexSet.load(allocator, &db, "users"); + defer set.deinit(); + try std.testing.expectEqual(@as(usize, 0), set.fields.items.len); + + try set.add("email"); + try set.add("age"); + try set.add("email"); + try std.testing.expectEqual(@as(usize, 2), set.fields.items.len); + try set.save(&db, "users"); + + var reloaded = try IndexSet.load(allocator, &db, "users"); + defer reloaded.deinit(); + try std.testing.expect(reloaded.has("email")); + try std.testing.expect(reloaded.has("age")); + try std.testing.expect(!reloaded.has("missing")); + + try std.testing.expect(reloaded.remove("email")); + try std.testing.expect(!reloaded.remove("email")); + try reloaded.save(&db, "users"); + + var third = try IndexSet.load(allocator, &db, "users"); + defer third.deinit(); + try std.testing.expect(!third.has("email")); + try std.testing.expect(third.has("age")); +} + +test "UniqueSet 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 UniqueSet.load(allocator, &db, "users"); + defer set.deinit(); + try std.testing.expectEqual(@as(usize, 0), set.fields.items.len); + + try set.add("email"); + try set.add("ssn"); + try set.add("email"); + try std.testing.expectEqual(@as(usize, 2), set.fields.items.len); + try set.save(&db, "users"); + + var reloaded = try UniqueSet.load(allocator, &db, "users"); + defer reloaded.deinit(); + try std.testing.expect(reloaded.has("email")); + try std.testing.expect(reloaded.has("ssn")); + try std.testing.expect(!reloaded.has("missing")); + + try std.testing.expect(reloaded.remove("email")); + try std.testing.expect(!reloaded.remove("email")); + try reloaded.save(&db, "users"); + + var third = try UniqueSet.load(allocator, &db, "users"); + defer third.deinit(); + try std.testing.expect(!third.has("email")); + try std.testing.expect(third.has("ssn")); +} + +test "UniqueSet and IndexSet on the same table never collide, each keeps its own membership" { + 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, "users"); + defer indexes.deinit(); + try indexes.add("email"); + try indexes.add("city"); + try indexes.save(&db, "users"); + + var unique = try UniqueSet.load(allocator, &db, "users"); + defer unique.deinit(); + try unique.add("email"); + try unique.save(&db, "users"); + + // "city" is indexed but not unique, "email" is both - the two sets + // must agree on "email" and disagree on "city" after an independent + // reload of each, proving neither save() overwrote the other's key. + var reloadedIndexes = try IndexSet.load(allocator, &db, "users"); + defer reloadedIndexes.deinit(); + var reloadedUnique = try UniqueSet.load(allocator, &db, "users"); + defer reloadedUnique.deinit(); + + try std.testing.expect(reloadedIndexes.has("email")); + try std.testing.expect(reloadedIndexes.has("city")); + try std.testing.expect(reloadedUnique.has("email")); + try std.testing.expect(!reloadedUnique.has("city")); +} + +test "ForeignKeySet 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 ForeignKeySet.load(allocator, &db, "orders"); + defer set.deinit(); + try std.testing.expectEqual(@as(usize, 0), set.specs.items.len); + + try set.add("customer_id", "customers", "__pk", .cascade); + try set.add("customer_id", "customers", "some_other_field", .restrict); + try std.testing.expectEqual(@as(usize, 1), set.specs.items.len); + try set.save(&db, "orders"); + + var reloaded = try ForeignKeySet.load(allocator, &db, "orders"); + defer reloaded.deinit(); + try std.testing.expect(reloaded.has("customer_id")); + try std.testing.expect(!reloaded.has("missing")); + const spec = reloaded.find("customer_id").?; + try std.testing.expectEqualStrings("customers", spec.refTable); + try std.testing.expectEqualStrings("__pk", spec.refField); + // First `add` wins (mirrors `has` guard above) - `onDelete` survives the + // save/load round trip as the mode from that first call, not the + // no-op second one. + try std.testing.expectEqual(OnDelete.cascade, spec.onDelete); + + try std.testing.expect(reloaded.remove("customer_id")); + try std.testing.expect(!reloaded.remove("customer_id")); + try reloaded.save(&db, "orders"); + + var third = try ForeignKeySet.load(allocator, &db, "orders"); + defer third.deinit(); + try std.testing.expect(!third.has("customer_id")); +} +