M3: the update operators, and 's modifiers #8

Merged
dev merged 9 commits from m3-operators into main 2026-08-10 18:41:26 +00:00
Showing only changes of commit 301a4538fd - Show all commits

View File

@@ -610,110 +610,142 @@ fn apply_operator(
value: bson.Value, value: bson.Value,
opts: Options, opts: Options,
) UpdateError!void { ) UpdateError!void {
const diag = opts.diag; // Every operator's argument is a document of path/operand pairs, so the
if (std.mem.eql(u8, op, "$set")) { // shape is checked once here rather than at the top of each.
const ops = doc_pairs(value) orelse return error.InvalidUpdate; const ops = doc_pairs(value) orelse return error.InvalidUpdate;
for (ops) |p| { if (std.mem.eql(u8, op, "$set")) return op_set(arena, pairs, ops, opts);
if (std.mem.eql(u8, p.key, "_id")) return error.ImmutableId; if (std.mem.eql(u8, op, "$unset")) return op_unset(arena, pairs, ops, opts);
for (try resolve(arena, pairs.items, p.key, opts)) |segs| { if (std.mem.eql(u8, op, "$inc")) return op_inc(arena, pairs, ops, opts);
try set_path(arena, pairs, segs, try bson.copy_value(arena, p.value), p.key, diag); if (std.mem.eql(u8, op, "$push")) return op_push(arena, pairs, ops, opts);
} if (std.mem.eql(u8, op, "$pull")) return op_pull(arena, pairs, ops, opts);
} if (std.mem.eql(u8, op, "$rename")) return op_rename(arena, pairs, ops, opts);
return;
}
if (std.mem.eql(u8, op, "$unset")) {
const ops = doc_pairs(value) orelse return error.InvalidUpdate;
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
unset_path(arena, pairs, segs);
}
}
return;
}
if (std.mem.eql(u8, op, "$inc")) {
const ops = doc_pairs(value) orelse return error.InvalidUpdate;
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
const current = get_value(pairs.items, segs) orelse bson.Value{ .int32 = 0 };
if (!current.is_number() or !p.value.is_number()) return error.InvalidUpdate;
const sum = try numeric_add(current, p.value);
try set_path(arena, pairs, segs, sum, p.key, diag);
}
}
return;
}
if (std.mem.eql(u8, op, "$push")) {
const ops = doc_pairs(value) orelse return error.InvalidUpdate;
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
const current_opt = get_value(pairs.items, segs);
var items: std.ArrayListUnmanaged(bson.Value) = .empty;
defer items.deinit(arena);
if (current_opt) |current| {
switch (current) {
.array => |arr| try items.appendSlice(arena, arr),
.null => {},
else => return error.InvalidUpdate, // non-array field
}
}
if (p.value == .doc) {
if (bson.get_pair(p.value.doc, "$each")) |each| {
const arr = switch (each) {
.array => |a| a,
else => return error.InvalidUpdate,
};
for (arr) |item| try items.append(arena, try bson.copy_value(arena, item));
try set_path(arena, pairs, segs, .{ .array = try items.toOwnedSlice(arena) }, p.key, diag);
continue;
}
}
try items.append(arena, try bson.copy_value(arena, p.value));
try set_path(arena, pairs, segs, .{ .array = try items.toOwnedSlice(arena) }, p.key, diag);
}
}
return;
}
if (std.mem.eql(u8, op, "$pull")) {
const ops = doc_pairs(value) orelse return error.InvalidUpdate;
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
const current = get_value(pairs.items, segs) orelse continue;
const arr = switch (current) {
.array => |a| a,
else => return error.InvalidUpdate,
};
var items: std.ArrayListUnmanaged(bson.Value) = .empty;
defer items.deinit(arena);
for (arr) |elem| {
if (!pull_matches(arena, p.value, elem)) {
try items.append(arena, elem);
}
}
try set_path(arena, pairs, segs, .{ .array = try items.toOwnedSlice(arena) }, p.key, diag);
}
}
return;
}
// `$rename` alone keeps the plain split: `validate` has already refused a
// positional path on either end of it, which is what mongod does too.
if (std.mem.eql(u8, op, "$rename")) {
const ops = doc_pairs(value) orelse return error.InvalidUpdate;
for (ops) |p| {
if (p.value != .string) return error.InvalidUpdate;
if (std.mem.eql(u8, p.key, "_id") or std.mem.eql(u8, p.value.string, "_id")) return error.ImmutableId;
var old_segs: [max_path_segments][]const u8 = undefined;
const old_n = split_path(p.key, &old_segs) orelse return error.InvalidUpdate;
const v = get_value(pairs.items, old_segs[0..old_n]) orelse continue; // no-op when absent
unset_path(arena, pairs, old_segs[0..old_n]);
var new_segs: [max_path_segments][]const u8 = undefined;
const new_n = split_path(p.value.string, &new_segs) orelse return error.InvalidUpdate;
try set_path(arena, pairs, new_segs[0..new_n], v, p.value.string, diag);
}
return;
}
return error.InvalidUpdate; return error.InvalidUpdate;
} }
fn op_set(
arena: std.mem.Allocator,
pairs: *std.ArrayListUnmanaged(bson.Pair),
ops: []const bson.Pair,
opts: Options,
) UpdateError!void {
for (ops) |p| {
if (std.mem.eql(u8, p.key, "_id")) return error.ImmutableId;
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
try set_path(arena, pairs, segs, try bson.copy_value(arena, p.value), p.key, opts.diag);
}
}
}
fn op_unset(
arena: std.mem.Allocator,
pairs: *std.ArrayListUnmanaged(bson.Pair),
ops: []const bson.Pair,
opts: Options,
) UpdateError!void {
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
unset_path(arena, pairs, segs);
}
}
}
fn op_inc(
arena: std.mem.Allocator,
pairs: *std.ArrayListUnmanaged(bson.Pair),
ops: []const bson.Pair,
opts: Options,
) UpdateError!void {
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
const current = get_value(pairs.items, segs) orelse bson.Value{ .int32 = 0 };
if (!current.is_number() or !p.value.is_number()) return error.InvalidUpdate;
const sum = try numeric_add(current, p.value);
try set_path(arena, pairs, segs, sum, p.key, opts.diag);
}
}
}
fn op_push(
arena: std.mem.Allocator,
pairs: *std.ArrayListUnmanaged(bson.Pair),
ops: []const bson.Pair,
opts: Options,
) UpdateError!void {
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
const current_opt = get_value(pairs.items, segs);
var items: std.ArrayListUnmanaged(bson.Value) = .empty;
defer items.deinit(arena);
if (current_opt) |current| {
switch (current) {
.array => |arr| try items.appendSlice(arena, arr),
.null => {},
else => return error.InvalidUpdate, // non-array field
}
}
if (p.value == .doc) {
if (bson.get_pair(p.value.doc, "$each")) |each| {
const arr = switch (each) {
.array => |a| a,
else => return error.InvalidUpdate,
};
for (arr) |item| try items.append(arena, try bson.copy_value(arena, item));
try set_path(arena, pairs, segs, .{ .array = try items.toOwnedSlice(arena) }, p.key, opts.diag);
continue;
}
}
try items.append(arena, try bson.copy_value(arena, p.value));
try set_path(arena, pairs, segs, .{ .array = try items.toOwnedSlice(arena) }, p.key, opts.diag);
}
}
}
fn op_pull(
arena: std.mem.Allocator,
pairs: *std.ArrayListUnmanaged(bson.Pair),
ops: []const bson.Pair,
opts: Options,
) UpdateError!void {
for (ops) |p| {
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
const current = get_value(pairs.items, segs) orelse continue;
const arr = switch (current) {
.array => |a| a,
else => return error.InvalidUpdate,
};
var items: std.ArrayListUnmanaged(bson.Value) = .empty;
defer items.deinit(arena);
for (arr) |elem| {
if (!pull_matches(arena, p.value, elem)) {
try items.append(arena, elem);
}
}
try set_path(arena, pairs, segs, .{ .array = try items.toOwnedSlice(arena) }, p.key, opts.diag);
}
}
}
/// `$rename` alone keeps the plain split: `validate` has already refused a
/// positional path on either end of it, which is what mongod does too.
fn op_rename(
arena: std.mem.Allocator,
pairs: *std.ArrayListUnmanaged(bson.Pair),
ops: []const bson.Pair,
opts: Options,
) UpdateError!void {
for (ops) |p| {
if (p.value != .string) return error.InvalidUpdate;
if (std.mem.eql(u8, p.key, "_id") or std.mem.eql(u8, p.value.string, "_id")) return error.ImmutableId;
var old_segs: [max_path_segments][]const u8 = undefined;
const old_n = split_path(p.key, &old_segs) orelse return error.InvalidUpdate;
const v = get_value(pairs.items, old_segs[0..old_n]) orelse continue; // no-op when absent
unset_path(arena, pairs, old_segs[0..old_n]);
var new_segs: [max_path_segments][]const u8 = undefined;
const new_n = split_path(p.value.string, &new_segs) orelse return error.InvalidUpdate;
try set_path(arena, pairs, new_segs[0..new_n], v, p.value.string, opts.diag);
}
}
fn doc_pairs(v: bson.Value) ?[]const bson.Pair { fn doc_pairs(v: bson.Value) ?[]const bson.Pair {
return switch (v) { return switch (v) {
.doc => |pairs| pairs, .doc => |pairs| pairs,