Skip to content

Commit 09f2a67

Browse files
fix(NODE-5628): bulkWriteResult.insertedIds does not filter out _ids that are not actually inserted (#3867)
Co-authored-by: Durran Jordan <durran@gmail.com>
1 parent 3932260 commit 09f2a67

File tree

2 files changed

+155
-8
lines changed

2 files changed

+155
-8
lines changed

src/bulk/common.ts

+38-7
Original file line numberDiff line numberDiff line change
@@ -197,15 +197,17 @@ export class BulkWriteResult {
197197
* Create a new BulkWriteResult instance
198198
* @internal
199199
*/
200-
constructor(bulkResult: BulkResult) {
200+
constructor(bulkResult: BulkResult, isOrdered: boolean) {
201201
this.result = bulkResult;
202202
this.insertedCount = this.result.nInserted ?? 0;
203203
this.matchedCount = this.result.nMatched ?? 0;
204204
this.modifiedCount = this.result.nModified ?? 0;
205205
this.deletedCount = this.result.nRemoved ?? 0;
206206
this.upsertedCount = this.result.upserted.length ?? 0;
207207
this.upsertedIds = BulkWriteResult.generateIdMap(this.result.upserted);
208-
this.insertedIds = BulkWriteResult.generateIdMap(this.result.insertedIds);
208+
this.insertedIds = BulkWriteResult.generateIdMap(
209+
this.getSuccessfullyInsertedIds(bulkResult, isOrdered)
210+
);
209211
Object.defineProperty(this, 'result', { value: this.result, enumerable: false });
210212
}
211213

@@ -214,6 +216,22 @@ export class BulkWriteResult {
214216
return this.result.ok;
215217
}
216218

219+
/**
220+
* Returns document_ids that were actually inserted
221+
* @internal
222+
*/
223+
private getSuccessfullyInsertedIds(bulkResult: BulkResult, isOrdered: boolean): Document[] {
224+
if (bulkResult.writeErrors.length === 0) return bulkResult.insertedIds;
225+
226+
if (isOrdered) {
227+
return bulkResult.insertedIds.slice(0, bulkResult.writeErrors[0].index);
228+
}
229+
230+
return bulkResult.insertedIds.filter(
231+
({ index }) => !bulkResult.writeErrors.some(writeError => index === writeError.index)
232+
);
233+
}
234+
217235
/** Returns the upserted id at the given index */
218236
getUpsertedIdAt(index: number): Document | undefined {
219237
return this.result.upserted[index];
@@ -479,7 +497,10 @@ function executeCommands(
479497
callback: Callback<BulkWriteResult>
480498
) {
481499
if (bulkOperation.s.batches.length === 0) {
482-
return callback(undefined, new BulkWriteResult(bulkOperation.s.bulkResult));
500+
return callback(
501+
undefined,
502+
new BulkWriteResult(bulkOperation.s.bulkResult, bulkOperation.isOrdered)
503+
);
483504
}
484505

485506
const batch = bulkOperation.s.batches.shift() as Batch;
@@ -488,17 +509,26 @@ function executeCommands(
488509
// Error is a driver related error not a bulk op error, return early
489510
if (err && 'message' in err && !(err instanceof MongoWriteConcernError)) {
490511
return callback(
491-
new MongoBulkWriteError(err, new BulkWriteResult(bulkOperation.s.bulkResult))
512+
new MongoBulkWriteError(
513+
err,
514+
new BulkWriteResult(bulkOperation.s.bulkResult, bulkOperation.isOrdered)
515+
)
492516
);
493517
}
494518

495519
if (err instanceof MongoWriteConcernError) {
496-
return handleMongoWriteConcernError(batch, bulkOperation.s.bulkResult, err, callback);
520+
return handleMongoWriteConcernError(
521+
batch,
522+
bulkOperation.s.bulkResult,
523+
bulkOperation.isOrdered,
524+
err,
525+
callback
526+
);
497527
}
498528

499529
// Merge the results together
500530
mergeBatchResults(batch, bulkOperation.s.bulkResult, err, result);
501-
const writeResult = new BulkWriteResult(bulkOperation.s.bulkResult);
531+
const writeResult = new BulkWriteResult(bulkOperation.s.bulkResult, bulkOperation.isOrdered);
502532
if (bulkOperation.handleWriteError(callback, writeResult)) return;
503533

504534
// Execute the next command in line
@@ -572,6 +602,7 @@ function executeCommands(
572602
function handleMongoWriteConcernError(
573603
batch: Batch,
574604
bulkResult: BulkResult,
605+
isOrdered: boolean,
575606
err: MongoWriteConcernError,
576607
callback: Callback<BulkWriteResult>
577608
) {
@@ -583,7 +614,7 @@ function handleMongoWriteConcernError(
583614
message: err.result?.writeConcernError.errmsg,
584615
code: err.result?.writeConcernError.result
585616
},
586-
new BulkWriteResult(bulkResult)
617+
new BulkWriteResult(bulkResult, isOrdered)
587618
)
588619
);
589620
}

test/integration/crud/bulk.test.ts

+117-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type Collection,
66
Long,
77
MongoBatchReExecutionError,
8+
MongoBulkWriteError,
89
type MongoClient,
910
MongoDriverError,
1011
MongoInvalidArgumentError
@@ -31,7 +32,6 @@ describe('Bulk', function () {
3132
.createCollection('test')
3233
.catch(() => null); // make ns exist
3334
});
34-
3535
afterEach(async function () {
3636
const cleanup = this.configuration.newClient();
3737
await cleanup
@@ -91,6 +91,7 @@ describe('Bulk', function () {
9191
}
9292
});
9393
});
94+
9495
context('when passed a valid document list', function () {
9596
it('insertMany should not throw a MongoInvalidArgument error when called with a valid operation', async function () {
9697
try {
@@ -104,6 +105,121 @@ describe('Bulk', function () {
104105
}
105106
});
106107
});
108+
109+
context('when inserting duplicate values', function () {
110+
let col;
111+
112+
beforeEach(async function () {
113+
const db = client.db();
114+
col = db.collection('test');
115+
await col.createIndex([{ a: 1 }], { unique: true, sparse: false });
116+
});
117+
118+
async function assertFailsWithDuplicateFields(input, isOrdered, expectedInsertedIds) {
119+
const error = await col.insertMany(input, { ordered: isOrdered }).catch(error => error);
120+
expect(error).to.be.instanceOf(MongoBulkWriteError);
121+
expect(error.result.insertedCount).to.equal(Object.keys(error.result.insertedIds).length);
122+
expect(error.result.insertedIds).to.deep.equal(expectedInsertedIds);
123+
}
124+
125+
context('when the insert is ordered', function () {
126+
it('contains the correct insertedIds on one duplicate insert', async function () {
127+
await assertFailsWithDuplicateFields(
128+
[
129+
{ _id: 0, a: 1 },
130+
{ _id: 1, a: 1 }
131+
],
132+
true,
133+
{ 0: 0 }
134+
);
135+
});
136+
137+
it('contains the correct insertedIds on multiple duplicate inserts', async function () {
138+
await assertFailsWithDuplicateFields(
139+
[
140+
{ _id: 0, a: 1 },
141+
{ _id: 1, a: 1 },
142+
{ _id: 2, a: 1 },
143+
{ _id: 3, b: 2 }
144+
],
145+
true,
146+
{ 0: 0 }
147+
);
148+
});
149+
});
150+
151+
context('when the insert is unordered', function () {
152+
it('contains the correct insertedIds on multiple duplicate inserts', async function () {
153+
await assertFailsWithDuplicateFields(
154+
[
155+
{ _id: 0, a: 1 },
156+
{ _id: 1, a: 1 },
157+
{ _id: 2, a: 1 },
158+
{ _id: 3, b: 2 }
159+
],
160+
false,
161+
{ 0: 0, 3: 3 }
162+
);
163+
});
164+
});
165+
});
166+
});
167+
168+
describe('#bulkWrite()', function () {
169+
context('when inserting duplicate values', function () {
170+
let col;
171+
172+
beforeEach(async function () {
173+
const db = client.db();
174+
col = db.collection('test');
175+
await col.createIndex([{ a: 1 }], { unique: true, sparse: false });
176+
});
177+
178+
async function assertFailsWithDuplicateFields(input, isOrdered, expectedInsertedIds) {
179+
const error = await col.bulkWrite(input, { ordered: isOrdered }).catch(error => error);
180+
expect(error).to.be.instanceOf(MongoBulkWriteError);
181+
expect(error.result.insertedCount).to.equal(Object.keys(error.result.insertedIds).length);
182+
expect(error.result.insertedIds).to.deep.equal(expectedInsertedIds);
183+
}
184+
185+
context('when the insert is ordered', function () {
186+
it('contains the correct insertedIds on one duplicate insert', async function () {
187+
await assertFailsWithDuplicateFields(
188+
[{ insertOne: { _id: 0, a: 1 } }, { insertOne: { _id: 1, a: 1 } }],
189+
true,
190+
{ 0: 0 }
191+
);
192+
});
193+
194+
it('contains the correct insertedIds on multiple duplicate inserts', async function () {
195+
await assertFailsWithDuplicateFields(
196+
[
197+
{ insertOne: { _id: 0, a: 1 } },
198+
{ insertOne: { _id: 1, a: 1 } },
199+
{ insertOne: { _id: 2, a: 1 } },
200+
{ insertOne: { _id: 3, b: 2 } }
201+
],
202+
true,
203+
{ 0: 0 }
204+
);
205+
});
206+
});
207+
208+
context('when the insert is unordered', function () {
209+
it('contains the correct insertedIds on multiple duplicate inserts', async function () {
210+
await assertFailsWithDuplicateFields(
211+
[
212+
{ insertOne: { _id: 0, a: 1 } },
213+
{ insertOne: { _id: 1, a: 1 } },
214+
{ insertOne: { _id: 2, a: 1 } },
215+
{ insertOne: { _id: 3, b: 2 } }
216+
],
217+
false,
218+
{ 0: 0, 3: 3 }
219+
);
220+
});
221+
});
222+
});
107223
});
108224
});
109225

0 commit comments

Comments
 (0)