commands: createIndexes/listIndexes/dropIndexes + planner wiring

Adds the three driver commands, parameterized E11000 messages (engine
dup_index carries the index name into writeErrors), the scan_matching
planner wiring (_id fast path → index plan → scan) and the first-$match
aggregate pushdown. The equivalence test (mixed-type corpus x 27 filters,
non-sparse and sparse indexes) drove out three real bugs: the two-bound
range under-approximation on multikey indexes (fall back to scan), a
dangling single-value option array in the planner, and update-time
unique violations now reporting writeErrors instead of corrupting state.
This commit is contained in:
2026-08-02 12:38:25 +03:00
parent a733fc1993
commit 7f2f7c6977
3 changed files with 750 additions and 34 deletions

View File

@@ -289,7 +289,9 @@ pub const Engine = struct {
const coll = try self.get_or_create_collection(db_name, coll_name);
var ix = try index.parse_spec(self.gpa, spec_doc);
var committed = false;
errdefer if (!committed) ix.deinit(self.gpa);
// Runs on every return path (including the idempotent no-op): the
// 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)) {
@@ -300,20 +302,16 @@ pub const Engine = struct {
// Build entries over the existing documents, checking uniqueness as
// we go (the index is not exposed until the end, so mutating it is
// safe). On any failure the built entries are freed and nothing is
// persisted.
var built_list: std.ArrayListUnmanaged(index.BuiltEntries) = .empty;
defer {
for (built_list.items) |*b| b.deinit(self.gpa);
built_list.deinit(self.gpa);
}
// 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.
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.*);
built_list.append(self.gpa, built) catch |err| {
built.deinit(self.gpa);
return err;
};
// 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.*);