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:
2026-08-03 17:21:33 +03:00
parent 411a380d38
commit f61416f44a
3 changed files with 117 additions and 33 deletions

View File

@@ -481,7 +481,7 @@ fn cmd_list_indexes(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !void
id_pairs[0] = .{ .key = "v", .value = .{ .int32 = 2 } };
id_pairs[1] = .{ .key = "key", .value = .{ .doc = &.{.{ .key = "_id", .value = .{ .int32 = 1 } }} } };
values[0] = .{ .doc = try index_pairs_append(reply, id_pairs, "_id_") };
for (coll.indexes.items, 0..) |*ix, i| {
for (coll.indexes.items, 0..) |ix, i| {
// The pairs live in the reply arena (freed with it); the values
// array below references them.
var pairs: std.ArrayListUnmanaged(bson.Pair) = .empty;