From ad7280226638eb43eccf000355ffed6d3246e5cb Mon Sep 17 00:00:00 2001 From: wuandy Date: Mon, 10 Aug 2026 17:37:50 +0000 Subject: [PATCH 1/2] feat(firestore): add DML stages (insert, upsert, delete, update), literals source, and atomic execution option to Node SDK pipelines --- .../dev/protos/firestore_v1_proto_api.d.ts | 6 + .../dev/src/pipelines/pipeline-util.ts | 5 +- .../firestore/dev/src/pipelines/pipelines.ts | 165 +++++++++++++- .../firestore/dev/src/pipelines/stage.ts | 210 +++++++++++++++++- .../dev/src/pipelines/structured-pipeline.ts | 3 +- .../firestore/dev/system-test/pipeline.ts | 64 ++++++ .../firestore/dev/test/pipelines/pipeline.ts | 97 ++++++++ handwritten/firestore/types/firestore.d.ts | 79 +++++++ .../types/protos/firestore_v1_proto_api.d.ts | 6 + 9 files changed, 624 insertions(+), 11 deletions(-) diff --git a/handwritten/firestore/dev/protos/firestore_v1_proto_api.d.ts b/handwritten/firestore/dev/protos/firestore_v1_proto_api.d.ts index f0353f5d340e..280691459009 100644 --- a/handwritten/firestore/dev/protos/firestore_v1_proto_api.d.ts +++ b/handwritten/firestore/dev/protos/firestore_v1_proto_api.d.ts @@ -2690,6 +2690,9 @@ export namespace google { /** ExecutePipelineRequest readTime */ readTime?: (google.protobuf.ITimestamp|null); + + /** ExecutePipelineRequest autoCommitTransaction */ + autoCommitTransaction?: (boolean|null); } /** Represents an ExecutePipelineRequest. */ @@ -2716,6 +2719,9 @@ export namespace google { /** ExecutePipelineRequest readTime. */ public readTime?: (google.protobuf.ITimestamp|null); + /** ExecutePipelineRequest autoCommitTransaction. */ + public autoCommitTransaction?: (boolean|null); + /** ExecutePipelineRequest pipelineType. */ public pipelineType?: "structuredPipeline"; diff --git a/handwritten/firestore/dev/src/pipelines/pipeline-util.ts b/handwritten/firestore/dev/src/pipelines/pipeline-util.ts index 17961b4da1e1..5dd89f5b7847 100644 --- a/handwritten/firestore/dev/src/pipelines/pipeline-util.ts +++ b/handwritten/firestore/dev/src/pipelines/pipeline-util.ts @@ -264,7 +264,10 @@ export class ExecutionUtil { structuredPipeline: structuredPipeline._toProto(this._serializer), }; - if (transactionOrReadTime instanceof Uint8Array) { + if (structuredPipeline.options?.atomic) { + request.newTransaction = {readWrite: {}}; + request.autoCommitTransaction = true; + } else if (transactionOrReadTime instanceof Uint8Array) { request.transaction = transactionOrReadTime; } else if (transactionOrReadTime instanceof Timestamp) { request.readTime = transactionOrReadTime.toProto().timestampValue; diff --git a/handwritten/firestore/dev/src/pipelines/pipelines.ts b/handwritten/firestore/dev/src/pipelines/pipelines.ts index 9342cedce784..2ed76f1af5ae 100644 --- a/handwritten/firestore/dev/src/pipelines/pipelines.ts +++ b/handwritten/firestore/dev/src/pipelines/pipelines.ts @@ -108,6 +108,12 @@ import { UpdateStage, Search, InternalSearchStageOptions, + InsertStage, + InternalInsertStageOptions, + UpsertStage, + InternalUpsertStageOptions, + LiteralsSource, + InternalLiteralsStageOptions, } from './stage'; import {StructuredPipeline} from './structured-pipeline'; import Selectable = FirebaseFirestore.Pipelines.Selectable; @@ -125,6 +131,46 @@ import { export class PipelineSource implements firestore.Pipelines.PipelineSource { constructor(private db: Firestore) {} + /** + * Set the pipeline's source to the in-memory documents specified by the given records. + * + * @param documents An array of objects/records specifying the in-memory documents. + * @param options Options defining how this LiteralsSource stage is evaluated. + */ + literals( + documents: Array>, + options?: firestore.Pipelines.LiteralsStageOptions, + ): Pipeline; + literals( + options: firestore.Pipelines.LiteralsStageOptions, + ): Pipeline; + literals( + docsOrOptions: + | Array> + | firestore.Pipelines.LiteralsStageOptions, + options?: firestore.Pipelines.LiteralsStageOptions, + ): Pipeline { + let documents: Array> = []; + let opts: InternalLiteralsStageOptions = {}; + + if (Array.isArray(docsOrOptions)) { + documents = docsOrOptions; + opts = options ?? {}; + } else if ( + docsOrOptions && + Array.isArray( + (docsOrOptions as firestore.Pipelines.LiteralsStageOptions).documents, + ) + ) { + const {documents: docs, ...rest} = + docsOrOptions as firestore.Pipelines.LiteralsStageOptions; + documents = docs ?? []; + opts = rest; + } + + return new Pipeline(this.db, [new LiteralsSource(documents, opts)]); + } + /** * Returns all documents from the entire collection. The collection can be nested. * @param collection - Name or reference to the collection that will be used as the Pipeline source. @@ -1757,8 +1803,123 @@ export class Pipeline implements firestore.Pipelines.Pipeline { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ update(transformedFields: AliasedExpression[]): Pipeline; - update(transformedFields?: AliasedExpression[]): Pipeline { - return this._addStage(new UpdateStage(transformedFields)); + /** + * @beta + * Performs an update operation using documents from previous stages. + * + * @param fieldsMap - Map of field transformations to apply. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + update( + fieldsMap: Record | Map, + ): Pipeline; + update( + transformedFieldsOrMap?: + | AliasedExpression[] + | Map + | Record, + ): Pipeline { + if ( + transformedFieldsOrMap instanceof Map || + Array.isArray(transformedFieldsOrMap) + ) { + return this._addStage(new UpdateStage(transformedFieldsOrMap)); + } else if (transformedFieldsOrMap && isPlainObject(transformedFieldsOrMap)) { + const map = new Map( + Object.entries(transformedFieldsOrMap as Record), + ); + return this._addStage(new UpdateStage(map)); + } + return this._addStage(new UpdateStage()); + } + + /** + * @beta + * Performs an insert operation on documents from previous stages. + * + * @param options - Options defining how this Insert stage is evaluated. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + insert(options?: firestore.Pipelines.InsertStageOptions): Pipeline { + return this._addStage( + new InsertStage((options ?? {}) as InternalInsertStageOptions), + ); + } + + /** + * @beta + * Performs an upsert operation on documents from previous stages. + * + * @param transforms - Transformations to apply on upsert. + * @param options - Options defining how this Upsert stage is evaluated. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + upsert( + transforms?: AliasedExpression[], + options?: Omit, + ): Pipeline; + upsert(options?: firestore.Pipelines.UpsertStageOptions): Pipeline; + upsert( + transformsOrOptions?: + | AliasedExpression[] + | firestore.Pipelines.UpsertStageOptions, + options?: Omit, + ): Pipeline { + let transforms: AliasedExpression[] = []; + let opts: InternalUpsertStageOptions = {}; + + if (Array.isArray(transformsOrOptions)) { + transforms = transformsOrOptions; + opts = (options ?? {}) as InternalUpsertStageOptions; + } else if (transformsOrOptions) { + const {transforms: t, ...rest} = + transformsOrOptions as firestore.Pipelines.UpsertStageOptions; + transforms = (t ?? []) as AliasedExpression[]; + opts = rest as InternalUpsertStageOptions; + } + + return this._addStage(new UpsertStage(transforms, opts)); + } + + /** + * Appends a literals stage to the pipeline. + * + * @param documents An array of objects/records specifying in-memory documents. + * @param options Options defining how this LiteralsSource stage is evaluated. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + literals( + documents: Array>, + options?: firestore.Pipelines.LiteralsStageOptions, + ): Pipeline; + literals( + options: firestore.Pipelines.LiteralsStageOptions, + ): Pipeline; + literals( + docsOrOptions: + | Array> + | firestore.Pipelines.LiteralsStageOptions, + options?: firestore.Pipelines.LiteralsStageOptions, + ): Pipeline { + let documents: Array> = []; + let opts: InternalLiteralsStageOptions = {}; + + if (Array.isArray(docsOrOptions)) { + documents = docsOrOptions; + opts = options ?? {}; + } else if ( + docsOrOptions && + Array.isArray( + (docsOrOptions as firestore.Pipelines.LiteralsStageOptions).documents, + ) + ) { + const {documents: docs, ...rest} = + docsOrOptions as firestore.Pipelines.LiteralsStageOptions; + documents = docs ?? []; + opts = rest; + } + + return this._addStage(new LiteralsSource(documents, opts)); } /** diff --git a/handwritten/firestore/dev/src/pipelines/stage.ts b/handwritten/firestore/dev/src/pipelines/stage.ts index c1153a03ad7e..f20d9848590e 100644 --- a/handwritten/firestore/dev/src/pipelines/stage.ts +++ b/handwritten/firestore/dev/src/pipelines/stage.ts @@ -17,7 +17,7 @@ import * as protos from '../../protos/firestore_v1_proto_api'; import api = protos.google.firestore.v1; import {DocumentReference} from '../reference/document-reference'; -import {ProtoSerializable, Serializer} from '../serializer'; +import {hasUserData, ProtoSerializable, Serializer} from '../serializer'; import { AggregateFunction, @@ -935,15 +935,23 @@ export class DeleteStage implements Stage { export class UpdateStage implements Stage { name = 'update'; readonly optionsUtil = new OptionsUtil({}); + private readonly transformedMap?: Map; - constructor(private transformedFields?: AliasedExpression[]) {} + constructor( + transformedFields?: AliasedExpression[] | Map, + ) { + if (transformedFields instanceof Map) { + this.transformedMap = transformedFields; + } else if (transformedFields && transformedFields.length > 0) { + this.transformedMap = selectablesToMap(transformedFields); + } + } _toProto(serializer: Serializer): api.Pipeline.IStage { const args: api.IValue[] = []; - if (this.transformedFields && this.transformedFields.length > 0) { - const mapped = selectablesToMap(this.transformedFields); - args.push(serializer.encodeValue(mapped)!); + if (this.transformedMap && this.transformedMap.size > 0) { + args.push(serializer.encodeValue(this.transformedMap)!); } else { args.push(serializer.encodeValue(new Map())!); } @@ -956,8 +964,196 @@ export class UpdateStage implements Stage { } _validateUserData(ignoreUndefinedProperties: boolean): void { - if (this.transformedFields) { - validateUserDataHelper(this.transformedFields, ignoreUndefinedProperties); + if (this.transformedMap) { + validateUserDataHelper(this.transformedMap, ignoreUndefinedProperties); + } + } +} + +/** + * Internal options for Insert stage. + */ +export type InternalInsertStageOptions = Omit< + firestore.Pipelines.InsertStageOptions, + 'collection' | 'documentId' +> & { + collection?: string | CollectionReference | firestore.CollectionReference; + documentId?: string | Expression | firestore.Pipelines.Expression; +}; + +/** + * Insert stage. + */ +export class InsertStage implements Stage { + name = 'insert'; + readonly optionsUtil = new OptionsUtil({}); + private readonly collectionPath?: string; + private readonly documentIdExpr?: Expression; + + constructor(private options: InternalInsertStageOptions = {}) { + if (options.collection) { + this.collectionPath = + typeof options.collection === 'string' + ? options.collection + : (options.collection as CollectionReference).path; + if (!this.collectionPath.startsWith('/')) { + this.collectionPath = '/' + this.collectionPath; + } + } + if (options.documentId) { + this.documentIdExpr = + typeof options.documentId === 'string' + ? field(options.documentId) + : (options.documentId as Expression); + } + } + + _toProto(serializer: Serializer): api.Pipeline.IStage { + const options = + this.optionsUtil.getOptionsProto( + serializer, + this.options, + this.options.rawOptions, + ) || {}; + + if (this.collectionPath) { + options['collection'] = serializer.encodeReference(this.collectionPath); + } + if (this.documentIdExpr) { + options['document_id'] = this.documentIdExpr._toProto(serializer); + } + + return { + name: this.name, + args: [], + options: Object.keys(options).length > 0 ? options : undefined, + }; + } + + _validateUserData(ignoreUndefinedProperties: boolean): void { + if (this.documentIdExpr) { + validateUserDataHelper(this.documentIdExpr, ignoreUndefinedProperties); + } + } +} + +/** + * Internal options for Upsert stage. + */ +export type InternalUpsertStageOptions = Omit< + firestore.Pipelines.UpsertStageOptions, + 'transforms' | 'collection' | 'documentId' +> & { + transforms?: AliasedExpression[]; + collection?: string | CollectionReference | firestore.CollectionReference; + documentId?: string | Expression | firestore.Pipelines.Expression; +}; + +/** + * Upsert stage. + */ +export class UpsertStage implements Stage { + name = 'upsert'; + readonly optionsUtil = new OptionsUtil({}); + private readonly collectionPath?: string; + private readonly documentIdExpr?: Expression; + private readonly transforms: Map; + + constructor( + transforms: AliasedExpression[] = [], + private options: InternalUpsertStageOptions = {}, + ) { + this.transforms = selectablesToMap(transforms); + if (options.collection) { + this.collectionPath = + typeof options.collection === 'string' + ? options.collection + : (options.collection as CollectionReference).path; + if (!this.collectionPath.startsWith('/')) { + this.collectionPath = '/' + this.collectionPath; + } + } + if (options.documentId) { + this.documentIdExpr = + typeof options.documentId === 'string' + ? field(options.documentId) + : (options.documentId as Expression); + } + } + + _toProto(serializer: Serializer): api.Pipeline.IStage { + const options = + this.optionsUtil.getOptionsProto( + serializer, + this.options, + this.options.rawOptions, + ) || {}; + + if (this.collectionPath) { + options['collection'] = serializer.encodeReference(this.collectionPath); + } + if (this.documentIdExpr) { + options['document_id'] = this.documentIdExpr._toProto(serializer); + } + + const args: api.IValue[] = [serializer.encodeValue(this.transforms)!]; + + return { + name: this.name, + args, + options: Object.keys(options).length > 0 ? options : undefined, + }; + } + + _validateUserData(ignoreUndefinedProperties: boolean): void { + validateUserDataHelper(this.transforms, ignoreUndefinedProperties); + if (this.documentIdExpr) { + validateUserDataHelper(this.documentIdExpr, ignoreUndefinedProperties); } } } + +/** + * Internal options for Literals stage. + */ +export type InternalLiteralsStageOptions = + firestore.Pipelines.LiteralsStageOptions; + +/** + * Literals stage. + */ +export class LiteralsSource implements Stage { + name = 'literals'; + readonly optionsUtil = new OptionsUtil({}); + + constructor( + readonly documents: Array>, + private options: InternalLiteralsStageOptions = {}, + ) {} + + _toProto(serializer: Serializer): api.Pipeline.IStage { + const args: api.IValue[] = this.documents.map( + doc => serializer.encodeValue(doc)!, + ); + return { + name: this.name, + args, + options: this.optionsUtil.getOptionsProto( + serializer, + this.options, + this.options.rawOptions, + ), + }; + } + + _validateUserData(ignoreUndefinedProperties: boolean): void { + this.documents.forEach(doc => { + Object.values(doc).forEach(val => { + if (hasUserData(val)) { + val._validateUserData(ignoreUndefinedProperties); + } + }); + }); + } +} + diff --git a/handwritten/firestore/dev/src/pipelines/structured-pipeline.ts b/handwritten/firestore/dev/src/pipelines/structured-pipeline.ts index bd74d236997a..18cef99879ec 100644 --- a/handwritten/firestore/dev/src/pipelines/structured-pipeline.ts +++ b/handwritten/firestore/dev/src/pipelines/structured-pipeline.ts @@ -27,6 +27,7 @@ import {OptionsUtil} from './options-util'; */ export type StructuredPipelineOptions = { indexMode?: 'recommended'; + atomic?: boolean; explainOptions?: { mode?: 'execute' | 'explain' | 'analyze'; outputFormat?: 'text'; @@ -72,7 +73,7 @@ export class StructuredPipeline */ constructor( private pipeline: ProtoSerializable, - private options: StructuredPipelineOptions, + public options: StructuredPipelineOptions, private optionsOverride: Record, ) {} diff --git a/handwritten/firestore/dev/system-test/pipeline.ts b/handwritten/firestore/dev/system-test/pipeline.ts index 48d0291d21eb..c7f8750678a1 100644 --- a/handwritten/firestore/dev/system-test/pipeline.ts +++ b/handwritten/firestore/dev/system-test/pipeline.ts @@ -464,6 +464,70 @@ describe.skipClassic('Pipeline class', () => { const res = await promise; expectResults(res, {documents_modified: 0}); }); + + it('can execute insert stage', async () => { + const newDocId = 'newBook_insert_1'; + const res = await firestore + .pipeline() + .literals([{title: 'New Book', author: 'Author 1'}]) + .insert({collection: dmlCol.path, documentId: newDocId}) + .execute(); + + expectResults(res, {documents_modified: 1}); + + const snap = await dmlCol.doc(newDocId).get(); + expect(snap.exists).to.be.true; + expect(snap.get('title')).to.equal('New Book'); + }); + + it('can execute upsert stage with transforms', async () => { + const upsertDocId = 'upsertBook_1'; + const res = await firestore + .pipeline() + .literals([{title: 'Upserted Book', count: 1}]) + .upsert([add(field('count'), constant(1)).as('count')], { + collection: dmlCol.path, + documentId: upsertDocId, + }) + .execute(); + + expectResults(res, {documents_modified: 1}); + + const snap = await dmlCol.doc(upsertDocId).get(); + expect(snap.exists).to.be.true; + expect(snap.get('title')).to.equal('Upserted Book'); + }); + + it('can execute DML with atomic option', async () => { + const atomicDocId = 'atomicBook_1'; + const res = await firestore + .pipeline() + .literals([{title: 'Atomic Book'}]) + .insert({collection: dmlCol.path, documentId: atomicDocId}) + .execute({atomic: true}); + + expectResults(res, {documents_modified: 1}); + + const snap = await dmlCol.doc(atomicDocId).get(); + expect(snap.exists).to.be.true; + }); + + it('can execute insert/upsert stages inside transaction runner', async () => { + const txDocId = 'txBook_1'; + await firestore.runTransaction(async transaction => { + const insertPpl = firestore + .pipeline() + .literals([{title: 'Tx Book'}]) + .insert({collection: dmlCol.path, documentId: txDocId}); + + const res = await transaction.execute(insertPpl); + expectResults(res, {documents_modified: 1}); + }); + + const snap = await dmlCol.doc(txDocId).get(); + expect(snap.exists).to.be.true; + expect(snap.get('title')).to.equal('Tx Book'); + }); }); it('empty snapshot as expected', async () => { diff --git a/handwritten/firestore/dev/test/pipelines/pipeline.ts b/handwritten/firestore/dev/test/pipelines/pipeline.ts index fa48b1c49ffb..1d84a7f004ff 100644 --- a/handwritten/firestore/dev/test/pipelines/pipeline.ts +++ b/handwritten/firestore/dev/test/pipelines/pipeline.ts @@ -179,4 +179,101 @@ describe('execute(Pipeline|PipelineExecuteOptions)', () => { executePipelineRequest, ); }); + + it('serializes atomic execute option', async () => { + const spy = sinon.fake.returns(stream()); + const firestore = await createInstance({ + executePipeline: spy, + }); + + await firestore + .pipeline() + .collection('foo') + .execute({ + atomic: true, + }); + + const executePipelineRequest: IExecutePipelineRequest = { + database: 'projects/test-project/databases/(default)', + newTransaction: { + readWrite: {}, + }, + autoCommitTransaction: true, + structuredPipeline: { + options: {}, + pipeline: { + stages: [ + { + args: [ + { + referenceValue: '/foo', + }, + ], + name: 'collection', + options: {}, + }, + ], + }, + }, + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest, + ); + }); + + it('serializes DML stages (delete, update, insert, upsert, literals)', async () => { + const spy = sinon.fake.returns(stream()); + const firestore = await createInstance({ + executePipeline: spy, + }); + + await firestore + .pipeline() + .literals([{foo: 'bar'}]) + .insert({collection: 'foo', documentId: 'doc1'}) + .update() + .upsert([], {collection: 'bar'}) + .delete() + .execute(); + + const stages = + spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]['structuredPipeline'][ + 'pipeline' + ]['stages']; + + expect(stages.length).to.equal(5); + expect(stages[0].name).to.equal('literals'); + expect(stages[0].args).to.deep.equal([ + { + mapValue: { + fields: { + foo: {stringValue: 'bar'}, + }, + }, + }, + ]); + + expect(stages[1].name).to.equal('insert'); + expect(stages[1].options).to.deep.equal({ + collection: {referenceValue: '/foo'}, + document_id: {fieldReferenceValue: 'doc1'}, + }); + + expect(stages[2].name).to.equal('update'); + expect(stages[2].args).to.deep.equal([ + { + mapValue: { + fields: {}, + }, + }, + ]); + + expect(stages[3].name).to.equal('upsert'); + expect(stages[3].options).to.deep.equal({ + collection: {referenceValue: '/bar'}, + }); + + expect(stages[4].name).to.equal('delete'); + }); }); + diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts index d30565bdb154..6719ccdd7ef8 100644 --- a/handwritten/firestore/types/firestore.d.ts +++ b/handwritten/firestore/types/firestore.d.ts @@ -12279,6 +12279,14 @@ declare namespace FirebaseFirestore { * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. */ createFrom(query: Query): Pipeline; + /** + * Set the pipeline's source to the in-memory documents specified by the given records. + */ + literals( + documents: Array>, + options?: LiteralsStageOptions, + ): Pipeline; + literals(options: LiteralsStageOptions): Pipeline; } /** @@ -12718,6 +12726,49 @@ declare namespace FirebaseFirestore { * @return A new {@link Pipeline} object with this stage appended to the stage list. */ update(transformedFields: AliasedExpression[]): Pipeline; + /** + * @beta + * Performs an update operation using documents from previous stages. + * + * @param fieldsMap - Map of field transformations to apply. + * @return A new {@link Pipeline} object with this stage appended to the stage list. + */ + update( + fieldsMap: Record | Map, + ): Pipeline; + /** + * @beta + * Performs an insert operation on documents from previous stages. + * + * @param options - Options defining how this Insert stage is evaluated. + * @return A new {@link Pipeline} object with this stage appended to the stage list. + */ + insert(options?: InsertStageOptions): Pipeline; + /** + * @beta + * Performs an upsert operation on documents from previous stages. + * + * @param transforms - Transformations to apply on upsert. + * @param options - Options defining how this Upsert stage is evaluated. + * @return A new {@link Pipeline} object with this stage appended to the stage list. + */ + upsert( + transforms?: AliasedExpression[], + options?: Omit, + ): Pipeline; + upsert(options?: UpsertStageOptions): Pipeline; + /** + * Appends a literals stage to the pipeline. + * + * @param documents An array of objects/records specifying in-memory documents. + * @param options Options defining how this LiteralsSource stage is evaluated. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + literals( + documents: Array>, + options?: LiteralsStageOptions, + ): Pipeline; + literals(options: LiteralsStageOptions): Pipeline; /** * Filters the documents from previous stages to only include those matching the specified {@link * BooleanExpression}. @@ -13418,6 +13469,10 @@ declare namespace FirebaseFirestore { * Specifies the index mode for the query. */ indexMode?: 'recommended'; + /** + * Indicates that the pipeline will be executed atomically. + */ + atomic?: boolean; /** * @beta Options used to configure explain queries. */ explainOptions?: { @@ -13895,6 +13950,30 @@ declare namespace FirebaseFirestore { // separator?: string; // }; + /** + * Options defining how an Insert stage is evaluated. + */ + export type InsertStageOptions = StageOptions & { + collection?: string | CollectionReference; + documentId?: string | Expression; + }; + + /** + * Options defining how an Upsert stage is evaluated. + */ + export type UpsertStageOptions = StageOptions & { + transforms?: AliasedExpression[]; + collection?: string | CollectionReference; + documentId?: string | Expression; + }; + + /** + * Options defining how a LiteralsSource stage is evaluated. + */ + export type LiteralsStageOptions = StageOptions & { + documents?: Array>; + }; + /** * Represents a field value within the explain statistics, which can be a primitive type (null, string, number, boolean) * or a recursively defined object where keys are strings and values are also `ExplainStatsFieldValue`. diff --git a/handwritten/firestore/types/protos/firestore_v1_proto_api.d.ts b/handwritten/firestore/types/protos/firestore_v1_proto_api.d.ts index 65ceec85751b..2b0de4830873 100644 --- a/handwritten/firestore/types/protos/firestore_v1_proto_api.d.ts +++ b/handwritten/firestore/types/protos/firestore_v1_proto_api.d.ts @@ -2690,6 +2690,9 @@ export namespace google { /** ExecutePipelineRequest readTime */ readTime?: (google.protobuf.ITimestamp|null); + + /** ExecutePipelineRequest autoCommitTransaction */ + autoCommitTransaction?: (boolean|null); } /** Represents an ExecutePipelineRequest. */ @@ -2716,6 +2719,9 @@ export namespace google { /** ExecutePipelineRequest readTime. */ public readTime?: (google.protobuf.ITimestamp|null); + /** ExecutePipelineRequest autoCommitTransaction. */ + public autoCommitTransaction?: (boolean|null); + /** ExecutePipelineRequest pipelineType. */ public pipelineType?: "structuredPipeline"; From b8d4a66a5e50c6b63623a19669d740d473d6c50e Mon Sep 17 00:00:00 2001 From: wuandy Date: Tue, 11 Aug 2026 11:46:08 +0000 Subject: [PATCH 2/2] test(firestore): refine Node SDK DML integration tests for backend execution --- handwritten/firestore/dev/system-test/pipeline.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/handwritten/firestore/dev/system-test/pipeline.ts b/handwritten/firestore/dev/system-test/pipeline.ts index c7f8750678a1..85db4f5358ec 100644 --- a/handwritten/firestore/dev/system-test/pipeline.ts +++ b/handwritten/firestore/dev/system-test/pipeline.ts @@ -470,7 +470,7 @@ describe.skipClassic('Pipeline class', () => { const res = await firestore .pipeline() .literals([{title: 'New Book', author: 'Author 1'}]) - .insert({collection: dmlCol.path, documentId: newDocId}) + .insert({collection: dmlCol.path, documentId: constant(newDocId)}) .execute(); expectResults(res, {documents_modified: 1}); @@ -487,9 +487,9 @@ describe.skipClassic('Pipeline class', () => { .literals([{title: 'Upserted Book', count: 1}]) .upsert([add(field('count'), constant(1)).as('count')], { collection: dmlCol.path, - documentId: upsertDocId, + documentId: constant(upsertDocId), }) - .execute(); + .execute({atomic: true}); expectResults(res, {documents_modified: 1}); @@ -503,7 +503,7 @@ describe.skipClassic('Pipeline class', () => { const res = await firestore .pipeline() .literals([{title: 'Atomic Book'}]) - .insert({collection: dmlCol.path, documentId: atomicDocId}) + .insert({collection: dmlCol.path, documentId: constant(atomicDocId)}) .execute({atomic: true}); expectResults(res, {documents_modified: 1}); @@ -518,7 +518,7 @@ describe.skipClassic('Pipeline class', () => { const insertPpl = firestore .pipeline() .literals([{title: 'Tx Book'}]) - .insert({collection: dmlCol.path, documentId: txDocId}); + .insert({collection: dmlCol.path, documentId: constant(txDocId)}); const res = await transaction.execute(insertPpl); expectResults(res, {documents_modified: 1});