feat: grow index
This commit is contained in:
1 parent
1ca072480d
commit
61151cc7dd
1 file changed
+198
+198
@@ -1737,3 +1737,201 @@ test "scanRange respects bounds and inclusivity" {
|
|||||||
try std.testing.expectEqual(@as(i64, 4), midExclusive[0].int);
|
try std.testing.expectEqual(@as(i64, 4), midExclusive[0].int);
|
||||||
try std.testing.expectEqual(@as(i64, 5), midExclusive[1].int);
|
try std.testing.expectEqual(@as(i64, 5), midExclusive[1].int);
|
||||||
|
|
||||||
|
const openAbove = try scanRange(&db, allocator, "events", "score", .{ .value = .{ .int = 8 }, .inclusive = true }, null);
|
||||||
|
defer {
|
||||||
|
for (openAbove) |v| v.free(allocator);
|
||||||
|
allocator.free(openAbove);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), openAbove.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "CompositeIndexSet 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 CompositeIndexSet.load(allocator, &db, "users");
|
||||||
|
defer set.deinit();
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), set.specs.items.len);
|
||||||
|
|
||||||
|
try set.add("city_age", &.{ "city", "age" });
|
||||||
|
try set.add("city_age", &.{ "city", "age" }); // no-op, already present
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), set.specs.items.len);
|
||||||
|
try set.save(&db, "users");
|
||||||
|
|
||||||
|
var reloaded = try CompositeIndexSet.load(allocator, &db, "users");
|
||||||
|
defer reloaded.deinit();
|
||||||
|
try std.testing.expect(reloaded.has("city_age"));
|
||||||
|
const spec = reloaded.find("city_age").?;
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), spec.fields.len);
|
||||||
|
try std.testing.expectEqualStrings("city", spec.fields[0]);
|
||||||
|
try std.testing.expectEqualStrings("age", spec.fields[1]);
|
||||||
|
|
||||||
|
try std.testing.expect(reloaded.remove("city_age"));
|
||||||
|
try std.testing.expect(!reloaded.remove("city_age"));
|
||||||
|
try reloaded.save(&db, "users");
|
||||||
|
|
||||||
|
var third = try CompositeIndexSet.load(allocator, &db, "users");
|
||||||
|
defer third.deinit();
|
||||||
|
try std.testing.expect(!third.has("city_age"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "single-field and composite index metadata never shadow each other" {
|
||||||
|
// Both live under the "M" tag with table-name-suffixed keys that differ
|
||||||
|
// only in "INDEXES" vs "CINDEXES" - this pins down that they stay two
|
||||||
|
// genuinely distinct Db keys, not one overwriting the other.
|
||||||
|
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 single = try IndexSet.load(allocator, &db, "users");
|
||||||
|
defer single.deinit();
|
||||||
|
try single.add("email");
|
||||||
|
try single.save(&db, "users");
|
||||||
|
|
||||||
|
var composite = try CompositeIndexSet.load(allocator, &db, "users");
|
||||||
|
defer composite.deinit();
|
||||||
|
try composite.add("city_age", &.{ "city", "age" });
|
||||||
|
try composite.save(&db, "users");
|
||||||
|
|
||||||
|
var singleReloaded = try IndexSet.load(allocator, &db, "users");
|
||||||
|
defer singleReloaded.deinit();
|
||||||
|
try std.testing.expect(singleReloaded.has("email"));
|
||||||
|
|
||||||
|
var compositeReloaded = try CompositeIndexSet.load(allocator, &db, "users");
|
||||||
|
defer compositeReloaded.deinit();
|
||||||
|
try std.testing.expect(compositeReloaded.has("city_age"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "addCompositeEntry scanCompositeEq removeCompositeEntry 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 addCompositeEntry(&db, allocator, "users", "city_age", &.{ .{ .text = "warsaw" }, .{ .int = 30 } }, .{ .int = 1 });
|
||||||
|
try addCompositeEntry(&db, allocator, "users", "city_age", &.{ .{ .text = "warsaw" }, .{ .int = 40 } }, .{ .int = 2 });
|
||||||
|
try addCompositeEntry(&db, allocator, "users", "city_age", &.{ .{ .text = "krakow" }, .{ .int = 30 } }, .{ .int = 3 });
|
||||||
|
|
||||||
|
// Full-tuple match: exactly one row.
|
||||||
|
const exact = try scanCompositeEq(&db, allocator, "users", "city_age", &.{ .{ .text = "warsaw" }, .{ .int = 30 } });
|
||||||
|
defer {
|
||||||
|
for (exact) |v| v.free(allocator);
|
||||||
|
allocator.free(exact);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), exact.len);
|
||||||
|
try std.testing.expectEqual(@as(i64, 1), exact[0].int);
|
||||||
|
|
||||||
|
// Leftmost-prefix match on just the first column: both warsaw rows,
|
||||||
|
// never the krakow one, even though it wasn't constrained on age at all.
|
||||||
|
const prefixMatch = try scanCompositeEq(&db, allocator, "users", "city_age", &.{.{ .text = "warsaw" }});
|
||||||
|
defer {
|
||||||
|
for (prefixMatch) |v| v.free(allocator);
|
||||||
|
allocator.free(prefixMatch);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), prefixMatch.len);
|
||||||
|
|
||||||
|
try removeCompositeEntry(&db, allocator, "users", "city_age", &.{ .{ .text = "warsaw" }, .{ .int = 30 } }, .{ .int = 1 });
|
||||||
|
const afterRemove = try scanCompositeEq(&db, allocator, "users", "city_age", &.{.{ .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 "scanCompositeRangeCursor narrows on the column after the matched prefix" {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Two cities, ages 20..29 in each - the composite index is (city, age).
|
||||||
|
var age: i64 = 20;
|
||||||
|
while (age < 30) : (age += 1) {
|
||||||
|
try addCompositeEntry(&db, allocator, "users", "city_age", &.{ .{ .text = "warsaw" }, .{ .int = age } }, .{ .int = age });
|
||||||
|
try addCompositeEntry(&db, allocator, "users", "city_age", &.{ .{ .text = "krakow" }, .{ .int = age } }, .{ .int = age + 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// city == "warsaw" and 23 <= age <= 26: never crosses into krakow's
|
||||||
|
// bucket even though its ages overlap byte-for-byte with warsaw's.
|
||||||
|
var midCursor = try scanCompositeRangeCursor(&db, allocator, "users", "city_age", &.{.{ .text = "warsaw" }}, .{ .value = .{ .int = 23 }, .inclusive = true }, .{ .value = .{ .int = 26 }, .inclusive = true });
|
||||||
|
var midInclusive = std.ArrayList(Value).init(allocator);
|
||||||
|
defer midInclusive.deinit();
|
||||||
|
while (try midCursor.next(allocator)) |pk| try midInclusive.append(pk);
|
||||||
|
midCursor.deinit();
|
||||||
|
try std.testing.expectEqual(@as(usize, 4), midInclusive.items.len);
|
||||||
|
try std.testing.expectEqual(@as(i64, 23), midInclusive.items[0].int);
|
||||||
|
try std.testing.expectEqual(@as(i64, 26), midInclusive.items[midInclusive.items.len - 1].int);
|
||||||
|
|
||||||
|
// Exclusive both sides: drops the two endpoints.
|
||||||
|
var exclusive = try scanCompositeRangeCursor(&db, allocator, "users", "city_age", &.{.{ .text = "warsaw" }}, .{ .value = .{ .int = 23 }, .inclusive = false }, .{ .value = .{ .int = 26 }, .inclusive = false });
|
||||||
|
defer exclusive.deinit();
|
||||||
|
const first = (try exclusive.next(allocator)).?;
|
||||||
|
defer first.free(allocator);
|
||||||
|
const second = (try exclusive.next(allocator)).?;
|
||||||
|
defer second.free(allocator);
|
||||||
|
try std.testing.expectEqual(@as(i64, 24), first.int);
|
||||||
|
try std.testing.expectEqual(@as(i64, 25), second.int);
|
||||||
|
try std.testing.expectEqual(@as(?Value, null), try exclusive.next(allocator));
|
||||||
|
|
||||||
|
// Open above: age >= 28 within warsaw only, never touching krakow.
|
||||||
|
var openAbove = try scanCompositeRangeCursor(&db, allocator, "users", "city_age", &.{.{ .text = "warsaw" }}, .{ .value = .{ .int = 28 }, .inclusive = true }, null);
|
||||||
|
defer openAbove.deinit();
|
||||||
|
const a1 = (try openAbove.next(allocator)).?;
|
||||||
|
defer a1.free(allocator);
|
||||||
|
const a2 = (try openAbove.next(allocator)).?;
|
||||||
|
defer a2.free(allocator);
|
||||||
|
try std.testing.expectEqual(@as(i64, 28), a1.int);
|
||||||
|
try std.testing.expectEqual(@as(i64, 29), a2.int);
|
||||||
|
try std.testing.expectEqual(@as(?Value, null), try openAbove.next(allocator));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "dropAllCompositeEntries clears an index without touching another one" {
|
||||||
|
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 addCompositeEntry(&db, allocator, "users", "city_age", &.{ .{ .text = "warsaw" }, .{ .int = 30 } }, .{ .int = 1 });
|
||||||
|
try addCompositeEntry(&db, allocator, "users", "other_idx", &.{ .{ .int = 1 }, .{ .int = 2 } }, .{ .int = 1 });
|
||||||
|
|
||||||
|
try dropAllCompositeEntries(&db, allocator, "users", "city_age");
|
||||||
|
|
||||||
|
const cityAge = try scanCompositeEq(&db, allocator, "users", "city_age", &.{.{ .text = "warsaw" }});
|
||||||
|
defer {
|
||||||
|
for (cityAge) |v| v.free(allocator);
|
||||||
|
allocator.free(cityAge);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), cityAge.len);
|
||||||
|
|
||||||
|
const other = try scanCompositeEq(&db, allocator, "users", "other_idx", &.{.{ .int = 1 }});
|
||||||
|
defer {
|
||||||
|
for (other) |v| v.free(allocator);
|
||||||
|
allocator.free(other);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), other.len);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user