baseline: mongo-light working tree before concurrency refactor
This commit is contained in:
767
src/bson.zig
Normal file
767
src/bson.zig
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user