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

@@ -14,6 +14,8 @@ const bson = @import("bson.zig");
pub const record_type_upsert: u8 = 1;
pub const record_type_delete: u8 = 2;
pub const record_type_index_create: u8 = 3;
pub const record_type_index_drop: u8 = 4;
pub const header_len: usize = 20; // len + crc + seq + type + reserved
@@ -26,7 +28,6 @@ pub const Record = struct {
type: u8,
db: []const u8, // transient: valid only during replay callback
coll: []const u8,
doc: []const u8, // raw bson bytes
};
pub const Error = error{
@@ -44,6 +45,9 @@ pub const Log = struct {
path: []const u8,
end_pos: u64,
log_bytes: u64, // bytes written since the log was last rewritten
// Reused record-framing buffer. Appends are single-writer (the engine's
// exclusive lock), so one buffer avoids a realloc cycle per record.
scratch: std.ArrayListUnmanaged(u8),
pub fn open(gpa: std.mem.Allocator, io: std.Io, path: []const u8) !Log {
// Resolve to an absolute path so compaction can rename the file
@@ -68,10 +72,12 @@ pub const Log = struct {
.path = abs_path,
.end_pos = 0,
.log_bytes = 0,
.scratch = .empty,
};
}
pub fn close(self: *Log) void {
self.scratch.deinit(self.gpa);
self.file.close(self.io);
self.gpa.free(self.path);
}
@@ -134,7 +140,6 @@ pub const Log = struct {
.type = rtype,
.db = db,
.coll = coll,
.doc = doc_bytes,
}, doc);
pos += total;
@@ -150,12 +155,23 @@ pub const Log = struct {
try self.append(record_type_delete, db, coll, doc, seq);
}
/// The payload is the canonical index spec document ({v, key, name,
/// unique?, sparse?}); only apply_record interprets it.
pub fn append_index_create(self: *Log, db: []const u8, coll: []const u8, doc: []const u8, seq: u64) !void {
try self.append(record_type_index_create, db, coll, doc, seq);
}
/// The payload is {name: "..."}; only apply_record interprets it.
pub fn append_index_drop(self: *Log, db: []const u8, coll: []const u8, doc: []const u8, seq: u64) !void {
try self.append(record_type_index_drop, db, coll, doc, seq);
}
fn append(self: *Log, rtype: u8, db: []const u8, coll: []const u8, doc: []const u8, seq: u64) !void {
if (std.mem.indexOfScalar(u8, db, 0) != null or std.mem.indexOfScalar(u8, coll, 0) != null) {
return error.NulInName;
}
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(self.gpa);
const buf = &self.scratch;
buf.clearRetainingCapacity();
try buf.appendNTimes(self.gpa, 0, header_len);
std.mem.writeInt(u64, buf.items[8..16], seq, .little);
buf.items[16] = rtype;
@@ -189,17 +205,19 @@ pub const Log = struct {
const testing = std.testing;
const TmpLog = struct {
/// Throwaway log file in the test temp dir, shared by the storage and
/// engine test suites.
pub const TmpLog = struct {
tmp: std.testing.TmpDir,
path: []u8,
fn init(gpa: std.mem.Allocator) !TmpLog {
pub fn init(gpa: std.mem.Allocator) !TmpLog {
const tmp = std.testing.tmpDir(.{});
const path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/test.log", .{tmp.sub_path});
return .{ .tmp = tmp, .path = path };
}
fn deinit(self: *TmpLog, gpa: std.mem.Allocator) void {
pub fn deinit(self: *TmpLog, gpa: std.mem.Allocator) void {
self.tmp.cleanup();
gpa.free(self.path);
}