index/db: heap-allocate secondary indexes
Collection.indexes held Index by value, so orderedRemove memmoved the whole ~5 KB struct and every *Index already handed out referred to a different index afterwards -- a query plan's `index` field, or a slice into an index's promoted-key buffer. The collection's own bookkeeping stayed consistent, which is why nothing noticed: only a caller holding a pointer across a drop could see it, and no test did. The new test does, and it is mutation-checked against the by-value code that this commit replaces: holding pointers to b_1 and c_1, then dropping a_1, the b_1 pointer reads "c_1". Now orderedRemove moves 8-byte pointers, the surviving indexes do not move, and only the removed one is freed. M0 needs this independently: an Index will own a file mapping once the node arena moves into the data file, and copying one by value would duplicate that ownership. Not done, though the milestone plan listed it: moving Index's inline scratch and promo buffers out of the struct. Their stated purpose was to keep those 5 KB out of a file-resident Index and to stop the memmove -- but only the node arena and overflow slab become file-resident, not the Index metadata, and boxing already fixed the memmove. Moving them would be churn with nothing left to buy.
This commit is contained in:
@@ -1489,8 +1489,8 @@ pub const Index = struct {
|
||||
/// The index whose key pattern is exactly `key_pairs` (same paths, same
|
||||
/// order, same directions), or null. Keeps the IndexKey layout — and what
|
||||
/// counts as a match — inside this module.
|
||||
pub fn find_by_key_pattern(indexes: []const Index, key_pairs: []const bson.Pair) ?*const Index {
|
||||
for (indexes) |*ix| {
|
||||
pub fn find_by_key_pattern(indexes: []const *Index, key_pairs: []const bson.Pair) ?*const Index {
|
||||
for (indexes) |ix| {
|
||||
if (ix.keys.len != key_pairs.len) continue;
|
||||
var match = true;
|
||||
for (ix.keys, key_pairs) |k, kp| {
|
||||
@@ -1838,7 +1838,7 @@ pub const Plan = struct {
|
||||
pub fn plan(
|
||||
gpa: std.mem.Allocator,
|
||||
id_ix: ?*const Index,
|
||||
indexes: []const Index,
|
||||
indexes: []const *Index,
|
||||
filter: []const bson.Pair,
|
||||
sort: []const query.SortKey,
|
||||
) !?Plan {
|
||||
@@ -1853,7 +1853,7 @@ pub fn plan(
|
||||
best = cand;
|
||||
}
|
||||
}
|
||||
for (indexes) |*ix| {
|
||||
for (indexes) |ix| {
|
||||
var cand = (try evaluate_index(gpa, ix, clauses.items, sort)) orelse continue;
|
||||
if (best) |b| {
|
||||
if (plan_better(&cand, &b)) {
|
||||
@@ -2883,7 +2883,7 @@ test "planner picks eq run, ranges, and bails on sparse null" {
|
||||
.{ .key = "a", .value = .{ .int32 = 1 } },
|
||||
.{ .key = "b", .value = .{ .int32 = 2 } },
|
||||
};
|
||||
var p = (try plan(gpa, null, &.{ix}, &f, &.{})).?;
|
||||
var p = (try plan(gpa, null, &.{&ix}, &f, &.{})).?;
|
||||
defer p.deinit(gpa);
|
||||
try testing.expectEqual(@as(usize, 2), p.key_len());
|
||||
try testing.expect(p.lo == null and p.hi == null);
|
||||
@@ -2894,7 +2894,7 @@ test "planner picks eq run, ranges, and bails on sparse null" {
|
||||
.{ .key = "a", .value = .{ .int32 = 1 } },
|
||||
.{ .key = "b", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 2 } }} } },
|
||||
};
|
||||
var p = (try plan(gpa, null, &.{ix}, &f, &.{})).?;
|
||||
var p = (try plan(gpa, null, &.{&ix}, &f, &.{})).?;
|
||||
defer p.deinit(gpa);
|
||||
try testing.expectEqual(@as(usize, 1), p.key_len());
|
||||
try testing.expect(p.hi == null and p.lo != null and !p.lo_incl);
|
||||
@@ -2902,14 +2902,14 @@ test "planner picks eq run, ranges, and bails on sparse null" {
|
||||
// {a: 1} only → prefix run of 1.
|
||||
{
|
||||
const f = [_]bson.Pair{.{ .key = "a", .value = .{ .int32 = 1 } }};
|
||||
var p = (try plan(gpa, null, &.{ix}, &f, &.{})).?;
|
||||
var p = (try plan(gpa, null, &.{&ix}, &f, &.{})).?;
|
||||
defer p.deinit(gpa);
|
||||
try testing.expectEqual(@as(usize, 1), p.key_len());
|
||||
}
|
||||
// Pure range on the first key → key_len 0 with a bound.
|
||||
{
|
||||
const f = [_]bson.Pair{.{ .key = "a", .value = .{ .doc = &.{.{ .key = "$gte", .value = .{ .int32 = 1 } }} } }};
|
||||
var p = (try plan(gpa, null, &.{ix}, &f, &.{})).?;
|
||||
var p = (try plan(gpa, null, &.{&ix}, &f, &.{})).?;
|
||||
defer p.deinit(gpa);
|
||||
try testing.expectEqual(@as(usize, 0), p.key_len());
|
||||
try testing.expect(p.lo != null and p.lo_incl);
|
||||
@@ -2917,23 +2917,23 @@ test "planner picks eq run, ranges, and bails on sparse null" {
|
||||
// Unusable filter → no plan.
|
||||
{
|
||||
const f = [_]bson.Pair{.{ .key = "a", .value = .{ .doc = &.{.{ .key = "$regex", .value = .{ .string = "^x" } }} } }};
|
||||
try testing.expect((try plan(gpa, null, &.{ix}, &f, &.{})) == null);
|
||||
try testing.expect((try plan(gpa, null, &.{&ix}, &f, &.{})) == null);
|
||||
const or_f = [_]bson.Pair{.{ .key = "$or", .value = .{ .array = &.{
|
||||
.{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} },
|
||||
} } }};
|
||||
try testing.expect((try plan(gpa, null, &.{ix}, &or_f, &.{})) == null);
|
||||
try testing.expect((try plan(gpa, null, &.{&ix}, &or_f, &.{})) == null);
|
||||
}
|
||||
// Sparse index bails on a null component.
|
||||
{
|
||||
const f = [_]bson.Pair{.{ .key = "a", .value = .null }};
|
||||
try testing.expect((try plan(gpa, null, &.{sp}, &f, &.{})) == null);
|
||||
try testing.expect((try plan(gpa, null, &.{&sp}, &f, &.{})) == null);
|
||||
// Non-sparse is fine with null.
|
||||
var p = (try plan(gpa, null, &.{ix}, &f, &.{})).?;
|
||||
var p = (try plan(gpa, null, &.{&ix}, &f, &.{})).?;
|
||||
defer p.deinit(gpa);
|
||||
try testing.expect(p.key_len() == 1);
|
||||
// A null inside $in bails too.
|
||||
const fin = [_]bson.Pair{.{ .key = "a", .value = .{ .doc = &.{.{ .key = "$in", .value = .{ .array = &.{ .{ .int32 = 1 }, .null } } }} } }};
|
||||
try testing.expect((try plan(gpa, null, &.{sp}, &fin, &.{})) == null);
|
||||
try testing.expect((try plan(gpa, null, &.{&sp}, &fin, &.{})) == null);
|
||||
}
|
||||
// $in cartesian product is capped.
|
||||
{
|
||||
@@ -2944,6 +2944,6 @@ test "planner picks eq run, ranges, and bails on sparse null" {
|
||||
.{ .key = "b", .value = .{ .doc = &.{.{ .key = "$in", .value = .{ .array = &members } }} } },
|
||||
};
|
||||
// 20 * 20 = 400 > 100 → fall back to a scan.
|
||||
try testing.expect((try plan(gpa, null, &.{ix}, &f, &.{})) == null);
|
||||
try testing.expect((try plan(gpa, null, &.{&ix}, &f, &.{})) == null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user