M3: the update operators, and 's modifiers #8
@@ -1977,6 +1977,7 @@ fn cmd_update(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !void {
|
||||
"update.updates.arrayFilters",
|
||||
) orelse return,
|
||||
.query = q,
|
||||
.now_ms = std.Io.Timestamp.now(ctx.io, .real).toMilliseconds(),
|
||||
.diag = &diag,
|
||||
};
|
||||
// Before the scan, not after: an update naming an identifier nothing
|
||||
@@ -2102,6 +2103,7 @@ fn cmd_find_and_modify(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !v
|
||||
"findAndModify.arrayFilters",
|
||||
) orelse return,
|
||||
.query = q,
|
||||
.now_ms = std.Io.Timestamp.now(ctx.io, .real).toMilliseconds(),
|
||||
.diag = &diag,
|
||||
};
|
||||
if (doc_arg(msg.body.get("update"))) |u| {
|
||||
@@ -4313,6 +4315,17 @@ fn update_refusal(reply: *wire.Reply, err: anyerror, diag: update.Diagnostic) !v
|
||||
"Unrecognized or invalid $push modifier '{s}' at field '{s}'",
|
||||
.{ diag.segment, diag.path },
|
||||
)),
|
||||
error.BadCurrentDateType => return bad_value(
|
||||
reply,
|
||||
"The '$type' string field is required to be 'date' or 'timestamp': " ++
|
||||
"{$currentDate: {field : {$type: 'date'}}}",
|
||||
),
|
||||
error.BadCurrentDateOperand => return bad_value(reply, try std.fmt.allocPrint(
|
||||
arena,
|
||||
"{s} is not valid type for $currentDate. Please use a boolean ('true') or a " ++
|
||||
"$type expression ({{$type: 'timestamp/date'}}).",
|
||||
.{diag.other},
|
||||
)),
|
||||
error.PathNotViable => return reply.put_error(
|
||||
@intFromEnum(ErrorCode.path_not_viable),
|
||||
"PathNotViable",
|
||||
@@ -4410,7 +4423,11 @@ fn build_upsert_doc(
|
||||
const owned = try arena.create(bson.Document);
|
||||
owned.* = bson.Document{ .arena = std.heap.ArenaAllocator.init(arena), .pairs = try pairs.toOwnedSlice(arena) };
|
||||
// Apply update operators to build the final doc; _id handled by insert.
|
||||
try update.apply(owned, &.{ .arena = undefined, .pairs = u_doc }, opts);
|
||||
// `inserting` is what `$setOnInsert` asks about, and this is the only
|
||||
// caller that answers yes.
|
||||
var insert_opts = opts;
|
||||
insert_opts.inserting = true;
|
||||
try update.apply(owned, &.{ .arena = undefined, .pairs = u_doc }, insert_opts);
|
||||
return owned;
|
||||
}
|
||||
|
||||
|
||||
170
src/update.zig
170
src/update.zig
@@ -34,6 +34,10 @@ pub const UpdateError = error{
|
||||
PullAllNeedsArray,
|
||||
/// `$each` that is not an array.
|
||||
BadEach,
|
||||
/// `$currentDate` handed a document that is not `{$type: date|timestamp}`.
|
||||
BadCurrentDateType,
|
||||
/// `$currentDate` handed something that is neither a bool nor a document.
|
||||
BadCurrentDateOperand,
|
||||
/// A `$push` modifier that is unknown, or whose operand is not what it
|
||||
/// takes: `$slice`/`$position` want a number, `$sort` a number or a
|
||||
/// one-field document.
|
||||
@@ -115,6 +119,15 @@ pub const Options = struct {
|
||||
/// The command's query. `$` resolves against it, so an update carrying one
|
||||
/// without this is refused rather than guessing at an element.
|
||||
query: ?[]const bson.Pair = null,
|
||||
/// Whether this application is building a document an upsert will insert.
|
||||
/// `$setOnInsert` is the one operator that asks.
|
||||
inserting: bool = false,
|
||||
/// The clock `$currentDate` reads, in Unix milliseconds. There is no
|
||||
/// fallback to a global one: this file has no `io` to read the real clock
|
||||
/// through, and a caller that forgets gets the epoch rather than a value
|
||||
/// that changes between runs. The command handlers pass the same clock
|
||||
/// `ttl_sweep` and the cursor sweep already use.
|
||||
now_ms: i64 = 0,
|
||||
diag: ?*Diagnostic = null,
|
||||
};
|
||||
|
||||
@@ -636,6 +649,15 @@ fn apply_operator(
|
||||
// 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);
|
||||
// `$setOnInsert` is `$set` on the branch that inserts and nothing at all
|
||||
// on the branch that updates -- including its `_id` rule, which is why it
|
||||
// does not go through `op_set`: writing `_id` is allowed on a document
|
||||
// being built and refused on one being rewritten. Measured.
|
||||
if (std.mem.eql(u8, op, "$setOnInsert")) {
|
||||
if (!opts.inserting) return;
|
||||
return op_set_on_insert(arena, pairs, ops, opts);
|
||||
}
|
||||
if (std.mem.eql(u8, op, "$currentDate")) return op_current_date(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_arith(arena, pairs, ops, opts, .add);
|
||||
if (std.mem.eql(u8, op, "$mul")) return op_arith(arena, pairs, ops, opts, .mul);
|
||||
@@ -664,6 +686,72 @@ fn op_set(
|
||||
}
|
||||
}
|
||||
|
||||
fn op_set_on_insert(
|
||||
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| {
|
||||
try set_path(arena, pairs, segs, try bson.copy_value(arena, p.value), p.key, opts.diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `$currentDate`: write the clock, as a date or as a BSON timestamp.
|
||||
///
|
||||
/// The operand says which. A bool -- **either** bool, measured: `false` writes
|
||||
/// a date too, the value is ignored -- means a date; `{$type: "date"}` and
|
||||
/// `{$type: "timestamp"}` say so; anything else is refused.
|
||||
fn op_current_date(
|
||||
arena: std.mem.Allocator,
|
||||
pairs: *std.ArrayListUnmanaged(bson.Pair),
|
||||
ops: []const bson.Pair,
|
||||
opts: Options,
|
||||
) UpdateError!void {
|
||||
const now = opts.now_ms;
|
||||
for (ops) |p| {
|
||||
const value: bson.Value = switch (try current_date_kind(p.value, opts, p.key)) {
|
||||
.date => .{ .datetime = now },
|
||||
// A BSON timestamp is seconds in the high 32 bits and an ordinal
|
||||
// in the low ones. mongod fills the ordinal from the oplog; a
|
||||
// standalone has no oplog, so it is 1 -- the same answer twice in
|
||||
// one second is the same timestamp, which nothing here reads.
|
||||
.timestamp => .{ .timestamp = (@as(u64, @intCast(@divFloor(now, 1000))) << 32) | 1 },
|
||||
};
|
||||
for (try resolve(arena, pairs.items, p.key, opts)) |segs| {
|
||||
try set_path(arena, pairs, segs, value, p.key, opts.diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_date_kind(
|
||||
v: bson.Value,
|
||||
opts: Options,
|
||||
path: []const u8,
|
||||
) UpdateError!enum { date, timestamp } {
|
||||
switch (v) {
|
||||
.bool => return .date,
|
||||
.doc => |spec| {
|
||||
const t = bson.get_pair(spec, "$type") orelse {
|
||||
note(opts.diag, path, "");
|
||||
return error.BadCurrentDateType;
|
||||
};
|
||||
if (t == .string) {
|
||||
if (std.mem.eql(u8, t.string, "date")) return .date;
|
||||
if (std.mem.eql(u8, t.string, "timestamp")) return .timestamp;
|
||||
}
|
||||
note(opts.diag, path, "");
|
||||
return error.BadCurrentDateType;
|
||||
},
|
||||
else => {
|
||||
if (opts.diag) |d| d.* = .{ .path = path, .other = v.type_name() };
|
||||
return error.BadCurrentDateOperand;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn op_unset(
|
||||
arena: std.mem.Allocator,
|
||||
pairs: *std.ArrayListUnmanaged(bson.Pair),
|
||||
@@ -2424,3 +2512,85 @@ test "a $push modifier that is not one, or is handed the wrong thing, is refused
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), doc.get("t").?.array.len);
|
||||
}
|
||||
|
||||
test "$setOnInsert writes only on the branch that inserts" {
|
||||
// Mutation check: drop the `opts.inserting` guard and the first half goes
|
||||
// red -- an update would gain a field that is only supposed to exist on a
|
||||
// document nobody had before.
|
||||
var updating = try doc_with(testing.allocator, &.{
|
||||
.{ .key = "_id", .value = .{ .int32 = 1 } },
|
||||
.{ .key = "a", .value = .{ .int32 = 100 } },
|
||||
});
|
||||
defer updating.arena.deinit();
|
||||
try apply(&updating, &doc_of(&.{
|
||||
.{ .key = "$setOnInsert", .value = .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} } },
|
||||
.{ .key = "$set", .value = .{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 2 } }} } },
|
||||
}), .{});
|
||||
try testing.expectEqual(@as(i32, 100), updating.get("a").?.int32);
|
||||
try testing.expectEqual(@as(i32, 2), updating.get("b").?.int32);
|
||||
|
||||
var inserting = try doc_with(testing.allocator, &.{.{ .key = "k", .value = .{ .int32 = 1 } }});
|
||||
defer inserting.arena.deinit();
|
||||
try apply(&inserting, &doc_of(&.{
|
||||
.{ .key = "$setOnInsert", .value = .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} } },
|
||||
}), .{ .inserting = true });
|
||||
try testing.expectEqual(@as(i32, 1), inserting.get("a").?.int32);
|
||||
}
|
||||
|
||||
test "$setOnInsert may write _id, where $set may not" {
|
||||
// The one place the immutability rule does not apply: a document being
|
||||
// built has no identity yet to change.
|
||||
var doc = try doc_with(testing.allocator, &.{.{ .key = "k", .value = .{ .int32 = 1 } }});
|
||||
defer doc.arena.deinit();
|
||||
try apply(&doc, &doc_of(&.{
|
||||
.{ .key = "$setOnInsert", .value = .{ .doc = &.{.{ .key = "_id", .value = .{ .int32 = 9 } }} } },
|
||||
}), .{ .inserting = true });
|
||||
try testing.expectEqual(@as(i32, 9), doc.get("_id").?.int32);
|
||||
|
||||
try testing.expectError(error.ImmutableId, apply(&doc, &doc_of(&.{
|
||||
.{ .key = "$set", .value = .{ .doc = &.{.{ .key = "_id", .value = .{ .int32 = 8 } }} } },
|
||||
}), .{ .inserting = true }));
|
||||
}
|
||||
|
||||
test "$currentDate writes the clock it was handed" {
|
||||
// The clock is a parameter, so the result is a value rather than a moving
|
||||
// target -- and the server passes the same one `ttl_sweep` reads.
|
||||
var doc = try doc_with(testing.allocator, &.{.{ .key = "a", .value = .{ .int32 = 1 } }});
|
||||
defer doc.arena.deinit();
|
||||
try apply(&doc, &doc_of(&.{
|
||||
.{ .key = "$currentDate", .value = .{ .doc = &.{
|
||||
.{ .key = "d", .value = .{ .bool = true } },
|
||||
// Measured: `false` writes a date too. The boolean says "a date",
|
||||
// not "whether".
|
||||
.{ .key = "f", .value = .{ .bool = false } },
|
||||
.{ .key = "e", .value = .{ .doc = &.{.{ .key = "$type", .value = .{ .string = "date" } }} } },
|
||||
.{ .key = "t", .value = .{ .doc = &.{.{ .key = "$type", .value = .{ .string = "timestamp" } }} } },
|
||||
} } },
|
||||
}), .{ .now_ms = 1_700_000_000_123 });
|
||||
try testing.expectEqual(@as(i64, 1_700_000_000_123), doc.get("d").?.datetime);
|
||||
try testing.expectEqual(@as(i64, 1_700_000_000_123), doc.get("f").?.datetime);
|
||||
try testing.expectEqual(@as(i64, 1_700_000_000_123), doc.get("e").?.datetime);
|
||||
// Seconds in the high 32 bits, an ordinal in the low ones.
|
||||
try testing.expectEqual(@as(u64, (1_700_000_000 << 32) | 1), doc.get("t").?.timestamp);
|
||||
}
|
||||
|
||||
test "$currentDate refuses an operand that names no type" {
|
||||
var doc = try doc_with(testing.allocator, &.{.{ .key = "a", .value = .{ .int32 = 1 } }});
|
||||
defer doc.arena.deinit();
|
||||
var diag: Diagnostic = .{};
|
||||
try testing.expectError(error.BadCurrentDateType, apply(&doc, &doc_of(&.{
|
||||
.{ .key = "$currentDate", .value = .{ .doc = &.{
|
||||
.{ .key = "d", .value = .{ .doc = &.{.{ .key = "$type", .value = .{ .string = "nope" } }} } },
|
||||
} } },
|
||||
}), .{ .diag = &diag }));
|
||||
try testing.expectError(error.BadCurrentDateType, apply(&doc, &doc_of(&.{
|
||||
.{ .key = "$currentDate", .value = .{ .doc = &.{
|
||||
.{ .key = "d", .value = .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} } },
|
||||
} } },
|
||||
}), .{ .diag = &diag }));
|
||||
try testing.expectError(error.BadCurrentDateOperand, apply(&doc, &doc_of(&.{
|
||||
.{ .key = "$currentDate", .value = .{ .doc = &.{.{ .key = "d", .value = .{ .int32 = 1 } }} } },
|
||||
}), .{ .diag = &diag }));
|
||||
try testing.expectEqualStrings("int", diag.other);
|
||||
try testing.expect(doc.get("d") == null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user