Skip to content

Commit c37e345

Browse files
authored
Merge pull request #14837 from Automattic/vkarpov15/gh-14763
fix(model): throw error if `bulkSave()` did not insert or update any documents
2 parents 94e1237 + 5b40f1b commit c37e345

File tree

2 files changed

+77
-14
lines changed

2 files changed

+77
-14
lines changed

lib/model.js

+35-14
Original file line numberDiff line numberDiff line change
@@ -3368,19 +3368,27 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
33683368
};
33693369

33703370
/**
3371-
* takes an array of documents, gets the changes and inserts/updates documents in the database
3372-
* according to whether or not the document is new, or whether it has changes or not.
3371+
* Takes an array of documents, gets the changes and inserts/updates documents in the database
3372+
* according to whether or not the document is new, or whether it has changes or not.
33733373
*
33743374
* `bulkSave` uses `bulkWrite` under the hood, so it's mostly useful when dealing with many documents (10K+)
33753375
*
3376+
* `bulkSave()` throws errors under the following conditions:
3377+
*
3378+
* - one of the provided documents fails validation. In this case, `bulkSave()` does not send a `bulkWrite()`, and throws the first validation error.
3379+
* - `bulkWrite()` fails (for example, due to being unable to connect to MongoDB or due to duplicate key error)
3380+
* - `bulkWrite()` did not insert or update **any** documents. In this case, `bulkSave()` will throw a DocumentNotFound error.
3381+
*
3382+
* Note that `bulkSave()` will **not** throw an error if only some of the `save()` calls succeeded.
3383+
*
33763384
* @param {Array<Document>} documents
33773385
* @param {Object} [options] options passed to the underlying `bulkWrite()`
33783386
* @param {Boolean} [options.timestamps] defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents.
33793387
* @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
33803388
* @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()) for more information.
33813389
* @param {number} [options.wtimeout=null] The [write concern timeout](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout).
33823390
* @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option)
3383-
*
3391+
* @return {BulkWriteResult} the return value from `bulkWrite()`
33843392
*/
33853393
Model.bulkSave = async function bulkSave(documents, options) {
33863394
options = options || {};
@@ -3408,18 +3416,31 @@ Model.bulkSave = async function bulkSave(documents, options) {
34083416
(err) => ({ bulkWriteResult: null, bulkWriteError: err })
34093417
);
34103418

3411-
await Promise.all(
3412-
documents.map(async(document) => {
3413-
const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
3414-
const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3415-
return writeErrorDocumentId.toString() === document._doc._id.toString();
3416-
});
3419+
const matchedCount = bulkWriteResult?.matchedCount ?? 0;
3420+
const insertedCount = bulkWriteResult?.insertedCount ?? 0;
3421+
if (writeOperations.length > 0 && matchedCount + insertedCount === 0 && !bulkWriteError) {
3422+
throw new DocumentNotFoundError(
3423+
writeOperations.filter(op => op.updateOne).map(op => op.updateOne.filter),
3424+
this.modelName,
3425+
writeOperations.length,
3426+
bulkWriteResult
3427+
);
3428+
}
34173429

3418-
if (documentError == null) {
3419-
await handleSuccessfulWrite(document);
3420-
}
3421-
})
3422-
);
3430+
const successfulDocuments = [];
3431+
for (let i = 0; i < documents.length; i++) {
3432+
const document = documents[i];
3433+
const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
3434+
const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3435+
return writeErrorDocumentId.toString() === document._doc._id.toString();
3436+
});
3437+
3438+
if (documentError == null) {
3439+
successfulDocuments.push(document);
3440+
}
3441+
}
3442+
3443+
await Promise.all(successfulDocuments.map(document => handleSuccessfulWrite(document)));
34233444

34243445
if (bulkWriteError && bulkWriteError.writeErrors && bulkWriteError.writeErrors.length) {
34253446
throw bulkWriteError;

test/model.test.js

+42
Original file line numberDiff line numberDiff line change
@@ -6973,6 +6973,48 @@ describe('Model', function() {
69736973
assert.ok(err == null);
69746974

69756975
});
6976+
it('should error if no documents were inserted or updated (gh-14763)', async function() {
6977+
const fooSchema = new mongoose.Schema({
6978+
bar: { type: Number }
6979+
}, { optimisticConcurrency: true });
6980+
const TestModel = db.model('Test', fooSchema);
6981+
6982+
const foo = await TestModel.create({
6983+
bar: 0
6984+
});
6985+
6986+
// update 1
6987+
foo.bar = 1;
6988+
await foo.save();
6989+
6990+
// parallel update
6991+
const fooCopy = await TestModel.findById(foo._id);
6992+
fooCopy.bar = 99;
6993+
await fooCopy.save();
6994+
6995+
foo.bar = 2;
6996+
const err = await TestModel.bulkSave([foo]).then(() => null, err => err);
6997+
assert.equal(err.name, 'DocumentNotFoundError');
6998+
assert.equal(err.numAffected, 1);
6999+
assert.ok(Array.isArray(err.filter));
7000+
});
7001+
it('should error if there is a validation error', async function() {
7002+
const fooSchema = new mongoose.Schema({
7003+
bar: { type: Number }
7004+
}, { optimisticConcurrency: true });
7005+
const TestModel = db.model('Test', fooSchema);
7006+
7007+
const docs = [
7008+
new TestModel({ bar: 42 }),
7009+
new TestModel({ bar: 'taco' })
7010+
];
7011+
const err = await TestModel.bulkSave(docs).then(() => null, err => err);
7012+
assert.equal(err.name, 'ValidationError');
7013+
7014+
// bulkSave() does not save any documents if any documents fail validation
7015+
const fromDb = await TestModel.find();
7016+
assert.equal(fromDb.length, 0);
7017+
});
69767018
it('Using bulkSave should not trigger an error (gh-11071)', async function() {
69777019

69787020
const pairSchema = mongoose.Schema({

0 commit comments

Comments
 (0)