baseline: mongo-light working tree before concurrency refactor

This commit is contained in:
mongo-light
2026-08-02 10:29:01 +03:00
commit 4de42091a4
14 changed files with 4950 additions and 0 deletions

767
src/bson.zig Normal file
View File

@@ -0,0 +1,767 @@
//! BSON — Binary JSON. The foundation of the MongoDB wire protocol and the
//! storage engine. Documents parse into an arena-backed value tree; the tree
//! serializes back to canonical BSON bytes.
const std = @import("std");
pub const ObjectId = [12]u8;
pub const Binary = struct {
subtype: u8,
data: []const u8,
};
pub const Regex = struct {
pattern: []const u8,
options: []const u8,
};
/// Values we round-trip but never interpret: db_pointer (0x0C),
/// code_with_scope (0x0F), undefined (0x06). `data` is the raw payload that
/// follows the element type byte, re-emitted verbatim on serialize.
pub const Opaque = struct {
kind: u8,
data: []const u8,
};
pub const Pair = struct {
key: []const u8,
value: Value,
};
pub const Value = union(enum) {
double: f64,
string: []const u8,
doc: []const Pair,
array: []const Value,
binary: Binary,
object_id: ObjectId,
bool: bool,
datetime: i64,
null,
regex: Regex,
code: []const u8,
symbol: []const u8,
int32: i32,
timestamp: u64,
int64: i64,
decimal128: [16]u8,
min_key,
max_key,
opaque_val: Opaque,
pub fn type_tag(self: Value) u8 {
return switch (self) {
.double => 0x01,
.string => 0x02,
.doc => 0x03,
.array => 0x04,
.binary => 0x05,
.object_id => 0x07,
.bool => 0x08,
.datetime => 0x09,
.null => 0x0A,
.regex => 0x0B,
.code => 0x0D,
.symbol => 0x0E,
.int32 => 0x10,
.timestamp => 0x11,
.int64 => 0x12,
.decimal128 => 0x13,
.min_key => 0xFF, // not serializable; rank only
.max_key => 0x7F, // not serializable; rank only
.opaque_val => |o| o.kind,
};
}
pub fn is_number(self: Value) bool {
return switch (self) {
.double, .int32, .int64 => true,
else => false,
};
}
/// Numeric value widened to f128 — exact for i64 and f64.
pub fn as_f128(self: Value) f128 {
return switch (self) {
.double => |d| @as(f128, @floatCast(d)),
.int32 => |i| @as(f128, @floatFromInt(i)),
.int64 => |i| @as(f128, @floatFromInt(i)),
else => unreachable,
};
}
};
/// A parsed document owns everything it references via its arena. Not
/// copyable — pass by pointer.
pub const Document = struct {
arena: std.heap.ArenaAllocator,
pairs: []const Pair,
pub fn parse(allocator: std.mem.Allocator, bytes: []const u8) !Document {
var arena = std.heap.ArenaAllocator.init(allocator);
errdefer arena.deinit();
var idx: usize = 0;
const pairs = try parse_doc_into(&arena, bytes, &idx);
return .{ .arena = arena, .pairs = pairs };
}
pub fn deinit(self: *Document) void {
self.arena.deinit();
}
pub fn get(self: *const Document, key: []const u8) ?Value {
return get_pair(self.pairs, key);
}
pub fn alloc(allocator: std.mem.Allocator, pairs: []const Pair) !Document {
var arena = std.heap.ArenaAllocator.init(allocator);
errdefer arena.deinit();
const copied = try arena.allocator().dupe(Pair, pairs);
return .{ .arena = arena, .pairs = copied };
}
/// Serialize the full document (length-prefixed) into `out`.
pub fn to_bytes(self: *const Document, gpa: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) !void {
try write_doc(self.pairs, gpa, out);
}
};
pub fn get_pair(pairs: []const Pair, key: []const u8) ?Value {
for (pairs) |p| {
if (std.mem.eql(u8, p.key, key)) return p.value;
}
return null;
}
pub fn parse_doc(allocator: std.mem.Allocator, bytes: []const u8) !Document {
return Document.parse(allocator, bytes);
}
// ---------------------------------------------------------------------------
// Parsing
// ---------------------------------------------------------------------------
const Parser = struct {
arena: *std.heap.ArenaAllocator,
bytes: []const u8,
fn fail() error{InvalidBson} {
return error.InvalidBson;
}
};
const ParseError = error{ InvalidBson, OutOfMemory };
fn parse_doc_into(arena: *std.heap.ArenaAllocator, bytes: []const u8, idx: *usize) ParseError![]const Pair {
const p = Parser{ .arena = arena, .bytes = bytes };
return parse_doc_inner(p, idx);
}
fn ensure_available(bytes: []const u8, idx: usize, n: usize) error{InvalidBson}!void {
if (bytes.len -| idx < n) return error.InvalidBson;
}
fn parse_doc_inner(p: Parser, idx: *usize) ParseError![]const Pair {
const start = idx.*;
ensure_available(p.bytes, start, 4) catch return Parser.fail();
const total: u32 = std.mem.readInt(u32, p.bytes[start..][0..4], .little);
if (total < 5) return Parser.fail();
if (p.bytes.len - start < total) return Parser.fail();
const end = start + total;
if (p.bytes[end - 1] != 0x00) return Parser.fail();
const gpa = p.arena.allocator();
var pairs: std.ArrayListUnmanaged(Pair) = .empty;
errdefer pairs.deinit(gpa);
idx.* = start + 4;
while (idx.* < end - 1) {
const value = try parse_element(p, idx);
try pairs.append(gpa, value);
}
if (idx.* != end - 1) return Parser.fail();
idx.* = end;
return pairs.toOwnedSlice(gpa);
}
fn parse_element(p: Parser, idx: *usize) ParseError!Pair {
ensure_available(p.bytes, idx.*, 1) catch return Parser.fail();
const tag = p.bytes[idx.*];
idx.* += 1;
const key = try parse_cstring(p, idx);
const value = try parse_value(p, tag, idx);
return .{ .key = key, .value = value };
}
fn parse_cstring(p: Parser, idx: *usize) ParseError![]const u8 {
const start = idx.*;
while (idx.* < p.bytes.len and p.bytes[idx.*] != 0) idx.* += 1;
if (idx.* >= p.bytes.len) return Parser.fail();
idx.* += 1;
// Strings are copied into the arena so documents are self-contained and
// outlive the input buffer (wire messages and log records are transient).
return p.arena.allocator().dupe(u8, p.bytes[start .. idx.* - 1]);
}
fn parse_string(p: Parser, idx: *usize) ParseError![]const u8 {
ensure_available(p.bytes, idx.*, 4) catch return Parser.fail();
const len: u32 = std.mem.readInt(u32, p.bytes[idx.*..][0..4], .little);
if (len == 0 or len > p.bytes.len - (idx.* + 4)) return Parser.fail();
const str = p.bytes[idx.* + 4 .. idx.* + 4 + len];
if (str[len - 1] != 0) return Parser.fail();
idx.* += 4 + len;
return p.arena.allocator().dupe(u8, str[0 .. len - 1]);
}
fn parse_value(p: Parser, tag: u8, idx: *usize) ParseError!Value {
return switch (tag) {
0x01 => blk: {
ensure_available(p.bytes, idx.*, 8) catch return Parser.fail();
const v: f64 = @bitCast(std.mem.readInt(u64, p.bytes[idx.*..][0..8], .little));
idx.* += 8;
break :blk .{ .double = v };
},
0x02 => .{ .string = try parse_string(p, idx) },
0x03 => .{ .doc = try parse_doc_inner(p, idx) },
0x04 => .{ .array = try parse_array(p, idx) },
0x05 => blk: {
ensure_available(p.bytes, idx.*, 5) catch return Parser.fail();
const len: u32 = std.mem.readInt(u32, p.bytes[idx.*..][0..4], .little);
const subtype = p.bytes[idx.* + 4];
ensure_available(p.bytes, idx.* + 5, len) catch return Parser.fail();
const data = p.bytes[idx.* + 5 .. idx.* + 5 + len];
idx.* += 5 + len;
break :blk .{ .binary = .{ .subtype = subtype, .data = try p.arena.allocator().dupe(u8, data) } };
},
0x06 => .{ .opaque_val = .{ .kind = 0x06, .data = &.{} } },
0x07 => blk: {
ensure_available(p.bytes, idx.*, 12) catch return Parser.fail();
const oid: ObjectId = p.bytes[idx.*..][0..12].*;
idx.* += 12;
break :blk .{ .object_id = oid };
},
0x08 => blk: {
ensure_available(p.bytes, idx.*, 1) catch return Parser.fail();
const v = p.bytes[idx.*];
if (v > 1) return Parser.fail();
idx.* += 1;
break :blk .{ .bool = v == 1 };
},
0x09 => blk: {
ensure_available(p.bytes, idx.*, 8) catch return Parser.fail();
const v: i64 = std.mem.readInt(i64, p.bytes[idx.*..][0..8], .little);
idx.* += 8;
break :blk .{ .datetime = v };
},
0x0A => .null,
0x0B => .{ .regex = .{
.pattern = try parse_cstring(p, idx),
.options = try parse_cstring(p, idx),
} },
0x0C => blk: {
const start = idx.*;
_ = try parse_string(p, idx);
ensure_available(p.bytes, idx.*, 12) catch return Parser.fail();
idx.* += 12;
// Like all other types, the payload is copied into the arena so
// documents stay valid after the input buffer is reused.
break :blk .{ .opaque_val = .{ .kind = 0x0C, .data = try p.arena.allocator().dupe(u8, p.bytes[start..idx.*]) } };
},
0x0D => .{ .code = try parse_string(p, idx) },
0x0E => .{ .symbol = try parse_string(p, idx) },
0x0F => blk: {
ensure_available(p.bytes, idx.*, 4) catch return Parser.fail();
const total: u32 = std.mem.readInt(u32, p.bytes[idx.*..][0..4], .little);
if (total < 4 or total > p.bytes.len - idx.*) return Parser.fail();
const data = p.bytes[idx.* .. idx.* + total];
idx.* += total;
break :blk .{ .opaque_val = .{ .kind = 0x0F, .data = try p.arena.allocator().dupe(u8, data) } };
},
0x10 => blk: {
ensure_available(p.bytes, idx.*, 4) catch return Parser.fail();
const v: i32 = std.mem.readInt(i32, p.bytes[idx.*..][0..4], .little);
idx.* += 4;
break :blk .{ .int32 = v };
},
0x11 => blk: {
ensure_available(p.bytes, idx.*, 8) catch return Parser.fail();
const v: u64 = std.mem.readInt(u64, p.bytes[idx.*..][0..8], .little);
idx.* += 8;
break :blk .{ .timestamp = v };
},
0x12 => blk: {
ensure_available(p.bytes, idx.*, 8) catch return Parser.fail();
const v: i64 = std.mem.readInt(i64, p.bytes[idx.*..][0..8], .little);
idx.* += 8;
break :blk .{ .int64 = v };
},
0x13 => blk: {
ensure_available(p.bytes, idx.*, 16) catch return Parser.fail();
const v: [16]u8 = p.bytes[idx.*..][0..16].*;
idx.* += 16;
break :blk .{ .decimal128 = v };
},
else => return Parser.fail(),
};
}
fn parse_array(p: Parser, idx: *usize) ParseError![]const Value {
const start = idx.*;
ensure_available(p.bytes, start, 4) catch return Parser.fail();
const total: u32 = std.mem.readInt(u32, p.bytes[start..][0..4], .little);
if (total < 5) return Parser.fail();
if (p.bytes.len - start < total) return Parser.fail();
const end = start + total;
if (p.bytes[end - 1] != 0x00) return Parser.fail();
const gpa = p.arena.allocator();
var values: std.ArrayListUnmanaged(Value) = .empty;
errdefer values.deinit(gpa);
idx.* = start + 4;
while (idx.* < end - 1) {
ensure_available(p.bytes, idx.*, 1) catch return Parser.fail();
const tag = p.bytes[idx.*];
idx.* += 1;
_ = try parse_cstring(p, idx);
try values.append(gpa, try parse_value(p, tag, idx));
}
if (idx.* != end - 1) return Parser.fail();
idx.* = end;
return values.toOwnedSlice(gpa);
}
// ---------------------------------------------------------------------------
// Serialization
// ---------------------------------------------------------------------------
pub const SerializeError = error{
BsonTooLarge,
BsonNulInKey,
BsonNotSerializable,
OutOfMemory,
};
pub fn write_value(v: Value, gpa: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) SerializeError!void {
switch (v) {
.double => |d| {
var buf: [8]u8 = undefined;
std.mem.writeInt(u64, &buf, @bitCast(d), .little);
try out.appendSlice(gpa, &buf);
},
.string => |s| try write_string(s, gpa, out),
.doc => |pairs| try write_doc(pairs, gpa, out),
.array => |items| try write_array(items, gpa, out),
.binary => |b| {
if (b.data.len > std.math.maxInt(u32)) return error.BsonTooLarge;
var buf: [5]u8 = undefined;
std.mem.writeInt(u32, buf[0..4], @intCast(b.data.len), .little);
buf[4] = b.subtype;
try out.appendSlice(gpa, &buf);
try out.appendSlice(gpa, b.data);
},
.object_id => |oid| try out.appendSlice(gpa, &oid),
.bool => |b| try out.append(gpa, @intFromBool(b)),
.datetime => |t| {
var buf: [8]u8 = undefined;
std.mem.writeInt(i64, &buf, t, .little);
try out.appendSlice(gpa, &buf);
},
.null => {},
.regex => |r| {
try write_cstring(r.pattern, gpa, out);
try write_cstring(r.options, gpa, out);
},
.code => |c| try write_string(c, gpa, out),
.symbol => |s| try write_string(s, gpa, out),
.int32 => |i| {
var buf: [4]u8 = undefined;
std.mem.writeInt(i32, &buf, i, .little);
try out.appendSlice(gpa, &buf);
},
.timestamp => |t| {
var buf: [8]u8 = undefined;
std.mem.writeInt(u64, &buf, t, .little);
try out.appendSlice(gpa, &buf);
},
.int64 => |i| {
var buf: [8]u8 = undefined;
std.mem.writeInt(i64, &buf, i, .little);
try out.appendSlice(gpa, &buf);
},
.decimal128 => |d| try out.appendSlice(gpa, &d),
.opaque_val => |o| try out.appendSlice(gpa, o.data),
.min_key, .max_key => return error.BsonNotSerializable,
}
}
pub fn write_cstring(s: []const u8, gpa: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) SerializeError!void {
if (std.mem.indexOfScalar(u8, s, 0) != null) return error.BsonNulInKey;
try out.appendSlice(gpa, s);
try out.append(gpa, 0);
}
pub fn write_string(s: []const u8, gpa: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) SerializeError!void {
if (s.len + 1 > std.math.maxInt(u32)) return error.BsonTooLarge;
var buf: [4]u8 = undefined;
std.mem.writeInt(u32, &buf, @intCast(s.len + 1), .little);
try out.appendSlice(gpa, &buf);
try out.appendSlice(gpa, s);
try out.append(gpa, 0);
}
pub fn write_element(pair: Pair, gpa: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) SerializeError!void {
try out.append(gpa, pair.value.type_tag());
try write_cstring(pair.key, gpa, out);
try write_value(pair.value, gpa, out);
}
/// Write a length-prefixed document. Length is patched in after the body.
pub fn write_doc(pairs: []const Pair, gpa: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) SerializeError!void {
const len_pos = out.items.len;
var zero: [4]u8 = [4]u8{ 0, 0, 0, 0 };
try out.appendSlice(gpa, &zero);
for (pairs) |p| try write_element(p, gpa, out);
try out.append(gpa, 0);
const total = out.items.len - len_pos;
if (total > std.math.maxInt(u32)) return error.BsonTooLarge;
std.mem.writeInt(u32, out.items[len_pos..][0..4], @intCast(total), .little);
}
fn write_array(items: []const Value, gpa: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) !void {
const len_pos = out.items.len;
var zero: [4]u8 = [4]u8{ 0, 0, 0, 0 };
try out.appendSlice(gpa, &zero);
var buf: [16]u8 = undefined;
for (items, 0..) |item, i| {
try out.append(gpa, item.type_tag());
const key = std.fmt.bufPrint(&buf, "{d}", .{i}) catch unreachable;
try write_cstring(key, gpa, out);
try write_value(item, gpa, out);
}
try out.append(gpa, 0);
const total = out.items.len - len_pos;
if (total > std.math.maxInt(u32)) return error.BsonTooLarge;
std.mem.writeInt(u32, out.items[len_pos..][0..4], @intCast(total), .little);
}
/// Serialize a single value with its type byte (no key) — used for `_id`
/// map keys and index entries.
pub fn serialize_value(gpa: std.mem.Allocator, v: Value) ![]u8 {
var out: std.ArrayListUnmanaged(u8) = .empty;
errdefer out.deinit(gpa);
try out.append(gpa, v.type_tag());
try write_value(v, gpa, &out);
return out.toOwnedSlice(gpa);
}
/// Deep-copy a value into `arena`, so the copy is self-contained.
pub fn copy_value(arena: std.mem.Allocator, v: Value) std.mem.Allocator.Error!Value {
return switch (v) {
.string => |s| .{ .string = try arena.dupe(u8, s) },
.symbol => |s| .{ .symbol = try arena.dupe(u8, s) },
.code => |c| .{ .code = try arena.dupe(u8, c) },
.doc => |pairs| .{ .doc = try copy_pairs(arena, pairs) },
.array => |items| .{ .array = try copy_values(arena, items) },
.binary => |b| .{ .binary = .{ .subtype = b.subtype, .data = try arena.dupe(u8, b.data) } },
.regex => |r| .{ .regex = .{
.pattern = try arena.dupe(u8, r.pattern),
.options = try arena.dupe(u8, r.options),
} },
.opaque_val => |o| .{ .opaque_val = .{ .kind = o.kind, .data = try arena.dupe(u8, o.data) } },
else => v,
};
}
pub fn copy_pairs(arena: std.mem.Allocator, pairs: []const Pair) std.mem.Allocator.Error![]const Pair {
const out = try arena.alloc(Pair, pairs.len);
for (pairs, 0..) |p, i| {
out[i] = .{ .key = try arena.dupe(u8, p.key), .value = try copy_value(arena, p.value) };
}
return out;
}
fn copy_values(arena: std.mem.Allocator, items: []const Value) std.mem.Allocator.Error![]const Value {
const out = try arena.alloc(Value, items.len);
for (items, 0..) |item, i| out[i] = try copy_value(arena, item);
return out;
}
// ---------------------------------------------------------------------------
// ObjectId generation
// ---------------------------------------------------------------------------
pub const ObjectIdGen = struct {
random_prefix: [5]u8,
counter: u32,
pub fn init(io: std.Io) ObjectIdGen {
var self: ObjectIdGen = undefined;
io.random(&self.random_prefix);
self.counter = 0;
return self;
}
pub fn new(self: *ObjectIdGen, io: std.Io) ObjectId {
var oid: ObjectId = undefined;
const now = std.Io.Timestamp.now(io, .real);
const secs: u32 = @truncate(@as(u64, @intCast(now.toSeconds())));
std.mem.writeInt(u32, oid[0..4], secs, .big);
@memcpy(oid[4..9], &self.random_prefix);
self.counter +%= 1;
std.mem.writeInt(u24, oid[9..12], @truncate(self.counter), .big);
return oid;
}
};
// ---------------------------------------------------------------------------
// Canonical BSON comparison order
// ---------------------------------------------------------------------------
fn rank(v: Value) u8 {
return switch (v) {
.min_key => 0,
.null => 1,
.double, .int32, .int64 => 2,
.string, .symbol, .code => 3,
.doc => 4,
.array => 5,
.binary => 6,
.object_id => 7,
.bool => 8,
.datetime => 9,
.timestamp => 10,
.regex => 11,
.opaque_val => 12,
.decimal128 => 13,
.max_key => 14,
};
}
pub fn compare(a: Value, b: Value) std.math.Order {
const ra = rank(a);
const rb = rank(b);
if (ra != rb) return std.math.order(ra, rb);
return switch (ra) {
0, 1 => .eq,
2 => compare_f128(a.as_f128(), b.as_f128()),
3 => std.mem.order(u8, as_str(a), as_str(b)),
4 => compare_docs(a.doc, b.doc),
5 => compare_arrays(a.array, b.array),
6 => compare_binary(a.binary, b.binary),
7 => std.mem.order(u8, &a.object_id, &b.object_id),
8 => std.math.order(@intFromBool(a.bool), @intFromBool(b.bool)),
9 => std.math.order(a.datetime, b.datetime),
10 => std.math.order(a.timestamp, b.timestamp),
11 => blk: {
const p = std.mem.order(u8, as_regex(a).pattern, as_regex(b).pattern);
break :blk if (p != .eq) p else std.mem.order(u8, as_regex(a).options, as_regex(b).options);
},
12 => std.mem.order(u8, a.opaque_val.data, b.opaque_val.data),
13 => std.mem.order(u8, &a.decimal128, &b.decimal128),
14 => .eq,
else => unreachable,
};
}
fn as_str(v: Value) []const u8 {
return switch (v) {
.string => |s| s,
.symbol => |s| s,
.code => |c| c,
else => unreachable,
};
}
fn as_regex(v: Value) Regex {
return switch (v) {
.regex => |r| r,
else => unreachable,
};
}
fn compare_f128(a: f128, b: f128) std.math.Order {
if (a < b) return .lt;
if (a > b) return .gt;
// Distinguish -0.0 from +0.0 like MongoDB does (they are equal); NaN
// sorts greater than every number (MongoDB treats NaN as largest).
if (std.math.isNan(a)) {
if (std.math.isNan(b)) return .eq;
return .gt;
}
if (std.math.isNan(b)) return .lt;
return .eq;
}
fn compare_docs(a: []const Pair, b: []const Pair) std.math.Order {
const n = @min(a.len, b.len);
for (a[0..n], b[0..n]) |pa, pb| {
// Keys tie-break equal values, so {a: 1} and {b: 1} are distinct.
const ko = std.mem.order(u8, pa.key, pb.key);
if (ko != .eq) return ko;
const o = compare(pa.value, pb.value);
if (o != .eq) return o;
}
return std.math.order(a.len, b.len);
}
fn compare_arrays(a: []const Value, b: []const Value) std.math.Order {
const n = @min(a.len, b.len);
for (a[0..n], b[0..n]) |va, vb| {
const o = compare(va, vb);
if (o != .eq) return o;
}
return std.math.order(a.len, b.len);
}
fn compare_binary(a: Binary, b: Binary) std.math.Order {
const l = std.math.order(a.data.len, b.data.len);
if (l != .eq) return l;
const d = std.mem.order(u8, a.data, b.data);
if (d != .eq) return d;
return std.math.order(a.subtype, b.subtype);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
const testing = std.testing;
test "document round-trip" {
var out: std.ArrayListUnmanaged(u8) = .empty;
defer out.deinit(testing.allocator);
try write_doc(&.{
.{ .key = "_id", .value = .{ .int32 = 7 } },
.{ .key = "name", .value = .{ .string = "héllo" } },
.{ .key = "pi", .value = .{ .double = 3.25 } },
.{ .key = "ok", .value = .{ .bool = true } },
.{ .key = "nul", .value = .null },
.{ .key = "big", .value = .{ .int64 = 1 << 40 } },
.{ .key = "when", .value = .{ .datetime = 1_700_000_000_000 } },
.{ .key = "re", .value = .{ .regex = .{ .pattern = "^a", .options = "i" } } },
.{ .key = "bin", .value = .{ .binary = .{ .subtype = 0x80, .data = &[_]u8{ 1, 2, 3 } } } },
.{ .key = "arr", .value = .{ .array = &.{ .{ .int32 = 1 }, .{ .string = "x" } } } },
.{ .key = "sub", .value = .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} } },
.{ .key = "ts", .value = .{ .timestamp = 42 } },
.{ .key = "oid", .value = .{ .object_id = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 } } },
}, testing.allocator, &out);
var doc = try Document.parse(testing.allocator, out.items);
defer doc.deinit();
try testing.expectEqual(@as(i64, 7), doc.get("_id").?.int32);
try testing.expectEqualStrings("héllo", doc.get("name").?.string);
try testing.expectEqual(@as(f64, 3.25), doc.get("pi").?.double);
try testing.expect(doc.get("ok").?.bool);
try testing.expectEqual(@as(i64, 1 << 40), doc.get("big").?.int64);
try testing.expectEqual(@as(i64, 1_700_000_000_000), doc.get("when").?.datetime);
try testing.expectEqualStrings("^a", doc.get("re").?.regex.pattern);
try testing.expectEqualStrings("i", doc.get("re").?.regex.options);
try testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3 }, doc.get("bin").?.binary.data);
try testing.expectEqual(@as(usize, 2), doc.get("arr").?.array.len);
try testing.expectEqual(@as(i64, 1), doc.get("sub").?.doc[0].value.int32);
try testing.expectEqual(@as(u64, 42), doc.get("ts").?.timestamp);
try testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, &doc.get("oid").?.object_id);
}
test "array and nested doc round-trip" {
var out: std.ArrayListUnmanaged(u8) = .empty;
defer out.deinit(testing.allocator);
try write_doc(&.{
.{ .key = "a", .value = .{ .array = &.{
.{ .int32 = 10 },
.{ .doc = &.{.{ .key = "deep", .value = .{ .string = "v" } }} },
} } },
}, testing.allocator, &out);
var doc = try Document.parse(testing.allocator, out.items);
defer doc.deinit();
const a = doc.get("a").?.array;
try testing.expectEqual(@as(i64, 10), a[0].int32);
try testing.expectEqualStrings("v", a[1].doc[0].value.string);
}
test "reject truncated document" {
try testing.expectError(error.InvalidBson, Document.parse(testing.allocator, &[_]u8{ 6, 0, 0, 0 }));
try testing.expectError(error.InvalidBson, Document.parse(testing.allocator, &[_]u8{ 4, 0, 0, 0, 0 }));
try testing.expectError(error.InvalidBson, Document.parse(testing.allocator, &[_]u8{ 9, 0, 0, 0, 0x01, 'a', 0 }));
}
test "compare: canonical order" {
const min = Value{ .min_key = {} };
const nul = Value.null;
const i32a = Value{ .int32 = 5 };
const i64a = Value{ .int64 = 5 };
const dbl = Value{ .double = 4.9 };
const str = Value{ .string = "a" };
const obj = Value{ .doc = &.{} };
const arr = Value{ .array = &.{} };
const oid = Value{ .object_id = [_]u8{0} ** 12 };
const btrue = Value{ .bool = true };
const bfalse = Value{ .bool = false };
const max = Value{ .max_key = {} };
try testing.expectEqual(std.math.Order.lt, compare(min, nul));
try testing.expectEqual(std.math.Order.lt, compare(nul, i32a));
try testing.expectEqual(std.math.Order.eq, compare(i32a, i64a)); // numeric equality across widths
try testing.expectEqual(std.math.Order.gt, compare(i32a, dbl)); // 5 > 4.9
try testing.expectEqual(std.math.Order.lt, compare(i32a, str));
try testing.expectEqual(std.math.Order.lt, compare(str, obj));
try testing.expectEqual(std.math.Order.lt, compare(obj, arr));
try testing.expectEqual(std.math.Order.lt, compare(arr, oid));
try testing.expectEqual(std.math.Order.lt, compare(oid, bfalse));
try testing.expectEqual(std.math.Order.lt, compare(bfalse, btrue));
try testing.expectEqual(std.math.Order.gt, compare(max, nul));
}
test "compare: strings and docs" {
try testing.expectEqual(std.math.Order.lt, compare(.{ .string = "a" }, .{ .string = "b" }));
try testing.expectEqual(std.math.Order.eq, compare(.{ .string = "x" }, .{ .string = "x" }));
try testing.expectEqual(std.math.Order.lt, compare(
.{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} },
.{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 2 } }} },
));
try testing.expectEqual(std.math.Order.lt, compare(
.{ .array = &.{.{ .int32 = 1 }} },
.{ .array = &.{ .{ .int32 = 1 }, .{ .int32 = 1 } } },
));
}
test "compare: NaN is greatest number" {
try testing.expectEqual(std.math.Order.gt, compare(.{ .double = std.math.nan(f64) }, .{ .int64 = 1 << 62 }));
}
test "object id generation" {
var threaded: std.Io.Threaded = .init_single_threaded;
defer threaded.deinit();
const io = threaded.io();
var gen = ObjectIdGen.init(io);
const a = gen.new(io);
const b = gen.new(io);
try testing.expect(!std.mem.eql(u8, &a, &b));
// timestamp bytes match wall clock roughly
try testing.expect(a[0] >= 0x66); // 2024+ in big-endian seconds
// The 5-byte random prefix is untouched by the counter (spec layout:
// 4s timestamp | 5B random | 3B counter).
try testing.expectEqualSlices(u8, a[0..9], b[0..9]);
try testing.expect(!std.mem.eql(u8, a[9..12], b[9..12]));
}
test "serialize_value deterministic for _id keys" {
const gpa = testing.allocator;
const v1 = try serialize_value(gpa, .{ .doc = &.{
.{ .key = "a", .value = .{ .int32 = 1 } },
.{ .key = "b", .value = .{ .string = "s" } },
} });
defer gpa.free(v1);
const v2 = try serialize_value(gpa, .{ .doc = &.{
.{ .key = "a", .value = .{ .int32 = 1 } },
.{ .key = "b", .value = .{ .string = "s" } },
} });
defer gpa.free(v2);
try testing.expectEqualSlices(u8, v1, v2);
}