M3: the positional operators and arrayFilters #7
142
src/commands.zig
142
src/commands.zig
@@ -2000,7 +2000,7 @@ fn cmd_update(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !void {
|
|||||||
const doc = try doc_tree(reply.arena_alloc(), coll, off);
|
const doc = try doc_tree(reply.arena_alloc(), coll, off);
|
||||||
const copy = try clone_doc(reply, doc);
|
const copy = try clone_doc(reply, doc);
|
||||||
var diag: update.Diagnostic = .{};
|
var diag: update.Diagnostic = .{};
|
||||||
update.apply(copy, &.{ .arena = undefined, .pairs = u_doc }, &diag) catch |err|
|
update.apply(copy, &.{ .arena = undefined, .pairs = u_doc }, .{ .diag = &diag }) catch |err|
|
||||||
return update_refusal(reply, err, diag);
|
return update_refusal(reply, err, diag);
|
||||||
const written = ctx.engine.replace(db_name, coll_name, copy, ctx.oid_gen) catch |err| switch (err) {
|
const written = ctx.engine.replace(db_name, coll_name, copy, ctx.oid_gen) catch |err| switch (err) {
|
||||||
error.DuplicateKey, error.DuplicateKeyIndex => {
|
error.DuplicateKey, error.DuplicateKeyIndex => {
|
||||||
@@ -2124,7 +2124,7 @@ fn cmd_find_and_modify(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !v
|
|||||||
const before = try bson.copy_pairs(arena, target.?.pairs);
|
const before = try bson.copy_pairs(arena, target.?.pairs);
|
||||||
const copy = try clone_doc(reply, target.?);
|
const copy = try clone_doc(reply, target.?);
|
||||||
var diag: update.Diagnostic = .{};
|
var diag: update.Diagnostic = .{};
|
||||||
update.apply(copy, &.{ .arena = undefined, .pairs = u_doc }, &diag) catch |err|
|
update.apply(copy, &.{ .arena = undefined, .pairs = u_doc }, .{ .diag = &diag }) catch |err|
|
||||||
return update_refusal(reply, err, diag);
|
return update_refusal(reply, err, diag);
|
||||||
// findAndModify reports `n` (matched) and `updatedExisting`, neither of
|
// findAndModify reports `n` (matched) and `updatedExisting`, neither of
|
||||||
// which distinguishes a no-op, so whether it wrote is not needed here.
|
// which distinguishes a no-op, so whether it wrote is not needed here.
|
||||||
@@ -4141,20 +4141,92 @@ fn clone_doc(reply: *wire.Reply, doc: *const bson.Document) !*bson.Document {
|
|||||||
/// `findAndModify` and the upsert path -- and the two new ones exist to stop
|
/// `findAndModify` and the upsert path -- and the two new ones exist to stop
|
||||||
/// a silent divergence in the first place.
|
/// a silent divergence in the first place.
|
||||||
///
|
///
|
||||||
/// The messages are this server's own words. mongod's `PathNotViable` text
|
/// Every message here was measured on mongod 8.3.7 and is reproduced verbatim,
|
||||||
/// embeds a shell-syntax rendering of the offending element (`Cannot create
|
/// except where mongod's text embeds a shell-syntax rendering of the offending
|
||||||
/// field 'nope' in element {y: [ { b: 3 }, { b: 1 } ]}`), and there is no BSON
|
/// BSON (`Cannot create field 'nope' in element {y: [ { b: 3 } ]}`): there is
|
||||||
/// formatter here that produces it. The code is what the corpus asserts and
|
/// no formatter here that produces it, and a half-copy would be worse than a
|
||||||
/// the code is exact; a half-copy of the text would be worse than a clear
|
/// clear sentence that does not pretend. Codes are exact throughout, and codes
|
||||||
/// sentence that does not pretend.
|
/// are what the corpus asserts.
|
||||||
|
///
|
||||||
|
/// mongod wraps the refusals it only reaches with a document in hand in
|
||||||
|
/// `Plan executor error during update :: caused by :: `. That prefix is
|
||||||
|
/// dropped here: it names a mongod component this server does not have.
|
||||||
fn update_refusal(reply: *wire.Reply, err: anyerror, diag: update.Diagnostic) !void {
|
fn update_refusal(reply: *wire.Reply, err: anyerror, diag: update.Diagnostic) !void {
|
||||||
const arena = reply.arena_alloc();
|
const arena = reply.arena_alloc();
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.PositionalUnsupported => return bad_value(reply, try std.fmt.allocPrint(
|
error.PositionalFirst => return bad_value(reply, if (std.mem.eql(u8, diag.segment, "$"))
|
||||||
|
try std.fmt.allocPrint(
|
||||||
arena,
|
arena,
|
||||||
"the positional operator '{s}' in path '{s}' is not implemented by this server",
|
"Cannot have positional (i.e. '$') element in the first position in path '{s}'",
|
||||||
|
.{diag.path},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"Cannot have array filter identifier (i.e. '$[<id>]') element in the " ++
|
||||||
|
"first position in path '{s}'",
|
||||||
|
.{diag.path},
|
||||||
|
)),
|
||||||
|
error.TooManyPositional => return bad_value(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"Too many positional (i.e. '$') elements found in path '{s}'",
|
||||||
|
.{diag.path},
|
||||||
|
)),
|
||||||
|
error.NoArrayFilter => return bad_value(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"No array filter found for identifier '{s}' in path '{s}'",
|
||||||
.{ diag.segment, diag.path },
|
.{ diag.segment, diag.path },
|
||||||
)),
|
)),
|
||||||
|
error.BadArrayFilterIdentifier => return bad_value(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"Error parsing array filter :: caused by :: The top-level field name must be an " ++
|
||||||
|
"alphanumeric string beginning with a lowercase letter, found '{s}'",
|
||||||
|
.{diag.segment},
|
||||||
|
)),
|
||||||
|
error.NoPositionalMatch => return bad_value(
|
||||||
|
reply,
|
||||||
|
"The positional operator did not find the match needed from the query.",
|
||||||
|
),
|
||||||
|
error.ArrayPathRequired => return bad_value(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"The path '{s}' must exist in the document in order to apply array updates.",
|
||||||
|
.{diag.path},
|
||||||
|
)),
|
||||||
|
error.NotAnArrayPath => return bad_value(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"Cannot apply array updates to non-array element at path '{s}'",
|
||||||
|
.{diag.path},
|
||||||
|
)),
|
||||||
|
error.RenameDynamicSource => return bad_value(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"The source field for $rename may not be dynamic: {s}",
|
||||||
|
.{diag.path},
|
||||||
|
)),
|
||||||
|
error.RenameDynamicDestination => return bad_value(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"The destination field for $rename may not be dynamic: {s}",
|
||||||
|
.{diag.path},
|
||||||
|
)),
|
||||||
|
error.UnusedArrayFilter => return failed_to_parse(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"The array filter for identifier '{s}' was not used in the update",
|
||||||
|
.{diag.segment},
|
||||||
|
)),
|
||||||
|
error.DuplicateArrayFilter => return failed_to_parse(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"Found multiple array filters with the same top-level field name {s}",
|
||||||
|
.{diag.segment},
|
||||||
|
)),
|
||||||
|
error.EmptyArrayFilter => return failed_to_parse(
|
||||||
|
reply,
|
||||||
|
"Cannot use an expression without a top-level field name in arrayFilters",
|
||||||
|
),
|
||||||
|
error.MultipleArrayFilterIdentifiers => return failed_to_parse(reply, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"Error parsing array filter :: caused by :: Expected a single top-level field " ++
|
||||||
|
"name, found '{s}' and '{s}'",
|
||||||
|
.{ diag.segment, diag.other },
|
||||||
|
)),
|
||||||
error.PathNotViable => return reply.put_error(
|
error.PathNotViable => return reply.put_error(
|
||||||
@intFromEnum(ErrorCode.path_not_viable),
|
@intFromEnum(ErrorCode.path_not_viable),
|
||||||
"PathNotViable",
|
"PathNotViable",
|
||||||
@@ -4190,7 +4262,7 @@ fn build_upsert_doc(
|
|||||||
const owned = try arena.create(bson.Document);
|
const owned = try arena.create(bson.Document);
|
||||||
owned.* = bson.Document{ .arena = std.heap.ArenaAllocator.init(arena), .pairs = try pairs.toOwnedSlice(arena) };
|
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.
|
// Apply update operators to build the final doc; _id handled by insert.
|
||||||
try update.apply(owned, &.{ .arena = undefined, .pairs = u_doc }, diag);
|
try update.apply(owned, &.{ .arena = undefined, .pairs = u_doc }, .{ .diag = diag });
|
||||||
return owned;
|
return owned;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5629,9 +5701,9 @@ test "a positional update is refused on the wire and stores nothing" {
|
|||||||
var ctx = tdb.ctx(io);
|
var ctx = tdb.ctx(io);
|
||||||
|
|
||||||
const cases = [_]struct { coll: []const u8, path: []const u8, code: i32 }{
|
const cases = [_]struct { coll: []const u8, path: []const u8, code: i32 }{
|
||||||
.{ .coll = "a1", .path = "y.$[i].b", .code = 2 }, // filtered positional
|
.{ .coll = "a1", .path = "y.$[i].b", .code = 2 }, // no array filter binds `i`
|
||||||
.{ .coll = "a2", .path = "y.$[].b", .code = 2 }, // all-positional
|
.{ .coll = "a2", .path = "$[]", .code = 2 }, // positional in first position
|
||||||
.{ .coll = "a3", .path = "y.$.b", .code = 2 }, // positional
|
.{ .coll = "a3", .path = "y.$.b", .code = 2 }, // no predicate for `$` to use
|
||||||
.{ .coll = "a4", .path = "y.nope.b", .code = 28 }, // PathNotViable, same branch
|
.{ .coll = "a4", .path = "y.nope.b", .code = 28 }, // PathNotViable, same branch
|
||||||
};
|
};
|
||||||
for (cases) |c| {
|
for (cases) |c| {
|
||||||
@@ -5669,6 +5741,48 @@ test "a positional update is refused on the wire and stores nothing" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "an all-positional update writes every element on the wire" {
|
||||||
|
// The other end of the same chain: `$[]` reaches the stored document, and
|
||||||
|
// reaches *all* of it. `distinct` on `y.b` is the check that says so in one
|
||||||
|
// number -- two values means only one element moved.
|
||||||
|
var threaded: std.Io.Threaded = .init_single_threaded;
|
||||||
|
defer threaded.deinit();
|
||||||
|
const io = threaded.io();
|
||||||
|
var tdb = try TestDb.init(io);
|
||||||
|
defer tdb.deinit();
|
||||||
|
var ctx = tdb.ctx(io);
|
||||||
|
|
||||||
|
try dispatch_insert(&tdb, io, "all", &.{
|
||||||
|
.{ .doc = &.{
|
||||||
|
.{ .key = "_id", .value = .{ .int32 = 1 } },
|
||||||
|
.{ .key = "y", .value = .{ .array = &.{
|
||||||
|
.{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 3 } }} },
|
||||||
|
.{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 1 } }} },
|
||||||
|
} } },
|
||||||
|
} },
|
||||||
|
});
|
||||||
|
|
||||||
|
const updates = [_]bson.Value{.{ .doc = &.{
|
||||||
|
.{ .key = "q", .value = .{ .doc = &.{} } },
|
||||||
|
.{ .key = "u", .value = .{ .doc = &.{
|
||||||
|
.{ .key = "$set", .value = .{ .doc = &.{
|
||||||
|
.{ .key = "y.$[].b", .value = .{ .int32 = 9 } },
|
||||||
|
} } },
|
||||||
|
} } },
|
||||||
|
} }};
|
||||||
|
try testing.expectEqual(@as(?i32, null), try run_for_code(&ctx, "update", .{ .string = "all" }, &.{
|
||||||
|
.{ .key = "updates", .value = .{ .array = &updates } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
var reply = wire.Reply.init(testing.allocator);
|
||||||
|
defer reply.deinit();
|
||||||
|
const values = try distinct_values(&tdb, io, &reply, "all", &.{
|
||||||
|
.{ .key = "key", .value = .{ .string = "y.b" } },
|
||||||
|
});
|
||||||
|
try testing.expectEqual(@as(usize, 1), values.len);
|
||||||
|
try testing.expectEqual(@as(i32, 9), values[0].int32);
|
||||||
|
}
|
||||||
|
|
||||||
test "aggregate $sort without a preceding $group sorts and frees correctly" {
|
test "aggregate $sort without a preceding $group sorts and frees correctly" {
|
||||||
// Regression test for a remote, client-triggerable invalid free: the
|
// Regression test for a remote, client-triggerable invalid free: the
|
||||||
// $sort stage materialized its document list from the reply arena and
|
// $sort stage materialized its document list from the reply arena and
|
||||||
|
|||||||
1061
src/update.zig
1061
src/update.zig
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user