index/db/commands: fold duplicated index logic into single definitions

Cleanup pass over the secondary-index feature.

add_doc is now the one entry-commit path. create_index and
build_all_indexes each hand-rolled build -> check_unique -> reserve ->
insert, and had already drifted on whether multikey is set before or
after the unique check; add_doc gained an enforce_unique flag so the
rebuild path keeps its tolerate-and-warn behavior. reserve_for and
insert_entries are now the only way the engine touches Index.entries.

One definition each for: prefix comparison and the prefix binary
searches (prefix_order + std.sort), the cartesian-product odometer
(advance_choice), the spec pair list (write_spec builds on spec_pairs,
so the log format and the listIndexes reply share one schema), the _id
clause parser (plan_id reuses analyze_clause), key-pattern direction
(index.descending, which desc_dir already disagreed with on non-numeric
values), option truthiness (query.truthy), the E11000 message, and
index-removal-by-name (Collection.find_index/remove_index). Key-pattern
matching moved out of the dispatcher into index.find_by_key_pattern.

Dead or redundant: ParallelArraysError, the unread `dropped` counter,
insert_entries' discarded gpa, a third pass computing multikey, the
has_id/is_id_index flag pair, Plan.key_len (always lookup_keys[0].len,
now a method), first_match_consumed (now stages = stages[1..]).

Cheaper hot paths: remove_id compacts in one pass instead of an
orderedRemove per hit; Plan.search skips the sort/dedupe when neither
multikey nor multiple lookup keys can produce a repeat; the _id fast
path reuses one scratch key buffer (bson.write_serialized_value); the
plan loop uses the bound collection instead of re-resolving it through
two hash lookups per candidate.

Behavior is unchanged except that dropping plan_id's fixed 16-clause
buffer enables the _id fast path on filters that previously exceeded it.
This commit is contained in:
2026-08-02 13:40:42 +03:00
parent 0d264c6c57
commit 7482042f34
5 changed files with 260 additions and 334 deletions

View File

@@ -17,6 +17,28 @@ pub const Collection = struct {
fn init() Collection {
return .{ .docs = .empty, .indexes = .empty };
}
/// The secondary index with this name, or null. The single by-name
/// lookup: index lifetime (who calls Index.deinit, and when) is decided
/// here rather than at each caller.
pub fn find_index(self: *Collection, name: []const u8) ?*index.Index {
for (self.indexes.items) |*ix| {
if (std.mem.eql(u8, ix.name, name)) return ix;
}
return null;
}
/// Remove and free the index with this name. Returns whether it existed.
fn remove_index(self: *Collection, gpa: std.mem.Allocator, name: []const u8) bool {
for (self.indexes.items, 0..) |ix, i| {
if (std.mem.eql(u8, ix.name, name)) {
var removed = self.indexes.orderedRemove(i);
removed.deinit(gpa);
return true;
}
}
return false;
}
};
pub const Db = struct {
@@ -204,8 +226,7 @@ pub const Engine = struct {
// 4. Reserve entry capacity — the last fallible step, so the entry
// insertion after the log append is infallible.
for (built_list.items) |*b| {
if (b.built.entries.items.len == 0) continue;
try b.ix.entries.ensureUnusedCapacity(self.gpa, b.built.entries.items.len);
try b.ix.reserve_for(self.gpa, b.built.entries.items.len);
}
// 5. Log (and sync) before anything becomes visible.
@@ -221,7 +242,7 @@ pub const Engine = struct {
try coll.docs.put(self.gpa, id_key, owned);
for (built_list.items) |*b| {
if (b.built.multikey) b.ix.multikey = true;
b.ix.insert_entries(self.gpa, &b.built);
b.ix.insert_entries(&b.built);
}
stored = true;
try self.maybe_compact();
@@ -293,32 +314,19 @@ pub const Engine = struct {
// parsed spec is only owned by the collection once committed.
defer if (!committed) ix.deinit(self.gpa);
for (coll.indexes.items) |*existing| {
if (std.mem.eql(u8, existing.name, ix.name)) {
if (index.Index.spec_equal(existing, &ix)) return existing;
return error.IndexOptionsConflict;
}
if (coll.find_index(ix.name)) |existing| {
if (index.Index.spec_equal(existing, &ix)) return existing;
return error.IndexOptionsConflict;
}
// Build entries over the existing documents, checking uniqueness as
// we go (the index is not exposed until the end, so mutating it is
// safe). Each batch is inserted into the index immediately (which
// drains it), so on any later failure the errdefer ix.deinit frees
// every inserted entry key; a batch that fails before insertion is
// freed by its own errdefer. Nothing is persisted on failure.
// safe). Each document's batch is inserted immediately, so on any
// later failure the deferred ix.deinit frees every inserted entry
// key. Nothing is persisted on failure.
var doc_it = coll.docs.iterator();
while (doc_it.next()) |entry| {
var built = try ix.build_entries(self.gpa, entry.value_ptr.*, entry.key_ptr.*);
// Runs on success too: insert_entries drains the keys, leaving
// only the (now empty) ArrayList buffer to free.
defer built.deinit(self.gpa);
if (built.multikey) ix.multikey = true;
if (ix.unique) {
try ix.check_unique(built.entries.items, entry.key_ptr.*);
}
if (built.entries.items.len == 0) continue;
try ix.entries.ensureUnusedCapacity(self.gpa, built.entries.items.len);
ix.insert_entries(self.gpa, &built);
_ = try ix.add_doc(self.gpa, entry.value_ptr.*, entry.key_ptr.*, true);
}
// Reserve the collection slot, then persist and publish.
@@ -339,14 +347,7 @@ pub const Engine = struct {
pub fn drop_index(self: *Engine, db_name: []const u8, coll_name: []const u8, index_name: []const u8) !bool {
const db = self.dbs.get(db_name) orelse return false;
const coll = db.collections.getPtr(coll_name) orelse return false;
var found = false;
for (coll.indexes.items) |ix| {
if (std.mem.eql(u8, ix.name, index_name)) {
found = true;
break;
}
}
if (!found) return false;
if (coll.find_index(index_name) == null) return false;
const name_pairs = [_]bson.Pair{.{ .key = "name", .value = .{ .string = index_name } }};
var name_doc: std.ArrayListUnmanaged(u8) = .empty;
@@ -355,13 +356,7 @@ pub const Engine = struct {
self.seq += 1;
try self.log.append_index_drop(db_name, coll_name, name_doc.items, self.seq);
var i: usize = 0;
while (i < coll.indexes.items.len) {
if (std.mem.eql(u8, coll.indexes.items[i].name, index_name)) {
var removed = coll.indexes.orderedRemove(i);
removed.deinit(self.gpa);
} else i += 1;
}
_ = coll.remove_index(self.gpa, index_name);
return true;
}
@@ -482,23 +477,16 @@ pub const Engine = struct {
if (ix.entries.items.len > 0) continue; // defensive
var doc_it = coll_entry.value_ptr.docs.iterator();
while (doc_it.next()) |doc_entry| {
var built = ix.build_entries(self.gpa, doc_entry.value_ptr.*, doc_entry.key_ptr.*) catch |err| switch (err) {
const duplicate = ix.add_doc(self.gpa, doc_entry.value_ptr.*, doc_entry.key_ptr.*, false) catch |err| switch (err) {
error.ParallelArrays => {
std.debug.print("mongo-light: WARNING: index '{s}' cannot index an existing document; entry skipped\n", .{ix.name});
continue;
},
else => return err,
};
defer built.deinit(self.gpa);
if (built.multikey) ix.multikey = true;
if (ix.unique) {
ix.check_unique(built.entries.items, doc_entry.key_ptr.*) catch {
std.debug.print("mongo-light: WARNING: unique index '{s}' has duplicate keys in existing data; duplicates not enforced for existing documents\n", .{ix.name});
};
if (duplicate) {
std.debug.print("mongo-light: WARNING: unique index '{s}' has duplicate keys in existing data; duplicates not enforced for existing documents\n", .{ix.name});
}
if (built.entries.items.len == 0) continue;
try ix.entries.ensureUnusedCapacity(self.gpa, built.entries.items.len);
ix.insert_entries(self.gpa, &built);
}
}
}
@@ -511,9 +499,7 @@ pub const Engine = struct {
var ix = try index.parse_spec(self.gpa, spec_doc);
var committed = false;
defer if (!committed) ix.deinit(self.gpa);
for (coll.indexes.items) |existing| {
if (std.mem.eql(u8, existing.name, ix.name)) return;
}
if (coll.find_index(ix.name) != null) return;
try coll.indexes.append(self.gpa, ix);
committed = true;
}
@@ -559,13 +545,7 @@ fn apply_record(ctx: *anyopaque, record: storage.Record, doc: *bson.Document) an
.string => |s| s,
else => return,
};
var i: usize = 0;
while (i < coll.indexes.items.len) {
if (std.mem.eql(u8, coll.indexes.items[i].name, name)) {
var removed = coll.indexes.orderedRemove(i);
removed.deinit(self.gpa);
} else i += 1;
}
_ = coll.remove_index(self.gpa, name);
return;
},
else => {},