diff --git a/CMakeLists.txt b/CMakeLists.txt index fd734f45389..28d03479fd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ endif() # don't complain about doing so. Our dependencies aren't good about declaring # BYPRODUCTS and we mix them all into a single superbuild so we can't enable # this policy until all dependencies are capable of doing so. -if(POLICY CMP0058) +if(POLICY CMP0058 AND CMAKE_VERSION VERSION_LESS "4.0") cmake_policy(SET CMP0058 OLD) endif() diff --git a/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.cc b/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.cc index ccd4d061db0..865975f3f66 100644 --- a/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.cc +++ b/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.cc @@ -515,6 +515,7 @@ inline constexpr ExecutePipelineRequest::Impl_::Impl_( : database_( &::google::protobuf::internal::fixed_address_empty_string, ::_pbi::ConstantInitialized()), + auto_commit_transaction_{false}, pipeline_type_{}, consistency_selector_{}, _cached_size_{0}, @@ -1024,6 +1025,7 @@ const ::uint32_t TableStruct_google_2ffirestore_2fv1_2ffirestore_2eproto::offset ::_pbi::kInvalidFieldOffsetTag, ::_pbi::kInvalidFieldOffsetTag, ::_pbi::kInvalidFieldOffsetTag, + PROTOBUF_FIELD_OFFSET(::google::firestore::v1::ExecutePipelineRequest, _impl_.auto_commit_transaction_), PROTOBUF_FIELD_OFFSET(::google::firestore::v1::ExecutePipelineRequest, _impl_.pipeline_type_), PROTOBUF_FIELD_OFFSET(::google::firestore::v1::ExecutePipelineRequest, _impl_.consistency_selector_), PROTOBUF_FIELD_OFFSET(::google::firestore::v1::ExecutePipelineResponse, _impl_._has_bits_), @@ -1266,22 +1268,22 @@ static const ::_pbi::MigrationSchema {192, -1, -1, sizeof(::google::firestore::v1::RunQueryRequest)}, {207, 219, -1, sizeof(::google::firestore::v1::RunQueryResponse)}, {223, -1, -1, sizeof(::google::firestore::v1::ExecutePipelineRequest)}, - {238, 250, -1, sizeof(::google::firestore::v1::ExecutePipelineResponse)}, - {254, -1, -1, sizeof(::google::firestore::v1::RunAggregationQueryRequest)}, - {269, 280, -1, sizeof(::google::firestore::v1::RunAggregationQueryResponse)}, - {283, 293, -1, sizeof(::google::firestore::v1::WriteRequest_LabelsEntry_DoNotUse)}, - {295, -1, -1, sizeof(::google::firestore::v1::WriteRequest)}, - {308, 320, -1, sizeof(::google::firestore::v1::WriteResponse)}, - {324, 334, -1, sizeof(::google::firestore::v1::ListenRequest_LabelsEntry_DoNotUse)}, - {336, -1, -1, sizeof(::google::firestore::v1::ListenRequest)}, - {349, -1, -1, sizeof(::google::firestore::v1::ListenResponse)}, - {363, -1, -1, sizeof(::google::firestore::v1::Target_DocumentsTarget)}, - {372, -1, -1, sizeof(::google::firestore::v1::Target_QueryTarget)}, - {383, -1, -1, sizeof(::google::firestore::v1::Target_PipelineQueryTarget)}, - {393, 411, -1, sizeof(::google::firestore::v1::Target)}, - {419, 432, -1, sizeof(::google::firestore::v1::TargetChange)}, - {437, -1, -1, sizeof(::google::firestore::v1::ListCollectionIdsRequest)}, - {448, -1, -1, sizeof(::google::firestore::v1::ListCollectionIdsResponse)}, + {239, 251, -1, sizeof(::google::firestore::v1::ExecutePipelineResponse)}, + {255, -1, -1, sizeof(::google::firestore::v1::RunAggregationQueryRequest)}, + {270, 281, -1, sizeof(::google::firestore::v1::RunAggregationQueryResponse)}, + {284, 294, -1, sizeof(::google::firestore::v1::WriteRequest_LabelsEntry_DoNotUse)}, + {296, -1, -1, sizeof(::google::firestore::v1::WriteRequest)}, + {309, 321, -1, sizeof(::google::firestore::v1::WriteResponse)}, + {325, 335, -1, sizeof(::google::firestore::v1::ListenRequest_LabelsEntry_DoNotUse)}, + {337, -1, -1, sizeof(::google::firestore::v1::ListenRequest)}, + {350, -1, -1, sizeof(::google::firestore::v1::ListenResponse)}, + {364, -1, -1, sizeof(::google::firestore::v1::Target_DocumentsTarget)}, + {373, -1, -1, sizeof(::google::firestore::v1::Target_QueryTarget)}, + {384, -1, -1, sizeof(::google::firestore::v1::Target_PipelineQueryTarget)}, + {394, 412, -1, sizeof(::google::firestore::v1::Target)}, + {420, 433, -1, sizeof(::google::firestore::v1::TargetChange)}, + {438, -1, -1, sizeof(::google::firestore::v1::ListCollectionIdsRequest)}, + {449, -1, -1, sizeof(::google::firestore::v1::ListCollectionIdsResponse)}, }; static const ::_pb::Message* const file_default_instances[] = { @@ -1392,158 +1394,159 @@ const char descriptor_table_protodef_google_2ffirestore_2fv1_2ffirestore_2eproto " \001(\014\022/\n\010document\030\001 \001(\0132\035.google.firestor" "e.v1.Document\022-\n\tread_time\030\003 \001(\0132\032.googl" "e.protobuf.Timestamp\022\027\n\017skipped_results\030" - "\004 \001(\005\"\254\002\n\026ExecutePipelineRequest\022\025\n\010data" + "\004 \001(\005\"\322\002\n\026ExecutePipelineRequest\022\025\n\010data" "base\030\001 \001(\tB\003\340A\002\022F\n\023structured_pipeline\030\002" " \001(\0132\'.google.firestore.v1.StructuredPip" "elineH\000\022\025\n\013transaction\030\005 \001(\014H\001\022B\n\017new_tr" "ansaction\030\006 \001(\0132\'.google.firestore.v1.Tr" "ansactionOptionsH\001\022/\n\tread_time\030\007 \001(\0132\032." - "google.protobuf.TimestampH\001B\017\n\rpipeline_" - "typeB\026\n\024consistency_selector\"\314\001\n\027Execute" - "PipelineResponse\022\023\n\013transaction\030\001 \001(\014\022.\n" - "\007results\030\002 \003(\0132\035.google.firestore.v1.Doc" - "ument\0222\n\016execution_time\030\003 \001(\0132\032.google.p" - "rotobuf.Timestamp\0228\n\rexplain_stats\030\004 \001(\013" - "2!.google.firestore.v1.ExplainStats\"\267\002\n\032" - "RunAggregationQueryRequest\022\016\n\006parent\030\001 \001" - "(\t\022W\n\034structured_aggregation_query\030\002 \001(\013" - "2/.google.firestore.v1.StructuredAggrega" - "tionQueryH\000\022\025\n\013transaction\030\004 \001(\014H\001\022B\n\017ne" - "w_transaction\030\005 \001(\0132\'.google.firestore.v" - "1.TransactionOptionsH\001\022/\n\tread_time\030\006 \001(" - "\0132\032.google.protobuf.TimestampH\001B\014\n\nquery" - "_typeB\026\n\024consistency_selector\"\231\001\n\033RunAgg" - "regationQueryResponse\0226\n\006result\030\001 \001(\0132&." - "google.firestore.v1.AggregationResult\022\023\n" - "\013transaction\030\002 \001(\014\022-\n\tread_time\030\003 \001(\0132\032." - "google.protobuf.Timestamp\"\343\001\n\014WriteReque" - "st\022\020\n\010database\030\001 \001(\t\022\021\n\tstream_id\030\002 \001(\t\022" - "*\n\006writes\030\003 \003(\0132\032.google.firestore.v1.Wr" - "ite\022\024\n\014stream_token\030\004 \001(\014\022=\n\006labels\030\005 \003(" - "\0132-.google.firestore.v1.WriteRequest.Lab" - "elsEntry\032-\n\013LabelsEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005" - "value\030\002 \001(\t:\0028\001\"\242\001\n\rWriteResponse\022\021\n\tstr" - "eam_id\030\001 \001(\t\022\024\n\014stream_token\030\002 \001(\014\0227\n\rwr" - "ite_results\030\003 \003(\0132 .google.firestore.v1." - "WriteResult\022/\n\013commit_time\030\004 \001(\0132\032.googl" - "e.protobuf.Timestamp\"\355\001\n\rListenRequest\022\020" - "\n\010database\030\001 \001(\t\0221\n\nadd_target\030\002 \001(\0132\033.g" - "oogle.firestore.v1.TargetH\000\022\027\n\rremove_ta" - "rget\030\003 \001(\005H\000\022>\n\006labels\030\004 \003(\0132..google.fi" - "restore.v1.ListenRequest.LabelsEntry\032-\n\013" - "LabelsEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:" - "\0028\001B\017\n\rtarget_change\"\325\002\n\016ListenResponse\022" - ":\n\rtarget_change\030\002 \001(\0132!.google.firestor" - "e.v1.TargetChangeH\000\022>\n\017document_change\030\003" - " \001(\0132#.google.firestore.v1.DocumentChang" - "eH\000\022>\n\017document_delete\030\004 \001(\0132#.google.fi" - "restore.v1.DocumentDeleteH\000\022>\n\017document_" - "remove\030\006 \001(\0132#.google.firestore.v1.Docum" - "entRemoveH\000\0226\n\006filter\030\005 \001(\0132$.google.fir" - "estore.v1.ExistenceFilterH\000B\017\n\rresponse_" - "type\"\221\005\n\006Target\0228\n\005query\030\002 \001(\0132\'.google." - "firestore.v1.Target.QueryTargetH\000\022@\n\tdoc" - "uments\030\003 \001(\0132+.google.firestore.v1.Targe" - "t.DocumentsTargetH\000\022I\n\016pipeline_query\030\r " - "\001(\0132/.google.firestore.v1.Target.Pipelin" - "eQueryTargetH\000\022\026\n\014resume_token\030\004 \001(\014H\001\022/" - "\n\tread_time\030\013 \001(\0132\032.google.protobuf.Time" - "stampH\001\022\021\n\ttarget_id\030\005 \001(\005\022\014\n\004once\030\006 \001(\010" - "\0223\n\016expected_count\030\014 \001(\0132\033.google.protob" - "uf.Int32Value\032$\n\017DocumentsTarget\022\021\n\tdocu" - "ments\030\002 \003(\t\032m\n\013QueryTarget\022\016\n\006parent\030\001 \001" - "(\t\022@\n\020structured_query\030\002 \001(\0132$.google.fi" - "restore.v1.StructuredQueryH\000B\014\n\nquery_ty" - "pe\032n\n\023PipelineQueryTarget\022F\n\023structured_" - "pipeline\030\001 \001(\0132\'.google.firestore.v1.Str" - "ucturedPipelineH\000B\017\n\rpipeline_typeB\r\n\013ta" - "rget_typeB\r\n\013resume_type\"\252\002\n\014TargetChang" - "e\022N\n\022target_change_type\030\001 \001(\01622.google.f" - "irestore.v1.TargetChange.TargetChangeTyp" - "e\022\022\n\ntarget_ids\030\002 \003(\005\022!\n\005cause\030\003 \001(\0132\022.g" - "oogle.rpc.Status\022\024\n\014resume_token\030\004 \001(\014\022-" - "\n\tread_time\030\006 \001(\0132\032.google.protobuf.Time" - "stamp\"N\n\020TargetChangeType\022\r\n\tNO_CHANGE\020\000" - "\022\007\n\003ADD\020\001\022\n\n\006REMOVE\020\002\022\013\n\007CURRENT\020\003\022\t\n\005RE" - "SET\020\004\"Q\n\030ListCollectionIdsRequest\022\016\n\006par" - "ent\030\001 \001(\t\022\021\n\tpage_size\030\002 \001(\005\022\022\n\npage_tok" - "en\030\003 \001(\t\"L\n\031ListCollectionIdsResponse\022\026\n" - "\016collection_ids\030\001 \003(\t\022\027\n\017next_page_token" - "\030\002 \001(\t2\333\025\n\tFirestore\022\217\001\n\013GetDocument\022\'.g" - "oogle.firestore.v1.GetDocumentRequest\032\035." - "google.firestore.v1.Document\"8\202\323\344\223\0022\0220/v" - "1/{name=projects/*/databases/*/documents" - "/*/**}\022\262\001\n\rListDocuments\022).google.firest" - "ore.v1.ListDocumentsRequest\032*.google.fir" - "estore.v1.ListDocumentsResponse\"J\202\323\344\223\002D\022" - "B/v1/{parent=projects/*/databases/*/docu" - "ments/*/**}/{collection_id}\022\257\001\n\016CreateDo" - "cument\022*.google.firestore.v1.CreateDocum" - "entRequest\032\035.google.firestore.v1.Documen" - "t\"R\202\323\344\223\002L\"@/v1/{parent=projects/*/databa" - "ses/*/documents/**}/{collection_id}:\010doc" - "ument\022\250\001\n\016UpdateDocument\022*.google.firest" - "ore.v1.UpdateDocumentRequest\032\035.google.fi" - "restore.v1.Document\"K\202\323\344\223\002E29/v1/{docume" - "nt.name=projects/*/databases/*/documents" - "/*/**}:\010document\022\216\001\n\016DeleteDocument\022*.go" - "ogle.firestore.v1.DeleteDocumentRequest\032" - "\026.google.protobuf.Empty\"8\202\323\344\223\0022*0/v1/{na" - "me=projects/*/databases/*/documents/*/**" - "}\022\271\001\n\021BatchGetDocuments\022-.google.firesto" - "re.v1.BatchGetDocumentsRequest\032..google." - "firestore.v1.BatchGetDocumentsResponse\"C" - "\202\323\344\223\002=\"8/v1/{database=projects/*/databas" - "es/*}/documents:batchGet:\001*0\001\022\274\001\n\020BeginT" - "ransaction\022,.google.firestore.v1.BeginTr" - "ansactionRequest\032-.google.firestore.v1.B" - "eginTransactionResponse\"K\202\323\344\223\002E\"@/v1/{da" - "tabase=projects/*/databases/*}/documents" - ":beginTransaction:\001*\022\224\001\n\006Commit\022\".google" - ".firestore.v1.CommitRequest\032#.google.fir" - "estore.v1.CommitResponse\"A\202\323\344\223\002;\"6/v1/{d" - "atabase=projects/*/databases/*}/document" - "s:commit:\001*\022\215\001\n\010Rollback\022$.google.firest" - "ore.v1.RollbackRequest\032\026.google.protobuf" - ".Empty\"C\202\323\344\223\002=\"8/v1/{database=projects/*" - "/databases/*}/documents:rollback:\001*\022\337\001\n\010" - "RunQuery\022$.google.firestore.v1.RunQueryR" - "equest\032%.google.firestore.v1.RunQueryRes" - "ponse\"\203\001\202\323\344\223\002}\"6/v1/{parent=projects/*/d" - "atabases/*/documents}:runQuery:\001*Z@\";/v1" - "/{parent=projects/*/databases/*/document" - "s/*/**}:runQuery:\001*0\001\022\272\001\n\017ExecutePipelin" - "e\022+.google.firestore.v1.ExecutePipelineR" - "equest\032,.google.firestore.v1.ExecutePipe" - "lineResponse\"J\202\323\344\223\002D\"\?/v1/{database=proj" - "ects/*/databases/*}/documents:executePip" - "eline:\001*0\001\022\227\002\n\023RunAggregationQuery\022/.goo" - "gle.firestore.v1.RunAggregationQueryRequ" - "est\0320.google.firestore.v1.RunAggregation" - "QueryResponse\"\232\001\202\323\344\223\002\223\001\"A/v1/{parent=pro" - "jects/*/databases/*/documents}:runAggreg" - "ationQuery:\001*ZK\"F/v1/{parent=projects/*/" - "databases/*/documents/*/**}:runAggregati" - "onQuery:\001*0\001\022\224\001\n\005Write\022!.google.firestor" - "e.v1.WriteRequest\032\".google.firestore.v1." - "WriteResponse\"@\202\323\344\223\002:\"5/v1/{database=pro" - "jects/*/databases/*}/documents:write:\001*(" - "\0010\001\022\230\001\n\006Listen\022\".google.firestore.v1.Lis" - "tenRequest\032#.google.firestore.v1.ListenR" - "esponse\"A\202\323\344\223\002;\"6/v1/{database=projects/" - "*/databases/*}/documents:listen:\001*(\0010\001\022\213" - "\002\n\021ListCollectionIds\022-.google.firestore." - "v1.ListCollectionIdsRequest\032..google.fir" - "estore.v1.ListCollectionIdsResponse\"\226\001\202\323" - "\344\223\002\217\001\"\?/v1/{parent=projects/*/databases/" - "*/documents}:listCollectionIds:\001*ZI\"D/v1" - "/{parent=projects/*/databases/*/document" - "s/*/**}:listCollectionIds:\001*B\262\001\n\027com.goo" - "gle.firestore.v1B\016FirestoreProtoP\001Z\n\006labels\030\004 \003(\0132..google.fire" + "store.v1.ListenRequest.LabelsEntry\032-\n\013La" + "belsEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028" + "\001B\017\n\rtarget_change\"\325\002\n\016ListenResponse\022:\n" + "\rtarget_change\030\002 \001(\0132!.google.firestore." + "v1.TargetChangeH\000\022>\n\017document_change\030\003 \001" + "(\0132#.google.firestore.v1.DocumentChangeH" + "\000\022>\n\017document_delete\030\004 \001(\0132#.google.fire" + "store.v1.DocumentDeleteH\000\022>\n\017document_re" + "move\030\006 \001(\0132#.google.firestore.v1.Documen" + "tRemoveH\000\0226\n\006filter\030\005 \001(\0132$.google.fires" + "tore.v1.ExistenceFilterH\000B\017\n\rresponse_ty" + "pe\"\221\005\n\006Target\0228\n\005query\030\002 \001(\0132\'.google.fi" + "restore.v1.Target.QueryTargetH\000\022@\n\tdocum" + "ents\030\003 \001(\0132+.google.firestore.v1.Target." + "DocumentsTargetH\000\022I\n\016pipeline_query\030\r \001(" + "\0132/.google.firestore.v1.Target.PipelineQ" + "ueryTargetH\000\022\026\n\014resume_token\030\004 \001(\014H\001\022/\n\t" + "read_time\030\013 \001(\0132\032.google.protobuf.Timest" + "ampH\001\022\021\n\ttarget_id\030\005 \001(\005\022\014\n\004once\030\006 \001(\010\0223" + "\n\016expected_count\030\014 \001(\0132\033.google.protobuf" + ".Int32Value\032$\n\017DocumentsTarget\022\021\n\tdocume" + "nts\030\002 \003(\t\032m\n\013QueryTarget\022\016\n\006parent\030\001 \001(\t" + "\022@\n\020structured_query\030\002 \001(\0132$.google.fire" + "store.v1.StructuredQueryH\000B\014\n\nquery_type" + "\032n\n\023PipelineQueryTarget\022F\n\023structured_pi" + "peline\030\001 \001(\0132\'.google.firestore.v1.Struc" + "turedPipelineH\000B\017\n\rpipeline_typeB\r\n\013targ" + "et_typeB\r\n\013resume_type\"\252\002\n\014TargetChange\022" + "N\n\022target_change_type\030\001 \001(\01622.google.fir" + "estore.v1.TargetChange.TargetChangeType\022" + "\022\n\ntarget_ids\030\002 \003(\005\022!\n\005cause\030\003 \001(\0132\022.goo" + "gle.rpc.Status\022\024\n\014resume_token\030\004 \001(\014\022-\n\t" + "read_time\030\006 \001(\0132\032.google.protobuf.Timest" + "amp\"N\n\020TargetChangeType\022\r\n\tNO_CHANGE\020\000\022\007" + "\n\003ADD\020\001\022\n\n\006REMOVE\020\002\022\013\n\007CURRENT\020\003\022\t\n\005RESE" + "T\020\004\"Q\n\030ListCollectionIdsRequest\022\016\n\006paren" + "t\030\001 \001(\t\022\021\n\tpage_size\030\002 \001(\005\022\022\n\npage_token" + "\030\003 \001(\t\"L\n\031ListCollectionIdsResponse\022\026\n\016c" + "ollection_ids\030\001 \003(\t\022\027\n\017next_page_token\030\002" + " \001(\t2\333\025\n\tFirestore\022\217\001\n\013GetDocument\022\'.goo" + "gle.firestore.v1.GetDocumentRequest\032\035.go" + "ogle.firestore.v1.Document\"8\202\323\344\223\0022\0220/v1/" + "{name=projects/*/databases/*/documents/*" + "/**}\022\262\001\n\rListDocuments\022).google.firestor" + "e.v1.ListDocumentsRequest\032*.google.fires" + "tore.v1.ListDocumentsResponse\"J\202\323\344\223\002D\022B/" + "v1/{parent=projects/*/databases/*/docume" + "nts/*/**}/{collection_id}\022\257\001\n\016CreateDocu" + "ment\022*.google.firestore.v1.CreateDocumen" + "tRequest\032\035.google.firestore.v1.Document\"" + "R\202\323\344\223\002L\"@/v1/{parent=projects/*/database" + "s/*/documents/**}/{collection_id}:\010docum" + "ent\022\250\001\n\016UpdateDocument\022*.google.firestor" + "e.v1.UpdateDocumentRequest\032\035.google.fire" + "store.v1.Document\"K\202\323\344\223\002E29/v1/{document" + ".name=projects/*/databases/*/documents/*" + "/**}:\010document\022\216\001\n\016DeleteDocument\022*.goog" + "le.firestore.v1.DeleteDocumentRequest\032\026." + "google.protobuf.Empty\"8\202\323\344\223\0022*0/v1/{name" + "=projects/*/databases/*/documents/*/**}\022" + "\271\001\n\021BatchGetDocuments\022-.google.firestore" + ".v1.BatchGetDocumentsRequest\032..google.fi" + "restore.v1.BatchGetDocumentsResponse\"C\202\323" + "\344\223\002=\"8/v1/{database=projects/*/databases" + "/*}/documents:batchGet:\001*0\001\022\274\001\n\020BeginTra" + "nsaction\022,.google.firestore.v1.BeginTran" + "sactionRequest\032-.google.firestore.v1.Beg" + "inTransactionResponse\"K\202\323\344\223\002E\"@/v1/{data" + "base=projects/*/databases/*}/documents:b" + "eginTransaction:\001*\022\224\001\n\006Commit\022\".google.f" + "irestore.v1.CommitRequest\032#.google.fires" + "tore.v1.CommitResponse\"A\202\323\344\223\002;\"6/v1/{dat" + "abase=projects/*/databases/*}/documents:" + "commit:\001*\022\215\001\n\010Rollback\022$.google.firestor" + "e.v1.RollbackRequest\032\026.google.protobuf.E" + "mpty\"C\202\323\344\223\002=\"8/v1/{database=projects/*/d" + "atabases/*}/documents:rollback:\001*\022\337\001\n\010Ru" + "nQuery\022$.google.firestore.v1.RunQueryReq" + "uest\032%.google.firestore.v1.RunQueryRespo" + "nse\"\203\001\202\323\344\223\002}\"6/v1/{parent=projects/*/dat" + "abases/*/documents}:runQuery:\001*Z@\";/v1/{" + "parent=projects/*/databases/*/documents/" + "*/**}:runQuery:\001*0\001\022\272\001\n\017ExecutePipeline\022" + "+.google.firestore.v1.ExecutePipelineReq" + "uest\032,.google.firestore.v1.ExecutePipeli" + "neResponse\"J\202\323\344\223\002D\"\?/v1/{database=projec" + "ts/*/databases/*}/documents:executePipel" + "ine:\001*0\001\022\227\002\n\023RunAggregationQuery\022/.googl" + "e.firestore.v1.RunAggregationQueryReques" + "t\0320.google.firestore.v1.RunAggregationQu" + "eryResponse\"\232\001\202\323\344\223\002\223\001\"A/v1/{parent=proje" + "cts/*/databases/*/documents}:runAggregat" + "ionQuery:\001*ZK\"F/v1/{parent=projects/*/da" + "tabases/*/documents/*/**}:runAggregation" + "Query:\001*0\001\022\224\001\n\005Write\022!.google.firestore." + "v1.WriteRequest\032\".google.firestore.v1.Wr" + "iteResponse\"@\202\323\344\223\002:\"5/v1/{database=proje" + "cts/*/databases/*}/documents:write:\001*(\0010" + "\001\022\230\001\n\006Listen\022\".google.firestore.v1.Liste" + "nRequest\032#.google.firestore.v1.ListenRes" + "ponse\"A\202\323\344\223\002;\"6/v1/{database=projects/*/" + "databases/*}/documents:listen:\001*(\0010\001\022\213\002\n" + "\021ListCollectionIds\022-.google.firestore.v1" + ".ListCollectionIdsRequest\032..google.fires" + "tore.v1.ListCollectionIdsResponse\"\226\001\202\323\344\223" + "\002\217\001\"\?/v1/{parent=projects/*/databases/*/" + "documents}:listCollectionIds:\001*ZI\"D/v1/{" + "parent=projects/*/databases/*/documents/" + "*/**}:listCollectionIds:\001*B\262\001\n\027com.googl" + "e.firestore.v1B\016FirestoreProtoP\001Z( from._internal_metadata_); new (&_impl_) Impl_(internal_visibility(), arena, from._impl_); + _impl_.auto_commit_transaction_ = from._impl_.auto_commit_transaction_; switch (pipeline_type_case()) { case PIPELINE_TYPE_NOT_SET: break; @@ -6592,6 +6596,7 @@ inline PROTOBUF_NDEBUG_INLINE ExecutePipelineRequest::Impl_::Impl_( inline void ExecutePipelineRequest::SharedCtor(::_pb::Arena* arena) { new (&_impl_) Impl_(internal_visibility(), arena); + _impl_.auto_commit_transaction_ = {}; } ExecutePipelineRequest::~ExecutePipelineRequest() { // @@protoc_insertion_point(destructor:google.firestore.v1.ExecutePipelineRequest) @@ -6663,6 +6668,7 @@ PROTOBUF_NOINLINE void ExecutePipelineRequest::Clear() { (void) cached_has_bits; _impl_.database_.ClearToEmpty(); + _impl_.auto_commit_transaction_ = false; clear_pipeline_type(); clear_consistency_selector(); _internal_metadata_.Clear<::google::protobuf::UnknownFieldSet>(); @@ -6676,15 +6682,15 @@ const char* ExecutePipelineRequest::_InternalParse( PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 -const ::_pbi::TcParseTable<0, 5, 3, 59, 2> ExecutePipelineRequest::_table_ = { +const ::_pbi::TcParseTable<0, 6, 3, 59, 2> ExecutePipelineRequest::_table_ = { { 0, // no _has_bits_ 0, // no _extensions_ - 7, 0, // max_field_number, fast_idx_mask + 9, 0, // max_field_number, fast_idx_mask offsetof(decltype(_table_), field_lookup_table), - 4294967180, // skipmap + 4294966924, // skipmap offsetof(decltype(_table_), field_entries), - 5, // num_field_entries + 6, // num_field_entries 3, // num_aux_entries offsetof(decltype(_table_), aux_entries), &_ExecutePipelineRequest_default_instance_._instance, @@ -6711,6 +6717,9 @@ const ::_pbi::TcParseTable<0, 5, 3, 59, 2> ExecutePipelineRequest::_table_ = { // .google.protobuf.Timestamp read_time = 7; {PROTOBUF_FIELD_OFFSET(ExecutePipelineRequest, _impl_.consistency_selector_.read_time_), _Internal::kOneofCaseOffset + 4, 2, (0 | ::_fl::kFcOneof | ::_fl::kMessage | ::_fl::kTvTable)}, + // bool auto_commit_transaction = 9 [(.google.api.field_behavior) = OPTIONAL]; + {PROTOBUF_FIELD_OFFSET(ExecutePipelineRequest, _impl_.auto_commit_transaction_), 0, 0, + (0 | ::_fl::kFcSingular | ::_fl::kBool)}, }}, {{ {::_pbi::TcParser::GetTable<::google::firestore::v1::StructuredPipeline>()}, {::_pbi::TcParser::GetTable<::google::firestore::v1::TransactionOptions>()}, @@ -6765,6 +6774,13 @@ ::uint8_t* ExecutePipelineRequest::_InternalSerialize( default: break; } + // bool auto_commit_transaction = 9 [(.google.api.field_behavior) = OPTIONAL]; + if (this->_internal_auto_commit_transaction() != 0) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray( + 9, this->_internal_auto_commit_transaction(), target); + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { target = ::_pbi::WireFormat::InternalSerializeUnknownFieldsToArray( @@ -6788,6 +6804,11 @@ ::size_t ExecutePipelineRequest::ByteSizeLong() const { this->_internal_database()); } + // bool auto_commit_transaction = 9 [(.google.api.field_behavior) = OPTIONAL]; + if (this->_internal_auto_commit_transaction() != 0) { + total_size += 2; + } + switch (pipeline_type_case()) { // .google.firestore.v1.StructuredPipeline structured_pipeline = 2; case kStructuredPipeline: { @@ -6844,6 +6865,9 @@ void ExecutePipelineRequest::MergeImpl(::google::protobuf::Message& to_msg, cons if (!from._internal_database().empty()) { _this->_internal_set_database(from._internal_database()); } + if (from._internal_auto_commit_transaction() != 0) { + _this->_internal_set_auto_commit_transaction(from._internal_auto_commit_transaction()); + } switch (from.pipeline_type_case()) { case kStructuredPipeline: { _this->_internal_mutable_structured_pipeline()->::google::firestore::v1::StructuredPipeline::MergeFrom( @@ -6896,6 +6920,7 @@ void ExecutePipelineRequest::InternalSwap(ExecutePipelineRequest* PROTOBUF_RESTR ABSL_DCHECK_EQ(arena, other->GetArena()); _internal_metadata_.InternalSwap(&other->_internal_metadata_); ::_pbi::ArenaStringPtr::InternalSwap(&_impl_.database_, &other->_impl_.database_, arena); + swap(_impl_.auto_commit_transaction_, other->_impl_.auto_commit_transaction_); swap(_impl_.pipeline_type_, other->_impl_.pipeline_type_); swap(_impl_.consistency_selector_, other->_impl_.consistency_selector_); swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]); diff --git a/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.h b/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.h index 22cfa2848e5..8dec8fb30ff 100644 --- a/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.h +++ b/Firestore/Protos/cpp/google/firestore/v1/firestore.pb.h @@ -4765,6 +4765,7 @@ class ExecutePipelineRequest final : enum : int { kDatabaseFieldNumber = 1, + kAutoCommitTransactionFieldNumber = 9, kStructuredPipelineFieldNumber = 2, kTransactionFieldNumber = 5, kNewTransactionFieldNumber = 6, @@ -4785,6 +4786,16 @@ class ExecutePipelineRequest final : const std::string& value); std::string* _internal_mutable_database(); + public: + // bool auto_commit_transaction = 9 [(.google.api.field_behavior) = OPTIONAL]; + void clear_auto_commit_transaction() ; + bool auto_commit_transaction() const; + void set_auto_commit_transaction(bool value); + + private: + bool _internal_auto_commit_transaction() const; + void _internal_set_auto_commit_transaction(bool value); + public: // .google.firestore.v1.StructuredPipeline structured_pipeline = 2; bool has_structured_pipeline() const; @@ -4880,7 +4891,7 @@ class ExecutePipelineRequest final : friend class ::google::protobuf::internal::TcParser; static const ::google::protobuf::internal::TcParseTable< - 0, 5, 3, + 0, 6, 3, 59, 2> _table_; friend class ::google::protobuf::MessageLite; @@ -4898,6 +4909,7 @@ class ExecutePipelineRequest final : inline explicit Impl_(::google::protobuf::internal::InternalVisibility visibility, ::google::protobuf::Arena* arena, const Impl_& from); ::google::protobuf::internal::ArenaStringPtr database_; + bool auto_commit_transaction_; union PipelineTypeUnion { constexpr PipelineTypeUnion() : _constinit_{} {} ::google::protobuf::internal::ConstantInitialized _constinit_; @@ -11781,6 +11793,29 @@ inline ::google::protobuf::Timestamp* ExecutePipelineRequest::mutable_read_time( return _msg; } +// bool auto_commit_transaction = 9 [(.google.api.field_behavior) = OPTIONAL]; +inline void ExecutePipelineRequest::clear_auto_commit_transaction() { + PROTOBUF_TSAN_WRITE(&_impl_._tsan_detect_race); + _impl_.auto_commit_transaction_ = false; +} +inline bool ExecutePipelineRequest::auto_commit_transaction() const { + // @@protoc_insertion_point(field_get:google.firestore.v1.ExecutePipelineRequest.auto_commit_transaction) + return _internal_auto_commit_transaction(); +} +inline void ExecutePipelineRequest::set_auto_commit_transaction(bool value) { + _internal_set_auto_commit_transaction(value); + // @@protoc_insertion_point(field_set:google.firestore.v1.ExecutePipelineRequest.auto_commit_transaction) +} +inline bool ExecutePipelineRequest::_internal_auto_commit_transaction() const { + PROTOBUF_TSAN_READ(&_impl_._tsan_detect_race); + return _impl_.auto_commit_transaction_; +} +inline void ExecutePipelineRequest::_internal_set_auto_commit_transaction(bool value) { + PROTOBUF_TSAN_WRITE(&_impl_._tsan_detect_race); + ; + _impl_.auto_commit_transaction_ = value; +} + inline bool ExecutePipelineRequest::has_pipeline_type() const { return pipeline_type_case() != PIPELINE_TYPE_NOT_SET; } diff --git a/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.cc b/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.cc index 429a2674fef..f992a1107b3 100644 --- a/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.cc +++ b/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.cc @@ -152,12 +152,13 @@ const pb_field_t google_firestore_v1_RunQueryResponse_fields[5] = { PB_LAST_FIELD }; -const pb_field_t google_firestore_v1_ExecutePipelineRequest_fields[6] = { +const pb_field_t google_firestore_v1_ExecutePipelineRequest_fields[7] = { PB_FIELD( 1, BYTES , SINGULAR, POINTER , FIRST, google_firestore_v1_ExecutePipelineRequest, database, database, 0), PB_ONEOF_FIELD(pipeline_type, 2, MESSAGE , ONEOF, STATIC , OTHER, google_firestore_v1_ExecutePipelineRequest, structured_pipeline, database, &google_firestore_v1_StructuredPipeline_fields), PB_ONEOF_FIELD(consistency_selector, 5, BYTES , ONEOF, POINTER , OTHER, google_firestore_v1_ExecutePipelineRequest, transaction, pipeline_type.structured_pipeline, 0), PB_ONEOF_FIELD(consistency_selector, 6, MESSAGE , ONEOF, STATIC , UNION, google_firestore_v1_ExecutePipelineRequest, new_transaction, pipeline_type.structured_pipeline, &google_firestore_v1_TransactionOptions_fields), PB_ONEOF_FIELD(consistency_selector, 7, MESSAGE , ONEOF, STATIC , UNION, google_firestore_v1_ExecutePipelineRequest, read_time, pipeline_type.structured_pipeline, &google_protobuf_Timestamp_fields), + PB_FIELD( 9, BOOL , SINGULAR, STATIC , OTHER, google_firestore_v1_ExecutePipelineRequest, auto_commit_transaction, consistency_selector.read_time, 0), PB_LAST_FIELD }; @@ -659,6 +660,8 @@ std::string google_firestore_v1_ExecutePipelineRequest::ToString(int indent) con consistency_selector.read_time, indent + 1, true); break; } + tostring_result += PrintPrimitiveField("auto_commit_transaction: ", + auto_commit_transaction, indent + 1, false); bool is_root = indent == 0; if (!tostring_result.empty() || is_root) { diff --git a/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.h b/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.h index 3d9e151126a..1f0670d926c 100644 --- a/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.h +++ b/Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.h @@ -228,6 +228,7 @@ typedef struct _google_firestore_v1_ExecutePipelineRequest { google_firestore_v1_TransactionOptions new_transaction; google_protobuf_Timestamp read_time; } consistency_selector; + bool auto_commit_transaction; std::string ToString(int indent = 0) const; /* @@protoc_insertion_point(struct:google_firestore_v1_ExecutePipelineRequest) */ @@ -459,7 +460,7 @@ typedef struct _google_firestore_v1_ListenRequest { #define google_firestore_v1_RollbackRequest_init_default {NULL, NULL} #define google_firestore_v1_RunQueryRequest_init_default {NULL, 0, {google_firestore_v1_StructuredQuery_init_default}, 0, {NULL}} #define google_firestore_v1_RunQueryResponse_init_default {google_firestore_v1_Document_init_default, NULL, google_protobuf_Timestamp_init_default, 0} -#define google_firestore_v1_ExecutePipelineRequest_init_default {NULL, 0, {google_firestore_v1_StructuredPipeline_init_default}, 0, {NULL}} +#define google_firestore_v1_ExecutePipelineRequest_init_default {NULL, 0, {google_firestore_v1_StructuredPipeline_init_default}, 0, {NULL}, 0} #define google_firestore_v1_ExecutePipelineResponse_init_default {NULL, 0, NULL, google_protobuf_Timestamp_init_default, google_firestore_v1_ExplainStats_init_default} #define google_firestore_v1_RunAggregationQueryRequest_init_default {NULL, 0, {google_firestore_v1_StructuredAggregationQuery_init_default}, 0, {NULL}} #define google_firestore_v1_RunAggregationQueryResponse_init_default {google_firestore_v1_AggregationResult_init_default, NULL, google_protobuf_Timestamp_init_default} @@ -491,7 +492,7 @@ typedef struct _google_firestore_v1_ListenRequest { #define google_firestore_v1_RollbackRequest_init_zero {NULL, NULL} #define google_firestore_v1_RunQueryRequest_init_zero {NULL, 0, {google_firestore_v1_StructuredQuery_init_zero}, 0, {NULL}} #define google_firestore_v1_RunQueryResponse_init_zero {google_firestore_v1_Document_init_zero, NULL, google_protobuf_Timestamp_init_zero, 0} -#define google_firestore_v1_ExecutePipelineRequest_init_zero {NULL, 0, {google_firestore_v1_StructuredPipeline_init_zero}, 0, {NULL}} +#define google_firestore_v1_ExecutePipelineRequest_init_zero {NULL, 0, {google_firestore_v1_StructuredPipeline_init_zero}, 0, {NULL}, 0} #define google_firestore_v1_ExecutePipelineResponse_init_zero {NULL, 0, NULL, google_protobuf_Timestamp_init_zero, google_firestore_v1_ExplainStats_init_zero} #define google_firestore_v1_RunAggregationQueryRequest_init_zero {NULL, 0, {google_firestore_v1_StructuredAggregationQuery_init_zero}, 0, {NULL}} #define google_firestore_v1_RunAggregationQueryResponse_init_zero {google_firestore_v1_AggregationResult_init_zero, NULL, google_protobuf_Timestamp_init_zero} @@ -556,6 +557,7 @@ typedef struct _google_firestore_v1_ListenRequest { #define google_firestore_v1_ExecutePipelineRequest_new_transaction_tag 6 #define google_firestore_v1_ExecutePipelineRequest_read_time_tag 7 #define google_firestore_v1_ExecutePipelineRequest_database_tag 1 +#define google_firestore_v1_ExecutePipelineRequest_auto_commit_transaction_tag 9 #define google_firestore_v1_ExecutePipelineResponse_transaction_tag 1 #define google_firestore_v1_ExecutePipelineResponse_results_tag 2 #define google_firestore_v1_ExecutePipelineResponse_execution_time_tag 3 @@ -643,7 +645,7 @@ extern const pb_field_t google_firestore_v1_CommitResponse_fields[3]; extern const pb_field_t google_firestore_v1_RollbackRequest_fields[3]; extern const pb_field_t google_firestore_v1_RunQueryRequest_fields[6]; extern const pb_field_t google_firestore_v1_RunQueryResponse_fields[5]; -extern const pb_field_t google_firestore_v1_ExecutePipelineRequest_fields[6]; +extern const pb_field_t google_firestore_v1_ExecutePipelineRequest_fields[7]; extern const pb_field_t google_firestore_v1_ExecutePipelineResponse_fields[5]; extern const pb_field_t google_firestore_v1_RunAggregationQueryRequest_fields[6]; extern const pb_field_t google_firestore_v1_RunAggregationQueryResponse_fields[4]; diff --git a/Firestore/Protos/protos/google/firestore/v1/firestore.proto b/Firestore/Protos/protos/google/firestore/v1/firestore.proto index 82523d254c1..9c216b2f3dc 100644 --- a/Firestore/Protos/protos/google/firestore/v1/firestore.proto +++ b/Firestore/Protos/protos/google/firestore/v1/firestore.proto @@ -554,6 +554,10 @@ message ExecutePipelineRequest { // minute timestamp within the past 7 days. google.protobuf.Timestamp read_time = 7; } + + // Automatically commits the transaction after the pipeline has been executed. + // Only permitted in combination with `transaction` or `new_transaction`. + bool auto_commit_transaction = 9 [(google.api.field_behavior) = OPTIONAL]; } // The response for [Firestore.Execute][]. diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 39dcf1fe84a..6f8957c7658 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -1372,6 +1372,203 @@ - (id)initWithCppChange:(api::PipelineResultChange)change db:(std::shared_ptr cpp_delete; +} + +- (id)init { + self = [super init]; + if (self) { + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + cpp_delete = std::make_shared(); + } + isUserDataRead = YES; + return cpp_delete; +} + +- (NSString *)name { + return @"delete"; +} +@end + +@implementation FIRUpdateStageBridge { + NSDictionary *_fields; + Boolean isUserDataRead; + std::shared_ptr cpp_update; +} + +- (id)initWithFields:(NSDictionary *)fields { + self = [super init]; + if (self) { + _fields = fields; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_fields; + if (_fields) { + for (NSString *key in _fields) { + cpp_fields[MakeString(key)] = [_fields[key] cppExprWithReader:reader]; + } + } + cpp_update = std::make_shared(std::move(cpp_fields)); + } + isUserDataRead = YES; + return cpp_update; +} + +- (NSString *)name { + return @"update"; +} +@end + +@implementation FIRInsertStageBridge { + NSString *_collectionPath; + FIRExprBridge *_Nullable _documentIdExpression; + Boolean isUserDataRead; + std::shared_ptr cpp_insert; +} + +- (id)initWithCollectionPath:(NSString *)collectionPath + documentIdExpression:(FIRExprBridge *_Nullable)documentIdExpression { + self = [super init]; + if (self) { + _collectionPath = collectionPath; + _documentIdExpression = documentIdExpression; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::shared_ptr cpp_doc_id = nil; + if (_documentIdExpression != nil) { + cpp_doc_id = [_documentIdExpression cppExprWithReader:reader]; + } + cpp_insert = std::make_shared(MakeString(_collectionPath ? _collectionPath : @""), + std::move(cpp_doc_id)); + } + isUserDataRead = YES; + return cpp_insert; +} + +- (NSString *)name { + return @"insert"; +} +@end + +@implementation FIRUpsertStageBridge { + NSDictionary *_fields; + NSString *_collectionPath; + FIRExprBridge *_Nullable _documentIdExpression; + Boolean isUserDataRead; + std::shared_ptr cpp_upsert; +} + +- (id)initWithFields:(NSDictionary *)fields + collectionPath:(NSString *_Nullable)collectionPath + documentIdExpression:(FIRExprBridge *_Nullable)documentIdExpression { + self = [super init]; + if (self) { + _fields = fields; + _collectionPath = collectionPath; + _documentIdExpression = documentIdExpression; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_fields; + if (_fields) { + for (NSString *key in _fields) { + cpp_fields[MakeString(key)] = [_fields[key] cppExprWithReader:reader]; + } + } + std::shared_ptr cpp_doc_id = nil; + if (_documentIdExpression != nil) { + cpp_doc_id = [_documentIdExpression cppExprWithReader:reader]; + } + cpp_upsert = std::make_shared(std::move(cpp_fields), + MakeString(_collectionPath ? _collectionPath : @""), + std::move(cpp_doc_id)); + } + isUserDataRead = YES; + return cpp_upsert; +} + +- (NSString *)name { + return @"upsert"; +} +@end + +@implementation FIRLiteralsSourceStageBridge { + NSArray *> *_data; + FIRFirestore *_db; + Boolean isUserDataRead; + std::shared_ptr cpp_literals; +} + +- (id)initWithData:(NSArray *> *)data + firestore:(FIRFirestore *)db { + self = [super init]; + if (self) { + _data = data; + _db = db; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector cpp_data; + for (NSDictionary *docMap in _data) { + firebase::firestore::google_firestore_v1_Value mapVal; + mapVal.which_value_type = google_firestore_v1_Value_map_value_tag; + + std::vector> cpp_fields; + for (NSString *key in docMap) { + id val = docMap[key]; + firebase::firestore::google_firestore_v1_Value entryVal; + if ([val isKindOfClass:[FIRExprBridge class]]) { + entryVal = [((FIRExprBridge *)val) cppExprWithReader:reader]->to_proto(); + } else { + entryVal = [reader parsedQueryValue:val]; + } + cpp_fields.emplace_back(MakeString(key), entryVal); + } + nanopb::SetRepeatedField( + &mapVal.map_value.fields, &mapVal.map_value.fields_count, cpp_fields, + [](const std::pair& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second}; + }); + cpp_data.push_back(mapVal); + } + cpp_literals = std::make_shared(std::move(cpp_data)); + } + isUserDataRead = YES; + return cpp_literals; +} + +- (NSString *)name { + return @"literals"; +} +@end + @implementation FIRPipelineExprBridge { NSArray *_stages; Boolean isUserDataRead; @@ -1399,15 +1596,26 @@ - (id)initWithStages:(NSArray *)stages { @implementation FIRPipelineBridge { NSArray *_stages; FIRFirestore *firestore; + BOOL _atomic; Boolean isUserDataRead; std::shared_ptr cpp_pipeline; } - (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db { - _stages = stages; - firestore = db; - isUserDataRead = NO; - return [super init]; + return [self initWithStages:stages db:db atomic:NO]; +} + +- (id)initWithStages:(NSArray *)stages + db:(FIRFirestore *)db + atomic:(BOOL)atomic { + self = [super init]; + if (self) { + _stages = stages; + firestore = db; + _atomic = atomic; + isUserDataRead = NO; + } + return self; } - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable result, @@ -1430,7 +1638,7 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r for (FIRStageBridge *stage in _stages) { cpp_stages.push_back([stage cppStageWithReader:reader]); } - cpp_pipeline = std::make_shared(cpp_stages, firestore.wrapped); + cpp_pipeline = std::make_shared(cpp_stages, firestore.wrapped, _atomic); } isUserDataRead = YES; diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index 8867433f27d..852f51d6a75 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -233,6 +233,40 @@ NS_SWIFT_NAME(RawStageBridge) options:(NSDictionary *_Nullable)options; @end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(DeleteStageBridge) +@interface FIRDeleteStageBridge : FIRStageBridge +- (id)init; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(UpdateStageBridge) +@interface FIRUpdateStageBridge : FIRStageBridge +- (id)initWithFields:(NSDictionary *)fields; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(InsertStageBridge) +@interface FIRInsertStageBridge : FIRStageBridge +- (id)initWithCollectionPath:(NSString *)collectionPath + documentIdExpression:(FIRExprBridge *_Nullable)documentIdExpression; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(UpsertStageBridge) +@interface FIRUpsertStageBridge : FIRStageBridge +- (id)initWithFields:(NSDictionary *)fields + collectionPath:(NSString *_Nullable)collectionPath + documentIdExpression:(FIRExprBridge *_Nullable)documentIdExpression; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(LiteralsSourceStageBridge) +@interface FIRLiteralsSourceStageBridge : FIRStageBridge +- (id)initWithData:(NSArray *> *)data + firestore:(FIRFirestore *)db; +@end + NS_SWIFT_SENDABLE NS_SWIFT_NAME(__PipelineResultBridge) @interface __FIRPipelineResultBridge : NSObject @@ -289,6 +323,9 @@ NS_SWIFT_NAME(PipelineBridge) /** :nodoc: */ - (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db; +- (id)initWithStages:(NSArray *)stages + db:(FIRFirestore *)db + atomic:(BOOL)atomic; - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable result, NSError *_Nullable error))completion; diff --git a/Firestore/Swift/Source/Stages.swift b/Firestore/Swift/Source/Stages.swift index dc2a1934479..a0860205196 100644 --- a/Firestore/Swift/Source/Stages.swift +++ b/Firestore/Swift/Source/Stages.swift @@ -519,3 +519,79 @@ class RawStage: Stage { bridge = RawStageBridge(name: name, params: bridgeParams, options: bridgeOptions) } } + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class DeleteStage: Stage { + let name: String = "delete" + let bridge: StageBridge + init() { + bridge = DeleteStageBridge() + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class UpdateStage: Stage { + let name: String = "update" + let bridge: StageBridge + let errorMessage: String? + + init(fields: [Selectable]) { + let (map, error) = Helper.selectablesToMap(selectables: fields) + if let error = error { + errorMessage = error.localizedDescription + bridge = UpdateStageBridge(fields: [:]) + } else { + errorMessage = nil + bridge = UpdateStageBridge(fields: map.mapValues { $0.toBridge() }) + } + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class InsertStage: Stage { + let name: String = "insert" + let bridge: StageBridge + + init(collectionPath: String, documentIdExpression: Expression?) { + bridge = InsertStageBridge( + collectionPath: collectionPath, + documentIdExpression: documentIdExpression?.toBridge() + ) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class UpsertStage: Stage { + let name: String = "upsert" + let bridge: StageBridge + let errorMessage: String? + + init(fields: [Selectable], collectionPath: String? = nil, documentIdExpression: Expression? = nil) { + let (map, error) = Helper.selectablesToMap(selectables: fields) + if let error = error { + errorMessage = error.localizedDescription + bridge = UpsertStageBridge( + fields: [:], + collectionPath: collectionPath, + documentIdExpression: documentIdExpression?.toBridge() + ) + } else { + errorMessage = nil + bridge = UpsertStageBridge( + fields: map.mapValues { $0.toBridge() }, + collectionPath: collectionPath, + documentIdExpression: documentIdExpression?.toBridge() + ) + } + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class LiteralsSourceStage: Stage { + let name: String = "literals" + let bridge: StageBridge + + init(data: [[String: Any]], db: Firestore) { + bridge = LiteralsSourceStageBridge(data: data, firestore: db) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 6753dcb7334..a0b8372d16c 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -93,6 +93,23 @@ public class Pipeline: @unchecked Sendable { self.db = db } + /// Options that control the execution of a `Pipeline`. + public struct ExecuteOptions: Sendable { + /// Whether the pipeline should execute atomically inside a single transaction. + public var atomic: Bool + + public init(atomic: Bool = false) { + self.atomic = atomic + } + + /// Sets whether atomic execution is enabled and returns a modified options instance. + public func withAtomic(_ atomic: Bool) -> ExecuteOptions { + var copy = self + copy.atomic = atomic + return copy + } + } + /// A `Pipeline.Snapshot` contains the results of a pipeline execution. public struct Snapshot: Sendable { /// An array of all the results in the `Pipeline.Snapshot`. @@ -113,25 +130,45 @@ public class Pipeline: @unchecked Sendable { /// Executes the defined pipeline and returns a `Pipeline.Snapshot` containing the results. /// /// This method asynchronously sends the pipeline definition to Firestore for execution. - /// The resulting documents, transformed and filtered by the pipeline stages, are returned + /// The resulting documents, transformed, filtered, or mutated by the pipeline stages, are returned /// within a `Pipeline.Snapshot`. /// + /// By default, the pipeline executes non-atomically. To execute all mutations within a single + /// atomic transaction, supply `Pipeline.ExecuteOptions(atomic: true)`. + /// /// ```swift - /// // let pipeline: Pipeline = ... // Assume a pipeline is already configured. + /// // Example 1: Standard query execution + /// do { + /// let snapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("genre").equal(Expression.constant("Sci-Fi"))) + /// .execute() + /// print("Results count: \(snapshot.results.count)") + /// } catch { + /// print("Execution failed: \(error)") + /// } + /// + /// // Example 2: Atomic transactional execution /// do { - /// let snapshot = try await pipeline.execute() - /// // Process snapshot.results - /// print("Pipeline executed successfully: \(snapshot.results)") + /// let options = Pipeline.ExecuteOptions(atomic: true) + /// let snapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("rating").lessThan(Expression.constant(2.0))) + /// .delete() + /// .execute(options: options) + /// print("Deleted matching books atomically at: \(snapshot.executionTime)") /// } catch { - /// print("Pipeline execution failed: \(error)") + /// print("Transaction failed: \(error)") /// } /// ``` /// + /// - Parameter options: Options controlling execution behavior, such as atomic transactions. + /// Defaults to `.init()` (non-atomic execution). /// - Throws: An error if the pipeline execution fails on the backend. /// - Returns: A `Pipeline.Snapshot` containing the result of the pipeline execution. - public func execute() async throws -> Pipeline.Snapshot { + public func execute(options: Pipeline.ExecuteOptions = .init()) async throws -> Pipeline.Snapshot { // Check if isolated subcollection execution is being attempted. - guard db != nil else { + guard let db = db else { throw NSError( domain: "com.google.firebase.firestore", code: 3 /* kErrorInvalidArgument */, @@ -150,8 +187,14 @@ public class Pipeline: @unchecked Sendable { ) } + let bridge = PipelineBridge( + stages: stages.map { $0.bridge }, + db: db, + atomic: options.atomic + ) + return try await withCheckedThrowingContinuation { continuation in - self.pipelineBridge.execute { result, error in + bridge.execute { result, error in if let error { continuation.resume(throwing: error) } else { @@ -971,4 +1014,236 @@ public class Pipeline: @unchecked Sendable { public func toScalarExpression() -> Expression { return FunctionExpression(functionName: "scalar", args: [PipelineExpression(self)]) } + + /// Appends a `delete` stage to the pipeline. + /// + /// When executed, the `delete` stage removes all documents matching preceding pipeline stages + /// from the Firestore database. This can be combined with filter stages like `where`, explicit document + /// sources, or other stages. + /// + /// Execution can be non-transactional (default) or executed atomically within a single transaction + /// by passing `Pipeline.ExecuteOptions(atomic: true)` to `execute(options:)`. + /// + /// ```swift + /// // Example 1: Non-transactional deletion of filtered documents + /// let snapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("title").equal(Expression.constant("The Hitchhiker's Guide to the Galaxy"))) + /// .delete() + /// .execute() + /// + /// // Example 2: Atomic transactional deletion of specific document references + /// let docPipeline = db.pipeline() + /// .documents([ + /// db.collection("books").document("book_1"), + /// db.collection("books").document("book_2") + /// ]) + /// .delete() + /// let atomicSnapshot = try await docPipeline.execute( + /// options: Pipeline.ExecuteOptions(atomic: true) + /// ) + /// ``` + /// + /// - Returns: A new `Pipeline` object with the `delete` stage appended. + public func delete() -> Pipeline { + return Pipeline(stages: stages + [DeleteStage()], db: db) + } + + /// Appends an `update` stage to the pipeline modifying specified fields using variadic expressions. + /// + /// The `update` stage modifies fields in-place on existing documents matched by preceding pipeline stages. + /// Each `Selectable` expression defines a field assignment via `.as("fieldName")` with a new constant or + /// computed value expression. + /// + /// ```swift + /// // Update multiple fields on matching documents + /// let snapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("genre").equal(Expression.constant("Sci-Fi"))) + /// .update( + /// Expression.constant("Science Fiction").as("genre"), + /// Expression.constant(true).as("featured") + /// ) + /// .execute(options: Pipeline.ExecuteOptions(atomic: true)) + /// ``` + /// + /// - Parameter fields: Variadic list of `Selectable` expressions representing updated field assignments. + /// - Returns: A new `Pipeline` object with the `update` stage appended. + public func update(_ fields: Selectable...) -> Pipeline { + return update(fields) + } + + /// Appends an `update` stage to the pipeline modifying specified fields using an array of expressions. + /// + /// The `update` stage modifies fields in-place on existing documents matched by preceding pipeline stages. + /// Each `Selectable` expression defines a field assignment via `.as("fieldName")` with a new constant or + /// computed value expression. + /// + /// ```swift + /// // Update fields on a specific document using an array of transforms + /// let snapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("__name__").equal(Expression.constant("book1"))) + /// .update([ + /// Expression.constant("Comedy Sci-Fi").as("genre"), + /// Field("rating").add(Expression.constant(0.5)).as("rating") + /// ]) + /// .execute(options: Pipeline.ExecuteOptions(atomic: true)) + /// ``` + /// + /// - Parameter fields: Array of `Selectable` expressions representing updated field assignments. + /// - Returns: A new `Pipeline` object with the `update` stage appended. + public func update(_ fields: [Selectable]) -> Pipeline { + return Pipeline(stages: stages + [UpdateStage(fields: fields)], db: db) + } + + /// Appends an `insert` stage to the pipeline writing documents into the specified collection. + /// + /// The `insert` stage creates new documents in Firestore from the records produced by preceding pipeline stages. + /// + /// - Note: The `collectionPath` parameter is **required** because newly inserted documents do not have an existing + /// database location; an explicit destination collection path is necessary to determine where the documents are written. + /// + /// If `documentIdExpression` is omitted or `nil`, Firestore automatically generates a unique document ID. If provided, + /// the expression (such as a `Field` reference or constant `Expression`) resolves the document ID. + /// + /// ```swift + /// // Example 1: Insert with auto-generated document ID + /// let autoIdSnapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("genre").equal(Expression.constant("Bestseller"))) + /// .insert(collectionPath: "bestsellers_backup") + /// .execute(options: Pipeline.ExecuteOptions(atomic: true)) + /// + /// // Example 2: Insert with custom document ID derived from a field expression + /// let customIdSnapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("__name__").equal(Expression.constant("book1"))) + /// .insert( + /// collectionPath: "books_archive", + /// documentIdExpression: Field("isbn") + /// ) + /// .execute() + /// + /// // Example 3: Bulk insert from literal document records + /// let literalSnapshot = try await db.pipeline() + /// .literals([ + /// ["id": "user_1", "name": "Charlie", "email": "charlie@example.com"], + /// ["id": "user_2", "name": "Dana", "email": "dana@example.com"] + /// ]) + /// .insert( + /// collectionPath: "users", + /// documentIdExpression: Field("id") + /// ) + /// .execute() + /// ``` + /// + /// - Parameters: + /// - collectionPath: The target collection path to insert documents into. This parameter is required because + /// newly created documents require an explicit destination collection. + /// - documentIdExpression: An optional `Expression` resolving to the document ID. If `nil`, Firestore auto-generates + /// a unique document ID. + /// - Returns: A new `Pipeline` object with the `insert` stage appended. + public func insert(collectionPath: String, documentIdExpression: Expression? = nil) -> Pipeline { + return Pipeline(stages: stages + [InsertStage(collectionPath: collectionPath, documentIdExpression: documentIdExpression)], db: db) + } + + /// Appends an `upsert` stage to the pipeline transforming fields using variadic expressions. + /// + /// The `upsert` stage inserts a document if it does not already exist, or updates its fields if it does. + /// + /// - Note: The `collectionPath` parameter is **optional**. When omitted (or `nil`), the operation performs an + /// in-place upsert directly within the pipeline's current source collection. When `collectionPath` is provided, + /// documents are upserted into the specified destination collection instead (e.g. copying/archiving or when + /// sourcing data from `literals(...)`). + /// + /// ```swift + /// // Example 1: In-place upsert with variadic transform arguments + /// let snapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("__name__").equal(Expression.constant("book1"))) + /// .upsert( + /// Expression.constant("Updated Genre").as("genre"), + /// Field("views").add(Expression.constant(10)).as("views") + /// ) + /// .execute(options: Pipeline.ExecuteOptions(atomic: true)) + /// + /// // Example 2: Upsert into a custom target collection + /// let targetSnapshot = try await db.pipeline() + /// .collection("books") + /// .upsert( + /// Expression.constant("Cataloged").as("status"), + /// collectionPath: "catalog", + /// documentIdExpression: Field("isbn") + /// ) + /// .execute() + /// ``` + /// + /// - Parameters: + /// - transforms: Variadic list of `Selectable` expressions representing updated field assignments. + /// - collectionPath: Optional target collection path. When `nil`, the upsert applies in-place to the source + /// collection; when provided, documents are upserted into the specified collection. + /// - documentIdExpression: Optional expression resolving to the document ID in the target collection. + /// - Returns: A new `Pipeline` object with the `upsert` stage appended. + public func upsert(_ transforms: Selectable..., collectionPath: String? = nil, documentIdExpression: Expression? = nil) -> Pipeline { + return upsert(transforms, collectionPath: collectionPath, documentIdExpression: documentIdExpression) + } + + /// Appends an `upsert` stage to the pipeline transforming fields using an array of expressions. + /// + /// The `upsert` stage inserts a document if it does not already exist, or updates its fields if it does. + /// + /// - Note: The `collectionPath` parameter is **optional**. When omitted (or `nil`), the operation performs an + /// in-place upsert directly within the pipeline's current source collection. When `collectionPath` is provided, + /// documents are upserted into the specified destination collection instead (e.g. copying/archiving or when + /// sourcing data from `literals(...)`). + /// + /// ```swift + /// // Example 1: In-place transactional upsert on document references + /// let inPlaceSnapshot = try await db.pipeline() + /// .documents([db.collection("books").document("new_upsert_doc_id")]) + /// .upsert([ + /// Expression.constant("Sci-Fi").as("genre"), + /// Expression.constant("New Book Title").as("title") + /// ]) + /// .execute(options: Pipeline.ExecuteOptions(atomic: true)) + /// + /// // Example 2: Target custom collection with custom document ID field + /// let targetSnapshot = try await db.pipeline() + /// .collection("books") + /// .where(Field("__name__").equal(Expression.constant("book1"))) + /// .addFields(Expression.constant("upserted_fixed_id").as("targetId")) + /// .upsert( + /// [ + /// Expression.constant("Upserted Genre").as("genre"), + /// Expression.constant("Upserted Title").as("title") + /// ], + /// collectionPath: "books_archive", + /// documentIdExpression: Field("targetId") + /// ) + /// .execute(options: Pipeline.ExecuteOptions(atomic: true)) + /// + /// // Example 3: Non-transactional bulk upsert from literals + /// let literalSnapshot = try await db.pipeline() + /// .literals([ + /// ["id": "user_1", "status": "Active"], + /// ["id": "user_2", "status": "Pending"] + /// ]) + /// .upsert( + /// [Field("status").as("accountStatus")], + /// collectionPath: "users", + /// documentIdExpression: Field("id") + /// ) + /// .execute() + /// ``` + /// + /// - Parameters: + /// - transforms: Array of `Selectable` expressions representing updated field assignments. + /// - collectionPath: Optional target collection path. When `nil`, the upsert applies in-place to the source + /// collection; when provided, documents are upserted into the specified collection. + /// - documentIdExpression: Optional expression resolving to the document ID in the target collection. + /// - Returns: A new `Pipeline` object with the `upsert` stage appended. + public func upsert(_ transforms: [Selectable], collectionPath: String? = nil, documentIdExpression: Expression? = nil) -> Pipeline { + return Pipeline(stages: stages + [UpsertStage(fields: transforms, collectionPath: collectionPath, documentIdExpression: documentIdExpression)], db: db) + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index b7b1347c3a2..d6521a38219 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -110,4 +110,76 @@ public struct PipelineSource: @unchecked Sendable { } return factory(stages, db) } + + /// Specifies an array of in-memory document dictionaries as the data source for the pipeline. + /// + /// Using literal documents as a data source allows constructing pipelines without fetching existing data + /// from Firestore. This is useful for bulk inserting or upserting predefined records, testing pipelines with + /// synthetic datasets, or applying transformations to static data. + /// + /// Dictionary values can include primitive types (such as `String`, `Int`, `Double`, `Bool`), nested collections, + /// or Firestore `Expression` objects. + /// + /// ```swift + /// // Example 1: Transform and project literal records + /// let snapshot = try await db.pipeline() + /// .literals([ + /// ["name": "Alice", "age": 30, "role": "Admin"], + /// ["name": "Bob", "age": 25, "role": "User"] + /// ]) + /// .select([Field("name"), Field("role")]) + /// .execute() + /// + /// // Example 2: Bulk insert literal documents into a collection + /// let insertSnapshot = try await db.pipeline() + /// .literals([ + /// ["id": "user_1", "name": "Charlie", "email": "charlie@example.com"], + /// ["id": "user_2", "name": "Dana", "email": "dana@example.com"] + /// ]) + /// .insert(collectionPath: "users", documentIdExpression: Field("id")) + /// .execute() + /// ``` + /// + /// - Parameter data: An array of dictionaries representing document literals. + /// - Returns: A `Pipeline` with the specified literal documents as its source. + public func literals(_ data: [[String: Any]]) -> Pipeline { + return factory([LiteralsSourceStage(data: data, db: db)], db) + } + + /// Specifies in-memory document dictionaries as variadic arguments for the pipeline data source. + /// + /// Using literal documents as a data source allows constructing pipelines without fetching existing data + /// from Firestore. This is useful for bulk inserting or upserting predefined records, testing pipelines with + /// synthetic datasets, or applying transformations to static data. + /// + /// Dictionary values can include primitive types (such as `String`, `Int`, `Double`, `Bool`), nested collections, + /// or Firestore `Expression` objects. + /// + /// ```swift + /// // Example 1: Execute literals containing expressions + /// let snapshot = try await db.pipeline() + /// .literals( + /// ["id": "doc_1", "baseValue": 10, "doubled": Expression.constant(20)] + /// ) + /// .execute() + /// + /// // Example 2: Bulk upsert variadic literal records into a collection + /// let upsertSnapshot = try await db.pipeline() + /// .literals( + /// ["id": "user_1", "status": "Active"], + /// ["id": "user_2", "status": "Pending"] + /// ) + /// .upsert( + /// [Field("status").as("accountStatus")], + /// collectionPath: "users", + /// documentIdExpression: Field("id") + /// ) + /// .execute() + /// ``` + /// + /// - Parameter data: Variadic dictionary arguments representing document literals. + /// - Returns: A `Pipeline` with the specified literal documents as its source. + public func literals(_ data: [String: Any]...) -> Pipeline { + return literals(data) + } } diff --git a/Firestore/Swift/Tests/Integration/PipelineDmlTests.swift b/Firestore/Swift/Tests/Integration/PipelineDmlTests.swift new file mode 100644 index 00000000000..7e718f3002c --- /dev/null +++ b/Firestore/Swift/Tests/Integration/PipelineDmlTests.swift @@ -0,0 +1,199 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseCore +@testable import FirebaseFirestore +import Foundation +import XCTest + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class PipelineDmlTests: FSTIntegrationTestCase { + + // Test 1: Delete Stage + func testDeleteStage() async throws { + let testDocs: [String: [String: Sendable]] = ["book1": ["title": "ToDelete"]] + let collRef = collectionRef(withDocuments: testDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("__name__").equal(Expression.constant("book1"))) + .delete() + + let snapshot = try await pipeline.execute(options: Pipeline.ExecuteOptions().withAtomic(true)) + XCTAssertNotNil(snapshot) + } + + // Test 2: Update Stage + func testUpdateStageWithTransforms() async throws { + let testDocs: [String: [String: Sendable]] = ["book1": ["title": "OldTitle", "rating": 4.0]] + let collRef = collectionRef(withDocuments: testDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("__name__").equal(Expression.constant("book1"))) + .update(Expression.constant("NewTitle").as("title")) + + let snapshot = try await pipeline.execute(options: Pipeline.ExecuteOptions().withAtomic(true)) + XCTAssertNotNil(snapshot) + } + + // Test 3: Insert stage using a field value as document ID + func testInsertWithFieldAsDocumentId() async throws { + let testDocs: [String: [String: Sendable]] = ["book1": ["title": "SciFi", "targetId": "custom_doc_1"]] + let sourceRef = collectionRef(withDocuments: testDocs) + let db = sourceRef.firestore + let targetRef = collectionRef() + + let pipeline = db.pipeline() + .collection(sourceRef.path) + .where(Field("__name__").equal(Expression.constant("book1"))) + .insert(collectionPath: targetRef.path, documentIdExpression: Field("targetId")) + + let snapshot = try await pipeline.execute(options: Pipeline.ExecuteOptions().withAtomic(true)) + XCTAssertNotNil(snapshot) + } + + // Test 4: Insert stage using an expression as document ID + func testInsertWithExpressionAsDocumentId() async throws { + let testDocs: [String: [String: Sendable]] = ["book1": ["title": "SciFi"]] + let sourceRef = collectionRef(withDocuments: testDocs) + let db = sourceRef.firestore + let targetRef = collectionRef() + + let pipeline = db.pipeline() + .collection(sourceRef.path) + .where(Field("__name__").equal(Expression.constant("book1"))) + .insert(collectionPath: targetRef.path, documentIdExpression: Expression.constant("custom_fixed_id")) + + let snapshot = try await pipeline.execute(options: Pipeline.ExecuteOptions().withAtomic(true)) + XCTAssertNotNil(snapshot) + } + + // Test 5: Upsert (insert) a new document if it does not exist + func testUpsertInsertsNewDocument() async throws { + let collRef = collectionRef() + let db = collRef.firestore + + let pipeline = db.pipeline() + .documents([collRef.document("new_upsert_doc")]) + .upsert([ + Expression.constant("New Upserted Title").as("title"), + Expression.constant("Sci-Fi").as("genre") + ]) + + let snapshot = try await pipeline.execute(options: Pipeline.ExecuteOptions().withAtomic(true)) + XCTAssertNotNil(snapshot) + } + + // Test 6: Upsert (update) an existing document by modifying fields + func testUpsertUpdatesExistingDocument() async throws { + let testDocs: [String: [String: Sendable]] = ["book1": ["title": "Old", "rating": 3.0]] + let collRef = collectionRef(withDocuments: testDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("__name__").equal(Expression.constant("book1"))) + .upsert([ + Expression.constant("Updated by Upsert").as("title"), + Expression.constant(4.5).as("rating") + ]) + + let snapshot = try await pipeline.execute(options: Pipeline.ExecuteOptions().withAtomic(true)) + XCTAssertNotNil(snapshot) + } + + // Test 7: Upsert with custom target collection and document ID field + func testUpsertWithCustomCollectionAndDocumentId() async throws { + let testDocs: [String: [String: Sendable]] = ["book1": ["title": "Base", "customId": "target_id_10"]] + let sourceRef = collectionRef(withDocuments: testDocs) + let db = sourceRef.firestore + let targetRef = collectionRef() + + let pipeline = db.pipeline() + .collection(sourceRef.path) + .where(Field("__name__").equal(Expression.constant("book1"))) + .upsert( + [Expression.constant("Target Title").as("title")], + collectionPath: targetRef.path, + documentIdExpression: Field("customId") + ) + + let snapshot = try await pipeline.execute(options: Pipeline.ExecuteOptions().withAtomic(true)) + XCTAssertNotNil(snapshot) + } + + // Test 8: Execute pipeline with literals stage source + func testLiteralsSourceBasicExecution() async throws { + let db = collectionRef().firestore + let pipeline = db.pipeline() + .literals([ + ["name": "Alice", "age": 30], + ["name": "Bob", "age": 25] + ]) + + let snapshot = try await pipeline.execute() + XCTAssertNotNil(snapshot) + } + + // Test 9: Execute literals stage containing expression transforms + func testLiteralsSourceWithExpressions() async throws { + let db = collectionRef().firestore + let pipeline = db.pipeline() + .literals([ + ["base": 10, "doubled": Expression.constant(20)] + ]) + + let snapshot = try await pipeline.execute() + XCTAssertNotNil(snapshot) + } + + // Test 10: Non-transactional insert from literals source + func testNonTransactionalInsertFromLiterals() async throws { + let collRef = collectionRef() + let db = collRef.firestore + + let pipeline = db.pipeline() + .literals([ + ["title": "Literal Inserted", "year": 2026] + ]) + .insert(collectionPath: collRef.path, documentIdExpression: Expression.constant("lit_doc_1")) + + let snapshot = try await pipeline.execute() + XCTAssertNotNil(snapshot) + } + + // Test 11: Non-transactional upsert from literals source + func testNonTransactionalUpsertFromLiterals() async throws { + let collRef = collectionRef() + let db = collRef.firestore + + let pipeline = db.pipeline() + .literals([ + ["id": "doc1", "title": "Literal Upserted"] + ]) + .upsert( + [Expression.constant("Literal Upserted Modified").as("title")], + collectionPath: collRef.path, + documentIdExpression: Expression.constant("doc1") + ) + + let snapshot = try await pipeline.execute() + XCTAssertNotNil(snapshot) + } +} diff --git a/Firestore/core/src/api/pipeline.cc b/Firestore/core/src/api/pipeline.cc index a74fa607074..ad9b9d4044d 100644 --- a/Firestore/core/src/api/pipeline.cc +++ b/Firestore/core/src/api/pipeline.cc @@ -31,7 +31,7 @@ Pipeline Pipeline::AddingStage(std::shared_ptr stage) { auto copy = std::vector>(this->stages_); copy.push_back(stage); - return {copy, this->firestore_}; + return {copy, this->firestore_, this->atomic_}; } const std::vector>& Pipeline::stages() const { diff --git a/Firestore/core/src/api/pipeline.h b/Firestore/core/src/api/pipeline.h index db127911950..3298c80c33a 100644 --- a/Firestore/core/src/api/pipeline.h +++ b/Firestore/core/src/api/pipeline.h @@ -33,14 +33,25 @@ namespace api { class Pipeline { public: Pipeline(std::vector> stages, - std::shared_ptr firestore) - : stages_(std::move(stages)), firestore_(firestore) { + std::shared_ptr firestore, + bool atomic = false) + : stages_(std::move(stages)), + firestore_(firestore), + atomic_(atomic) { } const std::shared_ptr& firestore() const { return firestore_; } + bool atomic() const { + return atomic_; + } + + void set_atomic(bool atomic) { + atomic_ = atomic; + } + Pipeline AddingStage(std::shared_ptr stage); const std::vector>& stages() const; @@ -52,6 +63,7 @@ class Pipeline { private: std::vector> stages_; std::shared_ptr firestore_; + bool atomic_ = false; }; google_firestore_v1_Value PipelineStagesToProto( diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index fc4e98b73ea..d13cde1e942 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -710,6 +710,146 @@ model::PipelineInputOutputVector SortStage::Evaluate( return input_copy; } +google_firestore_v1_Pipeline_Stage DeleteStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray(name()); + result.args_count = 0; + result.args = nullptr; + result.options_count = 0; + result.options = nullptr; + return result; +} + +UpdateStage::UpdateStage( + std::unordered_map> fields) + : fields_(std::move(fields)) { +} + +google_firestore_v1_Pipeline_Stage UpdateStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray(name()); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; + nanopb::SetRepeatedField( + &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, + fields_, [](const std::pair>& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; + }); + + result.options_count = 0; + result.options = nullptr; + return result; +} + +InsertStage::InsertStage(std::string collection_path, + std::shared_ptr document_id_expr) + : collection_path_(std::move(collection_path)), + document_id_expr_(std::move(document_id_expr)) { +} + +google_firestore_v1_Pipeline_Stage InsertStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray(name()); + result.args_count = 0; + result.args = nullptr; + + std::vector> opts; + if (!collection_path_.empty()) { + std::string ref = collection_path_[0] == '/' ? collection_path_ + : "/" + collection_path_; + google_firestore_v1_Value val; + val.which_value_type = google_firestore_v1_Value_reference_value_tag; + val.reference_value = nanopb::MakeBytesArray(ref); + opts.emplace_back("collection", val); + } + if (document_id_expr_) { + opts.emplace_back("document_id", document_id_expr_->to_proto()); + } + + result.options_count = static_cast(opts.size()); + nanopb::SetRepeatedField( + &result.options, &result.options_count, opts, + [](const std::pair& entry) { + return _google_firestore_v1_Pipeline_Stage_OptionsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second}; + }); + return result; +} + +UpsertStage::UpsertStage( + std::unordered_map> fields, + std::string collection_path, + std::shared_ptr document_id_expr) + : fields_(std::move(fields)), + collection_path_(std::move(collection_path)), + document_id_expr_(std::move(document_id_expr)) { +} + +google_firestore_v1_Pipeline_Stage UpsertStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray(name()); + + if (!fields_.empty()) { + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; + nanopb::SetRepeatedField( + &result.args[0].map_value.fields, + &result.args[0].map_value.fields_count, fields_, + [](const std::pair>& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; + }); + } else { + result.args_count = 0; + result.args = nullptr; + } + + std::vector> opts; + if (!collection_path_.empty()) { + std::string ref = collection_path_[0] == '/' ? collection_path_ + : "/" + collection_path_; + google_firestore_v1_Value val; + val.which_value_type = google_firestore_v1_Value_reference_value_tag; + val.reference_value = nanopb::MakeBytesArray(ref); + opts.emplace_back("collection", val); + } + if (document_id_expr_) { + opts.emplace_back("document_id", document_id_expr_->to_proto()); + } + + result.options_count = static_cast(opts.size()); + nanopb::SetRepeatedField( + &result.options, &result.options_count, opts, + [](const std::pair& entry) { + return _google_firestore_v1_Pipeline_Stage_OptionsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second}; + }); + return result; +} + +LiteralsSource::LiteralsSource(std::vector data) + : data_(std::move(data)) { +} + +google_firestore_v1_Pipeline_Stage LiteralsSource::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray(name()); + + result.args_count = static_cast(data_.size()); + result.args = nanopb::MakeArray(result.args_count); + for (size_t i = 0; i < result.args_count; ++i) { + result.args[i] = data_[i]; + } + + result.options_count = 0; + result.options = nullptr; + return result; +} + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index b794942032c..13e44e8fba0 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -597,6 +597,91 @@ class RawStage : public Stage { std::unordered_map> options_; }; +class DeleteStage : public Stage { + public: + DeleteStage() = default; + ~DeleteStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + const std::string& name() const override { + static const std::string kName = "delete"; + return kName; + } +}; + +class UpdateStage : public Stage { + public: + explicit UpdateStage( + std::unordered_map> fields); + ~UpdateStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + const std::string& name() const override { + static const std::string kName = "update"; + return kName; + } + + private: + std::unordered_map> fields_; +}; + +class InsertStage : public Stage { + public: + InsertStage(std::string collection_path, + std::shared_ptr document_id_expr); + ~InsertStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + const std::string& name() const override { + static const std::string kName = "insert"; + return kName; + } + + private: + std::string collection_path_; + std::shared_ptr document_id_expr_; +}; + +class UpsertStage : public Stage { + public: + UpsertStage( + std::unordered_map> fields, + std::string collection_path, + std::shared_ptr document_id_expr); + ~UpsertStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + const std::string& name() const override { + static const std::string kName = "upsert"; + return kName; + } + + private: + std::unordered_map> fields_; + std::string collection_path_; + std::shared_ptr document_id_expr_; +}; + +class LiteralsSource : public Stage { + public: + explicit LiteralsSource(std::vector data); + ~LiteralsSource() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + const std::string& name() const override { + static const std::string kName = "literals"; + return kName; + } + + private: + std::vector data_; +}; + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/remote/remote_objc_bridge.cc b/Firestore/core/src/remote/remote_objc_bridge.cc index 27faaa171d4..93912da8c54 100644 --- a/Firestore/core/src/remote/remote_objc_bridge.cc +++ b/Firestore/core/src/remote/remote_objc_bridge.cc @@ -401,6 +401,14 @@ DatastoreSerializer::EncodeExecutePipelineRequest( result->pipeline_type.structured_pipeline = serializer_.EncodePipeline(pipeline); + if (pipeline.atomic()) { + result->which_consistency_selector = + google_firestore_v1_ExecutePipelineRequest_new_transaction_tag; + result->consistency_selector.new_transaction.which_mode = + google_firestore_v1_TransactionOptions_read_write_tag; + result->auto_commit_transaction = true; + } + return result; } diff --git a/Firestore/core/src/util/ordered_code.h b/Firestore/core/src/util/ordered_code.h index aef6f316dbe..15411a616a8 100644 --- a/Firestore/core/src/util/ordered_code.h +++ b/Firestore/core/src/util/ordered_code.h @@ -39,6 +39,8 @@ #ifndef FIRESTORE_CORE_SRC_UTIL_ORDERED_CODE_H_ #define FIRESTORE_CORE_SRC_UTIL_ORDERED_CODE_H_ +#include +#include #include #include "absl/strings/string_view.h" diff --git a/Firestore/core/test/unit/remote/serializer_test.cc b/Firestore/core/test/unit/remote/serializer_test.cc index cc7074e6f5a..b8a3fc13072 100644 --- a/Firestore/core/test/unit/remote/serializer_test.cc +++ b/Firestore/core/test/unit/remote/serializer_test.cc @@ -40,7 +40,10 @@ #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/include/firebase/firestore/geo_point.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/api/pipeline.h" +#include "Firestore/core/src/api/stages.h" #include "Firestore/core/src/core/bound.h" +#include "Firestore/core/src/core/database_info.h" #include "Firestore/core/src/core/field_filter.h" #include "Firestore/core/src/core/filter.h" #include "Firestore/core/src/core/query.h" @@ -56,6 +59,7 @@ #include "Firestore/core/src/nanopb/message.h" #include "Firestore/core/src/nanopb/reader.h" #include "Firestore/core/src/nanopb/writer.h" +#include "Firestore/core/src/remote/remote_objc_bridge.h" #include "Firestore/core/src/timestamp_internal.h" #include "Firestore/core/src/util/status.h" #include "Firestore/core/test/unit/nanopb/nanopb_testing.h" @@ -75,6 +79,7 @@ namespace { namespace v1 = google::firestore::v1; using core::Bound; +using core::DatabaseInfo; using google::protobuf::Int32Value; using google::protobuf::util::MessageDifferencer; using local::QueryPurpose; @@ -2174,6 +2179,89 @@ TEST_F(SerializerTest, EncodesKeyFieldFilter) { ExpectRoundTrip(model, proto); } +TEST_F(SerializerTest, EncodesExecutePipelineRequestWithAtomic) { + DatabaseInfo db_info(DatabaseId(kProjectId, kDatabaseId), "key", "host", false); + DatastoreSerializer datastore_serializer(db_info); + + auto stage = std::make_shared("rooms"); + api::Pipeline pipeline({stage}, nullptr, /*atomic=*/true); + + auto request = datastore_serializer.EncodeExecutePipelineRequest(pipeline); + + EXPECT_EQ(request->which_consistency_selector, + google_firestore_v1_ExecutePipelineRequest_new_transaction_tag); + EXPECT_EQ(request->consistency_selector.new_transaction.which_mode, + google_firestore_v1_TransactionOptions_read_write_tag); + EXPECT_TRUE(request->auto_commit_transaction); + + ByteString bytes = nanopb::MakeByteString(request); + auto proto = ProtobufParse(bytes); + EXPECT_TRUE(proto.has_new_transaction()); + EXPECT_TRUE(proto.new_transaction().has_read_write()); + EXPECT_TRUE(proto.auto_commit_transaction()); +} + +TEST_F(SerializerTest, EncodesExecutePipelineRequestWithoutAtomic) { + DatabaseInfo db_info(DatabaseId(kProjectId, kDatabaseId), "key", "host", false); + DatastoreSerializer datastore_serializer(db_info); + + auto stage = std::make_shared("rooms"); + api::Pipeline pipeline({stage}, nullptr, /*atomic=*/false); + + auto request = datastore_serializer.EncodeExecutePipelineRequest(pipeline); + + EXPECT_FALSE(request->auto_commit_transaction); + EXPECT_NE(request->which_consistency_selector, + google_firestore_v1_ExecutePipelineRequest_new_transaction_tag); + + ByteString bytes = nanopb::MakeByteString(request); + auto proto = ProtobufParse(bytes); + EXPECT_FALSE(proto.has_new_transaction()); + EXPECT_FALSE(proto.auto_commit_transaction()); +} + +TEST_F(SerializerTest, EncodesExecutePipelineRequestDefaultAtomic) { + DatabaseInfo db_info(DatabaseId(kProjectId, kDatabaseId), "key", "host", false); + DatastoreSerializer datastore_serializer(db_info); + + auto stage = std::make_shared("rooms"); + api::Pipeline pipeline({stage}, nullptr); + + auto request = datastore_serializer.EncodeExecutePipelineRequest(pipeline); + + EXPECT_FALSE(request->auto_commit_transaction); + EXPECT_NE(request->which_consistency_selector, + google_firestore_v1_ExecutePipelineRequest_new_transaction_tag); + + ByteString bytes = nanopb::MakeByteString(request); + auto proto = ProtobufParse(bytes); + EXPECT_FALSE(proto.has_new_transaction()); + EXPECT_FALSE(proto.auto_commit_transaction()); +} + +TEST_F(SerializerTest, EncodesExecutePipelineRequestSetAtomic) { + DatabaseInfo db_info(DatabaseId(kProjectId, kDatabaseId), "key", "host", false); + DatastoreSerializer datastore_serializer(db_info); + + auto stage = std::make_shared("rooms"); + api::Pipeline pipeline({stage}, nullptr); + pipeline.set_atomic(true); + + auto request = datastore_serializer.EncodeExecutePipelineRequest(pipeline); + + EXPECT_EQ(request->which_consistency_selector, + google_firestore_v1_ExecutePipelineRequest_new_transaction_tag); + EXPECT_EQ(request->consistency_selector.new_transaction.which_mode, + google_firestore_v1_TransactionOptions_read_write_tag); + EXPECT_TRUE(request->auto_commit_transaction); + + ByteString bytes = nanopb::MakeByteString(request); + auto proto = ProtobufParse(bytes); + EXPECT_TRUE(proto.has_new_transaction()); + EXPECT_TRUE(proto.new_transaction().has_read_write()); + EXPECT_TRUE(proto.auto_commit_transaction()); +} + // TODO(rsgowman): Test [en|de]coding multiple protos into the same output // vector.