diff --git a/src/commands.zig b/src/commands.zig index 8c5eaf2..11f687b 100644 --- a/src/commands.zig +++ b/src/commands.zig @@ -872,6 +872,21 @@ fn cmd_update(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !void { const u_doc = doc_arg(spec.get("u")) orelse return bad_value(reply, "update spec requires u"); const multi = bool_arg(spec.get("multi")) orelse false; const upsert = bool_arg(spec.get("upsert")) orelse false; + // `sort` on an update spec picks *which* match to write when the filter + // matches several -- a MongoDB 8.0 addition. This server advertises 4.4, + // and ignoring the field would be the worst of the three possible + // answers: the client asked for a specific document and would silently + // get a different one. So refuse it, which is also what a real 4.4 does + // with an unknown update-spec field. + if (spec.get("sort") != null) { + return failed_to_parse(reply, "Unknown option to update: sort"); + } + // A replacement describes one document, so there is no coherent meaning + // for applying it to many: every match would end up identical apart from + // its `_id`. MongoDB rejects the combination rather than doing that. + if (multi and update.is_replacement(u_doc)) { + return failed_to_parse(reply, "multi update is not supported for replacement-style update"); + } var matched: std.ArrayListUnmanaged(u64) = .empty; defer matched.deinit(ctx.gpa); @@ -1605,6 +1620,10 @@ fn bad_value(reply: *wire.Reply, msg: []const u8) !void { return reply.put_error(@intFromEnum(ErrorCode.bad_value), "BadValue", msg); } +fn failed_to_parse(reply: *wire.Reply, msg: []const u8) !void { + return reply.put_error(@intFromEnum(ErrorCode.failed_to_parse), "FailedToParse", msg); +} + /// The E11000 text, shared by the top-level error reply and the per-document /// `writeErrors` entries of a batch insert. Uses the collection's /// dup_index (set by a rejected unique-index write) when the conflict came diff --git a/src/update.zig b/src/update.zig index 99b90ba..d432b46 100644 --- a/src/update.zig +++ b/src/update.zig @@ -10,17 +10,88 @@ pub const UpdateError = error{ ImmutableId, InvalidUpdate, OutOfMemory }; const max_path_segments = 16; -/// Apply an update document (whose fields are operator documents) to `doc`. +/// Whether an update document is a *replacement* rather than a set of +/// operators. MongoDB decides on the first field and nothing else: a +/// `$`-prefixed one means operators. An empty document is a replacement, and a +/// legal one -- it strips every field but `_id`. +/// +/// This distinction is the whole of `replaceOne`, `findOneAndReplace` and +/// `bulkWrite`'s `replaceOne`. Without it `apply` rejected every update whose +/// first key was not `$`-prefixed, so all three failed with "bad update". +pub fn is_replacement(pairs: []const bson.Pair) bool { + if (pairs.len == 0) return true; + return !is_operator_key(pairs[0].key); +} + +fn is_operator_key(key: []const u8) bool { + return key.len > 0 and key[0] == '$'; +} + +/// Apply an update document to `doc`: a replacement, or a set of operators. pub fn apply(doc: *bson.Document, update: *const bson.Document) UpdateError!void { + if (is_replacement(update.pairs)) return apply_replacement(doc, update.pairs); const arena = doc.arena.allocator(); var pairs = try copy_to_list(bson.Pair, arena, doc.pairs); for (update.pairs) |op| { - if (op.key.len == 0 or op.key[0] != '$') return error.InvalidUpdate; + // The first field decided this is an operator update, so a field that + // is not one is a mixed document -- which MongoDB rejects rather than + // guessing at. + if (!is_operator_key(op.key)) return error.InvalidUpdate; try apply_operator(arena, &pairs, op.key, op.value); } doc.pairs = try pairs.toOwnedSlice(arena); } +/// Replace every field of `doc` with `replacement`'s, except `_id`. +/// +/// `_id` is immutable, so it survives and keeps its position at the front (which +/// is also where MongoDB stores it, and what the `_id_` index descends on). A +/// replacement carrying an `_id` is allowed only when it is the *same* `_id`; +/// anything else is an attempt to change a document's identity by rewriting it. +/// +/// `doc` may legitimately have no `_id` yet: that is the upsert path, where the +/// caller has seeded the document from the filter's equalities and `insert` +/// generates an ObjectId afterwards. Then the replacement's own `_id`, if it has +/// one, is what the new document gets. +fn apply_replacement(doc: *bson.Document, replacement: []const bson.Pair) UpdateError!void { + const arena = doc.arena.allocator(); + const old_id = doc.get("_id"); + + var pairs: std.ArrayListUnmanaged(bson.Pair) = .empty; + errdefer pairs.deinit(arena); + try pairs.ensureTotalCapacity(arena, replacement.len + 1); + + if (old_id) |id| { + try pairs.append(arena, .{ + .key = try arena.dupe(u8, "_id"), + .value = try bson.copy_value(arena, id), + }); + } + for (replacement) |p| { + // A replacement is data, not instructions. `{$set: {...}}` reaching here + // would mean the first field was not an operator and a later one was, + // i.e. a mixed document. + if (is_operator_key(p.key)) return error.InvalidUpdate; + if (std.mem.eql(u8, p.key, "_id")) { + if (old_id) |id| { + if (bson.compare(id, p.value) != .eq) return error.ImmutableId; + continue; // already at the front + } + // No stored `_id`: this replacement supplies it. + try pairs.insert(arena, 0, .{ + .key = try arena.dupe(u8, "_id"), + .value = try bson.copy_value(arena, p.value), + }); + continue; + } + try pairs.append(arena, .{ + .key = try arena.dupe(u8, p.key), + .value = try bson.copy_value(arena, p.value), + }); + } + doc.pairs = try pairs.toOwnedSlice(arena); +} + fn apply_operator( arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.Pair), @@ -417,10 +488,128 @@ test "$set nested creation and _id protection" { }))); } -test "non-operator update rejected" { +test "a replacement keeps _id and drops every other field" { + // Mutation check: seed `apply_replacement`'s list from `doc.pairs` (as the + // operator path does) instead of starting empty. Red -- `gone` survives, and + // a replacement that does not remove fields is not a replacement. var doc = bson.Document{ .arena = std.heap.ArenaAllocator.init(testing.allocator), .pairs = &.{} }; defer doc.arena.deinit(); + doc.pairs = try doc.arena.allocator().dupe(bson.Pair, &.{ + .{ .key = "_id", .value = .{ .int32 = 7 } }, + .{ .key = "gone", .value = .{ .int32 = 1 } }, + .{ .key = "also_gone", .value = .{ .string = "x" } }, + }); + + try apply(&doc, &doc_of(&.{ + .{ .key = "fresh", .value = .{ .int32 = 42 } }, + })); + + try testing.expectEqual(@as(usize, 2), doc.pairs.len); + // _id survives, and stays at the front where it is stored. + try testing.expectEqualStrings("_id", doc.pairs[0].key); + try testing.expectEqual(@as(i64, 7), doc.pairs[0].value.int32); + try testing.expectEqual(@as(i64, 42), bson.get_pair(doc.pairs, "fresh").?.int32); + try testing.expect(bson.get_pair(doc.pairs, "gone") == null); + try testing.expect(bson.get_pair(doc.pairs, "also_gone") == null); +} + +test "an empty replacement leaves a document holding only its _id" { + var doc = bson.Document{ .arena = std.heap.ArenaAllocator.init(testing.allocator), .pairs = &.{} }; + defer doc.arena.deinit(); + doc.pairs = try doc.arena.allocator().dupe(bson.Pair, &.{ + .{ .key = "_id", .value = .{ .int32 = 7 } }, + .{ .key = "a", .value = .{ .int32 = 1 } }, + }); + + // Legal, and the reason `is_replacement` treats an empty document as one + // rather than as a no-op set of operators. + try apply(&doc, &doc_of(&.{})); + try testing.expectEqual(@as(usize, 1), doc.pairs.len); + try testing.expectEqualStrings("_id", doc.pairs[0].key); +} + +test "a replacement may repeat the _id it is replacing, but not change it" { + // Mutation check: drop the `bson.compare` in `apply_replacement`. Red on the + // second half -- a replacement would silently rewrite a document's identity, + // and since the `_id_` index is keyed on it, the stored entry and the stored + // document would disagree. + var doc = bson.Document{ .arena = std.heap.ArenaAllocator.init(testing.allocator), .pairs = &.{} }; + defer doc.arena.deinit(); + const original = [_]bson.Pair{ + .{ .key = "_id", .value = .{ .int32 = 7 } }, + .{ .key = "a", .value = .{ .int32 = 1 } }, + }; + doc.pairs = try doc.arena.allocator().dupe(bson.Pair, &original); + + // The same _id, restated: accepted. + try apply(&doc, &doc_of(&.{ + .{ .key = "_id", .value = .{ .int32 = 7 } }, + .{ .key = "b", .value = .{ .int32 = 2 } }, + })); + try testing.expectEqual(@as(i64, 7), doc.pairs[0].value.int32); + try testing.expectEqual(@as(i64, 2), bson.get_pair(doc.pairs, "b").?.int32); + // And only once, not twice. + try testing.expectEqual(@as(usize, 2), doc.pairs.len); + + // A different _id: refused. + try testing.expectError(error.ImmutableId, apply(&doc, &doc_of(&.{ + .{ .key = "_id", .value = .{ .int32 = 8 } }, + }))); + + // Equal across numeric types is the same _id, matching the canonical key + // encoding the _id_ index descends on. + try apply(&doc, &doc_of(&.{ + .{ .key = "_id", .value = .{ .double = 7.0 } }, + .{ .key = "c", .value = .{ .int32 = 3 } }, + })); + try testing.expectEqual(@as(i64, 3), bson.get_pair(doc.pairs, "c").?.int32); +} + +test "a replacement supplies the _id when the document has none" { + // The upsert path: the caller seeds a document from the filter's equalities + // and the replacement carries the _id. + // + // Mutation check: make the `old_id == null` arm `continue` without adding the + // pair. Red -- the upserted document loses the _id the client asked for and + // gets a generated ObjectId instead, so a retried upsert inserts twice. + var doc = bson.Document{ .arena = std.heap.ArenaAllocator.init(testing.allocator), .pairs = &.{} }; + defer doc.arena.deinit(); + + try apply(&doc, &doc_of(&.{ + .{ .key = "a", .value = .{ .int32 = 1 } }, + .{ .key = "_id", .value = .{ .int32 = 99 } }, + })); + + try testing.expectEqual(@as(usize, 2), doc.pairs.len); + try testing.expectEqualStrings("_id", doc.pairs[0].key); + try testing.expectEqual(@as(i64, 99), doc.pairs[0].value.int32); +} + +test "a mixed update document is refused from either side" { + // Mutation check: drop the `is_operator_key` guard in `apply`'s operator loop + // (first case) or in `apply_replacement`'s loop (second). Each leaves one + // half green and the other red. + var doc = bson.Document{ .arena = std.heap.ArenaAllocator.init(testing.allocator), .pairs = &.{} }; + defer doc.arena.deinit(); + + // Starts with an operator, so operators are expected throughout. try testing.expectError(error.InvalidUpdate, apply(&doc, &doc_of(&.{ + .{ .key = "$set", .value = .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} } }, .{ .key = "plain", .value = .{ .int32 = 1 } }, }))); + + // Starts with data, so it is a replacement and an operator has no meaning. + try testing.expectError(error.InvalidUpdate, apply(&doc, &doc_of(&.{ + .{ .key = "plain", .value = .{ .int32 = 1 } }, + .{ .key = "$set", .value = .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} } }, + }))); +} + +test "is_replacement decides on the first field only" { + try testing.expect(is_replacement(&.{})); + try testing.expect(is_replacement(&.{.{ .key = "a", .value = .{ .int32 = 1 } }})); + try testing.expect(!is_replacement(&.{.{ .key = "$set", .value = .{ .doc = &.{} } }})); + // An empty key cannot be an operator, so it reads as data -- and the + // replacement path then stores it, which is what MongoDB does with it. + try testing.expect(is_replacement(&.{.{ .key = "", .value = .{ .int32 = 1 } }})); } diff --git a/tests/e2e/e2e.js b/tests/e2e/e2e.js index 09cab52..351e5f2 100644 --- a/tests/e2e/e2e.js +++ b/tests/e2e/e2e.js @@ -68,6 +68,66 @@ async function main() { check('upsert', ups.upsertedCount === 1 && ups.matchedCount === 0, ups); check('upsert doc exists', (await users.findOne({ name: 'erin' }))?.age === 28); + // --- replacement-style writes --- + // A replacement is data, not instructions: it replaces every field except + // `_id`. Distinct enough from operator updates to be worth its own block -- + // these all used to fail with "bad update", because the update path required + // a $-prefixed first field. + const repl = db.collection('repl'); + await repl.drop().catch(() => {}); + await repl.insertMany([{ _id: 1, a: 1, gone: 'x' }, { _id: 2, a: 2 }, { _id: 3, a: 3 }]); + + const rp = await repl.replaceOne({ _id: 1 }, { fresh: 9 }); + check('replaceOne modifies one', rp.modifiedCount === 1, JSON.stringify(rp)); + const rpDoc = await repl.findOne({ _id: 1 }); + check( + 'replaceOne keeps _id and drops the old fields', + rpDoc._id === 1 && rpDoc.fresh === 9 && rpDoc.a === undefined && rpDoc.gone === undefined, + JSON.stringify(rpDoc), + ); + + const farRaw = await repl.findOneAndReplace({ _id: 2 }, { swapped: true }, { returnDocument: 'after' }); + const far = farRaw.value ?? farRaw; + check('findOneAndReplace returns the new document', far.swapped === true && far._id === 2, JSON.stringify(farRaw)); + + const bulkRep = await repl.bulkWrite([{ replaceOne: { filter: { _id: 3 }, replacement: { via: 'bulk' } } }]); + check('bulkWrite replaceOne', bulkRep.modifiedCount === 1, String(bulkRep.modifiedCount)); + + const rpUp = await repl.replaceOne({ _id: 99 }, { u: 1 }, { upsert: true }); + check('replacement upsert takes the _id from the filter', String(rpUp.upsertedId) === '99', JSON.stringify(rpUp.upsertedId)); + const rpUpDoc = await repl.findOne({ _id: 99 }); + check( + 'upserted document is the replacement plus _id', + rpUpDoc && rpUpDoc.u === 1 && Object.keys(rpUpDoc).length === 2, + JSON.stringify(rpUpDoc), + ); + + let idChangeRefused = false; + try { await repl.replaceOne({ _id: 1 }, { _id: 1234, x: 1 }); } catch { idChangeRefused = true; } + check('a replacement may not change _id', idChangeRefused); + + const rpEmpty = await repl.replaceOne({ _id: 1 }, {}); + const rpEmptyDoc = await repl.findOne({ _id: 1 }); + check( + 'an empty replacement leaves only _id', + rpEmpty.modifiedCount === 1 && Object.keys(rpEmptyDoc).length === 1, + JSON.stringify(rpEmptyDoc), + ); + + // Raw command: the driver refuses updateMany-with-a-replacement client side, + // so going through it would test the driver rather than this server. + let multiRefused = null; + try { + await db.command({ update: 'repl', updates: [{ q: {}, u: { a: 1 }, multi: true }] }); + } catch (e) { multiRefused = e; } + check( + 'multi with a replacement is refused with FailedToParse', + multiRefused?.code === 9 && /replacement-style/.test(multiRefused.message), + `code=${multiRefused?.code} ${multiRefused?.message?.slice(0, 50)}`, + ); + const multiOps = await db.command({ update: 'repl', updates: [{ q: {}, u: { $set: { touched: 1 } }, multi: true }] }); + check('multi with operators still runs', multiOps.ok === 1 && multiOps.n === 4, JSON.stringify(multiOps)); + // --- findOneAndUpdate (findAndModify) --- const fam = await users.findOneAndUpdate( { name: 'bob' }, diff --git a/tests/spec/scorecard.txt b/tests/spec/scorecard.txt index c5dd7a4..5a0d678 100644 --- a/tests/spec/scorecard.txt +++ b/tests/spec/scorecard.txt @@ -13,7 +13,7 @@ # semantics; ignoring them makes some cases pass that a full runner would # fail, so treat `pass` as an upper bound until M1 wires events up. -total 131 pass 161 fail 195 skip 175 files 0 errored +total 161 pass 131 fail 195 skip 175 files 0 errored # per-file: name pass fail skip aggregate-allowdiskuse.json 3 0 0 @@ -28,7 +28,7 @@ aggregate-write-readPreference.json 0 0 4 aggregate.json 5 0 2 bulkWrite-arrayFilters.json 0 3 0 bulkWrite-collation.json 0 2 0 -bulkWrite-comment.json 0 2 1 +bulkWrite-comment.json 2 0 1 bulkWrite-delete-hint-serverError.json 0 0 2 bulkWrite-delete-hint.json 2 0 0 bulkWrite-deleteMany-hint-unacknowledged.json 0 2 2 @@ -39,12 +39,12 @@ bulkWrite-deleteOne-let.json 0 1 1 bulkWrite-deleteOne-rawdata.json 1 0 1 bulkWrite-errorResponse.json 0 0 1 bulkWrite-insertOne-dots_and_dollars.json 3 1 1 -bulkWrite-replaceOne-dots_and_dollars.json 1 2 1 +bulkWrite-replaceOne-dots_and_dollars.json 2 1 1 bulkWrite-replaceOne-hint-unacknowledged.json 0 2 0 bulkWrite-replaceOne-let.json 0 1 1 bulkWrite-replaceOne-rawdata.json 1 0 1 bulkWrite-replaceOne-sort.json 1 0 1 -bulkWrite-update-hint.json 2 1 0 +bulkWrite-update-hint.json 3 0 0 bulkWrite-update-validation.json 3 0 0 bulkWrite-updateMany-dots_and_dollars.json 0 0 4 bulkWrite-updateMany-hint-unacknowledged.json 0 2 0 @@ -57,8 +57,8 @@ bulkWrite-updateOne-let.json 0 1 1 bulkWrite-updateOne-pipeline.json 0 1 0 bulkWrite-updateOne-rawdata.json 0 1 1 bulkWrite-updateOne-sort.json 1 0 1 -bulkWrite.json 5 5 0 -bypassDocumentValidation.json 6 3 0 +bulkWrite.json 8 2 0 +bypassDocumentValidation.json 8 1 0 client-bulkWrite-delete-options.json 0 0 2 client-bulkWrite-delete-rawdata.json 0 0 2 client-bulkWrite-errorResponse.json 0 0 1 @@ -127,15 +127,15 @@ findOneAndDelete-let.json 0 1 1 findOneAndDelete-rawdata.json 1 0 1 findOneAndDelete.json 3 0 0 findOneAndReplace-collation.json 0 1 0 -findOneAndReplace-comment.json 0 2 1 -findOneAndReplace-dots_and_dollars.json 1 2 1 +findOneAndReplace-comment.json 2 0 1 +findOneAndReplace-dots_and_dollars.json 2 1 1 findOneAndReplace-hint-serverError.json 0 0 2 findOneAndReplace-hint-unacknowledged.json 0 2 2 -findOneAndReplace-hint.json 0 2 0 +findOneAndReplace-hint.json 2 0 0 findOneAndReplace-let.json 0 1 1 -findOneAndReplace-rawdata.json 0 1 1 -findOneAndReplace-upsert.json 0 4 0 -findOneAndReplace.json 2 4 0 +findOneAndReplace-rawdata.json 1 0 1 +findOneAndReplace-upsert.json 2 2 0 +findOneAndReplace.json 4 2 0 findOneAndUpdate-arrayFilters.json 0 3 0 findOneAndUpdate-collation.json 0 1 0 findOneAndUpdate-comment.json 0 2 1 @@ -158,15 +158,15 @@ insertOne-errorResponse.json 0 0 1 insertOne-rawdata.json 1 0 1 insertOne.json 1 0 0 replaceOne-collation.json 0 1 0 -replaceOne-comment.json 0 2 1 -replaceOne-dots_and_dollars.json 2 2 1 +replaceOne-comment.json 2 0 1 +replaceOne-dots_and_dollars.json 3 1 1 replaceOne-hint-unacknowledged.json 0 2 0 -replaceOne-hint.json 0 2 0 +replaceOne-hint.json 2 0 0 replaceOne-let.json 0 1 1 -replaceOne-rawdata.json 0 1 1 +replaceOne-rawdata.json 1 0 1 replaceOne-sort.json 1 0 1 replaceOne-validation.json 1 0 0 -replaceOne.json 1 4 0 +replaceOne.json 5 0 0 updateMany-arrayFilters.json 0 3 0 updateMany-collation.json 0 1 0 updateMany-comment.json 2 0 1 @@ -188,7 +188,7 @@ updateOne-hint.json 2 0 0 updateOne-let.json 0 1 1 updateOne-pipeline.json 0 1 0 updateOne-rawdata.json 1 0 1 -updateOne-sort.json 0 1 1 +updateOne-sort.json 1 0 1 updateOne-validation.json 1 0 0 updateOne.json 4 0 0 @@ -215,9 +215,7 @@ bulkWrite-arrayFilters.json FAIL BulkWrite updateOne with arrayFilters outcome c bulkWrite-arrayFilters.json FAIL BulkWrite updateMany with arrayFilters outcome crud-tests.test[0].y: expected an array, got {"$[i]":{"b":2}} bulkWrite-arrayFilters.json FAIL BulkWrite with arrayFilters outcome crud-tests.test[0].y: expected an array, got {"$[i]":{"b":2}} bulkWrite-collation.json FAIL BulkWrite with delete operations and collation bulkWrite.deletedCount: expected 4, got 0 -bulkWrite-collation.json FAIL BulkWrite with update operations and collation MongoBulkWriteError: internal error -bulkWrite-comment.json FAIL BulkWrite with string comment MongoBulkWriteError: bad update -bulkWrite-comment.json FAIL BulkWrite with document comment MongoBulkWriteError: bad update +bulkWrite-collation.json FAIL BulkWrite with update operations and collation bulkWrite.matchedCount: expected 6, got 2 bulkWrite-comment.json SKIP BulkWrite with comment - pre 4.4 needs server <= 4.2.99 bulkWrite-delete-hint-serverError.json SKIP * needs server <= 4.3.3 bulkWrite-deleteMany-hint-unacknowledged.json SKIP Unacknowledged deleteMany with hint string fails with client-side error on pre-4.4 server needs server <= 4.2.99 @@ -237,16 +235,14 @@ bulkWrite-deleteOne-rawdata.json SKIP BulkWrite deleteOne with rawData option ne bulkWrite-errorResponse.json SKIP bulkWrite operations support errorResponse assertions runner: failPoint bulkWrite-insertOne-dots_and_dollars.json SKIP Inserting document with top-level dollar-prefixed key on 5.0+ server needs server >= 5.0 bulkWrite-insertOne-dots_and_dollars.json FAIL Inserting document with top-level dollar-prefixed key on pre-5.0 server yields server-side error bulkWrite: expected an error, the operation succeeded -bulkWrite-replaceOne-dots_and_dollars.json FAIL Replacing document with top-level dotted key on 3.6+ server MongoBulkWriteError: bad update bulkWrite-replaceOne-dots_and_dollars.json SKIP Replacing document with dollar-prefixed key in embedded doc on 5.0+ server needs server >= 5.0 -bulkWrite-replaceOne-dots_and_dollars.json FAIL Replacing document with dotted key in embedded doc on 3.6+ server MongoBulkWriteError: bad update -bulkWrite-replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint string on 4.2+ server MongoBulkWriteError: bad update -bulkWrite-replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint document on 4.2+ server MongoBulkWriteError: bad update +bulkWrite-replaceOne-dots_and_dollars.json FAIL Replacing document with dollar-prefixed key in embedded doc on pre-5.0 server yields server-side error bulkWrite: expected an error, the operation succeeded +bulkWrite-replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint string on 4.2+ server bulkWrite: unexpected extra keys ["insertedCount","matchedCount","modifiedCount","deletedCount","upsertedCount","upsertedIds","insertedIds"] +bulkWrite-replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint document on 4.2+ server bulkWrite: unexpected extra keys ["insertedCount","matchedCount","modifiedCount","deletedCount","upsertedCount","upsertedIds","insertedIds"] bulkWrite-replaceOne-let.json SKIP BulkWrite replaceOne with let option needs server >= 5.0 bulkWrite-replaceOne-let.json FAIL BulkWrite replaceOne with let option unsupported (server-side error) bulkWrite: expected an error, the operation succeeded bulkWrite-replaceOne-rawdata.json SKIP BulkWrite replaceOne with rawData option needs server >= 8.2.0 bulkWrite-replaceOne-sort.json SKIP BulkWrite replaceOne with sort option needs server >= 8.0 -bulkWrite-update-hint.json FAIL BulkWrite replaceOne with update hints MongoBulkWriteError: bad update bulkWrite-updateMany-dots_and_dollars.json SKIP Updating document to set top-level dollar-prefixed key on 5.0+ server needs server >= 5.0 bulkWrite-updateMany-dots_and_dollars.json SKIP Updating document to set top-level dotted key on 5.0+ server needs server >= 5.0 bulkWrite-updateMany-dots_and_dollars.json SKIP Updating document to set dollar-prefixed key in embedded doc on 5.0+ server needs server >= 5.0 @@ -270,14 +266,9 @@ bulkWrite-updateOne-pipeline.json FAIL UpdateOne in bulk write using pipelines M bulkWrite-updateOne-rawdata.json SKIP BulkWrite updateOne with rawData option needs server >= 8.2.0 bulkWrite-updateOne-rawdata.json FAIL BulkWrite updateOne with rawData option on less than 8.2.0 - ignore argument MongoBulkWriteError: update spec requires u bulkWrite-updateOne-sort.json SKIP BulkWrite updateOne with sort option needs server >= 8.0 -bulkWrite.json FAIL BulkWrite with replaceOne operations MongoBulkWriteError: bad update bulkWrite.json FAIL BulkWrite with updateOne operations bulkWrite.modifiedCount: expected 1, got 2 bulkWrite.json FAIL BulkWrite with updateMany operations bulkWrite.modifiedCount: expected 2, got 4 -bulkWrite.json FAIL BulkWrite with mixed ordered operations MongoBulkWriteError: internal error -bulkWrite.json FAIL BulkWrite with mixed unordered operations MongoBulkWriteError: internal error bypassDocumentValidation.json FAIL Aggregate with $out passes bypassDocumentValidation: false MongoServerError: Unrecognized pipeline stage name: '$out' -bypassDocumentValidation.json FAIL FindOneAndReplace passes bypassDocumentValidation: false MongoServerError: bad update -bypassDocumentValidation.json FAIL ReplaceOne passes bypassDocumentValidation: false MongoServerError: bad update client-bulkWrite-delete-options.json SKIP * needs server >= 8.0 client-bulkWrite-delete-rawdata.json SKIP client bulk write delete with rawData option needs server >= 8.2.0 client-bulkWrite-delete-rawdata.json SKIP client bulk write delete with rawData option on less than 8.2.0 - ignore argument needs server >= 8.0 @@ -313,7 +304,7 @@ create-null-ids.json FAIL inserting _id with type null via insertOne countDocume create-null-ids.json FAIL inserting _id with type null via insertMany countDocuments: expected 1, got 0 create-null-ids.json FAIL inserting _id with type null via updateOne countDocuments: expected 1, got 0 create-null-ids.json FAIL inserting _id with type null via updateMany countDocuments: expected 1, got 0 -create-null-ids.json FAIL inserting _id with type null via replaceOne MongoServerError: internal error +create-null-ids.json FAIL inserting _id with type null via replaceOne countDocuments: expected 1, got 0 create-null-ids.json FAIL inserting _id with type null via bulkWrite countDocuments: expected 1, got 0 create-null-ids.json SKIP inserting _id with type null via clientBulkWrite needs server >= 8.0 db-aggregate-rawdata.json SKIP Aggregate with rawData option needs server >= 8.2.0, needs topology replicaset @@ -380,31 +371,21 @@ findOneAndDelete-let.json SKIP findOneAndDelete with let option needs server >= findOneAndDelete-let.json FAIL findOneAndDelete with let option unsupported (server-side error) findOneAndDelete: expected an error, the operation succeeded findOneAndDelete-rawdata.json SKIP findOneAndDelete with rawData option needs server >= 8.2.0 findOneAndReplace-collation.json FAIL FindOneAndReplace when one document matches with collation returning the document after modification findOneAndReplace: expected a document, got null -findOneAndReplace-comment.json FAIL findOneAndReplace with string comment MongoServerError: bad update -findOneAndReplace-comment.json FAIL findOneAndReplace with document comment MongoServerError: bad update findOneAndReplace-comment.json SKIP findOneAndReplace with comment - pre 4.4 needs server <= 4.2.99 -findOneAndReplace-dots_and_dollars.json FAIL Replacing document with top-level dotted key on 3.6+ server MongoServerError: bad update findOneAndReplace-dots_and_dollars.json SKIP Replacing document with dollar-prefixed key in embedded doc on 5.0+ server needs server >= 5.0 -findOneAndReplace-dots_and_dollars.json FAIL Replacing document with dotted key in embedded doc on 3.6+ server MongoServerError: bad update +findOneAndReplace-dots_and_dollars.json FAIL Replacing document with dollar-prefixed key in embedded doc on pre-5.0 server yields server-side error findOneAndReplace: expected an error, the operation succeeded findOneAndReplace-hint-serverError.json SKIP * needs server <= 4.3.0 findOneAndReplace-hint-unacknowledged.json SKIP Unacknowledged findOneAndReplace with hint string fails with client-side error on pre-4.4 server needs server <= 4.2.99 findOneAndReplace-hint-unacknowledged.json SKIP Unacknowledged findOneAndReplace with hint document fails with client-side error on pre-4.4 server needs server <= 4.2.99 -findOneAndReplace-hint-unacknowledged.json FAIL Unacknowledged findOneAndReplace with hint string on 4.4+ server MongoServerError: bad update -findOneAndReplace-hint-unacknowledged.json FAIL Unacknowledged findOneAndReplace with hint document on 4.4+ server MongoServerError: bad update -findOneAndReplace-hint.json FAIL FindOneAndReplace with hint string MongoServerError: bad update -findOneAndReplace-hint.json FAIL FindOneAndReplace with hint document MongoServerError: bad update +findOneAndReplace-hint-unacknowledged.json FAIL Unacknowledged findOneAndReplace with hint string on 4.4+ server findOneAndReplace: expected null, got {"_id":2,"x":22} +findOneAndReplace-hint-unacknowledged.json FAIL Unacknowledged findOneAndReplace with hint document on 4.4+ server findOneAndReplace: expected null, got {"_id":2,"x":22} findOneAndReplace-let.json SKIP findOneAndReplace with let option needs server >= 5.0 findOneAndReplace-let.json FAIL findOneAndReplace with let option unsupported (server-side error) findOneAndReplace: expected an error, the operation succeeded findOneAndReplace-rawdata.json SKIP findOneAndReplace with rawData option needs server >= 8.2.0 -findOneAndReplace-rawdata.json FAIL findOneAndReplace with rawData option on less than 8.2.0 - ignore argument MongoServerError: bad update -findOneAndReplace-upsert.json FAIL FindOneAndReplace when no documents match without id specified with upsert returning the document before modification MongoServerError: internal error -findOneAndReplace-upsert.json FAIL FindOneAndReplace when no documents match without id specified with upsert returning the document after modification MongoServerError: internal error -findOneAndReplace-upsert.json FAIL FindOneAndReplace when no documents match with id specified with upsert returning the document before modification MongoServerError: internal error -findOneAndReplace-upsert.json FAIL FindOneAndReplace when no documents match with id specified with upsert returning the document after modification MongoServerError: internal error -findOneAndReplace.json FAIL FindOneAndReplace when many documents match returning the document before modification MongoServerError: bad update -findOneAndReplace.json FAIL FindOneAndReplace when many documents match returning the document after modification MongoServerError: bad update -findOneAndReplace.json FAIL FindOneAndReplace when one document matches returning the document before modification MongoServerError: bad update -findOneAndReplace.json FAIL FindOneAndReplace when one document matches returning the document after modification MongoServerError: bad update +findOneAndReplace-upsert.json FAIL FindOneAndReplace when no documents match without id specified with upsert returning the document after modification findOneAndReplace: expected a document, got null +findOneAndReplace-upsert.json FAIL FindOneAndReplace when no documents match with id specified with upsert returning the document after modification findOneAndReplace: expected a document, got null +findOneAndReplace.json FAIL FindOneAndReplace when many documents match returning the document after modification findOneAndReplace.x: expected 32, got 22 +findOneAndReplace.json FAIL FindOneAndReplace when one document matches returning the document after modification findOneAndReplace.x: expected 32, got 22 findOneAndUpdate-arrayFilters.json FAIL FindOneAndUpdate when no document matches arrayFilters outcome crud-v1.coll[0].y: expected an array, got {"$[i]":{"b":2}} findOneAndUpdate-arrayFilters.json FAIL FindOneAndUpdate when one document matches arrayFilters outcome crud-v1.coll[0].y: expected an array, got {"$[i]":{"b":2}} findOneAndUpdate-arrayFilters.json FAIL FindOneAndUpdate when multiple documents match arrayFilters outcome crud-v1.coll[0].y: expected an array, got {"$[i]":{"b":2}} @@ -447,25 +428,15 @@ insertOne-dots_and_dollars.json FAIL Unacknowledged write using dollar-prefixed insertOne-errorResponse.json SKIP insert operations support errorResponse assertions runner: failPoint insertOne-rawdata.json SKIP insertOne with rawData option needs server >= 8.2.0 replaceOne-collation.json FAIL ReplaceOne when one document matches with collation replaceOne.matchedCount: expected 1, got 0 -replaceOne-comment.json FAIL ReplaceOne with string comment MongoServerError: bad update -replaceOne-comment.json FAIL ReplaceOne with document comment MongoServerError: bad update replaceOne-comment.json SKIP ReplaceOne with comment - pre 4.4 needs server <= 4.2.99 -replaceOne-dots_and_dollars.json FAIL Replacing document with top-level dotted key on 3.6+ server MongoServerError: bad update replaceOne-dots_and_dollars.json SKIP Replacing document with dollar-prefixed key in embedded doc on 5.0+ server needs server >= 5.0 -replaceOne-dots_and_dollars.json FAIL Replacing document with dotted key in embedded doc on 3.6+ server MongoServerError: bad update -replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint string on 4.2+ server MongoServerError: bad update -replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint document on 4.2+ server MongoServerError: bad update -replaceOne-hint.json FAIL ReplaceOne with hint string MongoServerError: bad update -replaceOne-hint.json FAIL ReplaceOne with hint document MongoServerError: bad update +replaceOne-dots_and_dollars.json FAIL Replacing document with dollar-prefixed key in embedded doc on pre-5.0 server yields server-side error replaceOne: expected an error, the operation succeeded +replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint string on 4.2+ server replaceOne: unexpected extra keys ["modifiedCount","upsertedId","upsertedCount","matchedCount"] +replaceOne-hint-unacknowledged.json FAIL Unacknowledged replaceOne with hint document on 4.2+ server replaceOne: unexpected extra keys ["modifiedCount","upsertedId","upsertedCount","matchedCount"] replaceOne-let.json SKIP ReplaceOne with let option needs server >= 5.0 replaceOne-let.json FAIL ReplaceOne with let option unsupported (server-side error) replaceOne: expected an error, the operation succeeded replaceOne-rawdata.json SKIP ReplaceOne with rawData option needs server >= 8.2.0 -replaceOne-rawdata.json FAIL ReplaceOne with rawData option on less than 8.2.0 - ignore argument MongoServerError: bad update replaceOne-sort.json SKIP ReplaceOne with sort option needs server >= 8.0 -replaceOne.json FAIL ReplaceOne when many documents match MongoServerError: bad update -replaceOne.json FAIL ReplaceOne when one document matches MongoServerError: bad update -replaceOne.json FAIL ReplaceOne with upsert when no documents match without an id specified MongoServerError: internal error -replaceOne.json FAIL ReplaceOne with upsert when no documents match with an id specified MongoServerError: internal error updateMany-arrayFilters.json FAIL UpdateMany when no documents match arrayFilters updateMany.modifiedCount: expected 0, got 2 updateMany-arrayFilters.json FAIL UpdateMany when one document matches arrayFilters updateMany.modifiedCount: expected 1, got 2 updateMany-arrayFilters.json FAIL UpdateMany when multiple documents match arrayFilters outcome crud-v1.coll[0].y: expected an array, got {"$[i]":{"b":2}} @@ -500,4 +471,3 @@ updateOne-let.json FAIL UpdateOne with let option unsupported (server-side error updateOne-pipeline.json FAIL UpdateOne using pipelines MongoServerError: update spec requires u updateOne-rawdata.json SKIP UpdateOne with rawData option needs server >= 8.2.0 updateOne-sort.json SKIP UpdateOne with sort option needs server >= 8.0 -updateOne-sort.json FAIL updateOne with sort option unsupported (server-side error) updateOne: expected an error, the operation succeeded