index: secondary index core — entries, search, planner, _id fast path

Adds src/index.zig with the full secondary-index machinery: entry
generation mirroring field_matches (array value + elements), BSON-order
sorted entries with binary search, compound prefix and range lookups,
unique/sparse options, the query planner (longest equality/$in run +
optional range, $in cartesian cap, sparse/null bail), and the _id_ fast
path guarded against serialization-ambiguous values (numbers, strings,
symbols, codes, opaque payloads).

query.collect_values is now pub so entry generation can mirror it exactly.
storage.zig gains record_type_index_create/drop; lib.zig exports index.
This commit is contained in:
2026-08-02 12:21:22 +03:00
parent 662df9b121
commit a38ddc2f50
10 changed files with 1824 additions and 782 deletions

View File

@@ -13,9 +13,6 @@ pub const op_code_reply: i32 = 2001;
pub const max_message_size: usize = 48 * 1024 * 1024;
pub const max_bson_object_size: i32 = 16 * 1024 * 1024;
pub const flag_checksum_present: u32 = 1 << 1;
pub const flag_more_to_come: u32 = 1 << 0;
pub const Seq = struct {
name: []const u8,
docs: []bson.Document,
@@ -175,6 +172,37 @@ pub const Message = struct {
else => null,
};
}
/// Documents of a batch argument (`documents`, `updates`, `deletes`).
/// Drivers send them either as an OP_MSG document sequence or as an array
/// inside the command body; callers should not have to care which. The
/// result borrows this message's storage — never deinit the documents.
pub fn batch(self: *Message, name: []const u8) BatchError![]const bson.Document {
for (self.seqs) |seq| {
if (std.mem.eql(u8, seq.name, name) and seq.docs.len > 0) return seq.docs;
}
const arr = switch (bson.get_pair(self.body.pairs, name) orelse return error.MissingBatch) {
.array => |a| a,
else => return error.BatchNotArray,
};
const docs = try self.arena.allocator().alloc(bson.Document, arr.len);
for (arr, 0..) |item, i| {
docs[i] = switch (item) {
// Pairs are borrowed from the body document, which owns the
// arena; these views must never be deinited.
.doc => |pairs| .{ .arena = undefined, .pairs = pairs },
else => return error.BatchElementNotDoc,
};
}
return docs;
}
};
pub const BatchError = error{
MissingBatch, // no sequence and no body field of that name
BatchNotArray, // body field is present but not an array
BatchElementNotDoc, // an array element is not a document
OutOfMemory,
};
/// Builder for a command reply document. Strings for keys and values must
@@ -221,6 +249,32 @@ pub const Reply = struct {
}
};
/// Write the 16-byte message header with a zero length placeholder. Returns
/// the offset `end_message` needs to patch the length in.
fn begin_message(
gpa: std.mem.Allocator,
out: *std.ArrayListUnmanaged(u8),
request_id: u32,
response_to: u32,
op_code: i32,
) !usize {
const len_pos = out.items.len;
var header: [16]u8 = undefined;
std.mem.writeInt(u32, header[0..4], 0, .little); // patched by end_message
std.mem.writeInt(u32, header[4..8], request_id, .little);
std.mem.writeInt(u32, header[8..12], response_to, .little);
std.mem.writeInt(i32, header[12..16], op_code, .little);
try out.appendSlice(gpa, &header);
return len_pos;
}
/// Patch in the total length of the message started at `len_pos`.
fn end_message(out: *std.ArrayListUnmanaged(u8), len_pos: usize) !void {
const total = out.items.len - len_pos;
if (total > std.math.maxInt(u32)) return error.MessageTooLarge;
std.mem.writeInt(u32, out.items[len_pos..][0..4], @intCast(total), .little);
}
/// Serialize an OP_MSG reply: header + flags + single body section.
pub fn write_message(
gpa: std.mem.Allocator,
@@ -230,18 +284,13 @@ pub fn write_message(
body: []const bson.Pair,
out: *std.ArrayListUnmanaged(u8),
) !void {
const len_pos = out.items.len;
var header: [20]u8 = undefined;
std.mem.writeInt(u32, header[4..8], request_id, .little);
std.mem.writeInt(u32, header[8..12], response_to, .little);
std.mem.writeInt(i32, header[12..16], op_code_msg, .little);
std.mem.writeInt(u32, header[16..20], flags, .little);
try out.appendSlice(gpa, &header);
const len_pos = try begin_message(gpa, out, request_id, response_to, op_code_msg);
var flag_bytes: [4]u8 = undefined;
std.mem.writeInt(u32, &flag_bytes, flags, .little);
try out.appendSlice(gpa, &flag_bytes);
try out.append(gpa, 0x00); // single body section
try bson.write_doc(body, gpa, out);
const total = out.items.len - len_pos;
if (total > std.math.maxInt(u32)) return error.MessageTooLarge;
std.mem.writeInt(u32, out.items[len_pos..][0..4], @intCast(total), .little);
try end_message(out, len_pos);
}
/// Serialize an OP_REPLY (legacy): header + responseFlags | cursorID |
@@ -254,19 +303,12 @@ pub fn write_reply_query(
body: []const bson.Pair,
out: *std.ArrayListUnmanaged(u8),
) !void {
const len_pos = out.items.len;
var header: [16]u8 = undefined;
std.mem.writeInt(u32, header[4..8], request_id, .little);
std.mem.writeInt(u32, header[8..12], response_to, .little);
std.mem.writeInt(i32, header[12..16], op_code_reply, .little);
try out.appendSlice(gpa, &header);
const len_pos = try begin_message(gpa, out, request_id, response_to, op_code_reply);
var reply_fields: [20]u8 = [_]u8{0} ** 20; // responseFlags | cursorID | startingFrom | numberReturned
std.mem.writeInt(i32, reply_fields[16..20], 1, .little); // numberReturned = 1
try out.appendSlice(gpa, &reply_fields);
try bson.write_doc(body, gpa, out);
const total = out.items.len - len_pos;
if (total > std.math.maxInt(u32)) return error.MessageTooLarge;
std.mem.writeInt(u32, out.items[len_pos..][0..4], @intCast(total), .little);
try end_message(out, len_pos);
}
// ---------------------------------------------------------------------------