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, 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