From 1389c2b61a8f3ff2708b5a567359570a45c6880c Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Mar 2025 14:48:54 +0100 Subject: [PATCH 1/2] feat: add Mongo bulkWrite API MONGOSH-1100 --- .../src/all-transport-types.ts | 2 + .../service-provider-core/src/writable.ts | 16 ++++- .../src/node-driver-service-provider.ts | 15 +++- packages/shell-api/src/mongo.spec.ts | 68 +++++++++++++++++++ packages/shell-api/src/mongo.ts | 15 ++++ 5 files changed, 114 insertions(+), 2 deletions(-) diff --git a/packages/service-provider-core/src/all-transport-types.ts b/packages/service-provider-core/src/all-transport-types.ts index c87f8cb130..b4bccece61 100644 --- a/packages/service-provider-core/src/all-transport-types.ts +++ b/packages/service-provider-core/src/all-transport-types.ts @@ -2,9 +2,11 @@ export type { AggregateOptions, AggregationCursor, AnyBulkWriteOperation, + AnyClientBulkWriteModel, Batch, BulkWriteOptions, BulkWriteResult, + ClientBulkWriteResult, ChangeStream, ChangeStreamOptions, ClientSession, diff --git a/packages/service-provider-core/src/writable.ts b/packages/service-provider-core/src/writable.ts index 436b93b4db..6e6cab08e9 100644 --- a/packages/service-provider-core/src/writable.ts +++ b/packages/service-provider-core/src/writable.ts @@ -1,4 +1,4 @@ -import type { RunCursorCommandOptions } from 'mongodb'; +import type { ClientBulkWriteOptions, RunCursorCommandOptions } from 'mongodb'; import type { Document, InsertOneOptions, @@ -10,6 +10,8 @@ import type { FindOneAndUpdateOptions, BulkWriteOptions, AnyBulkWriteOperation, + AnyClientBulkWriteModel, + ClientBulkWriteResult, DeleteOptions, DeleteResult, InsertManyResult, @@ -108,6 +110,18 @@ export default interface Writable { dbOptions?: DbOptions ): Promise; + /** + * Executes a client bulk write operation, available on server 8.0+. + * @param models - The client bulk write models. + * @param options - The bulk write options. + * + * @returns {Promise} The promise of the result. + */ + clientBulkWrite( + models: AnyClientBulkWriteModel[], + options: ClientBulkWriteOptions + ): Promise; + /** * Delete multiple documents from the collection. * diff --git a/packages/service-provider-node-driver/src/node-driver-service-provider.ts b/packages/service-provider-node-driver/src/node-driver-service-provider.ts index ceb4683657..a256915575 100644 --- a/packages/service-provider-node-driver/src/node-driver-service-provider.ts +++ b/packages/service-provider-node-driver/src/node-driver-service-provider.ts @@ -23,8 +23,10 @@ import type { AggregateOptions, AggregationCursor, AnyBulkWriteOperation, + AnyClientBulkWriteModel, BulkWriteOptions, BulkWriteResult, + ClientBulkWriteResult, ClientSessionOptions, Collection, CountDocumentsOptions, @@ -630,6 +632,17 @@ export class NodeDriverServiceProvider .bulkWrite(requests, options); } + /** + * Executes a client bulk write operation, available on server 8.0+. + */ + clientBulkWrite( + models: AnyClientBulkWriteModel[], + options: BulkWriteOptions = {} + ): Promise { + options = { ...this.baseCmdOptions, ...options }; + return this.mongoClient.bulkWrite(models, options); + } + /** * Close the connection. * @@ -997,7 +1010,7 @@ export class NodeDriverServiceProvider options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) - .replaceOne(filter, replacement, options) as Promise; + .replaceOne(filter, replacement, options); // `as UpdateResult` because we know we didn't request .explain() here. } diff --git a/packages/shell-api/src/mongo.spec.ts b/packages/shell-api/src/mongo.spec.ts index dcbd6556a5..91c23d395c 100644 --- a/packages/shell-api/src/mongo.spec.ts +++ b/packages/shell-api/src/mongo.spec.ts @@ -1015,6 +1015,74 @@ describe('Mongo', function () { ).toArray(); }); }); + + context('post-8.0', function () { + skipIfServerVersion(testServer, '< 8.0'); + let mongo: Mongo; + + describe('bulkWrite', function () { + beforeEach(async function () { + mongo = await instanceState.shellApi.Mongo(uri, undefined, { + api: { version: '1' }, + }); + }); + + it('should allow inserts across collections and databases', async function () { + expect( + await mongo.bulkWrite([ + { + name: 'insertOne', + namespace: 'db.authors', + document: { name: 'King' }, + }, + { + name: 'deleteOne', + namespace: 'db.authors', + filter: { name: 'King' }, + }, + { + name: 'insertOne', + namespace: 'db.moreAuthors', + document: { name: 'Queen' }, + }, + { + name: 'insertOne', + namespace: 'otherDb.authors', + document: { name: 'Prince' }, + }, + ]) + ).deep.equals({ + acknowledged: true, + insertedCount: 3, + upsertedCount: 0, + matchedCount: 0, + modifiedCount: 0, + deletedCount: 1, + insertResults: undefined, + updateResults: undefined, + deleteResults: undefined, + }); + + expect( + await mongo.getDB('db').getCollection('authors').count() + ).equals(0); + + expect( + await mongo + .getDB('db') + .getCollection('moreAuthors') + .count({ name: 'Queen' }) + ).equals(1); + + expect( + await mongo + .getDB('otherDb') + .getCollection('authors') + .count({ name: 'Prince' }) + ).equals(1); + }); + }); + }); }); }); }); diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index e163f72a30..681f23a87b 100644 --- a/packages/shell-api/src/mongo.ts +++ b/packages/shell-api/src/mongo.ts @@ -36,6 +36,8 @@ import type { ServerApi, ServerApiVersion, WriteConcern, + AnyClientBulkWriteModel, + ClientBulkWriteResult, } from '@mongosh/service-provider-core'; import type { ConnectionInfo } from '@mongosh/arg-parser'; import { @@ -62,6 +64,7 @@ import { ShellApiErrors } from './error-codes'; import type { LogEntry } from './log-entry'; import { parseAnyLogEntry } from './log-entry'; import type { ShellBson } from './shell-bson'; +import type { ClientBulkWriteOptions } from 'mongodb'; /* Utility, inverse of Readonly */ type Mutable = { @@ -365,6 +368,18 @@ export default class Mongo extends ShellApiClass { return await this._listDatabases(options); } + @returnsPromise + @serverVersions(['8.0.0', ServerVersions.latest]) + @apiVersions([1]) + bulkWrite( + models: AnyClientBulkWriteModel[], + options: ClientBulkWriteOptions = {} + ): Promise { + this._emitMongoApiCall('bulkWrite', { options }); + + return this._serviceProvider.clientBulkWrite(models, options); + } + @returnsPromise @apiVersions([1]) async getDBNames(options: ListDatabasesOptions = {}): Promise { From bf1396d7a76176f4c7a87493e21c3df6357a7c8f Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 27 Mar 2025 11:52:47 +0100 Subject: [PATCH 2/2] fix: use own types and add documentation --- packages/i18n/src/locales/en_US.ts | 43 +++++++++++++ .../src/all-transport-types.ts | 1 + .../service-provider-core/src/writable.ts | 3 +- .../src/node-driver-service-provider.ts | 3 +- packages/shell-api/src/mongo.spec.ts | 25 ++++---- packages/shell-api/src/mongo.ts | 30 +++++++-- packages/shell-api/src/result.ts | 63 +++++++++++++++++++ 7 files changed, 151 insertions(+), 17 deletions(-) diff --git a/packages/i18n/src/locales/en_US.ts b/packages/i18n/src/locales/en_US.ts index a541ab43e8..311c526389 100644 --- a/packages/i18n/src/locales/en_US.ts +++ b/packages/i18n/src/locales/en_US.ts @@ -422,6 +422,43 @@ const translations: Catalog = { }, }, }, + ClientBulkWriteResult: { + help: { + description: 'ClientBulkWriteResult Class', + attributes: { + acknowledged: { + description: 'Acknowledged returned from the server', + }, + insertedCount: { + description: 'Number of documents inserted', + }, + matchedCount: { + description: 'Number of documents matched', + }, + modifiedCount: { + description: 'Number of documents modified', + }, + deletedCount: { + description: 'Number of documents deleted', + }, + upsertedCount: { + description: 'Number of documents upserted', + }, + insertResults: { + description: + 'The results of each individual insert operation that was successfully performed', + }, + updateResults: { + description: + 'The results of each individual update operation that was successfully performed', + }, + deleteResults: { + description: + 'The results of each individual delete operation that was successfully performed.', + }, + }, + }, + }, CommandResult: { help: { description: 'CommandResult Class', @@ -2240,6 +2277,12 @@ const translations: Catalog = { link: 'https://docs.mongodb.com/manual/reference/method/Mongo.startSession/', description: 'Starts a session for the connection.', }, + bulkWrite: { + link: 'https://docs.mongodb.com/manual/reference/method/Mongo.bulkWrite', + description: + 'Performs multiple write operations across databases and collections with controls for order of execution.', + example: 'db.getMongo().bulkWrite(operations, options)', + }, getCollection: { link: 'https://docs.mongodb.com/manual/reference/method/Mongo.getCollection', description: diff --git a/packages/service-provider-core/src/all-transport-types.ts b/packages/service-provider-core/src/all-transport-types.ts index b4bccece61..84ccab65ae 100644 --- a/packages/service-provider-core/src/all-transport-types.ts +++ b/packages/service-provider-core/src/all-transport-types.ts @@ -7,6 +7,7 @@ export type { BulkWriteOptions, BulkWriteResult, ClientBulkWriteResult, + ClientBulkWriteOptions, ChangeStream, ChangeStreamOptions, ClientSession, diff --git a/packages/service-provider-core/src/writable.ts b/packages/service-provider-core/src/writable.ts index 6e6cab08e9..169d1b0fc5 100644 --- a/packages/service-provider-core/src/writable.ts +++ b/packages/service-provider-core/src/writable.ts @@ -1,4 +1,4 @@ -import type { ClientBulkWriteOptions, RunCursorCommandOptions } from 'mongodb'; +import type { RunCursorCommandOptions } from 'mongodb'; import type { Document, InsertOneOptions, @@ -12,6 +12,7 @@ import type { AnyBulkWriteOperation, AnyClientBulkWriteModel, ClientBulkWriteResult, + ClientBulkWriteOptions, DeleteOptions, DeleteResult, InsertManyResult, diff --git a/packages/service-provider-node-driver/src/node-driver-service-provider.ts b/packages/service-provider-node-driver/src/node-driver-service-provider.ts index a256915575..1216b5f847 100644 --- a/packages/service-provider-node-driver/src/node-driver-service-provider.ts +++ b/packages/service-provider-node-driver/src/node-driver-service-provider.ts @@ -27,6 +27,7 @@ import type { BulkWriteOptions, BulkWriteResult, ClientBulkWriteResult, + ClientBulkWriteOptions, ClientSessionOptions, Collection, CountDocumentsOptions, @@ -637,7 +638,7 @@ export class NodeDriverServiceProvider */ clientBulkWrite( models: AnyClientBulkWriteModel[], - options: BulkWriteOptions = {} + options: ClientBulkWriteOptions = {} ): Promise { options = { ...this.baseCmdOptions, ...options }; return this.mongoClient.bulkWrite(models, options); diff --git a/packages/shell-api/src/mongo.spec.ts b/packages/shell-api/src/mongo.spec.ts index 91c23d395c..303a57dfec 100644 --- a/packages/shell-api/src/mongo.spec.ts +++ b/packages/shell-api/src/mongo.spec.ts @@ -35,6 +35,7 @@ import { startSharedTestServer, } from '../../../testing/integration-testing-hooks'; import { dummyOptions } from './helpers.spec'; +import { ClientBulkWriteResult } from './result'; const sampleOpts = { causalConsistency: false, @@ -1051,17 +1052,19 @@ describe('Mongo', function () { document: { name: 'Prince' }, }, ]) - ).deep.equals({ - acknowledged: true, - insertedCount: 3, - upsertedCount: 0, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 1, - insertResults: undefined, - updateResults: undefined, - deleteResults: undefined, - }); + ).deep.equals( + new ClientBulkWriteResult({ + acknowledged: true, + insertedCount: 3, + upsertedCount: 0, + matchedCount: 0, + modifiedCount: 0, + deletedCount: 1, + insertResults: undefined, + updateResults: undefined, + deleteResults: undefined, + }) + ); expect( await mongo.getDB('db').getCollection('authors').count() diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index 681f23a87b..193b91c0d8 100644 --- a/packages/shell-api/src/mongo.ts +++ b/packages/shell-api/src/mongo.ts @@ -37,7 +37,7 @@ import type { ServerApiVersion, WriteConcern, AnyClientBulkWriteModel, - ClientBulkWriteResult, + ClientBulkWriteOptions, } from '@mongosh/service-provider-core'; import type { ConnectionInfo } from '@mongosh/arg-parser'; import { @@ -47,6 +47,7 @@ import { import type Collection from './collection'; import Database from './database'; import type ShellInstanceState from './shell-instance-state'; +import { ClientBulkWriteResult } from './result'; import { CommandResult } from './result'; import { redactURICredentials } from '@mongosh/history'; import { asPrintable, ServerVersions, Topologies } from './enums'; @@ -64,7 +65,6 @@ import { ShellApiErrors } from './error-codes'; import type { LogEntry } from './log-entry'; import { parseAnyLogEntry } from './log-entry'; import type { ShellBson } from './shell-bson'; -import type { ClientBulkWriteOptions } from 'mongodb'; /* Utility, inverse of Readonly */ type Mutable = { @@ -371,13 +371,35 @@ export default class Mongo extends ShellApiClass { @returnsPromise @serverVersions(['8.0.0', ServerVersions.latest]) @apiVersions([1]) - bulkWrite( + async bulkWrite( models: AnyClientBulkWriteModel[], options: ClientBulkWriteOptions = {} ): Promise { this._emitMongoApiCall('bulkWrite', { options }); - return this._serviceProvider.clientBulkWrite(models, options); + const { + acknowledged, + insertedCount, + matchedCount, + modifiedCount, + deletedCount, + upsertedCount, + insertResults, + updateResults, + deleteResults, + } = await this._serviceProvider.clientBulkWrite(models, options); + + return new ClientBulkWriteResult({ + acknowledged, + insertedCount, + matchedCount, + modifiedCount, + deletedCount, + upsertedCount, + insertResults, + updateResults, + deleteResults, + }); } @returnsPromise diff --git a/packages/shell-api/src/result.ts b/packages/shell-api/src/result.ts index f4d2b3162f..6b6b85bc4a 100644 --- a/packages/shell-api/src/result.ts +++ b/packages/shell-api/src/result.ts @@ -25,6 +25,69 @@ export class CommandResult extends ShellApiValueClass { } } +export type ClientInsertResult = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + insertedId: any; +}; + +export type ClientUpdateResult = { + matchedCount: number; + modifiedCount: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + upsertedId?: any; + didUpsert: boolean; +}; + +export type ClientDeleteResult = { + deletedCount: number; +}; + +@shellApiClassDefault +export class ClientBulkWriteResult extends ShellApiValueClass { + acknowledged: boolean; + insertedCount: number; + matchedCount: number; + modifiedCount: number; + deletedCount: number; + upsertedCount: number; + insertResults?: ReadonlyMap; + updateResults?: ReadonlyMap; + deleteResults?: ReadonlyMap; + + constructor({ + acknowledged, + insertedCount, + matchedCount, + modifiedCount, + deletedCount, + upsertedCount, + insertResults, + updateResults, + deleteResults, + }: { + acknowledged: boolean; + insertedCount: number; + matchedCount: number; + modifiedCount: number; + deletedCount: number; + upsertedCount: number; + insertResults?: ReadonlyMap; + updateResults?: ReadonlyMap; + deleteResults?: ReadonlyMap; + }) { + super(); + this.acknowledged = acknowledged; + this.insertedCount = insertedCount; + this.matchedCount = matchedCount; + this.modifiedCount = modifiedCount; + this.deletedCount = deletedCount; + this.upsertedCount = upsertedCount; + this.insertResults = insertResults; + this.updateResults = updateResults; + this.deleteResults = deleteResults; + } +} + @shellApiClassDefault export class BulkWriteResult extends ShellApiValueClass { acknowledged: boolean;