From 1ca072480da26a2d6b64b16ab08305eea82928cc Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 13:39:07 +0200 Subject: [PATCH] feat: build out index --- src/index.zig | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/index.zig b/src/index.zig index 5dae488..f91cb10 100644 --- a/src/index.zig +++ b/src/index.zig @@ -1577,3 +1577,163 @@ test "ForeignKeySet, UniqueSet, and IndexSet on the same table never collide" { try checks.add("qty_positive", "qty gt 0"); try checks.save(&db, "orders"); + var reloadedIndexes = try IndexSet.load(allocator, &db, "orders"); + defer reloadedIndexes.deinit(); + var reloadedUnique = try UniqueSet.load(allocator, &db, "orders"); + defer reloadedUnique.deinit(); + var reloadedFkeys = try ForeignKeySet.load(allocator, &db, "orders"); + defer reloadedFkeys.deinit(); + var reloadedChecks = try CheckConstraintSet.load(allocator, &db, "orders"); + defer reloadedChecks.deinit(); + + try std.testing.expect(reloadedIndexes.has("customer_id")); + try std.testing.expect(reloadedUnique.has("order_number")); + try std.testing.expect(!reloadedUnique.has("customer_id")); + try std.testing.expect(reloadedFkeys.has("customer_id")); + try std.testing.expect(!reloadedFkeys.has("order_number")); + try std.testing.expect(reloadedChecks.has("qty_positive")); + try std.testing.expect(!reloadedChecks.has("customer_id")); +} + +test "addEntry scanEq removeEntry 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(); + + try addEntry(&db, allocator, "users", "city", .{ .text = "warsaw" }, .{ .int = 1 }); + try addEntry(&db, allocator, "users", "city", .{ .text = "warsaw" }, .{ .int = 2 }); + try addEntry(&db, allocator, "users", "city", .{ .text = "krakow" }, .{ .int = 3 }); + + const warsawPks = try scanEq(&db, allocator, "users", "city", .{ .text = "warsaw" }); + defer { + for (warsawPks) |v| v.free(allocator); + allocator.free(warsawPks); + } + try std.testing.expectEqual(@as(usize, 2), warsawPks.len); + + try removeEntry(&db, allocator, "users", "city", .{ .text = "warsaw" }, .{ .int = 1 }); + const afterRemove = try scanEq(&db, allocator, "users", "city", .{ .text = "warsaw" }); + defer { + for (afterRemove) |v| v.free(allocator); + allocator.free(afterRemove); + } + try std.testing.expectEqual(@as(usize, 1), afterRemove.len); + try std.testing.expectEqual(@as(i64, 2), afterRemove[0].int); +} + +test "EqCursor streams the same pks as eager scanEq and can stop early" { + 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 i: i64 = 0; + while (i < 20) : (i += 1) { + try addEntry(&db, allocator, "users", "city", .{ .text = "warsaw" }, .{ .int = i }); + } + + const eager = try scanEq(&db, allocator, "users", "city", .{ .text = "warsaw" }); + defer { + for (eager) |v| v.free(allocator); + allocator.free(eager); + } + try std.testing.expectEqual(@as(usize, 20), eager.len); + + // Pull only the first three through the cursor, then drop it: this must + // neither read nor decode the remaining seventeen entries, and the + // testing allocator would flag a leak if the partially-consumed + // RangeIterator's resources were not released on an early deinit. + var cursor = try scanEqCursor(&db, allocator, "users", "city", .{ .text = "warsaw" }); + defer cursor.deinit(); + var pulled = std.ArrayList(Value).init(allocator); + defer { + for (pulled.items) |v| v.free(allocator); + pulled.deinit(); + } + var n: usize = 0; + while (n < 3) : (n += 1) { + const pk = (try cursor.next(allocator)).?; + try pulled.append(pk); + } + try std.testing.expectEqual(@as(usize, 3), pulled.items.len); + for (pulled.items, 0..) |v, idx2| { + try std.testing.expectEqual(eager[idx2].int, v.int); + } +} + +test "RangeCursor streams the same pks as eager scanRange and can stop early" { + 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 i: i64 = 0; + while (i < 30) : (i += 1) { + try addEntry(&db, allocator, "events", "score", .{ .int = i }, .{ .int = i }); + } + + var cursor = try scanRangeCursor(&db, allocator, "events", "score", .{ .value = .{ .int = 5 }, .inclusive = true }, null); + defer cursor.deinit(); + const first = (try cursor.next(allocator)).?; + defer first.free(allocator); + const second = (try cursor.next(allocator)).?; + defer second.free(allocator); + try std.testing.expectEqual(@as(i64, 5), first.int); + try std.testing.expectEqual(@as(i64, 6), second.int); + // cursor and its underlying iterator are dropped by `defer` here without + // consuming the other 23 matches. +} + +test "scanRange respects bounds and inclusivity" { + 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 i: i64 = 0; + while (i < 10) : (i += 1) { + try addEntry(&db, allocator, "events", "score", .{ .int = i }, .{ .int = i }); + } + + const all = try scanRange(&db, allocator, "events", "score", null, null); + defer { + for (all) |v| v.free(allocator); + allocator.free(all); + } + try std.testing.expectEqual(@as(usize, 10), all.len); + + const midInclusive = try scanRange(&db, allocator, "events", "score", .{ .value = .{ .int = 3 }, .inclusive = true }, .{ .value = .{ .int = 6 }, .inclusive = true }); + defer { + for (midInclusive) |v| v.free(allocator); + allocator.free(midInclusive); + } + try std.testing.expectEqual(@as(usize, 4), midInclusive.len); + try std.testing.expectEqual(@as(i64, 3), midInclusive[0].int); + try std.testing.expectEqual(@as(i64, 6), midInclusive[midInclusive.len - 1].int); + + const midExclusive = try scanRange(&db, allocator, "events", "score", .{ .value = .{ .int = 3 }, .inclusive = false }, .{ .value = .{ .int = 6 }, .inclusive = false }); + defer { + for (midExclusive) |v| v.free(allocator); + allocator.free(midExclusive); + } + try std.testing.expectEqual(@as(usize, 2), midExclusive.len); + try std.testing.expectEqual(@as(i64, 4), midExclusive[0].int); + try std.testing.expectEqual(@as(i64, 5), midExclusive[1].int); +