diff --git a/src/update.zig b/src/update.zig index 44035cc..e62cce0 100644 --- a/src/update.zig +++ b/src/update.zig @@ -610,110 +610,142 @@ fn apply_operator( value: bson.Value, opts: Options, ) UpdateError!void { - const diag = opts.diag; - if (std.mem.eql(u8, op, "$set")) { - const ops = doc_pairs(value) orelse return error.InvalidUpdate; - 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, diag); - } - } - 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; - } + // Every operator's argument is a document of path/operand pairs, so the + // shape is checked once here rather than at the top of each. + const ops = doc_pairs(value) orelse return error.InvalidUpdate; + if (std.mem.eql(u8, op, "$set")) return op_set(arena, pairs, ops, opts); + if (std.mem.eql(u8, op, "$unset")) return op_unset(arena, pairs, ops, opts); + if (std.mem.eql(u8, op, "$inc")) return op_inc(arena, pairs, ops, opts); + 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 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 { return switch (v) { .doc => |pairs| pairs,