diff --git a/.github/workflows/firestore-nightly.yml b/.github/workflows/firestore-nightly.yml index 36beeadb4ac..cd2afee4f43 100644 --- a/.github/workflows/firestore-nightly.yml +++ b/.github/workflows/firestore-nightly.yml @@ -15,6 +15,8 @@ name: firestore_nightly on: + pull_request: + branches: [ "cheryllin/pplapi", "cheryllin/ppl" ] workflow_dispatch: concurrency: @@ -49,6 +51,7 @@ jobs: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} MINT_PATH: ${{ github.workspace }}/mint TARGET_DATABASE_ID: ${{ matrix.databaseId }} + USE_LATEST_CMAKE: false runs-on: ${{ matrix.os }} steps: @@ -77,6 +80,11 @@ jobs: run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/firestore-nightly.plist.gpg \ Firestore/Example/App/GoogleService-Info.plist "$plist_secret" + - name: Setup cmake + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.31.1' + # Skipping terraform index creation because we are not allowed to download SA key json. - name: Setup build diff --git a/.gitignore b/.gitignore index dc909fe4338..74607c5993b 100644 --- a/.gitignore +++ b/.gitignore @@ -155,6 +155,7 @@ FirebaseAppCheck/Apps/AppCheckCustomProvideApp/AppCheckCustomProvideApp/GoogleSe /Example/FirestoreSample/ui-debug.log /Example/FirestoreSample/firestore-debug.log /Example/FirestoreSample/firebase-debug.log +Firestore/Example/GoogleService-Info.plist # generated Terraform docs .terraform/* diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 1146b8a5dde..abba41a6f55 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -104,7 +104,6 @@ 0E17927CE45F5E3FC6691E24 /* firebase_auth_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = F869D85E900E5AF6CD02E2FC /* firebase_auth_credentials_provider_test.mm */; }; 0E4C94369FFF7EC0C9229752 /* iterator_adaptors_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */; }; 0E4F266A9FDF55CD38BB6D0F /* leveldb_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = DB1F1E1B1ED15E8D042144B1 /* leveldb_query_engine_test.cc */; }; - 0E7A39BD9C87CC33F91A672F /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 682582E5728F3F1C531990EA /* explain_stats.pb.cc */; }; 0EA40EDACC28F445F9A3F32F /* pretty_printing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB323F9553050F4F6490F9FF /* pretty_printing_test.cc */; }; 0EC3921AE220410F7394729B /* aggregation_result.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D872D754B8AD88E28AF28B28 /* aggregation_result.pb.cc */; }; 0EDFC8A6593477E1D17CDD8F /* leveldb_bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8E9CD82E60893DDD7757B798 /* leveldb_bundle_cache_test.cc */; }; @@ -127,9 +126,6 @@ 11F8EE69182C9699E90A9E3D /* database_info_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D92E20235D22000A432D /* database_info_test.cc */; }; 12158DFCEE09D24B7988A340 /* maybe_document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */; }; 121F0FB9DCCBFB7573C7AF48 /* bundle_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B5C2A94EE24E60543F62CC35 /* bundle_serializer_test.cc */; }; - 12260A2A2D56A3CE001766EB /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12260A292D56A3CE001766EB /* PipelineTests.swift */; }; - 12260A2B2D56A3CE001766EB /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12260A292D56A3CE001766EB /* PipelineTests.swift */; }; - 12260A2C2D56A3CE001766EB /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12260A292D56A3CE001766EB /* PipelineTests.swift */; }; 124AAEE987451820F24EEA8E /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CCC9BD953F121B9E29F9AA42 /* user_test.cc */; }; 125B1048ECB755C2106802EB /* executor_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4687208F9B9100554BA2 /* executor_std_test.cc */; }; 1290FA77A922B76503AE407C /* lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */; }; @@ -233,7 +229,6 @@ 1F4930A8366F74288121F627 /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = CF39535F2C41AB0006FA6C0E /* create_noop_connectivity_monitor.cc */; }; 1F56F51EB6DF0951B1F4F85B /* lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */; }; 1F998DDECB54A66222CC66AA /* string_format_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54131E9620ADE678001DF3FF /* string_format_test.cc */; }; - 1F9FFAE375C88EFF88CBB6F8 /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */; }; 1FE23E911F0761AA896FAD67 /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D8E530B27D5641B9C26A452C /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json */; }; 2045517602D767BD01EA71D9 /* overlay_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E1459FA70B8FC18DE4B80D0D /* overlay_test.cc */; }; 205601D1C6A40A4DD3BBAA04 /* target_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 526D755F65AC676234F57125 /* target_test.cc */; }; @@ -300,7 +295,6 @@ 2A86AB04B38DBB770A1D8B13 /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3369AC938F82A70685C5ED58 /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json */; }; 2AAEABFD550255271E3BAC91 /* to_string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68B1E002213A764008977EF /* to_string_apple_test.mm */; }; 2ABA80088D70E7A58F95F7D8 /* delayed_constructor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */; }; - 2AD2CB51469AE35331C39258 /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */; }; 2AD8EE91928AE68DF268BEDA /* limbo_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */; }; 2AD98CD29CC6F820A74CDD5E /* Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B59C0A7B2A4548496ED4E7D /* Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json */; }; 2AE3914BBC4EDF91BD852939 /* memory_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6A33BC2D84233C355F1D0 /* memory_query_engine_test.cc */; }; @@ -328,9 +322,11 @@ 2EC1C4D202A01A632339A161 /* field_transform_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7515B47C92ABEEC66864B55C /* field_transform_test.cc */; }; 2F3740131CC8F8230351B91D /* byte_stream_cpp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 01D10113ECC5B446DB35E96D /* byte_stream_cpp_test.cc */; }; 2F69187F601E00054469F4A5 /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */; }; + 2F72DBE2EC6E24A81C69DEF0 /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */; }; 2F8FDF35BBB549A6F4D2118E /* FSTMemorySpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */; }; 2FA0BAE32D587DF2EA5EEB97 /* async_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB467B208E9A8200554BA2 /* async_queue_test.cc */; }; 2FAE0BCBE559ED7214AEFEB7 /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 0D964D4936953635AC7E0834 /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json */; }; + 2FC2B732841BF2C425EB35DF /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */; }; 3040FD156E1B7C92B0F2A70C /* ordered_code_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */; }; 3056418E81BC7584FBE8AD6C /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CCC9BD953F121B9E29F9AA42 /* user_test.cc */; }; 306E762DC6B829CED4FD995D /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; }; @@ -348,7 +344,6 @@ 32F022CB75AEE48CDDAF2982 /* mutation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C8522DE226C467C54E6788D8 /* mutation_test.cc */; }; 32F8B4652010E8224E353041 /* persistence_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */; }; 330DE2A5AE6AF8D66C9C849F /* Validation_BloomFilterTest_MD5_5000_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8582DFD74E8060C7072104B /* Validation_BloomFilterTest_MD5_5000_0001_membership_test_result.json */; }; - 332E7D2D8489E6DA42947C59 /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */; }; 336E415DD06E719F9C9E2A14 /* grpc_stream_tester.cc in Sources */ = {isa = PBXBuildFile; fileRef = 87553338E42B8ECA05BA987E /* grpc_stream_tester.cc */; }; 338DFD5BCD142DF6C82A0D56 /* cc_compilation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1B342370EAE3AA02393E33EB /* cc_compilation_test.cc */; }; 339CFFD1323BDCA61EAAFE31 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; }; @@ -388,6 +383,7 @@ 39790AC7E71BC06D48144BED /* memory_globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */; }; 3987A3E8534BAA496D966735 /* memory_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = DB5A1E760451189DA36028B3 /* memory_index_manager_test.cc */; }; 39CDC9EC5FD2E891D6D49151 /* secure_random_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A531FC913E500713A1A /* secure_random_test.cc */; }; + 3A110ECBF96B6E44BA77011A /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */; }; 3A307F319553A977258BB3D6 /* view_snapshot_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CC572A9168BBEF7B83E4BBC5 /* view_snapshot_test.cc */; }; 3A7CB01751697ED599F2D9A1 /* executor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4688208F9B9100554BA2 /* executor_test.cc */; }; 3A93D8FB318C6491A6B654F5 /* Validation_BloomFilterTest_MD5_50000_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B44DD11682C4803B73DCC34 /* Validation_BloomFilterTest_MD5_50000_01_bloom_filter_proto.json */; }; @@ -399,7 +395,6 @@ 3B256CCF6AEEE12E22F16BB8 /* hashing_test_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = B69CF3F02227386500B281C8 /* hashing_test_apple.mm */; }; 3B37BD3C13A66625EC82CF77 /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; }; 3B47CC43DBA24434E215B8ED /* memory_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = DB5A1E760451189DA36028B3 /* memory_index_manager_test.cc */; }; - 3B4CFB45208A7EEF1EA58ADC /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */; }; 3B5CEA04AC1627256A1AE8BA /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; 3BA4EEA6153B3833F86B8104 /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; }; @@ -409,6 +404,7 @@ 3CCABD7BB5ED39DF1140B5F0 /* leveldb_globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FC44D934D4A52C790659C8D6 /* leveldb_globals_cache_test.cc */; }; 3CFFA6F016231446367E3A69 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; }; 3D22F56C0DE7C7256C75DC06 /* tree_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4D20A36DBB00BCEB75 /* tree_sorted_map_test.cc */; }; + 3D5F7AA7BB68529F47BE4B12 /* PipelineApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BF06E5A4988F9F949DD871 /* PipelineApiTests.swift */; }; 3D6AC48D6197E6539BBBD28F /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; 3D9619906F09108E34FF0C95 /* FSTSmokeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */; }; 3DBB48F077C97200F32B51A0 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 40F9D09063A07F710811A84F /* value_util_test.cc */; }; @@ -523,7 +519,6 @@ 50454F81EC4584D4EB5F5ED5 /* serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 61F72C5520BC48FD001A68CB /* serializer_test.cc */; }; 50B749CA98365368AE34B71C /* filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F02F734F272C3C70D1307076 /* filter_test.cc */; }; 50C852E08626CFA7DC889EEA /* field_index_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BF76A8DA34B5B67B4DD74666 /* field_index_test.cc */; }; - 50EA1F41D766C92894E9B078 /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 682582E5728F3F1C531990EA /* explain_stats.pb.cc */; }; 51018EA27CF914DD1CC79CB3 /* thread_safe_memoizer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */; }; 513D34C9964E8C60C5C2EE1C /* leveldb_bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8E9CD82E60893DDD7757B798 /* leveldb_bundle_cache_test.cc */; }; 5150E9F256E6E82D6F3CB3F1 /* bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F7FC06E0A47D393DE1759AE1 /* bundle_cache_test.cc */; }; @@ -693,6 +688,7 @@ 5BE49546D57C43DDFCDB6FBD /* to_string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68B1E002213A764008977EF /* to_string_apple_test.mm */; }; 5C9B5696644675636A052018 /* token_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A082AFDD981B07B5AD78FDE8 /* token_test.cc */; }; 5CADE71A1CA6358E1599F0F9 /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; + 5CDD24225992674A4D3E3D4E /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */; }; 5CEB0E83DA68652927D2CF07 /* memory_document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */; }; 5D405BE298CE4692CB00790A /* Pods_Firestore_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */; }; 5D45CC300ED037358EF33A8F /* snapshot_version_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */; }; @@ -741,6 +737,7 @@ 604B75044D6BEC2B7515EA1B /* index_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 8C7278B604B8799F074F4E8C /* index_spec_test.json */; }; 60985657831B8DDE2C65AC8B /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; }; 60C72F86D2231B1B6592A5E6 /* filesystem_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F51859B394D01C0C507282F1 /* filesystem_test.cc */; }; + 60DA778E447F9ACD402FDA2F /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */; }; 6105A1365831B79A7DEEA4F3 /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; }; 6141D3FDF5728FCE9CC1DBFA /* bundle_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 79EAA9F7B1B9592B5F053923 /* bundle_spec_test.json */; }; 6156C6A837D78D49ED8B8812 /* index_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 8C7278B604B8799F074F4E8C /* index_spec_test.json */; }; @@ -778,6 +775,7 @@ 64D8241E9F56973DAD3077BC /* Validation_BloomFilterTest_MD5_1_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 5C68EE4CB94C0DD6E333F546 /* Validation_BloomFilterTest_MD5_1_01_membership_test_result.json */; }; 650B31A5EC6F8D2AEA79C350 /* index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AE4A9E38D65688EE000EE2A1 /* index_manager_test.cc */; }; 65537B22A73E3909666FB5BC /* remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7EB299CF85034F09CFD6F3FD /* remote_document_cache_test.cc */; }; + 655F8647F57E5F2155DFF7B5 /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 861684E49DAC993D153E60D0 /* PipelineTests.swift */; }; 658CBF4A717EA160E27C973E /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = A5D9044B72061CAF284BC9E4 /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json */; }; 659FFE071CD0F60DAEADD50B /* bloom_filter.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7C0DCD2790019E66D8CC /* bloom_filter.pb.cc */; }; 65D54B964A2021E5A36AB21F /* bundle_loader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A853C81A6A5A51C9D0389EDA /* bundle_loader_test.cc */; }; @@ -792,7 +790,6 @@ 66DFEA9E324797E6EA81CBA9 /* perf_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */; }; 66FAB8EAC012A3822BD4D0C9 /* leveldb_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */; }; 6711E75A10EBA662341F5C9D /* leveldb_document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AE89CFF09C6804573841397F /* leveldb_document_overlay_cache_test.cc */; }; - 676933F59F2F0A0D221A4F8F /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 682582E5728F3F1C531990EA /* explain_stats.pb.cc */; }; 677C833244550767B71DB1BA /* log_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54C2294E1FECABAE007D065B /* log_test.cc */; }; 67B8C34BDF0FFD7532D7BE4F /* Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 478DC75A0DCA6249A616DD30 /* Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json */; }; 67BC2B77C1CC47388E79D774 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; }; @@ -850,7 +847,6 @@ 6FF2B680CC8631B06C7BD7AB /* FSTMemorySpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */; }; 70A171FC43BE328767D1B243 /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; }; 70AB665EB6A473FF6C4CFD31 /* CodableTimestampTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B65C996438B84DBC7616640 /* CodableTimestampTests.swift */; }; - 715A0E92C83AE4384A13B882 /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */; }; 716289F99B5316B3CC5E5CE9 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; }; 71702588BFBF5D3A670508E7 /* ordered_code_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */; }; 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; @@ -872,7 +868,6 @@ 73E42D984FB36173A2BDA57C /* FSTEventAccumulator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */; }; 73FE5066020EF9B2892C86BF /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; }; 743DF2DF38CE289F13F44043 /* status_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA33F964042646FDDAF9F9 /* status_testing.cc */; }; - 7492C447277CDC8CB7A165CB /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 682582E5728F3F1C531990EA /* explain_stats.pb.cc */; }; 7495E3BAE536CD839EE20F31 /* FSTLevelDBSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */; }; 74985DE2C7EF4150D7A455FD /* statusor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352D20A3B3D7003E0143 /* statusor_test.cc */; }; 74A63A931F834D1D6CF3BA9A /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3369AC938F82A70685C5ED58 /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json */; }; @@ -909,6 +904,7 @@ 7AD020FC27493FF8E659436C /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; }; 7B0EA399F899537ACCC84E53 /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; }; 7B0F073BDB6D0D6E542E23D4 /* query.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D621C2DDC800EFB9CC /* query.pb.cc */; }; + 7B58861D0978827BC4CB1DFA /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */; }; 7B74447D211586D9D1CC82BB /* datastore_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3167BD972EFF8EC636530E59 /* datastore_test.cc */; }; 7B8320F12E8092BC86FFCC2C /* fields_array_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA4CBA48204C9E25B56993BC /* fields_array_test.cc */; }; 7B86B1B21FD0EF2A67547F66 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; }; @@ -919,6 +915,7 @@ 7C1DC1B44729381126D083AE /* leveldb_snappy_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D9D94300B9C02F7069523C00 /* leveldb_snappy_test.cc */; }; 7C5E017689012489AAB7718D /* CodableGeoPointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5495EB022040E90200EBA509 /* CodableGeoPointTests.swift */; }; 7C7BA1DB0B66EB899A928283 /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; + 7CAF0E8C47FB2DD486240D47 /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */; }; 7D25D41B013BB70ADE526055 /* target_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 526D755F65AC676234F57125 /* target_test.cc */; }; 7D320113FD076A1EF9A8B612 /* filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F02F734F272C3C70D1307076 /* filter_test.cc */; }; 7D3207DEE229EFCF16E52693 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BD051DBE754950FEAC7A446 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json */; }; @@ -965,6 +962,7 @@ 8405FF2BFBB233031A887398 /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; }; 8413BD9958F6DD52C466D70F /* sorted_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */; }; 84285C3F63D916A4786724A8 /* field_index_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BF76A8DA34B5B67B4DD74666 /* field_index_test.cc */; }; + 8429E18EFBAF473209731E01 /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */; }; 843EE932AA9A8F43721F189E /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; }; 8460C97C9209D7DAF07090BD /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; }; 84E75527F3739131C09BEAA5 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; }; @@ -984,7 +982,6 @@ 8683BBC3AC7B01937606A83B /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; 86B413EC49E3BBBEBF1FB7A0 /* Validation_BloomFilterTest_MD5_500_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AB49283E544497A9C5A0E59 /* Validation_BloomFilterTest_MD5_500_1_membership_test_result.json */; }; 86E6FC2B7657C35B342E1436 /* sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4E20A36DBB00BCEB75 /* sorted_map_test.cc */; }; - 86E73F6286E87834CF37D5D9 /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */; }; 8705C4856498F66E471A0997 /* FIRWriteBatchTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06F202154D600B64F25 /* FIRWriteBatchTests.mm */; }; 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; 8778C1711059598070F86D3C /* leveldb_globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FC44D934D4A52C790659C8D6 /* leveldb_globals_cache_test.cc */; }; @@ -1019,7 +1016,6 @@ 8F3AE423677A4C50F7E0E5C0 /* database_info_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D92E20235D22000A432D /* database_info_test.cc */; }; 8F4F40E9BC7ED588F67734D5 /* app_testing.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FB07203E6A44009C9584 /* app_testing.mm */; }; 8F781F527ED72DC6C123689E /* autoid_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A521FC913E500713A1A /* autoid_test.cc */; }; - 8FE63980976481EBA001B789 /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 682582E5728F3F1C531990EA /* explain_stats.pb.cc */; }; 9009C285F418EA80C46CF06B /* fake_target_metadata_provider.cc in Sources */ = {isa = PBXBuildFile; fileRef = 71140E5D09C6E76F7C71B2FC /* fake_target_metadata_provider.cc */; }; 900D0E9F18CE3DB954DD0D1E /* async_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB467B208E9A8200554BA2 /* async_queue_test.cc */; }; 9012B0E121B99B9C7E54160B /* query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B8A853940305237AFDA8050B /* query_engine_test.cc */; }; @@ -1038,7 +1034,6 @@ 920B6ABF76FDB3547F1CCD84 /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; 9236478E01DF2EC7DF58B1FC /* index_backfiller_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F50E872B3F117A674DA8E94 /* index_backfiller_test.cc */; }; 925BE64990449E93242A00A2 /* memory_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 74FBEFA4FE4B12C435011763 /* memory_mutation_queue_test.cc */; }; - 92B593DCD86543D8C90F64F9 /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */; }; 92D7081085679497DC112EDB /* persistence_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9113B6F513D0473AEABBAF1F /* persistence_testing.cc */; }; 92EFF0CC2993B43CBC7A61FF /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; }; 9382BE7190E7750EE7CCCE7C /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; }; @@ -1085,7 +1080,6 @@ 9C366448F9BA7A4AC0821AF7 /* bundle_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 79EAA9F7B1B9592B5F053923 /* bundle_spec_test.json */; }; 9C86EEDEA131BFD50255EEF1 /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; 9CC32ACF397022BB7DF11B52 /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D22D4C211AC32E4F8B4883DA /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json */; }; - 9CD1E9301EC44ED10DAEA5FB /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */; }; 9CE07BAAD3D3BC5F069D38FE /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; }; 9CFF379C7404F7CE6B26AF29 /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 9D71628E38D9F64C965DF29E /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; @@ -1114,6 +1108,7 @@ A25FF76DEF542E01A2DF3B0E /* time_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5497CB76229DECDE000FB92F /* time_testing.cc */; }; A27096F764227BC73526FED3 /* leveldb_remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0840319686A223CC4AD3FAB1 /* leveldb_remote_document_cache_test.cc */; }; A27908A198E1D2230C1801AC /* bundle_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B5C2A94EE24E60543F62CC35 /* bundle_serializer_test.cc */; }; + A296B0110550890E1D8D59A3 /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */; }; A2E9978E02F7BCB016555F09 /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3369AC938F82A70685C5ED58 /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json */; }; A3262936317851958C8EABAF /* byte_stream_cpp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 01D10113ECC5B446DB35E96D /* byte_stream_cpp_test.cc */; }; A4757C171D2407F61332EA38 /* byte_stream_cpp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 01D10113ECC5B446DB35E96D /* byte_stream_cpp_test.cc */; }; @@ -1121,7 +1116,6 @@ A4AD189BDEF7A609953457A6 /* leveldb_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */; }; A4ECA8335000CBDF94586C94 /* FSTDatastoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07E202154EC00B64F25 /* FSTDatastoreTests.mm */; }; A5175CA2E677E13CC5F23D72 /* document_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB6B908320322E4D00CC290A /* document_test.cc */; }; - A5301AA55748A11801E3EE47 /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */; }; A55266E6C986251D283CE948 /* FIRCursorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E070202154D600B64F25 /* FIRCursorTests.mm */; }; A5583822218F9D5B1E86FCAC /* overlay_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E1459FA70B8FC18DE4B80D0D /* overlay_test.cc */; }; A57EC303CD2D6AA4F4745551 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; @@ -1166,6 +1160,7 @@ AB8209455BAA17850D5E196D /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; }; AB9FF792C60FC581909EF381 /* recovery_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 9C1AFCC9E616EC33D6E169CF /* recovery_spec_test.json */; }; ABA495BB202B7E80008A7851 /* snapshot_version_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */; }; + ABE599C3BF9FB6AFF18AA901 /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */; }; ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; }; ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; ABFD599019CF312CFF96B3EC /* perf_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */; }; @@ -1211,7 +1206,6 @@ B220E091D8F4E6DE1EA44F57 /* executor_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */; }; B235E260EA0DCB7BAC04F69B /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; B2554A2BA211D10823646DBE /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BD051DBE754950FEAC7A446 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json */; }; - B280370F84393808250B28BC /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 682582E5728F3F1C531990EA /* explain_stats.pb.cc */; }; B28ACC69EB1F232AE612E77B /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; B2A9965ED0114E39A911FD09 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4375BDCDBCA9938C7F086730 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json */; }; B31B5E0D4EA72C5916CC71F5 /* thread_safe_memoizer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */; }; @@ -1292,11 +1286,13 @@ BC549E3F3F119D80741D8612 /* leveldb_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */; }; BC5AC8890974E0821431267E /* limit_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */; }; BC8DFBCB023DBD914E27AA7D /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; + BC9966788F245D79A63C2E47 /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */; }; BCA720A0F54D23654F806323 /* ConditionalConformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3228F51DCDC2E90D5C58F97 /* ConditionalConformanceTests.swift */; }; BCAC9F7A865BD2320A4D8752 /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; BD0882A40BD8AE042629C179 /* thread_safe_memoizer_testing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */; }; BD3A421C9E40C57D25697E75 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BD051DBE754950FEAC7A446 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json */; }; BD6CC8614970A3D7D2CF0D49 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; }; + BD74B0E1FC752236A7376BC3 /* PipelineApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BF06E5A4988F9F949DD871 /* PipelineApiTests.swift */; }; BDD2D1812BAD962E3C81A53F /* hashing_test_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = B69CF3F02227386500B281C8 /* hashing_test_apple.mm */; }; BDDAE67000DBF10E9EA7FED0 /* nanopb_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F5B6C1399F92FD60F2C582B /* nanopb_util_test.cc */; }; BDF3A6C121F2773BB3A347A7 /* counting_query_engine.cc in Sources */ = {isa = PBXBuildFile; fileRef = 99434327614FEFF7F7DC88EC /* counting_query_engine.cc */; }; @@ -1356,9 +1352,11 @@ C840AD39F7EC5524F1C0F5AE /* filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F02F734F272C3C70D1307076 /* filter_test.cc */; }; C86E85101352B5CDBF5909F9 /* md5_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3D050936A2D52257FD17FB6E /* md5_test.cc */; }; C8722550B56CEB96F84DCE94 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; }; + C8889F3C37F1CC3E64558287 /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */; }; C8A573895D819A92BF16B5E5 /* mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3068AA9DFBBA86C1FE2A946E /* mutation_queue_test.cc */; }; C8BA36C8B5E26C173F91E677 /* aggregation_result.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D872D754B8AD88E28AF28B28 /* aggregation_result.pb.cc */; }; C8BC50508337800E8B098F57 /* bundle_loader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A853C81A6A5A51C9D0389EDA /* bundle_loader_test.cc */; }; + C8C2B945D84DD98391145F3F /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 861684E49DAC993D153E60D0 /* PipelineTests.swift */; }; C8C4CB7B6E23FC340BEC6D7F /* load_bundle_task_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8F1A7B4158D9DD76EE4836BF /* load_bundle_task_test.cc */; }; C8D3CE2343E53223E6487F2C /* Pods_Firestore_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5918805E993304321A05E82B /* Pods_Firestore_Example_iOS.framework */; }; C901A1BFD553B6DD70BB7CC7 /* bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F7FC06E0A47D393DE1759AE1 /* bundle_cache_test.cc */; }; @@ -1425,6 +1423,7 @@ D5B25CBF07F65E885C9D68AB /* perf_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */; }; D5E9954FC1C5ABBC7A180B33 /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; }; D5F6AAA1A1B9AE84205ECE27 /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B3E4A77493524333133C5DC /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json */; }; + D64792BBFA130E26CB3D1028 /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */; }; D6486C7FFA8BE6F9C7D2F4C4 /* filesystem_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F51859B394D01C0C507282F1 /* filesystem_test.cc */; }; D658E6DA5A218E08810E1688 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; }; D6962E598CEDABA312D87760 /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; }; @@ -1437,7 +1436,6 @@ D73BBA4AB42940AB187169E3 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; }; D756A1A63E626572EE8DF592 /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; D77941FD93DBE862AEF1F623 /* FSTTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */; }; - D8F427680C3165DCD1A6BA2A /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */; }; D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; D928302820891CCCAD0437DD /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; D9366A834BFF13246DC3AF9E /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; @@ -1473,7 +1471,7 @@ DD6C480629B3F87933FAF440 /* filesystem_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA02DA2FCD0001CFC6EB08DA /* filesystem_testing.cc */; }; DD935E243A64A4EB688E4C1C /* credentials_provider_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2F4FA4576525144C5069A7A5 /* credentials_provider_test.cc */; }; DD941BF189E38312E7A2CB21 /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D8E530B27D5641B9C26A452C /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json */; }; - DDABEDF95A5B44E590064EF7 /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */; }; + DDC782CBA37AA9B0EA373B7A /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */; }; DDD219222EEE13E3F9F2C703 /* leveldb_transaction_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 88CF09277CFA45EE1273E3BA /* leveldb_transaction_test.cc */; }; DDDE74C752E65DE7D39A7166 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; @@ -1486,11 +1484,13 @@ DEC033E4FB3E09A3C7CE6016 /* aggregate_query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AF924C79F49F793992A84879 /* aggregate_query_test.cc */; }; DEF4BF5FAA83C37100408F89 /* bundle_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 79EAA9F7B1B9592B5F053923 /* bundle_spec_test.json */; }; DF4B3835C5AA4835C01CD255 /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; }; + DF6FBE5BBD578B0DD34CEFA1 /* PipelineApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BF06E5A4988F9F949DD871 /* PipelineApiTests.swift */; }; DF7ABEB48A650117CBEBCD26 /* object_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 214877F52A705012D6720CA0 /* object_value_test.cc */; }; DF96816EC67F9B8DF19B0CFD /* document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FFCA39825D9678A03D1845D0 /* document_overlay_cache_test.cc */; }; DF983A9C1FBF758AF3AF110D /* aggregation_result.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D872D754B8AD88E28AF28B28 /* aggregation_result.pb.cc */; }; E042112665DD2504E3F495D5 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4375BDCDBCA9938C7F086730 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json */; }; E04607A1E2964684184E8AEA /* index_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 8C7278B604B8799F074F4E8C /* index_spec_test.json */; }; + E04CB0D580980748D5DC453F /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 861684E49DAC993D153E60D0 /* PipelineTests.swift */; }; E08297B35E12106105F448EB /* ordered_code_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */; }; E084921EFB7CF8CB1E950D6C /* iterator_adaptors_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */; }; E0E640226A1439C59BBBA9C1 /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; }; @@ -1542,8 +1542,10 @@ E884336B43BBD1194C17E3C4 /* status_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA33F964042646FDDAF9F9 /* status_testing.cc */; }; E8AB8024B70F6C960D8C7530 /* document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FFCA39825D9678A03D1845D0 /* document_overlay_cache_test.cc */; }; E8BA7055EDB8B03CC99A528F /* recovery_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 9C1AFCC9E616EC33D6E169CF /* recovery_spec_test.json */; }; + E9071BE412DC42300B936BAF /* explain_stats.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */; }; E962CA641FB1312638593131 /* leveldb_document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AE89CFF09C6804573841397F /* leveldb_document_overlay_cache_test.cc */; }; E99D5467483B746D4AA44F74 /* fields_array_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA4CBA48204C9E25B56993BC /* fields_array_test.cc */; }; + E9BC6A5BC2B209B1BA2F8BD6 /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */; }; EA38690795FBAA182A9AA63E /* FIRDatabaseTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06C202154D500B64F25 /* FIRDatabaseTests.mm */; }; EA46611779C3EEF12822508C /* annotations.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */; }; EAA1962BFBA0EBFBA53B343F /* bundle_builder.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */; }; @@ -1605,6 +1607,7 @@ F19B749671F2552E964422F7 /* FIRListenerRegistrationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */; }; F1EAEE9DF819C017A9506AEB /* FIRIndexingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 795AA8FC31D2AF6864B07D39 /* FIRIndexingTests.mm */; }; F1F8FB9254E9A5107161A7B2 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = DD990FD89C165F4064B4F608 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json */; }; + F21A3E06BBEC807FADB43AAF /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */; }; F272A8C41D2353700A11D1FB /* field_mask_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */; }; F27347560A963E8162C56FF3 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; }; F2876F16CF689FD7FFBA9DFA /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 0D964D4936953635AC7E0834 /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json */; }; @@ -1621,7 +1624,6 @@ F58A23FEF328EB74F681FE83 /* index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AE4A9E38D65688EE000EE2A1 /* index_manager_test.cc */; }; F5A654E92FF6F3FF16B93E6B /* mutation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C8522DE226C467C54E6788D8 /* mutation_test.cc */; }; F5B1F219E912F645FB79D08E /* firebase_app_check_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = F119BDDF2F06B3C0883B8297 /* firebase_app_check_credentials_provider_test.mm */; }; - F5BA649242983E2E54345BDD /* pipeline.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */; }; F5BDECEB3B43BD1591EEADBD /* FSTUserDataReaderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D9892F204959C50613F16C8 /* FSTUserDataReaderTests.mm */; }; F6079BFC9460B190DA85C2E6 /* pretty_printing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB323F9553050F4F6490F9FF /* pretty_printing_test.cc */; }; F609600E9A88A4D44FD1FCEB /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; }; @@ -1652,6 +1654,7 @@ FB2111D9205822CC8E7368C2 /* FIRDocumentReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E049202154AA00B64F25 /* FIRDocumentReferenceTests.mm */; }; FB2D5208A6B5816A7244D77A /* query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B8A853940305237AFDA8050B /* query_engine_test.cc */; }; FB3D9E01547436163C456A3C /* message_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CE37875365497FFA8687B745 /* message_test.cc */; }; + FB462B2C6D3C167DF32BA0E1 /* field_behavior.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */; }; FBBB13329D3B5827C21AE7AB /* reference_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 132E32997D781B896672D30A /* reference_set_test.cc */; }; FC1D22B6EC4E5F089AE39B8C /* memory_target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2286F308EFB0534B1BDE05B9 /* memory_target_cache_test.cc */; }; FC6C9D1A8B24A5C9507272F7 /* globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4564AD9C55EC39C080EB9476 /* globals_cache_test.cc */; }; @@ -1738,7 +1741,6 @@ 0D964D4936953635AC7E0834 /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json; sourceTree = ""; }; 0EE5300F8233D14025EF0456 /* string_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_test.mm; sourceTree = ""; }; 11984BA0A99D7A7ABA5B0D90 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = ""; }; - 12260A292D56A3CE001766EB /* PipelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineTests.swift; sourceTree = ""; }; 1235769122B7E915007DDFA9 /* EncodableFieldValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodableFieldValueTests.swift; sourceTree = ""; }; 1235769422B86E65007DDFA9 /* FirestoreEncoderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirestoreEncoderTests.swift; sourceTree = ""; }; 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableIntegrationTests.swift; sourceTree = ""; }; @@ -1746,20 +1748,20 @@ 129A369928CA555B005AE7E2 /* FIRCountTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRCountTests.mm; sourceTree = ""; }; 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 132E32997D781B896672D30A /* reference_set_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = reference_set_test.cc; sourceTree = ""; }; + 15249D092D85B40EFC8A1459 /* pipeline.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = pipeline.pb.h; sourceTree = ""; }; 166CE73C03AB4366AAC5201C /* leveldb_index_manager_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_index_manager_test.cc; sourceTree = ""; }; 1A7D48A017ECB54FD381D126 /* Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json; sourceTree = ""; }; 1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = thread_safe_memoizer_test.cc; sourceTree = ""; }; 1B342370EAE3AA02393E33EB /* cc_compilation_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = cc_compilation_test.cc; path = api/cc_compilation_test.cc; sourceTree = ""; }; 1B9F95EC29FAD3F100EEC075 /* FIRAggregateQueryUnitTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateQueryUnitTests.mm; sourceTree = ""; }; - 1BAFC713D2B1A2DBD55B2593 /* field_behavior.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = field_behavior.pb.h; sourceTree = ""; }; 1C01D8CE367C56BB2624E299 /* index.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = index.pb.h; path = admin/index.pb.h; sourceTree = ""; }; 1C3F7302BF4AE6CBC00ECDD0 /* resource.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = resource.pb.cc; sourceTree = ""; }; 1CA9800A53669EFBFFB824E3 /* memory_remote_document_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_remote_document_cache_test.cc; sourceTree = ""; }; 1E0C7C0DCD2790019E66D8CC /* bloom_filter.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bloom_filter.pb.cc; sourceTree = ""; }; 1F50E872B3F117A674DA8E94 /* index_backfiller_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = index_backfiller_test.cc; sourceTree = ""; }; + 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = field_behavior.pb.cc; sourceTree = ""; }; 214877F52A705012D6720CA0 /* object_value_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = object_value_test.cc; sourceTree = ""; }; 2220F583583EFC28DE792ABE /* Pods_Firestore_IntegrationTests_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 2258E6EBCFB8E8B1693C1347 /* explain_stats.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = explain_stats.pb.h; sourceTree = ""; }; 2286F308EFB0534B1BDE05B9 /* memory_target_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_target_cache_test.cc; sourceTree = ""; }; 26DDBA115DEB88631B93F203 /* thread_safe_memoizer_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = thread_safe_memoizer_testing.h; sourceTree = ""; }; 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = lru_garbage_collector_test.cc; sourceTree = ""; }; @@ -1797,11 +1799,12 @@ 403DBF6EFB541DFD01582AA3 /* path_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = path_test.cc; sourceTree = ""; }; 40F9D09063A07F710811A84F /* value_util_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = value_util_test.cc; sourceTree = ""; }; 4132F30044D5DF1FB15B2A9D /* fake_credentials_provider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = fake_credentials_provider.h; sourceTree = ""; }; + 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = explain_stats.pb.cc; sourceTree = ""; }; 432C71959255C5DBDF522F52 /* byte_stream_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = byte_stream_test.cc; sourceTree = ""; }; 4334F87873015E3763954578 /* status_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = status_testing.h; sourceTree = ""; }; 4375BDCDBCA9938C7F086730 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json; sourceTree = ""; }; 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = hard_assert_test.cc; sourceTree = ""; }; - 4564AD9C55EC39C080EB9476 /* globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = globals_cache_test.cc; sourceTree = ""; }; + 4564AD9C55EC39C080EB9476 /* globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = globals_cache_test.cc; sourceTree = ""; }; 478DC75A0DCA6249A616DD30 /* Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json; sourceTree = ""; }; 48D0915834C3D234E5A875A9 /* grpc_stream_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = grpc_stream_tester.h; sourceTree = ""; }; 4B3E4A77493524333133C5DC /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json; sourceTree = ""; }; @@ -1916,10 +1919,11 @@ 57F8EE51B5EFC9FAB185B66C /* Validation_BloomFilterTest_MD5_5000_01_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_5000_01_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_5000_01_bloom_filter_proto.json; sourceTree = ""; }; 584AE2C37A55B408541A6FF3 /* remote_event_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = remote_event_test.cc; sourceTree = ""; }; 5918805E993304321A05E82B /* Pods_Firestore_Example_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 59BF06E5A4988F9F949DD871 /* PipelineApiTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PipelineApiTests.swift; sourceTree = ""; }; 5B5414D28802BC76FDADABD6 /* stream_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = stream_test.cc; sourceTree = ""; }; 5B96CC29E9946508F022859C /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json; sourceTree = ""; }; 5C68EE4CB94C0DD6E333F546 /* Validation_BloomFilterTest_MD5_1_01_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_01_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_01_membership_test_result.json; sourceTree = ""; }; - 5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_globals_cache_test.cc; sourceTree = ""; }; + 5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = memory_globals_cache_test.cc; sourceTree = ""; }; 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_mutation_queue_test.cc; sourceTree = ""; }; 5CAE131920FFFED600BE9A4A /* Firestore_Benchmarks_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Benchmarks_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5CAE131D20FFFED600BE9A4A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1966,7 +1970,6 @@ 64AA92CFA356A2360F3C5646 /* filesystem_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = filesystem_testing.h; sourceTree = ""; }; 65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRCompositeIndexQueryTests.mm; sourceTree = ""; }; 67786C62C76A740AEDBD8CD3 /* FSTTestingHooks.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = FSTTestingHooks.h; sourceTree = ""; }; - 682582E5728F3F1C531990EA /* explain_stats.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = explain_stats.pb.cc; sourceTree = ""; }; 69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; sourceTree = ""; }; 6A7A30A2DB3367E08939E789 /* bloom_filter.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bloom_filter.pb.h; sourceTree = ""; }; 6AE927CDFC7A72BF825BE4CB /* Pods-Firestore_Tests_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_tvOS/Pods-Firestore_Tests_tvOS.release.xcconfig"; sourceTree = ""; }; @@ -1983,7 +1986,6 @@ 6F57521E161450FAF89075ED /* event_manager_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = event_manager_test.cc; sourceTree = ""; }; 6F5B6C1399F92FD60F2C582B /* nanopb_util_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = nanopb_util_test.cc; path = nanopb/nanopb_util_test.cc; sourceTree = ""; }; 71140E5D09C6E76F7C71B2FC /* fake_target_metadata_provider.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = fake_target_metadata_provider.cc; sourceTree = ""; }; - 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = pipeline.pb.cc; sourceTree = ""; }; 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 728F617782600536F2561463 /* Validation_BloomFilterTest_MD5_5000_0001_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_5000_0001_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_5000_0001_bloom_filter_proto.json; sourceTree = ""; }; 731541602214AFFA0037F4DC /* query_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = query_spec_test.json; sourceTree = ""; }; @@ -2009,6 +2011,8 @@ 7EB299CF85034F09CFD6F3FD /* remote_document_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = remote_document_cache_test.cc; sourceTree = ""; }; 84076EADF6872C78CDAC7291 /* bundle_builder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bundle_builder.h; sourceTree = ""; }; 84434E57CA72951015FC71BC /* Pods-Firestore_FuzzTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; sourceTree = ""; }; + 861684E49DAC993D153E60D0 /* PipelineTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PipelineTests.swift; sourceTree = ""; }; + 86C7F725E6E1DA312807D8D3 /* explain_stats.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = explain_stats.pb.h; sourceTree = ""; }; 872C92ABD71B12784A1C5520 /* async_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = async_testing.cc; sourceTree = ""; }; 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 87553338E42B8ECA05BA987E /* grpc_stream_tester.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = grpc_stream_tester.cc; sourceTree = ""; }; @@ -2040,6 +2044,7 @@ A20BAA3D2F994384279727EC /* md5_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = md5_testing.h; sourceTree = ""; }; A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bloom_filter_test.cc; sourceTree = ""; }; A366F6AE1A5A77548485C091 /* bundle.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bundle.pb.cc; sourceTree = ""; }; + A4192EB032E23129EF23605A /* field_behavior.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = field_behavior.pb.h; sourceTree = ""; }; A5466E7809AD2871FFDE6C76 /* view_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = view_testing.cc; sourceTree = ""; }; A5D9044B72061CAF284BC9E4 /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json; sourceTree = ""; }; A5FA86650A18F3B7A8162287 /* Pods-Firestore_Benchmarks_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Benchmarks_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS.release.xcconfig"; sourceTree = ""; }; @@ -2114,13 +2119,13 @@ D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = delayed_constructor_test.cc; sourceTree = ""; }; D22D4C211AC32E4F8B4883DA /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json; sourceTree = ""; }; D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = pipeline.pb.cc; sourceTree = ""; }; D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = perf_spec_test.json; sourceTree = ""; }; D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRNumericTransformTests.mm; sourceTree = ""; }; D7DF4A6F740086A2D8C0E28E /* Pods_Firestore_Tests_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D85AC18C55650ED230A71B82 /* FSTTestingHooks.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTTestingHooks.mm; sourceTree = ""; }; D872D754B8AD88E28AF28B28 /* aggregation_result.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = aggregation_result.pb.cc; sourceTree = ""; }; D8A6D52723B1BABE1B7B8D8F /* leveldb_overlay_migration_manager_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_overlay_migration_manager_test.cc; sourceTree = ""; }; - D8DAE1269481D15A291E0B49 /* pipeline.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = pipeline.pb.h; sourceTree = ""; }; D8E530B27D5641B9C26A452C /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json; sourceTree = ""; }; D9D94300B9C02F7069523C00 /* leveldb_snappy_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_snappy_test.cc; sourceTree = ""; }; DAFF0CF521E64AC30062958F /* Firestore_Example_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example_macOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2168,7 +2173,6 @@ F848C41C03A25C42AD5A4BC2 /* target_cache_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = target_cache_test.h; sourceTree = ""; }; F869D85E900E5AF6CD02E2FC /* firebase_auth_credentials_provider_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = firebase_auth_credentials_provider_test.mm; path = credentials/firebase_auth_credentials_provider_test.mm; sourceTree = ""; }; FA2E9952BA2B299C1156C43C /* Pods-Firestore_Benchmarks_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Benchmarks_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS.debug.xcconfig"; sourceTree = ""; }; - FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = field_behavior.pb.cc; sourceTree = ""; }; FC44D934D4A52C790659C8D6 /* leveldb_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = leveldb_globals_cache_test.cc; sourceTree = ""; }; FC738525340E594EBFAB121E /* Pods-Firestore_Example_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_tvOS/Pods-Firestore_Example_tvOS.release.xcconfig"; sourceTree = ""; }; FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRQueryUnitTests.mm; sourceTree = ""; }; @@ -2300,7 +2304,8 @@ 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */, 3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */, 62E54B832A9E910A003347C8 /* IndexingTests.swift */, - 12260A292D56A3CE001766EB /* PipelineTests.swift */, + 59BF06E5A4988F9F949DD871 /* PipelineApiTests.swift */, + 861684E49DAC993D153E60D0 /* PipelineTests.swift */, 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */, 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */, EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */, @@ -2339,12 +2344,12 @@ 544129D121C2DDC800EFB9CC /* common.pb.h */, 544129D821C2DDC800EFB9CC /* document.pb.cc */, 544129D721C2DDC800EFB9CC /* document.pb.h */, - 682582E5728F3F1C531990EA /* explain_stats.pb.cc */, - 2258E6EBCFB8E8B1693C1347 /* explain_stats.pb.h */, + 428662F00938E9E21F7080D7 /* explain_stats.pb.cc */, + 86C7F725E6E1DA312807D8D3 /* explain_stats.pb.h */, 544129D421C2DDC800EFB9CC /* firestore.pb.cc */, 544129D321C2DDC800EFB9CC /* firestore.pb.h */, - 7142B5EC46E88349FAB3384F /* pipeline.pb.cc */, - D8DAE1269481D15A291E0B49 /* pipeline.pb.h */, + D49E7AEE500651D25C5360C3 /* pipeline.pb.cc */, + 15249D092D85B40EFC8A1459 /* pipeline.pb.h */, 544129D621C2DDC800EFB9CC /* query.pb.cc */, 544129D021C2DDC800EFB9CC /* query.pb.h */, 544129D921C2DDC800EFB9CC /* write.pb.cc */, @@ -2811,8 +2816,8 @@ children = ( 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */, 618BBE9620B89AAC00B5BCE7 /* annotations.pb.h */, - FAAF1A69F4A315C38357BDC4 /* field_behavior.pb.cc */, - 1BAFC713D2B1A2DBD55B2593 /* field_behavior.pb.h */, + 1F78CD3208A1D5885B4C134E /* field_behavior.pb.cc */, + A4192EB032E23129EF23605A /* field_behavior.pb.h */, 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */, 618BBE9420B89AAC00B5BCE7 /* http.pb.h */, 1C3F7302BF4AE6CBC00ECDD0 /* resource.pb.cc */, @@ -4262,10 +4267,10 @@ AC6C1E57B18730428CB15E03 /* executor_libdispatch_test.mm in Sources */, E7D415B8717701B952C344E5 /* executor_std_test.cc in Sources */, 470A37727BBF516B05ED276A /* executor_test.cc in Sources */, - B280370F84393808250B28BC /* explain_stats.pb.cc in Sources */, + 2F72DBE2EC6E24A81C69DEF0 /* explain_stats.pb.cc in Sources */, 2E0BBA7E627EB240BA11B0D0 /* exponential_backoff_test.cc in Sources */, 9009C285F418EA80C46CF06B /* fake_target_metadata_provider.cc in Sources */, - A5301AA55748A11801E3EE47 /* field_behavior.pb.cc in Sources */, + 7B58861D0978827BC4CB1DFA /* field_behavior.pb.cc in Sources */, 2E373EA9D5FF8C6DE2507675 /* field_index_test.cc in Sources */, 07B1E8C62772758BC82FEBEE /* field_mask_test.cc in Sources */, D9366A834BFF13246DC3AF9E /* field_path_test.cc in Sources */, @@ -4341,7 +4346,7 @@ BE1D7C7E413449AFFBA21BCB /* overlay_test.cc in Sources */, DB7E9C5A59CCCDDB7F0C238A /* path_test.cc in Sources */, E30BF9E316316446371C956C /* persistence_testing.cc in Sources */, - 715A0E92C83AE4384A13B882 /* pipeline.pb.cc in Sources */, + 60DA778E447F9ACD402FDA2F /* pipeline.pb.cc in Sources */, 0455FC6E2A281BD755FD933A /* precondition_test.cc in Sources */, 5ECE040F87E9FCD0A5D215DB /* pretty_printing_test.cc in Sources */, 938F2AF6EC5CD0B839300DB0 /* query.pb.cc in Sources */, @@ -4488,10 +4493,10 @@ B220E091D8F4E6DE1EA44F57 /* executor_libdispatch_test.mm in Sources */, BAB43C839445782040657239 /* executor_std_test.cc in Sources */, 3A7CB01751697ED599F2D9A1 /* executor_test.cc in Sources */, - 8FE63980976481EBA001B789 /* explain_stats.pb.cc in Sources */, + 7CAF0E8C47FB2DD486240D47 /* explain_stats.pb.cc in Sources */, EF3518F84255BAF3EBD317F6 /* exponential_backoff_test.cc in Sources */, 4DAFC3A3FD5E96910A517320 /* fake_target_metadata_provider.cc in Sources */, - 86E73F6286E87834CF37D5D9 /* field_behavior.pb.cc in Sources */, + E9BC6A5BC2B209B1BA2F8BD6 /* field_behavior.pb.cc in Sources */, 69D3AD697D1A7BF803A08160 /* field_index_test.cc in Sources */, ED4E2AC80CAF2A8FDDAC3DEE /* field_mask_test.cc in Sources */, 41EAC526C543064B8F3F7EDA /* field_path_test.cc in Sources */, @@ -4567,7 +4572,7 @@ 2045517602D767BD01EA71D9 /* overlay_test.cc in Sources */, 0963F6D7B0F9AE1E24B82866 /* path_test.cc in Sources */, 92D7081085679497DC112EDB /* persistence_testing.cc in Sources */, - 9CD1E9301EC44ED10DAEA5FB /* pipeline.pb.cc in Sources */, + 8429E18EFBAF473209731E01 /* pipeline.pb.cc in Sources */, 152543FD706D5E8851C8DA92 /* precondition_test.cc in Sources */, 2639ABDA17EECEB7F62D1D83 /* pretty_printing_test.cc in Sources */, 5FA3DB52A478B01384D3A2ED /* query.pb.cc in Sources */, @@ -4690,7 +4695,8 @@ 432056C4D1259F76C80FC2A8 /* FSTUserDataReaderTests.mm in Sources */, 3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */, 62E54B862A9E910B003347C8 /* IndexingTests.swift in Sources */, - 12260A2C2D56A3CE001766EB /* PipelineTests.swift in Sources */, + 3D5F7AA7BB68529F47BE4B12 /* PipelineApiTests.swift in Sources */, + 655F8647F57E5F2155DFF7B5 /* PipelineTests.swift in Sources */, 621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAC2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, @@ -4739,10 +4745,10 @@ 5F6CE37B34C542704C5605A4 /* executor_libdispatch_test.mm in Sources */, AECCD9663BB3DC52199F954A /* executor_std_test.cc in Sources */, 18F644E6AA98E6D6F3F1F809 /* executor_test.cc in Sources */, - 0E7A39BD9C87CC33F91A672F /* explain_stats.pb.cc in Sources */, + ABE599C3BF9FB6AFF18AA901 /* explain_stats.pb.cc in Sources */, 6938575C8B5E6FE0D562547A /* exponential_backoff_test.cc in Sources */, 258B372CF33B7E7984BBA659 /* fake_target_metadata_provider.cc in Sources */, - D8F427680C3165DCD1A6BA2A /* field_behavior.pb.cc in Sources */, + 2FC2B732841BF2C425EB35DF /* field_behavior.pb.cc in Sources */, F8BD2F61EFA35C2D5120D9EB /* field_index_test.cc in Sources */, F272A8C41D2353700A11D1FB /* field_mask_test.cc in Sources */, AF6D6C47F9A25C65BFDCBBA0 /* field_path_test.cc in Sources */, @@ -4818,7 +4824,7 @@ A5583822218F9D5B1E86FCAC /* overlay_test.cc in Sources */, 70A171FC43BE328767D1B243 /* path_test.cc in Sources */, EECC1EC64CA963A8376FA55C /* persistence_testing.cc in Sources */, - 92B593DCD86543D8C90F64F9 /* pipeline.pb.cc in Sources */, + 5CDD24225992674A4D3E3D4E /* pipeline.pb.cc in Sources */, 34D69886DAD4A2029BFC5C63 /* precondition_test.cc in Sources */, F56E9334642C207D7D85D428 /* pretty_printing_test.cc in Sources */, 22A00AC39CAB3426A943E037 /* query.pb.cc in Sources */, @@ -4941,7 +4947,8 @@ 75A176239B37354588769206 /* FSTUserDataReaderTests.mm in Sources */, 5E89B1A5A5430713C79C4854 /* FirestoreEncoderTests.swift in Sources */, 62E54B852A9E910B003347C8 /* IndexingTests.swift in Sources */, - 12260A2B2D56A3CE001766EB /* PipelineTests.swift in Sources */, + DF6FBE5BBD578B0DD34CEFA1 /* PipelineApiTests.swift in Sources */, + C8C2B945D84DD98391145F3F /* PipelineTests.swift in Sources */, 621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAB2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, @@ -4990,10 +4997,10 @@ 49C593017B5438B216FAF593 /* executor_libdispatch_test.mm in Sources */, 17DFF30CF61D87883986E8B6 /* executor_std_test.cc in Sources */, 814724DE70EFC3DDF439CD78 /* executor_test.cc in Sources */, - 7492C447277CDC8CB7A165CB /* explain_stats.pb.cc in Sources */, + A296B0110550890E1D8D59A3 /* explain_stats.pb.cc in Sources */, BD6CC8614970A3D7D2CF0D49 /* exponential_backoff_test.cc in Sources */, 4D2655C5675D83205C3749DC /* fake_target_metadata_provider.cc in Sources */, - DDABEDF95A5B44E590064EF7 /* field_behavior.pb.cc in Sources */, + FB462B2C6D3C167DF32BA0E1 /* field_behavior.pb.cc in Sources */, 50C852E08626CFA7DC889EEA /* field_index_test.cc in Sources */, A1563EFEB021936D3FFE07E3 /* field_mask_test.cc in Sources */, B235E260EA0DCB7BAC04F69B /* field_path_test.cc in Sources */, @@ -5069,7 +5076,7 @@ D1BCDAEACF6408200DFB9870 /* overlay_test.cc in Sources */, B3A309CCF5D75A555C7196E1 /* path_test.cc in Sources */, 46EAC2828CD942F27834F497 /* persistence_testing.cc in Sources */, - 3B4CFB45208A7EEF1EA58ADC /* pipeline.pb.cc in Sources */, + D64792BBFA130E26CB3D1028 /* pipeline.pb.cc in Sources */, 9EE1447AA8E68DF98D0590FF /* precondition_test.cc in Sources */, F6079BFC9460B190DA85C2E6 /* pretty_printing_test.cc in Sources */, 7B0F073BDB6D0D6E542E23D4 /* query.pb.cc in Sources */, @@ -5226,10 +5233,10 @@ B6FB468E208F9BAB00554BA2 /* executor_libdispatch_test.mm in Sources */, B6FB468F208F9BAE00554BA2 /* executor_std_test.cc in Sources */, B6FB4690208F9BB300554BA2 /* executor_test.cc in Sources */, - 50EA1F41D766C92894E9B078 /* explain_stats.pb.cc in Sources */, + DDC782CBA37AA9B0EA373B7A /* explain_stats.pb.cc in Sources */, B6D1B68520E2AB1B00B35856 /* exponential_backoff_test.cc in Sources */, FAE5DA6ED3E1842DC21453EE /* fake_target_metadata_provider.cc in Sources */, - 1F9FFAE375C88EFF88CBB6F8 /* field_behavior.pb.cc in Sources */, + F21A3E06BBEC807FADB43AAF /* field_behavior.pb.cc in Sources */, 03AEB9E07A605AE1B5827548 /* field_index_test.cc in Sources */, 549CCA5720A36E1F00BCEB75 /* field_mask_test.cc in Sources */, B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */, @@ -5305,7 +5312,7 @@ 4D20563D846FA0F3BEBFDE9D /* overlay_test.cc in Sources */, 5A080105CCBFDB6BF3F3772D /* path_test.cc in Sources */, 21C17F15579341289AD01051 /* persistence_testing.cc in Sources */, - F5BA649242983E2E54345BDD /* pipeline.pb.cc in Sources */, + C8889F3C37F1CC3E64558287 /* pipeline.pb.cc in Sources */, 549CCA5920A36E1F00BCEB75 /* precondition_test.cc in Sources */, 6A94393D83EB338DFAF6A0D2 /* pretty_printing_test.cc in Sources */, 544129DC21C2DDC800EFB9CC /* query.pb.cc in Sources */, @@ -5447,7 +5454,8 @@ F5BDECEB3B43BD1591EEADBD /* FSTUserDataReaderTests.mm in Sources */, 6F45846C159D3C063DBD3CBE /* FirestoreEncoderTests.swift in Sources */, 62E54B842A9E910B003347C8 /* IndexingTests.swift in Sources */, - 12260A2A2D56A3CE001766EB /* PipelineTests.swift in Sources */, + BD74B0E1FC752236A7376BC3 /* PipelineApiTests.swift in Sources */, + E04CB0D580980748D5DC453F /* PipelineTests.swift in Sources */, 621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAA2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, @@ -5496,10 +5504,10 @@ B6BF6EFEF887B072068BA658 /* executor_libdispatch_test.mm in Sources */, 125B1048ECB755C2106802EB /* executor_std_test.cc in Sources */, DABB9FB61B1733F985CBF713 /* executor_test.cc in Sources */, - 676933F59F2F0A0D221A4F8F /* explain_stats.pb.cc in Sources */, + E9071BE412DC42300B936BAF /* explain_stats.pb.cc in Sources */, 7BCF050BA04537B0E7D44730 /* exponential_backoff_test.cc in Sources */, BA1C5EAE87393D8E60F5AE6D /* fake_target_metadata_provider.cc in Sources */, - 332E7D2D8489E6DA42947C59 /* field_behavior.pb.cc in Sources */, + 3A110ECBF96B6E44BA77011A /* field_behavior.pb.cc in Sources */, 84285C3F63D916A4786724A8 /* field_index_test.cc in Sources */, 6A40835DB2C02B9F07C02E88 /* field_mask_test.cc in Sources */, D00E69F7FDF2BE674115AD3F /* field_path_test.cc in Sources */, @@ -5575,7 +5583,7 @@ 4D7900401B1BF3D3C24DDC7E /* overlay_test.cc in Sources */, 6105A1365831B79A7DEEA4F3 /* path_test.cc in Sources */, CB8BEF34CC4A996C7BE85119 /* persistence_testing.cc in Sources */, - 2AD2CB51469AE35331C39258 /* pipeline.pb.cc in Sources */, + BC9966788F245D79A63C2E47 /* pipeline.pb.cc in Sources */, 4194B7BB8B0352E1AC5D69B9 /* precondition_test.cc in Sources */, 0EA40EDACC28F445F9A3F32F /* pretty_printing_test.cc in Sources */, 63B91FC476F3915A44F00796 /* query.pb.cc in Sources */, diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h index faca06dee8c..5365f4696ff 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -51,6 +51,8 @@ extern "C" { /** Returns the default Firestore database ID for testing. */ + (NSString *)databaseID; ++ (void)switchToEnterpriseMode; + + (bool)isRunningAgainstEmulator; /** Returns a FirestoreSettings configured to use either hexa or the emulator. */ diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index c64b1e80706..9d69be35af4 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -86,6 +86,7 @@ static NSString *defaultProjectId; static NSString *defaultDatabaseId = @"(default)"; +static NSString *enterpriseDatabaseId = @"enterprise"; static FIRFirestoreSettings *defaultSettings; static bool runningAgainstEmulator = false; @@ -273,6 +274,10 @@ + (NSString *)databaseID { return defaultDatabaseId; } ++ (void)switchToEnterpriseMode { + defaultDatabaseId = enterpriseDatabaseId; +} + + (bool)isRunningAgainstEmulator { // The only way to determine whether or not we're running against the emulator is to figure out // which testing environment we're using. Essentially `setUpDefaults` determines diff --git a/Firestore/Source/API/FIRPipelineBridge+Internal.h b/Firestore/Source/API/FIRPipelineBridge+Internal.h index 30bee14aa02..603bc7b88ac 100644 --- a/Firestore/Source/API/FIRPipelineBridge+Internal.h +++ b/Firestore/Source/API/FIRPipelineBridge+Internal.h @@ -35,6 +35,12 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface FIROrderingBridge (Internal) + +- (std::shared_ptr)cppOrderingWithReader:(FSTUserDataReader *)reader; + +@end + @interface FIRStageBridge (Internal) - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader; @@ -53,4 +59,10 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface FIRPipelineBridge (Internal) + +- (std::shared_ptr)cppPipelineWithReader:(FSTUserDataReader *)reader; + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index c10a05f4d88..ac3091249e0 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -16,18 +16,25 @@ #import "FIRPipelineBridge.h" +#import + #include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRPipelineBridge+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" #import "Firestore/Source/API/FSTUserDataWriter.h" +#import "Firestore/Source/API/converters.h" +#import "Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h" #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/api/aggregate_expressions.h" #include "Firestore/core/src/api/document_reference.h" #include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/api/ordering.h" #include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/api/pipeline_result.h" #include "Firestore/core/src/api/pipeline_snapshot.h" @@ -36,17 +43,40 @@ #include "Firestore/core/src/util/status.h" #include "Firestore/core/src/util/string_apple.h" +using firebase::firestore::api::AddFields; +using firebase::firestore::api::AggregateFunction; +using firebase::firestore::api::AggregateStage; +using firebase::firestore::api::CollectionGroupSource; using firebase::firestore::api::CollectionSource; using firebase::firestore::api::Constant; +using firebase::firestore::api::DatabaseSource; +using firebase::firestore::api::DistinctStage; using firebase::firestore::api::DocumentReference; +using firebase::firestore::api::DocumentsSource; using firebase::firestore::api::Expr; using firebase::firestore::api::Field; +using firebase::firestore::api::FindNearestStage; using firebase::firestore::api::FunctionExpr; +using firebase::firestore::api::GenericStage; +using firebase::firestore::api::LimitStage; +using firebase::firestore::api::MakeFIRTimestamp; +using firebase::firestore::api::OffsetStage; +using firebase::firestore::api::Ordering; using firebase::firestore::api::Pipeline; +using firebase::firestore::api::RemoveFieldsStage; +using firebase::firestore::api::ReplaceWith; +using firebase::firestore::api::Sample; +using firebase::firestore::api::SelectStage; +using firebase::firestore::api::SortStage; +using firebase::firestore::api::Union; +using firebase::firestore::api::Unnest; using firebase::firestore::api::Where; +using firebase::firestore::model::FieldPath; +using firebase::firestore::nanopb::SharedMessage; using firebase::firestore::util::MakeCallback; using firebase::firestore::util::MakeNSString; using firebase::firestore::util::MakeString; +using firebase::firestore::util::ThrowInvalidArgument; NS_ASSUME_NONNULL_BEGIN @@ -72,7 +102,7 @@ - (id)init:(NSString *)name { @end @implementation FIRConstantBridge { - std::shared_ptr constant; + std::shared_ptr cpp_constant; id _input; Boolean isUserDataRead; } @@ -85,17 +115,17 @@ - (id)init:(id)input { - (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - constant = std::make_shared([reader parsedQueryValue:_input]); + cpp_constant = std::make_shared([reader parsedQueryValue:_input]); } isUserDataRead = YES; - return constant; + return cpp_constant; } @end @implementation FIRFunctionExprBridge { - std::shared_ptr eq; + std::shared_ptr cpp_function; NSString *_name; NSArray *_args; Boolean isUserDataRead; @@ -115,11 +145,66 @@ - (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray(MakeString(_name), std::move(cpp_args)); + cpp_function = std::make_shared(MakeString(_name), std::move(cpp_args)); + } + + isUserDataRead = YES; + return cpp_function; +} + +@end + +@implementation FIRAggregateFunctionBridge { + std::shared_ptr cpp_function; + NSString *_name; + NSArray *_args; + Boolean isUserDataRead; +} + +- (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray *)args { + _name = name; + _args = args; + isUserDataRead = NO; + return self; +} + +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector> cpp_args; + for (FIRExprBridge *arg in _args) { + cpp_args.push_back([arg cppExprWithReader:reader]); + } + cpp_function = std::make_shared(MakeString(_name), std::move(cpp_args)); } isUserDataRead = YES; - return eq; + return cpp_function; +} + +@end + +@implementation FIROrderingBridge { + std::shared_ptr cpp_ordering; + NSString *_direction; + FIRExprBridge *_expr; + Boolean isUserDataRead; +} + +- (nonnull id)initWithExpr:(FIRExprBridge *)expr Direction:(NSString *)direction { + _expr = expr; + _direction = direction; + isUserDataRead = NO; + return self; +} + +- (std::shared_ptr)cppOrderingWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + cpp_ordering = std::make_shared( + [_expr cppExprWithReader:reader], Ordering::DirectionFromString(MakeString(_direction))); + } + + isUserDataRead = YES; + return cpp_ordering; } @end @@ -145,10 +230,68 @@ - (id)initWithPath:(NSString *)path { @end +@implementation FIRDatabaseSourceStageBridge { + std::shared_ptr cpp_database_source; +} + +- (id)init { + self = [super init]; + if (self) { + cpp_database_source = std::make_shared(); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + return cpp_database_source; +} + +@end + +@implementation FIRCollectionGroupSourceStageBridge { + std::shared_ptr cpp_collection_group_source; +} + +- (id)initWithCollectionId:(NSString *)id { + self = [super init]; + if (self) { + cpp_collection_group_source = std::make_shared(MakeString(id)); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + return cpp_collection_group_source; +} + +@end + +@implementation FIRDocumentsSourceStageBridge { + std::shared_ptr cpp_document_source; +} + +- (id)initWithDocuments:(NSArray *)documents { + self = [super init]; + if (self) { + std::vector cpp_documents; + for (NSString *doc in documents) { + cpp_documents.push_back(MakeString(doc)); + } + cpp_document_source = std::make_shared(std::move(cpp_documents)); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + return cpp_document_source; +} + +@end + @implementation FIRWhereStageBridge { FIRExprBridge *_exprBridge; Boolean isUserDataRead; - std::shared_ptr where; + std::shared_ptr cpp_where; } - (id)initWithExpr:(FIRExprBridge *)expr { @@ -162,18 +305,501 @@ - (id)initWithExpr:(FIRExprBridge *)expr { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - where = std::make_shared([_exprBridge cppExprWithReader:reader]); + cpp_where = std::make_shared([_exprBridge cppExprWithReader:reader]); + } + + isUserDataRead = YES; + return cpp_where; +} + +@end + +@implementation FIRLimitStageBridge { + Boolean isUserDataRead; + std::shared_ptr cpp_limit_stage; + int32_t limit; +} + +- (id)initWithLimit:(NSInteger)value { + self = [super init]; + if (self) { + isUserDataRead = NO; + limit = static_cast(value); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + cpp_limit_stage = std::make_shared(limit); + } + + isUserDataRead = YES; + return cpp_limit_stage; +} + +@end + +@implementation FIROffsetStageBridge { + Boolean isUserDataRead; + std::shared_ptr cpp_offset_stage; + int32_t offset; +} + +- (id)initWithOffset:(NSInteger)value { + self = [super init]; + if (self) { + isUserDataRead = NO; + offset = static_cast(value); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + cpp_offset_stage = std::make_shared(offset); + } + + isUserDataRead = YES; + return cpp_offset_stage; +} + +@end + +// TBD + +@implementation FIRAddFieldsStageBridge { + NSDictionary *_fields; + Boolean isUserDataRead; + std::shared_ptr cpp_add_fields; +} + +- (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; + for (NSString *key in _fields) { + cpp_fields[MakeString(key)] = [_fields[key] cppExprWithReader:reader]; + } + cpp_add_fields = std::make_shared(std::move(cpp_fields)); + } + + isUserDataRead = YES; + return cpp_add_fields; +} + +@end + +@implementation FIRRemoveFieldsStageBridge { + NSArray *_fields; + Boolean isUserDataRead; + std::shared_ptr cpp_remove_fields; +} + +- (id)initWithFields:(NSArray *)fields { + self = [super init]; + if (self) { + _fields = fields; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector cpp_fields; + for (id field in _fields) { + cpp_fields.push_back(Field(MakeString(field))); + } + cpp_remove_fields = std::make_shared(std::move(cpp_fields)); + } + + isUserDataRead = YES; + return cpp_remove_fields; +} + +@end + +@implementation FIRSelectStageBridge { + NSDictionary *_selections; + Boolean isUserDataRead; + std::shared_ptr cpp_select; +} + +- (id)initWithSelections:(NSDictionary *)selections { + self = [super init]; + if (self) { + _selections = selections; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_selections; + for (NSString *key in _selections) { + cpp_selections[MakeString(key)] = [_selections[key] cppExprWithReader:reader]; + } + cpp_select = std::make_shared(std::move(cpp_selections)); + } + + isUserDataRead = YES; + return cpp_select; +} + +@end + +@implementation FIRDistinctStageBridge { + NSDictionary *_groups; + Boolean isUserDataRead; + std::shared_ptr cpp_distinct; +} + +- (id)initWithGroups:(NSDictionary *)groups { + self = [super init]; + if (self) { + _groups = groups; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_groups; + for (NSString *key in _groups) { + cpp_groups[MakeString(key)] = [_groups[key] cppExprWithReader:reader]; + } + cpp_distinct = std::make_shared(std::move(cpp_groups)); + } + + isUserDataRead = YES; + return cpp_distinct; +} + +@end + +@implementation FIRAggregateStageBridge { + NSDictionary *_accumulators; + NSDictionary *_groups; + Boolean isUserDataRead; + std::shared_ptr cpp_aggregate; +} + +- (id)initWithAccumulators:(NSDictionary *)accumulators + groups:(NSDictionary *)groups { + self = [super init]; + if (self) { + _accumulators = accumulators; + _groups = groups; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_accumulators; + for (NSString *key in _accumulators) { + cpp_accumulators[MakeString(key)] = [_accumulators[key] cppExprWithReader:reader]; + } + + std::unordered_map> cpp_groups; + for (NSString *key in _groups) { + cpp_groups[MakeString(key)] = [_groups[key] cppExprWithReader:reader]; + } + cpp_aggregate = + std::make_shared(std::move(cpp_accumulators), std::move(cpp_groups)); } isUserDataRead = YES; - return where; + return cpp_aggregate; +} + +@end + +@implementation FIRFindNearestStageBridge { + FIRFieldBridge *_field; + FIRVectorValue *_vectorValue; + NSString *_distanceMeasure; + NSNumber *_limit; + NSString *_Nullable _distanceField; + Boolean isUserDataRead; + std::shared_ptr cpp_find_nearest; +} + +- (id)initWithField:(FIRFieldBridge *)field + vectorValue:(FIRVectorValue *)vectorValue + distanceMeasure:(NSString *)distanceMeasure + limit:(NSNumber *_Nullable)limit + distanceField:(NSString *_Nullable)distanceField { + self = [super init]; + if (self) { + _field = field; + _vectorValue = vectorValue; + _distanceMeasure = distanceMeasure; + _limit = limit; + _distanceField = distanceField; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> + optional_value; + if (_limit) { + optional_value.emplace( + std::make_pair(std::string("limit"), + nanopb::SharedMessage( + [reader parsedQueryValue:_limit]))); + } + + if (_distanceField) { + optional_value.emplace( + std::make_pair(std::string("distance_field"), + nanopb::SharedMessage( + [reader parsedQueryValue:_distanceField]))); + } + + FindNearestStage::DistanceMeasure::Measure measure_enum; + if ([_distanceMeasure isEqualToString:@"cosine"]) { + measure_enum = FindNearestStage::DistanceMeasure::COSINE; + } else if ([_distanceMeasure isEqualToString:@"dot_product"]) { + measure_enum = FindNearestStage::DistanceMeasure::DOT_PRODUCT; + } else { + measure_enum = FindNearestStage::DistanceMeasure::EUCLIDEAN; + } + + cpp_find_nearest = std::make_shared( + [_field cppExprWithReader:reader], [reader parsedQueryValue:_vectorValue], + FindNearestStage::DistanceMeasure(measure_enum), optional_value); + } + + isUserDataRead = YES; + return cpp_find_nearest; +} + +@end + +@implementation FIRSorStageBridge { + NSArray *_orderings; + Boolean isUserDataRead; + std::shared_ptr cpp_sort; +} + +- (id)initWithOrderings:(NSArray *)orderings { + self = [super init]; + if (self) { + _orderings = orderings; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector> cpp_orderings; + for (FIROrderingBridge *ordering in _orderings) { + cpp_orderings.push_back([ordering cppOrderingWithReader:reader]); + } + cpp_sort = std::make_shared(std::move(cpp_orderings)); + } + + isUserDataRead = YES; + return cpp_sort; +} + +@end + +@implementation FIRReplaceWithStageBridge { + FIRExprBridge *_expr; + Boolean isUserDataRead; + std::shared_ptr cpp_replace_with; +} + +- (id)initWithExpr:(FIRExprBridge *)expr { + self = [super init]; + if (self) { + _expr = expr; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + cpp_replace_with = std::make_shared([_expr cppExprWithReader:reader]); + } + + isUserDataRead = YES; + return cpp_replace_with; +} + +@end + +@implementation FIRSampleStageBridge { + int64_t _count; + double _percentage; + Boolean isUserDataRead; + NSString *type; + std::shared_ptr cpp_sample; +} + +- (id)initWithCount:(int64_t)count { + self = [super init]; + if (self) { + _count = count; + _percentage = 0; + type = @"count"; + isUserDataRead = NO; + } + return self; +} + +- (id)initWithPercentage:(double)percentage { + self = [super init]; + if (self) { + _percentage = percentage; + _count = 0; + type = @"percentage"; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + if ([type isEqualToString:@"count"]) { + cpp_sample = std::make_shared("count", _count, 0); + } else { + cpp_sample = std::make_shared("percentage", 0, _percentage); + } + } + + isUserDataRead = YES; + return cpp_sample; +} + +@end + +@implementation FIRUnionStageBridge { + FIRPipelineBridge *_other; + Boolean isUserDataRead; + std::shared_ptr cpp_union_stage; +} + +- (id)initWithOther:(FIRPipelineBridge *)other { + self = [super init]; + if (self) { + _other = other; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + cpp_union_stage = std::make_shared([_other cppPipelineWithReader:reader]); + } + + isUserDataRead = YES; + return cpp_union_stage; +} + +@end + +@implementation FIRUnnestStageBridge { + FIRExprBridge *_field; + NSString *_Nullable _indexField; + Boolean isUserDataRead; + std::shared_ptr cpp_unnest; +} + +- (id)initWithField:(FIRExprBridge *)field indexField:(NSString *_Nullable)indexField { + self = [super init]; + if (self) { + _field = field; + _indexField = indexField; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + absl::optional cpp_index_field; + if (_indexField != nil) { + cpp_index_field = MakeString(_indexField); + } else { + cpp_index_field = absl::nullopt; + } + cpp_unnest = std::make_shared([_field cppExprWithReader:reader], cpp_index_field); + } + + isUserDataRead = YES; + return cpp_unnest; +} + +@end + +@implementation FIRGenericStageBridge { + NSString *_name; + NSArray *_params; + NSDictionary *_Nullable _options; + Boolean isUserDataRead; + std::shared_ptr cpp_generic_stage; +} + +- (id)initWithName:(NSString *)name + params:(NSArray *)params + options:(NSDictionary *_Nullable)options { + self = [super init]; + if (self) { + _name = name; + _params = params; + _options = options; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector> cpp_params; + for (FIRExprBridge *param in _params) { + cpp_params.push_back([param cppExprWithReader:reader]); + } + std::unordered_map> cpp_options; + if (_options) { + for (NSString *key in _options) { + cpp_options[MakeString(key)] = [_options[key] cppExprWithReader:reader]; + } + } + cpp_generic_stage = std::make_shared(MakeString(_name), std::move(cpp_params), + std::move(cpp_options)); + } + + isUserDataRead = YES; + return cpp_generic_stage; } @end @interface __FIRPipelineSnapshotBridge () -@property(nonatomic, strong, readwrite) NSArray<__FIRPipelineSnapshotBridge *> *results; +@property(nonatomic, strong, readwrite) NSArray<__FIRPipelineResultBridge *> *results; @end @@ -206,6 +832,14 @@ - (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot { return results_; } +- (FIRTimestamp *)execution_time { + if (!snapshot_.has_value()) { + return nil; + } else { + return MakeFIRTimestamp(snapshot_.value().execution_time().timestamp()); + } +} + @end @implementation __FIRPipelineResultBridge { @@ -213,13 +847,13 @@ @implementation __FIRPipelineResultBridge { std::shared_ptr _db; } -- (FIRDocumentReference *)reference { +- (nullable FIRDocumentReference *)reference { if (!_result.internal_key().has_value()) return nil; return [[FIRDocumentReference alloc] initWithKey:_result.internal_key().value() firestore:_db]; } -- (NSString *)documentID { +- (nullable NSString *)documentID { if (!_result.document_id().has_value()) { return nil; } @@ -227,6 +861,22 @@ - (NSString *)documentID { return MakeNSString(_result.document_id().value()); } +- (nullable FIRTimestamp *)create_time { + if (!_result.create_time().has_value()) { + return nil; + } + + return MakeFIRTimestamp(_result.create_time().value().timestamp()); +} + +- (nullable FIRTimestamp *)update_time { + if (!_result.update_time().has_value()) { + return nil; + } + + return MakeFIRTimestamp(_result.update_time().value().timestamp()); +} + - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr)db { self = [super init]; if (self) { @@ -237,15 +887,15 @@ - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr *)data { +- (NSDictionary *)data { return [self dataWithServerTimestampBehavior:FIRServerTimestampBehaviorNone]; } -- (nullable NSDictionary *)dataWithServerTimestampBehavior: +- (NSDictionary *)dataWithServerTimestampBehavior: (FIRServerTimestampBehavior)serverTimestampBehavior { absl::optional data = _result.internal_value()->Get(); - if (!data) return nil; + if (!data) return [NSDictionary dictionary]; FSTUserDataWriter *dataWriter = [[FSTUserDataWriter alloc] initWithFirestore:_db @@ -253,12 +903,35 @@ - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr fieldValue = + _result.internal_value()->Get(fieldPath); + if (!fieldValue) return nil; + FSTUserDataWriter *dataWriter = + [[FSTUserDataWriter alloc] initWithFirestore:_db + serverTimestampBehavior:serverTimestampBehavior]; + return [dataWriter convertedValue:*fieldValue]; +} + @end @implementation FIRPipelineBridge { NSArray *_stages; FIRFirestore *firestore; - std::shared_ptr pipeline; + std::shared_ptr cpp_pipeline; } - (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db { @@ -273,9 +946,9 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r for (FIRStageBridge *stage in _stages) { cpp_stages.push_back([stage cppStageWithReader:firestore.dataReader]); } - pipeline = std::make_shared(cpp_stages, firestore.wrapped); + cpp_pipeline = std::make_shared(cpp_stages, firestore.wrapped); - pipeline->execute([completion](StatusOr maybe_value) { + cpp_pipeline->execute([completion](StatusOr maybe_value) { if (maybe_value.ok()) { __FIRPipelineSnapshotBridge *bridge = [[__FIRPipelineSnapshotBridge alloc] initWithCppSnapshot:std::move(maybe_value).ValueOrDie()]; @@ -286,6 +959,10 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r }); } +- (std::shared_ptr)cppPipelineWithReader:(FSTUserDataReader *)reader { + return cpp_pipeline; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index a27b2b7aa18..7b8ebf80e9b 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -20,31 +20,53 @@ #import "FIRDocumentSnapshot.h" +@class FIRTimestamp; +@class FIRVectorValue; +@class FIRPipelineBridge; + NS_ASSUME_NONNULL_BEGIN +NS_SWIFT_SENDABLE NS_SWIFT_NAME(ExprBridge) @interface FIRExprBridge : NSObject @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(FieldBridge) @interface FIRFieldBridge : FIRExprBridge - (id)init:(NSString *)name; @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(ConstantBridge) @interface FIRConstantBridge : FIRExprBridge - (id)init:(id)input; @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(FunctionExprBridge) @interface FIRFunctionExprBridge : FIRExprBridge - (id)initWithName:(NSString *)name Args:(NSArray *)args; @end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(AggregateFunctionBridge) +@interface FIRAggregateFunctionBridge : NSObject +- (id)initWithName:(NSString *)name Args:(NSArray *)args; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(OrderingBridge) +@interface FIROrderingBridge : NSObject +- (id)initWithExpr:(FIRExprBridge *)expr Direction:(NSString *)direction; +@end + +NS_SWIFT_SENDABLE NS_SWIFT_NAME(StageBridge) @interface FIRStageBridge : NSObject @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(CollectionSourceStageBridge) @interface FIRCollectionSourceStageBridge : FIRStageBridge @@ -52,6 +74,31 @@ NS_SWIFT_NAME(CollectionSourceStageBridge) @end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(DatabaseSourceStageBridge) +@interface FIRDatabaseSourceStageBridge : FIRStageBridge + +- (id)init; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(CollectionGroupSourceStageBridge) +@interface FIRCollectionGroupSourceStageBridge : FIRStageBridge + +- (id)initWithCollectionId:(NSString *)id; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(DocumentsSourceStageBridge) +@interface FIRDocumentsSourceStageBridge : FIRStageBridge + +- (id)initWithDocuments:(NSArray *)documents; + +@end + +NS_SWIFT_SENDABLE NS_SWIFT_NAME(WhereStageBridge) @interface FIRWhereStageBridge : FIRStageBridge @@ -59,26 +106,134 @@ NS_SWIFT_NAME(WhereStageBridge) @end -NS_SWIFT_NAME(__PipelineSnapshotBridge) -@interface __FIRPipelineSnapshotBridge : NSObject +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(LimitStageBridge) +@interface FIRLimitStageBridge : FIRStageBridge + +- (id)initWithLimit:(NSInteger)value; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(OffsetStageBridge) +@interface FIROffsetStageBridge : FIRStageBridge + +- (id)initWithOffset:(NSInteger)value; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(AddFieldsStageBridge) +@interface FIRAddFieldsStageBridge : FIRStageBridge +- (id)initWithFields:(NSDictionary *)fields; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(RemoveFieldsStageBridge) +@interface FIRRemoveFieldsStageBridge : FIRStageBridge +- (id)initWithFields:(NSArray *)fields; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(SelectStageBridge) +@interface FIRSelectStageBridge : FIRStageBridge +- (id)initWithSelections:(NSDictionary *)selections; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(DistinctStageBridge) +@interface FIRDistinctStageBridge : FIRStageBridge +- (id)initWithGroups:(NSDictionary *)groups; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(AggregateStageBridge) +@interface FIRAggregateStageBridge : FIRStageBridge +- (id)initWithAccumulators:(NSDictionary *)accumulators + groups:(NSDictionary *)groups; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(FindNearestStageBridge) +@interface FIRFindNearestStageBridge : FIRStageBridge +- (id)initWithField:(FIRFieldBridge *)field + vectorValue:(FIRVectorValue *)vectorValue + distanceMeasure:(NSString *)distanceMeasure + limit:(NSNumber *_Nullable)limit + distanceField:(NSString *_Nullable)distanceField; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(SortStageBridge) +@interface FIRSorStageBridge : FIRStageBridge +- (id)initWithOrderings:(NSArray *)orderings; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(ReplaceWithStageBridge) +@interface FIRReplaceWithStageBridge : FIRStageBridge +- (id)initWithExpr:(FIRExprBridge *)expr; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(SampleStageBridge) +@interface FIRSampleStageBridge : FIRStageBridge +- (id)initWithCount:(int64_t)count; +- (id)initWithPercentage:(double)percentage; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(UnionStageBridge) +@interface FIRUnionStageBridge : FIRStageBridge +- (id)initWithOther:(FIRPipelineBridge *)other; +@end -@property(nonatomic, strong, readonly) NSArray<__FIRPipelineSnapshotBridge *> *results; +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(UnnestStageBridge) +@interface FIRUnnestStageBridge : FIRStageBridge +- (id)initWithField:(FIRExprBridge *)field indexField:(NSString *_Nullable)indexField; +@end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(GenericStageBridge) +@interface FIRGenericStageBridge : FIRStageBridge +- (id)initWithName:(NSString *)name + params:(NSArray *)params + options:(NSDictionary *_Nullable)options; @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(__PipelineResultBridge) @interface __FIRPipelineResultBridge : NSObject -@property(nonatomic, strong, readonly) FIRDocumentReference *reference; +@property(nonatomic, strong, readonly, nullable) FIRDocumentReference *reference; -@property(nonatomic, copy, readonly) NSString *documentID; +@property(nonatomic, copy, readonly, nullable) NSString *documentID; -- (nullable NSDictionary *)data; -- (nullable NSDictionary *)dataWithServerTimestampBehavior: +@property(nonatomic, strong, readonly, nullable) FIRTimestamp *create_time; + +@property(nonatomic, strong, readonly, nullable) FIRTimestamp *update_time; + +- (NSDictionary *)data; + +- (NSDictionary *)dataWithServerTimestampBehavior: (FIRServerTimestampBehavior)serverTimestampBehavior; +- (nullable id)get:(id)field; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(__PipelineSnapshotBridge) +@interface __FIRPipelineSnapshotBridge : NSObject + +@property(nonatomic, strong, readonly) NSArray<__FIRPipelineResultBridge *> *results; + +@property(nonatomic, strong, readonly) FIRTimestamp *execution_time; + @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(PipelineBridge) @interface FIRPipelineBridge : NSObject diff --git a/Firestore/Swift/Source/ExprImpl.swift b/Firestore/Swift/Source/ExprImpl.swift new file mode 100644 index 00000000000..6d55a7b479b --- /dev/null +++ b/Firestore/Swift/Source/ExprImpl.swift @@ -0,0 +1,607 @@ +// Copyright 2025 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. + +extension Expr { + func toBridge() -> ExprBridge { + return (self as! BridgeWrapper).bridge + } +} + +public extension Expr { + func `as`(_ name: String) -> ExprWithAlias { + return ExprWithAlias(self, name) + } + + // MARK: Arithmetic Operators + + func add(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("add", [self, second] + others) + } + + func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("add", exprs) + } + + func subtract(_ other: Expr) -> FunctionExpr { + return FunctionExpr("subtract", [self, other]) + } + + func subtract(_ other: Sendable) -> FunctionExpr { + return FunctionExpr("subtract", [self, Helper.sendableToExpr(other)]) + } + + func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("multiply", [self, second] + others) + } + + func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("multiply", exprs) + } + + func divide(_ other: Expr) -> FunctionExpr { + return FunctionExpr("divide", [self, other]) + } + + func divide(_ other: Sendable) -> FunctionExpr { + return FunctionExpr("divide", [self, Helper.sendableToExpr(other)]) + } + + func mod(_ other: Expr) -> FunctionExpr { + return FunctionExpr("mod", [self, other]) + } + + func mod(_ other: Sendable) -> FunctionExpr { + return FunctionExpr("mod", [self, Helper.sendableToExpr(other)]) + } + + // MARK: Array Operations + + func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr { + return FunctionExpr("array_concat", [self, secondArray] + otherArrays) + } + + func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(secondArray)] + otherArrays + .map { Helper.sendableToExpr($0) } + return FunctionExpr("array_concat", exprs) + } + + func arrayContains(_ element: Expr) -> BooleanExpr { + return BooleanExpr("array_contains", [self, element]) + } + + func arrayContains(_ element: Sendable) -> BooleanExpr { + return BooleanExpr("array_contains", [self, Helper.sendableToExpr(element)]) + } + + func arrayContainsAll(_ values: Expr...) -> BooleanExpr { + return BooleanExpr("array_contains_all", [self] + values) + } + + func arrayContainsAll(_ values: Sendable...) -> BooleanExpr { + let exprValues = values.map { Helper.sendableToExpr($0) } + return BooleanExpr("array_contains_all", [self] + exprValues) + } + + func arrayContainsAny(_ values: Expr...) -> BooleanExpr { + return BooleanExpr("array_contains_any", [self] + values) + } + + func arrayContainsAny(_ values: Sendable...) -> BooleanExpr { + let exprValues = values.map { Helper.sendableToExpr($0) } + return BooleanExpr("array_contains_any", [self] + exprValues) + } + + func arrayLength() -> FunctionExpr { + return FunctionExpr("array_length", [self]) + } + + func arrayOffset(_ offset: Int) -> FunctionExpr { + return FunctionExpr("array_offset", [self, Helper.sendableToExpr(offset)]) + } + + func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr { + return FunctionExpr("array_offset", [self, offsetExpr]) + } + + func gt(_ other: Expr) -> BooleanExpr { + return BooleanExpr("gt", [self, other]) + } + + func gt(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("gt", [self, exprOther]) + } + + // MARK: - Greater Than or Equal (gte) + + func gte(_ other: Expr) -> BooleanExpr { + return BooleanExpr("gte", [self, other]) + } + + func gte(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("gte", [self, exprOther]) + } + + // MARK: - Less Than (lt) + + func lt(_ other: Expr) -> BooleanExpr { + return BooleanExpr("lt", [self, other]) + } + + func lt(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("lt", [self, exprOther]) + } + + // MARK: - Less Than or Equal (lte) + + func lte(_ other: Expr) -> BooleanExpr { + return BooleanExpr("lte", [self, other]) + } + + func lte(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("lte", [self, exprOther]) + } + + // MARK: - Equal (eq) + + func eq(_ other: Expr) -> BooleanExpr { + return BooleanExpr("eq", [self, other]) + } + + func eq(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("eq", [self, exprOther]) + } + + func neq(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("neq", [self] + others) + } + + func neq(_ others: Sendable...) -> BooleanExpr { + let exprOthers = others.map { Helper.sendableToExpr($0) } + return BooleanExpr("neq", [self] + exprOthers) + } + + func eqAny(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("eq_any", [self] + others) + } + + func eqAny(_ others: Sendable...) -> BooleanExpr { + let exprOthers = others.map { Helper.sendableToExpr($0) } + return BooleanExpr("eq_any", [self] + exprOthers) + } + + func notEqAny(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("not_eq_any", [self] + others) + } + + func notEqAny(_ others: Sendable...) -> BooleanExpr { + let exprOthers = others.map { Helper.sendableToExpr($0) } + return BooleanExpr("not_eq_any", [self] + exprOthers) + } + + // MARK: Checks + + // --- Added Type Check Operations --- + + func isNan() -> BooleanExpr { + return BooleanExpr("is_nan", [self]) + } + + func isNull() -> BooleanExpr { + return BooleanExpr("is_null", [self]) + } + + func exists() -> BooleanExpr { + return BooleanExpr("exists", [self]) + } + + func isError() -> BooleanExpr { + return BooleanExpr("is_error", [self]) + } + + func isAbsent() -> BooleanExpr { + return BooleanExpr("is_absent", [self]) + } + + func isNotNull() -> BooleanExpr { + return BooleanExpr("is_not_null", [self]) + } + + func isNotNan() -> BooleanExpr { + return BooleanExpr("is_not_nan", [self]) + } + + // --- Added String Operations --- + + func charLength() -> FunctionExpr { + return FunctionExpr("char_length", [self]) + } + + func like(_ pattern: String) -> FunctionExpr { + return FunctionExpr("like", [self, Helper.sendableToExpr(pattern)]) + } + + func like(_ pattern: Expr) -> FunctionExpr { + return FunctionExpr("like", [self, pattern]) + } + + func regexContains(_ pattern: String) -> BooleanExpr { + return BooleanExpr("regex_contains", [self, Helper.sendableToExpr(pattern)]) + } + + func regexContains(_ pattern: Expr) -> BooleanExpr { + return BooleanExpr("regex_contains", [self, pattern]) + } + + func regexMatch(_ pattern: String) -> BooleanExpr { + return BooleanExpr("regex_match", [self, Helper.sendableToExpr(pattern)]) + } + + func regexMatch(_ pattern: Expr) -> BooleanExpr { + return BooleanExpr("regex_match", [self, pattern]) + } + + func strContains(_ substring: String) -> BooleanExpr { + return BooleanExpr("str_contains", [self, Helper.sendableToExpr(substring)]) + } + + func strContains(_ expr: Expr) -> BooleanExpr { + return BooleanExpr("str_contains", [self, expr]) + } + + func startsWith(_ prefix: String) -> BooleanExpr { + return BooleanExpr("starts_with", [self, Helper.sendableToExpr(prefix)]) + } + + func startsWith(_ prefix: Expr) -> BooleanExpr { + return BooleanExpr("starts_with", [self, prefix]) + } + + func endsWith(_ suffix: String) -> BooleanExpr { + return BooleanExpr("ends_with", [self, Helper.sendableToExpr(suffix)]) + } + + func endsWith(_ suffix: Expr) -> BooleanExpr { + return BooleanExpr("ends_with", [self, suffix]) + } + + func lowercased() -> FunctionExpr { + return FunctionExpr("to_lower", [self]) + } + + func uppercased() -> FunctionExpr { + return FunctionExpr("to_upper", [self]) + } + + func trim() -> FunctionExpr { + return FunctionExpr("trim", [self]) + } + + func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr { + return FunctionExpr("str_concat", [self, secondString] + otherStrings) + } + + func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(secondString)] + otherStrings + .map { Helper.sendableToExpr($0) } + return FunctionExpr("str_concat", exprs) + } + + func reverse() -> FunctionExpr { + return FunctionExpr("reverse", [self]) + } + + func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr { + return FunctionExpr( + "replace_first", + [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] + ) + } + + func replaceFirst(_ find: Expr, _ replace: Expr) -> FunctionExpr { + return FunctionExpr("replace_first", [self, find, replace]) + } + + func replaceAll(_ find: String, _ replace: String) -> FunctionExpr { + return FunctionExpr( + "replace_all", + [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] + ) + } + + func replaceAll(_ find: Expr, _ replace: Expr) -> FunctionExpr { + return FunctionExpr("replace_all", [self, find, replace]) + } + + func byteLength() -> FunctionExpr { + return FunctionExpr("byte_length", [self]) + } + + func substr(_ position: Int, _ length: Int? = nil) -> FunctionExpr { + let positionExpr = Helper.sendableToExpr(position) + if let length = length { + return FunctionExpr("substr", [self, positionExpr, Helper.sendableToExpr(length)]) + } else { + return FunctionExpr("substr", [self, positionExpr]) + } + } + + func substr(_ position: Expr, _ length: Expr? = nil) -> FunctionExpr { + if let length = length { + return FunctionExpr("substr", [self, position, length]) + } else { + return FunctionExpr("substr", [self, position]) + } + } + + // --- Added Map Operations --- + + func mapGet(_ subfield: String) -> FunctionExpr { + return FunctionExpr("map_get", [self, Constant(subfield)]) + } + + func mapRemove(_ key: String) -> FunctionExpr { + return FunctionExpr("map_remove", [self, Helper.sendableToExpr(key)]) + } + + func mapRemove(_ keyExpr: Expr) -> FunctionExpr { + return FunctionExpr("map_remove", [self, keyExpr]) + } + + func mapMerge(_ secondMap: [String: Sendable], + _ otherMaps: [String: Sendable]...) -> FunctionExpr { + let secondMapExpr = Helper.sendableToExpr(secondMap) + let otherMapExprs = otherMaps.map { Helper.sendableToExpr($0) } + return FunctionExpr("map_merge", [self, secondMapExpr] + otherMapExprs) + } + + func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr { + return FunctionExpr("map_merge", [self, secondMap] + otherMaps) + } + + // --- Added Aggregate Operations (on Expr) --- + + func count() -> AggregateFunction { + return AggregateFunction("count", [self]) + } + + func sum() -> AggregateFunction { + return AggregateFunction("sum", [self]) + } + + func avg() -> AggregateFunction { + return AggregateFunction("avg", [self]) + } + + func minimum() -> AggregateFunction { + return AggregateFunction("minimum", [self]) + } + + func maximum() -> AggregateFunction { + return AggregateFunction("maximum", [self]) + } + + // MARK: Logical min/max + + func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("logical_maximum", [self, second] + others) + } + + func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("logical_maximum", exprs) + } + + func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("logical_min", [self, second] + others) + } + + func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("logical_min", exprs) + } + + // MARK: Vector Operations + + func vectorLength() -> FunctionExpr { + return FunctionExpr("vector_length", [self]) + } + + func cosineDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, other]) + } + + func cosineDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) + } + + func cosineDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) + } + + func dotProduct(_ other: Expr) -> FunctionExpr { + return FunctionExpr("dot_product", [self, other]) + } + + func dotProduct(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) + } + + func dotProduct(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) + } + + func euclideanDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, other]) + } + + func euclideanDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) + } + + func euclideanDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) + } + + func manhattanDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, other]) + } + + func manhattanDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) + } + + func manhattanDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) + } + + // MARK: Timestamp operations + + func unixMicrosToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_micros_to_timestamp", [self]) + } + + func timestampToUnixMicros() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_micros", [self]) + } + + func unixMillisToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_millis_to_timestamp", [self]) + } + + func timestampToUnixMillis() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_millis", [self]) + } + + func unixSecondsToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_seconds_to_timestamp", [self]) + } + + func timestampToUnixSeconds() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_seconds", [self]) + } + + func timestampAdd(_ unit: Expr, _ amount: Expr) -> FunctionExpr { + return FunctionExpr("timestamp_add", [self, unit, amount]) + } + + func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { + return FunctionExpr( + "timestamp_add", + [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] + ) + } + + func timestampSub(_ unit: Expr, _ amount: Expr) -> FunctionExpr { + return FunctionExpr("timestamp_sub", [self, unit, amount]) + } + + func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { + return FunctionExpr( + "timestamp_sub", + [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] + ) + } + + // MARK: - Bitwise operations + + func bitAnd(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitAnd(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitAnd(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_and", [self, bitsExpression]) + } + + func bitOr(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitOr(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitOr(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_or", [self, bitsExpression]) + } + + func bitXor(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitXor(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitXor(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, bitsExpression]) + } + + func bitNot() -> FunctionExpr { + return FunctionExpr("bit_not", [self]) + } + + func bitLeftShift(_ y: Int) -> FunctionExpr { + return FunctionExpr("bit_left_shift", [self, Helper.sendableToExpr(y)]) + } + + func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr { + return FunctionExpr("bit_left_shift", [self, numberExpr]) + } + + func bitRightShift(_ y: Int) -> FunctionExpr { + return FunctionExpr("bit_right_shift", [self, Helper.sendableToExpr(y)]) + } + + func bitRightShift(_ numberExpr: Expr) -> FunctionExpr { + return FunctionExpr("bit_right_shift", [self, numberExpr]) + } + + func documentId() -> FunctionExpr { + return FunctionExpr("document_id", [self]) + } + + func ifError(_ catchExpr: Expr) -> FunctionExpr { + return FunctionExpr("if_error", [self, catchExpr]) + } + + func ifError(_ catchValue: Sendable) -> FunctionExpr { + return FunctionExpr("if_error", [self, Helper.sendableToExpr(catchValue)]) + } + + // MARK: Sorting + + func ascending() -> Ordering { + return Ordering(expr: self, direction: .ascending) + } + + func descending() -> Ordering { + return Ordering(expr: self, direction: .descending) + } +} diff --git a/Firestore/Swift/Source/Helper/PipelineHelper.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift new file mode 100644 index 00000000000..582e90021b1 --- /dev/null +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -0,0 +1,51 @@ +// Copyright 2025 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. + +enum Helper { + static func sendableToExpr(_ value: Sendable) -> Expr { + if value is Expr { + return value as! Expr + } else if value is [String: Sendable] { + return map(value as! [String: Sendable]) + } else if value is [Sendable] { + return array(value as! [Sendable]) + } else { + return Constant(value) + } + } + + static func selectablesToMap(selectables: [Selectable]) -> [String: Expr] { + let exprMap = selectables.reduce(into: [String: Expr]()) { result, selectable in + let value = selectable as! SelectableWrapper + result[value.alias] = value.expr + } + return exprMap + } + + static func map(_ elements: [String: Sendable]) -> FunctionExpr { + var result: [Expr] = [] + for (key, value) in elements { + result.append(Constant(key)) + result.append(sendableToExpr(value)) + } + return FunctionExpr("map", result) + } + + static func array(_ elements: [Sendable]) -> FunctionExpr { + let transformedElements = elements.map { element in + sendableToExpr(element) + } + return FunctionExpr("array", transformedElements) + } +} diff --git a/Firestore/Swift/Source/PipelineWrapper.swift b/Firestore/Swift/Source/PipelineWrapper.swift new file mode 100644 index 00000000000..a057c2e4ea2 --- /dev/null +++ b/Firestore/Swift/Source/PipelineWrapper.swift @@ -0,0 +1,26 @@ +// Copyright 2025 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. + +protocol BridgeWrapper { + var bridge: ExprBridge { get } +} + +protocol AggregateBridgeWrapper { + var bridge: AggregateFunctionBridge { get } +} + +protocol SelectableWrapper: Sendable { + var alias: String { get } + var expr: Expr { get } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Expressions.swift b/Firestore/Swift/Source/SwiftAPI/Expressions.swift deleted file mode 100644 index 22af7ae6471..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Expressions.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2025 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 Foundation - -public protocol Expr {} - -protocol BridgeWrapper { - var bridge: ExprBridge { get } -} - -public struct Constant: Expr, BridgeWrapper { - var bridge: ExprBridge - - var value: Any - init(value: Any) { - self.value = value - bridge = ConstantBridge(value) - } -} - -public func constant(_ number: Any) -> Constant { - return Constant(value: number) -} - -public struct Field: Expr, BridgeWrapper { - var bridge: ExprBridge - - var name: String - init(name: String) { - self.name = name - bridge = FieldBridge(name) - } -} - -public func field(_ name: String) -> Field { - return Field(name: name) -} - -protocol Function: Expr { - var name: String { get } -} - -public struct FunctionExpr: Function, BridgeWrapper { - var bridge: ExprBridge - - var name: String - private var args: [Expr] - - init(name: String, args: [Expr]) { - self.name = name - self.args = args - bridge = FunctionExprBridge( - name: name, - args: args.map { ($0 as! (Expr & BridgeWrapper)).bridge - } - ) - } -} - -public func eq(_ left: Expr, _ right: Expr) -> FunctionExpr { - return FunctionExpr(name: "eq", args: [left, right]) -} diff --git a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift index 0179ece4e04..e35a9bceac5 100644 --- a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift @@ -14,10 +14,16 @@ * limitations under the License. */ +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE import Foundation @objc public extension Firestore { + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @nonobjc func pipeline() -> PipelineSource { - return PipelineSource(db: self) + return PipelineSource(self) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline.swift deleted file mode 100644 index 8c8a4364d30..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2025 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 Foundation - -public struct Pipeline { - private var stages: [Stage] - private var bridge: PipelineBridge - private let db: Firestore - - init(stages: [Stage], db: Firestore) { - self.stages = stages - self.db = db - bridge = PipelineBridge(stages: stages.map { $0.bridge }, db: db) - } - - public func `where`(_ condition: Expr) -> Pipeline { - return Pipeline(stages: stages + [Where(condition: condition)], db: db) - } - - public func execute() async throws -> PipelineSnapshot { - return try await withCheckedThrowingContinuation { continuation in - self.bridge.execute { result, error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: PipelineSnapshot(result!)) - } - } - } - } -} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift new file mode 100644 index 00000000000..ed7c25bd129 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift @@ -0,0 +1,34 @@ +// Copyright 2025 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. + +public class AggregateFunction: AggregateBridgeWrapper, @unchecked Sendable { + let bridge: AggregateFunctionBridge + + let functionName: String + let args: [Expr] + + public init(_ functionName: String, _ args: [Expr]) { + self.functionName = functionName + self.args = args + bridge = AggregateFunctionBridge( + name: functionName, + args: self.args.map { $0.toBridge() + } + ) + } + + public func `as`(_ name: String) -> AggregateWithAlias { + return AggregateWithAlias(aggregate: self, alias: name) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateWithAlias.swift new file mode 100644 index 00000000000..8a1871907c6 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateWithAlias.swift @@ -0,0 +1,18 @@ +// Copyright 2025 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. + +public struct AggregateWithAlias { + public let aggregate: AggregateFunction + public let alias: String +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/CountAll.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/CountAll.swift new file mode 100644 index 00000000000..064eb6d99bc --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/CountAll.swift @@ -0,0 +1,19 @@ +// Copyright 2025 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. + +public class CountAll: AggregateFunction, @unchecked Sendable { + public init() { + super.init("count", []) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift new file mode 100644 index 00000000000..7a70cfbc77b --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift @@ -0,0 +1,19 @@ +// Copyright 2025 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. + +public class ArrayContains: BooleanExpr, @unchecked Sendable { + public init(fieldName: String, values: Sendable...) { + super.init("array_contains", values.map { Helper.sendableToExpr($0) }) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ascending.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ascending.swift new file mode 100644 index 00000000000..e872b6e7f8a --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ascending.swift @@ -0,0 +1,19 @@ +// Copyright 2025 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. + +public class Ascending: Ordering, @unchecked Sendable { + public init(_ fieldName: String) { + super.init(expr: Field(fieldName), direction: .ascending) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Descending.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Descending.swift new file mode 100644 index 00000000000..584d7b7ada3 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Descending.swift @@ -0,0 +1,19 @@ +// Copyright 2025 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. + +public class Descending: Ordering, @unchecked Sendable { + public init(_ fieldName: String) { + super.init(expr: Field(fieldName), direction: .descending) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift new file mode 100644 index 00000000000..6bd54e9e71b --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift @@ -0,0 +1,47 @@ +// Copyright 2025 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +import Foundation + +public struct DistanceMeasure: Sendable, Equatable, Hashable { + let kind: Kind + + enum Kind: String { + case euclidean + case cosine + case dotProduct = "dot_product" + } + + public static var euclidean: DistanceMeasure { + return self.init(kind: .euclidean) + } + + public static var cosine: DistanceMeasure { + return self.init(kind: .cosine) + } + + public static var dotProduct: DistanceMeasure { + return self.init(kind: .dotProduct) + } + + init(kind: Kind) { + self.kind = kind + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift new file mode 100644 index 00000000000..7cd9b0d5adf --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -0,0 +1,1558 @@ +// Copyright 2025 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + +// TODO: the implementation of `Expr` is not complete +public protocol Expr: Sendable { + /// Assigns an alias to this expression. + /// + /// Aliases are useful for renaming fields in the output of a stage or for giving meaningful + /// names to calculated values. + /// + /// ```swift + /// // Calculate total price and alias it "totalPrice" + /// Field("price").multiply(Field("quantity")).`as`("totalPrice") + /// ``` + /// + /// - Parameter name: The alias to assign to this expression. + /// - Returns: A new `ExprWithAlias` wrapping this expression with the alias. + func `as`(_ name: String) -> ExprWithAlias + + // --- Added Mathematical Operations --- + + /// Creates an expression that adds this expression to one or more other expressions. + /// Assumes `self` and all parameters evaluate to compatible types for addition (e.g., numbers, or + /// string/array concatenation if supported by the specific "add" implementation). + /// + /// ```swift + /// // Add the value of the 'quantity' field and the 'reserve' field. + /// Field("quantity").add(Field("reserve")) + /// + /// // Add multiple numeric fields + /// Field("subtotal").add(Field("tax"), Field("shipping")) + /// ``` + /// + /// - Parameter second: An `Expr` to add to this expression. + /// - Parameter others: Optional additional `Expr` values to add. + /// - Returns: A new `FunctionExpr` representing the addition operation. + func add(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that adds this expression to one or more literal values. + /// Assumes `self` and all parameters evaluate to compatible types for addition. + /// + /// ```swift + /// // Add 5 to the 'count' field + /// Field("count").add(5) + /// + /// // Add multiple literal numbers + /// Field("score").add(10, 20, -5) + /// ``` + /// + /// - Parameter second: A `Sendable` literal value to add to this expression. + /// - Parameter others: Optional additional `Sendable` literal values to add. + /// - Returns: A new `FunctionExpr` representing the addition operation. + func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr + + /// Creates an expression that subtracts another expression from this expression. + /// Assumes `self` and `other` evaluate to numeric types. + /// + /// ```swift + /// // Subtract the 'discount' field from the 'price' field + /// Field("price").subtract(Field("discount")) + /// ``` + /// + /// - Parameter other: The `Expr` (evaluating to a number) to subtract from this expression. + /// - Returns: A new `FunctionExpr` representing the subtraction operation. + func subtract(_ other: Expr) -> FunctionExpr + + /// Creates an expression that subtracts a literal value from this expression. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Subtract 20 from the value of the 'total' field + /// Field("total").subtract(20) + /// ``` + /// + /// - Parameter other: The `Sendable` literal (numeric) value to subtract from this expression. + /// - Returns: A new `FunctionExpr` representing the subtraction operation. + func subtract(_ other: Sendable) -> FunctionExpr + + /// Creates an expression that multiplies this expression by one or more other expressions. + /// Assumes `self` and all parameters evaluate to numeric types. + /// + /// ```swift + /// // Multiply the 'quantity' field by the 'price' field + /// Field("quantity").multiply(Field("price")) + /// + /// // Multiply 'rate' by 'time' and 'conversionFactor' fields + /// Field("rate").multiply(Field("time"), Field("conversionFactor")) + /// ``` + /// + /// - Parameter second: An `Expr` to multiply by. + /// - Parameter others: Optional additional `Expr` values to multiply by. + /// - Returns: A new `FunctionExpr` representing the multiplication operation. + func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that multiplies this expression by one or more literal values. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Multiply the 'score' by 1.1 + /// Field("score").multiply(1.1) + /// + /// // Multiply 'base' by 2 and then by 3.0 + /// Field("base").multiply(2, 3.0) + /// ``` + /// + /// - Parameter second: A `Sendable` literal value to multiply by. + /// - Parameter others: Optional additional `Sendable` literal values to multiply by. + /// - Returns: A new `FunctionExpr` representing the multiplication operation. + func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr + + /// Creates an expression that divides this expression by another expression. + /// Assumes `self` and `other` evaluate to numeric types. + /// + /// ```swift + /// // Divide the 'total' field by the 'count' field + /// Field("total").divide(Field("count")) + /// ``` + /// + /// - Parameter other: The `Expr` (evaluating to a number) to divide by. + /// - Returns: A new `FunctionExpr` representing the division operation. + func divide(_ other: Expr) -> FunctionExpr + + /// Creates an expression that divides this expression by a literal value. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Divide the 'value' field by 10 + /// Field("value").divide(10) + /// ``` + /// + /// - Parameter other: The `Sendable` literal (numeric) value to divide by. + /// - Returns: A new `FunctionExpr` representing the division operation. + func divide(_ other: Sendable) -> FunctionExpr + + /// Creates an expression that calculates the modulo (remainder) of dividing this expression by + /// another expression. + /// Assumes `self` and `other` evaluate to numeric types. + /// + /// ```swift + /// // Calculate the remainder of dividing the 'value' field by the 'divisor' field + /// Field("value").mod(Field("divisor")) + /// ``` + /// + /// - Parameter other: The `Expr` (evaluating to a number) to use as the divisor. + /// - Returns: A new `FunctionExpr` representing the modulo operation. + func mod(_ other: Expr) -> FunctionExpr + + /// Creates an expression that calculates the modulo (remainder) of dividing this expression by a + /// literal value. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Calculate the remainder of dividing the 'value' field by 10 + /// Field("value").mod(10) + /// ``` + /// + /// - Parameter other: The `Sendable` literal (numeric) value to use as the divisor. + /// - Returns: A new `FunctionExpr` representing the modulo operation. + func mod(_ other: Sendable) -> FunctionExpr + + // --- Added Array Operations --- + + /// Creates an expression that concatenates an array expression (from `self`) with one or more + /// other array expressions. + /// Assumes `self` and all parameters evaluate to arrays. + /// + /// ```swift + /// // Combine the 'items' array with 'otherItems' and 'archiveItems' array fields. + /// Field("items").arrayConcat(Field("otherItems"), Field("archiveItems")) + /// ``` + /// - Parameter secondArray: An `Expr` (evaluating to an array) to concatenate. + /// - Parameter otherArrays: Optional additional `Expr` values (evaluating to arrays) to + /// concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated array. + func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr + + /// Creates an expression that concatenates an array expression (from `self`) with one or more + /// array literals. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Combine 'tags' (an array field) with ["new", "featured"] and ["urgent"] + /// Field("tags").arrayConcat(["new", "featured"], ["urgent"]) + /// ``` + /// - Parameter secondArray: An array literal of `Sendable` values to concatenate. + /// - Parameter otherArrays: Optional additional array literals of `Sendable` values to + /// concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated array. + func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr + + /// Creates an expression that checks if an array (from `self`) contains a specific element + /// expression. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'sizes' contains the value from 'selectedSize' field + /// Field("sizes").arrayContains(Field("selectedSize")) + /// ``` + /// + /// - Parameter element: The `Expr` representing the element to search for in the array. + /// - Returns: A new `BooleanExpr` representing the 'array_contains' comparison. + func arrayContains(_ element: Expr) -> BooleanExpr + + /// Creates an expression that checks if an array (from `self`) contains a specific literal + /// element. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'colors' array contains "red" + /// Field("colors").arrayContains("red") + /// ``` + /// + /// - Parameter element: The `Sendable` literal element to search for in the array. + /// - Returns: A new `BooleanExpr` representing the 'array_contains' comparison. + func arrayContains(_ element: Sendable) -> BooleanExpr + + /// Creates an expression that checks if an array (from `self`) contains all the specified element + /// expressions. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'candidateSkills' contains all skills from 'requiredSkill1' and 'requiredSkill2' + /// fields + /// Field("candidateSkills").arrayContainsAll(Field("requiredSkill1"), Field("requiredSkill2")) + /// ``` + /// + /// - Parameter values: A variadic list of `Expr` elements to check for in the array represented + /// by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_all' comparison. + func arrayContainsAll(_ values: Expr...) -> BooleanExpr + + /// Creates an expression that checks if an array (from `self`) contains all the specified literal + /// elements. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'tags' contains both "urgent" and "review" + /// Field("tags").arrayContainsAll("urgent", "review") + /// ``` + /// + /// - Parameter values: A variadic list of `Sendable` literal elements to check for in the array + /// represented by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_all' comparison. + func arrayContainsAll(_ values: Sendable...) -> BooleanExpr + + /// Creates an expression that checks if an array (from `self`) contains any of the specified + /// element expressions. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'userGroups' contains any group from 'allowedGroup1' or 'allowedGroup2' fields + /// Field("userGroups").arrayContainsAny(Field("allowedGroup1"), Field("allowedGroup2")) + /// ``` + /// + /// - Parameter values: A variadic list of `Expr` elements to check for in the array represented + /// by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_any' comparison. + func arrayContainsAny(_ values: Expr...) -> BooleanExpr + + /// Creates an expression that checks if an array (from `self`) contains any of the specified + /// literal elements. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'categories' contains either "electronics" or "books" + /// Field("categories").arrayContainsAny("electronics", "books") + /// ``` + /// + /// - Parameter values: A variadic list of `Sendable` literal elements to check for in the array + /// represented by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_any' comparison. + func arrayContainsAny(_ values: Sendable...) -> BooleanExpr + + /// Creates an expression that calculates the length of an array. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Get the number of items in the 'cart' array + /// Field("cart").arrayLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length of the array. + func arrayLength() -> FunctionExpr + + /// Creates an expression that accesses an element in an array (from `self`) at the specified + /// integer offset. + /// A negative offset starts from the end. If the offset is out of bounds, an error may be + /// returned during evaluation. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Return the value in the 'tags' field array at index 1. + /// Field("tags").arrayOffset(1) + /// // Return the last element in the 'tags' field array. + /// Field("tags").arrayOffset(-1) + /// ``` + /// + /// - Parameter offset: The literal `Int` offset of the element to return. + /// - Returns: A new `FunctionExpr` representing the 'arrayOffset' operation. + func arrayOffset(_ offset: Int) -> FunctionExpr + + /// Creates an expression that accesses an element in an array (from `self`) at the offset + /// specified by an expression. + /// A negative offset starts from the end. If the offset is out of bounds, an error may be + /// returned during evaluation. + /// Assumes `self` evaluates to an array and `offsetExpr` evaluates to an integer. + /// + /// ```swift + /// // Return the value in the tags field array at index specified by field 'favoriteTagIndex'. + /// Field("tags").arrayOffset(Field("favoriteTagIndex")) + /// ``` + /// + /// - Parameter offsetExpr: An `Expr` (evaluating to an Int) representing the offset of the + /// element to return. + /// - Returns: A new `FunctionExpr` representing the 'arrayOffset' operation. + func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr + + // MARK: Equality with Sendable + + /// Creates an expression that checks if this expression is equal to any of the provided + /// expression values. + /// This is similar to an "IN" operator in SQL. + /// + /// ```swift + /// // Check if 'categoryID' field is equal to 'featuredCategory' or 'popularCategory' fields + /// Field("categoryID").eqAny(Field("featuredCategory"), Field("popularCategory")) + /// ``` + /// + /// - Parameter others: A variadic list of `Expr` values to check against. + /// - Returns: A new `BooleanExpr` representing the 'IN' comparison (eq_any). + func eqAny(_ others: Expr...) -> BooleanExpr + + /// Creates an expression that checks if this expression is equal to any of the provided literal + /// values. + /// This is similar to an "IN" operator in SQL. + /// + /// ```swift + /// // Check if 'category' is "Electronics", "Books", or "Home Goods" + /// Field("category").eqAny("Electronics", "Books", "Home Goods") + /// ``` + /// + /// - Parameter others: A variadic list of `Sendable` literal values to check against. + /// - Returns: A new `BooleanExpr` representing the 'IN' comparison (eq_any). + func eqAny(_ others: Sendable...) -> BooleanExpr + + /// Creates an expression that checks if this expression is not equal to any of the provided + /// expression values. + /// This is similar to a "NOT IN" operator in SQL. + /// + /// ```swift + /// // Check if 'statusValue' is not equal to 'archivedStatus' or 'deletedStatus' fields + /// Field("statusValue").notEqAny(Field("archivedStatus"), Field("deletedStatus")) + /// ``` + /// + /// - Parameter others: A variadic list of `Expr` values to check against. + /// - Returns: A new `BooleanExpr` representing the 'NOT IN' comparison (not_eq_any). + func notEqAny(_ others: Expr...) -> BooleanExpr + + /// Creates an expression that checks if this expression is not equal to any of the provided + /// literal values. + /// This is similar to a "NOT IN" operator in SQL. + /// + /// ```swift + /// // Check if 'status' is neither "pending" nor "archived" + /// Field("status").notEqAny("pending", "archived") + /// ``` + /// + /// - Parameter others: A variadic list of `Sendable` literal values to check against. + /// - Returns: A new `BooleanExpr` representing the 'NOT IN' comparison (not_eq_any). + func notEqAny(_ others: Sendable...) -> BooleanExpr + + // MARK: Checks + + /// Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Check if the result of a calculation is NaN + /// Field("value").divide(0).isNan() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNaN' check. + func isNan() -> BooleanExpr + + /// Creates an expression that checks if this expression evaluates to 'Null'. + /// + /// ```swift + /// // Check if the 'optionalField' is null + /// Field("optionalField").isNull() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNull' check. + func isNull() -> BooleanExpr + + /// Creates an expression that checks if a field exists in the document. + /// + /// - Note: This typically only makes sense when `self` is a `Field` expression. + /// + /// ```swift + /// // Check if the document has a field named "phoneNumber" + /// Field("phoneNumber").exists() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'exists' check. + func exists() -> BooleanExpr + + /// Creates an expression that checks if this expression produces an error during evaluation. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Check if accessing a non-existent array index causes an error + /// Field("myArray").arrayOffset(100).isError() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isError' check. + func isError() -> BooleanExpr + + /// Creates an expression that returns `true` if the result of this expression + /// is absent (e.g., a field does not exist in a map). Otherwise, returns `false`, even if the + /// value is `null`. + /// + /// - Note: This API is in beta. + /// - Note: This typically only makes sense when `self` is a `Field` expression. + /// + /// ```swift + /// // Check if the field `value` is absent. + /// Field("value").isAbsent() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isAbsent' check. + func isAbsent() -> BooleanExpr + + /// Creates an expression that checks if the result of this expression is not null. + /// + /// ```swift + /// // Check if the value of the 'name' field is not null + /// Field("name").isNotNull() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNotNull' check. + func isNotNull() -> BooleanExpr + + /// Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a + /// Number). + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Check if the result of a calculation is NOT NaN + /// Field("value").divide(Field("count")).isNotNan() // Assuming count might be 0 + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNotNaN' check. + func isNotNan() -> BooleanExpr + + // MARK: String Operations + + /// Creates an expression that calculates the character length of a string in UTF-8. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Get the character length of the 'name' field in its UTF-8 form. + /// Field("name").charLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length of the string. + func charLength() -> FunctionExpr + + /// Creates an expression that performs a case-sensitive string comparison using wildcards against + /// a literal pattern. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'title' field contains the word "guide" (case-sensitive) + /// Field("title").like("%guide%") + /// ``` + /// + /// - Parameter pattern: The literal string pattern to search for. Use "%" as a wildcard. + /// - Returns: A new `FunctionExpr` representing the 'like' comparison. + func like(_ pattern: String) -> FunctionExpr + + /// Creates an expression that performs a case-sensitive string comparison using wildcards against + /// an expression pattern. + /// Assumes `self` evaluates to a string, and `pattern` evaluates to a string. + /// + /// ```swift + /// // Check if 'filename' matches a pattern stored in 'patternField' + /// Field("filename").like(Field("patternField")) + /// ``` + /// + /// - Parameter pattern: An `Expr` (evaluating to a string) representing the pattern to search + /// for. + /// - Returns: A new `FunctionExpr` representing the 'like' comparison. + func like(_ pattern: Expr) -> FunctionExpr + + /// Creates an expression that checks if a string (from `self`) contains a specified regular + /// expression literal as a substring. + /// Uses RE2 syntax. Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if 'description' contains "example" (case-insensitive) + /// Field("description").regexContains("(?i)example") + /// ``` + /// + /// - Parameter pattern: The literal string regular expression to use for the search. + /// - Returns: A new `BooleanExpr` representing the 'regex_contains' comparison. + func regexContains(_ pattern: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) contains a specified regular + /// expression (from an expression) as a substring. + /// Uses RE2 syntax. Assumes `self` evaluates to a string, and `pattern` evaluates to a string. + /// + /// ```swift + /// // Check if 'logEntry' contains a pattern from 'errorPattern' field + /// Field("logEntry").regexContains(Field("errorPattern")) + /// ``` + /// + /// - Parameter pattern: An `Expr` (evaluating to a string) representing the regular expression to + /// use for the search. + /// - Returns: A new `BooleanExpr` representing the 'regex_contains' comparison. + func regexContains(_ pattern: Expr) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) matches a specified regular + /// expression literal entirely. + /// Uses RE2 syntax. Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'email' field matches a valid email pattern + /// Field("email").regexMatch("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}") + /// ``` + /// + /// - Parameter pattern: The literal string regular expression to use for the match. + /// - Returns: A new `BooleanExpr` representing the regular expression match. + func regexMatch(_ pattern: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) matches a specified regular + /// expression (from an expression) entirely. + /// Uses RE2 syntax. Assumes `self` evaluates to a string, and `pattern` evaluates to a string. + /// + /// ```swift + /// // Check if 'input' matches the regex stored in 'validationRegex' + /// Field("input").regexMatch(Field("validationRegex")) + /// ``` + /// + /// - Parameter pattern: An `Expr` (evaluating to a string) representing the regular expression to + /// use for the match. + /// - Returns: A new `BooleanExpr` representing the regular expression match. + func regexMatch(_ pattern: Expr) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) contains a specified literal + /// substring (case-sensitive). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'description' field contains "example". + /// Field("description").strContains("example") + /// ``` + /// + /// - Parameter substring: The literal string substring to search for. + /// - Returns: A new `BooleanExpr` representing the 'str_contains' comparison. + func strContains(_ substring: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) contains a specified substring + /// from an expression (case-sensitive). + /// Assumes `self` evaluates to a string, and `expr` evaluates to a string. + /// + /// ```swift + /// // Check if the 'message' field contains the value of the 'keyword' field. + /// Field("message").strContains(Field("keyword")) + /// ``` + /// + /// - Parameter expr: An `Expr` (evaluating to a string) representing the substring to search for. + /// - Returns: A new `BooleanExpr` representing the 'str_contains' comparison. + func strContains(_ expr: Expr) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) starts with a given literal prefix + /// (case-sensitive). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'name' field starts with "Mr." + /// Field("name").startsWith("Mr.") + /// ``` + /// + /// - Parameter prefix: The literal string prefix to check for. + /// - Returns: A new `BooleanExpr` representing the 'starts_with' comparison. + func startsWith(_ prefix: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) starts with a given prefix from an + /// expression (case-sensitive). + /// Assumes `self` evaluates to a string, and `prefix` evaluates to a string. + /// + /// ```swift + /// // Check if 'fullName' starts with the value of 'firstName' + /// Field("fullName").startsWith(Field("firstName")) + /// ``` + /// + /// - Parameter prefix: An `Expr` (evaluating to a string) representing the prefix to check for. + /// - Returns: A new `BooleanExpr` representing the 'starts_with' comparison. + func startsWith(_ prefix: Expr) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) ends with a given literal suffix + /// (case-sensitive). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'filename' field ends with ".txt" + /// Field("filename").endsWith(".txt") + /// ``` + /// + /// - Parameter suffix: The literal string suffix to check for. + /// - Returns: A new `BooleanExpr` representing the 'ends_with' comparison. + func endsWith(_ suffix: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) ends with a given suffix from an + /// expression (case-sensitive). + /// Assumes `self` evaluates to a string, and `suffix` evaluates to a string. + /// + /// ```swift + /// // Check if 'url' ends with the value of 'extension' field + /// Field("url").endsWith(Field("extension")) + /// ``` + /// + /// - Parameter suffix: An `Expr` (evaluating to a string) representing the suffix to check for. + /// - Returns: A new `BooleanExpr` representing the 'ends_with' comparison. + func endsWith(_ suffix: Expr) -> BooleanExpr + + /// Creates an expression that converts a string (from `self`) to lowercase. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Convert the 'name' field to lowercase + /// Field("name").lowercased() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the lowercase string. + func lowercased() -> FunctionExpr + + /// Creates an expression that converts a string (from `self`) to uppercase. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Convert the 'title' field to uppercase + /// Field("title").uppercased() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the uppercase string. + func uppercased() -> FunctionExpr + + /// Creates an expression that removes leading and trailing whitespace from a string (from + /// `self`). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Trim whitespace from the 'userInput' field + /// Field("userInput").trim() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the trimmed string. + func trim() -> FunctionExpr + + /// Creates an expression that concatenates this string expression with other string expressions. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Combine 'part1', 'part2', and 'part3' fields + /// Field("part1").strConcat(Field("part2"), Field("part3")) + /// ``` + /// + /// - Parameter secondString: An `Expr` (evaluating to a string) to concatenate. + /// - Parameter otherStrings: Optional additional `Expr` (evaluating to strings) to concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated string. + func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr + + /// Creates an expression that concatenates this string expression with other string literals. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Combine 'firstName', " ", and 'lastName' + /// Field("firstName").strConcat(" ", "lastName") + /// ``` + /// + /// - Parameter secondString: A string literal to concatenate. + /// - Parameter otherStrings: Optional additional string literals to concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated string. + func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr + + /// Creates an expression that reverses this string expression. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Reverse the value of the 'myString' field. + /// Field("myString").reverse() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the reversed string. + func reverse() -> FunctionExpr + + /// Creates an expression that replaces the first occurrence of a literal substring within this + /// string expression with another literal substring. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Replace the first "hello" with "hi" in the 'message' field + /// Field("message").replaceFirst("hello", "hi") + /// ``` + /// + /// - Parameter find: The literal string substring to search for. + /// - Parameter replace: The literal string substring to replace the first occurrence with. + /// - Returns: A new `FunctionExpr` representing the string with the first occurrence replaced. + func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr + + /// Creates an expression that replaces the first occurrence of a substring (from an expression) + /// within this string expression with another substring (from an expression). + /// Assumes `self` evaluates to a string, and `find`/`replace` evaluate to strings. + /// + /// ```swift + /// // Replace first occurrence of field 'findPattern' with field 'replacePattern' in 'text' + /// Field("text").replaceFirst(Field("findPattern"), Field("replacePattern")) + /// ``` + /// + /// - Parameter find: An `Expr` (evaluating to a string) for the substring to search for. + /// - Parameter replace: An `Expr` (evaluating to a string) for the substring to replace the first + /// occurrence with. + /// - Returns: A new `FunctionExpr` representing the string with the first occurrence replaced. + func replaceFirst(_ find: Expr, _ replace: Expr) -> FunctionExpr + + /// Creates an expression that replaces all occurrences of a literal substring within this string + /// expression with another literal substring. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Replace all occurrences of " " with "_" in 'description' + /// Field("description").replaceAll(" ", "_") + /// ``` + /// + /// - Parameter find: The literal string substring to search for. + /// - Parameter replace: The literal string substring to replace all occurrences with. + /// - Returns: A new `FunctionExpr` representing the string with all occurrences replaced. + func replaceAll(_ find: String, _ replace: String) -> FunctionExpr + + /// Creates an expression that replaces all occurrences of a substring (from an expression) within + /// this string expression with another substring (from an expression). + /// Assumes `self` evaluates to a string, and `find`/`replace` evaluate to strings. + /// + /// ```swift + /// // Replace all occurrences of field 'target' with field 'replacement' in 'content' + /// Field("content").replaceAll(Field("target"), Field("replacement")) + /// ``` + /// + /// - Parameter find: An `Expr` (evaluating to a string) for the substring to search for. + /// - Parameter replace: An `Expr` (evaluating to a string) for the substring to replace all + /// occurrences with. + /// - Returns: A new `FunctionExpr` representing the string with all occurrences replaced. + func replaceAll(_ find: Expr, _ replace: Expr) -> FunctionExpr + + /// Creates an expression that calculates the length of this string or bytes expression in bytes. + /// Assumes `self` evaluates to a string or bytes. + /// + /// ```swift + /// // Calculate the length of the 'myString' field in bytes. + /// Field("myString").byteLength() + /// + /// // Calculate the size of the 'avatar' (Data/Bytes) field. + /// Field("avatar").byteLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length in bytes. + func byteLength() -> FunctionExpr + + /// Creates an expression that returns a substring of this expression (String or Bytes) using + /// literal integers for position and optional length. + /// Indexing is 0-based. Assumes `self` evaluates to a string or bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Get substring from index 5 with length 10 + /// Field("myString").substr(5, 10) + /// + /// // Get substring from 'myString' starting at index 3 to the end + /// Field("myString").substr(3, nil) + /// ``` + /// + /// - Parameter position: Literal `Int` index of the first character/byte. + /// - Parameter length: Optional literal `Int` length of the substring. If `nil`, goes to the end. + /// - Returns: A new `FunctionExpr` representing the substring. + func substr(_ position: Int, _ length: Int?) -> FunctionExpr + + /// Creates an expression that returns a substring of this expression (String or Bytes) using + /// expressions for position and optional length. + /// Indexing is 0-based. Assumes `self` evaluates to a string or bytes, and parameters evaluate to + /// integers. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Get substring from index calculated by Field("start") with length from Field("len") + /// Field("myString").substr(Field("start"), Field("len")) + /// + /// // Get substring from index calculated by Field("start") to the end + /// Field("myString").substr(Field("start"), nil) // Passing nil for optional Expr length + /// ``` + /// + /// - Parameter position: An `Expr` (evaluating to an Int) for the index of the first + /// character/byte. + /// - Parameter length: Optional `Expr` (evaluating to an Int) for the length of the substring. If + /// `nil`, goes to the end. + /// - Returns: A new `FunctionExpr` representing the substring. + func substr(_ position: Expr, _ length: Expr?) -> FunctionExpr + + // MARK: Map Operations + + /// Accesses a value from a map (object) field using the provided literal string key. + /// Assumes `self` evaluates to a Map. + /// + /// ```swift + /// // Get the 'city' value from the 'address' map field + /// Field("address").mapGet("city") + /// ``` + /// + /// - Parameter subfield: The literal string key to access in the map. + /// - Returns: A new `FunctionExpr` representing the value associated with the given key. + func mapGet(_ subfield: String) -> FunctionExpr + + /// Creates an expression that removes a key (specified by a literal string) from the map produced + /// by evaluating this expression. + /// Assumes `self` evaluates to a Map. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Removes the key 'baz' from the map held in field 'myMap' + /// Field("myMap").mapRemove("baz") + /// ``` + /// + /// - Parameter key: The literal string key to remove from the map. + /// - Returns: A new `FunctionExpr` representing the 'map_remove' operation. + func mapRemove(_ key: String) -> FunctionExpr + + /// Creates an expression that removes a key (specified by an expression) from the map produced by + /// evaluating this expression. + /// Assumes `self` evaluates to a Map, and `keyExpr` evaluates to a string. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Removes the key specified by field 'keyToRemove' from the map in 'settings' + /// Field("settings").mapRemove(Field("keyToRemove")) + /// ``` + /// + /// - Parameter keyExpr: An `Expr` (evaluating to a string) representing the key to remove from + /// the map. + /// - Returns: A new `FunctionExpr` representing the 'map_remove' operation. + func mapRemove(_ keyExpr: Expr) -> FunctionExpr + + /// Creates an expression that merges this map with multiple other map literals. + /// Assumes `self` evaluates to a Map. Later maps overwrite keys from earlier maps. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Merge 'settings' field with { "enabled": true } and another map literal { "priority": 1 } + /// Field("settings").mapMerge(["enabled": true], ["priority": 1]) + /// ``` + /// + /// - Parameter secondMap: A required second map (dictionary literal with `Sendable` values) to + /// merge. + /// - Parameter otherMaps: Optional additional maps (dictionary literals with `Sendable` values) + /// to merge. + /// - Returns: A new `FunctionExpr` representing the 'map_merge' operation. + func mapMerge(_ secondMap: [String: Sendable], _ otherMaps: [String: Sendable]...) -> FunctionExpr + + /// Creates an expression that merges this map with multiple other map expressions. + /// Assumes `self` and other arguments evaluate to Maps. Later maps overwrite keys from earlier + /// maps. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Merge 'baseSettings' field with 'userOverrides' field and 'adminConfig' field + /// Field("baseSettings").mapMerge(Field("userOverrides"), Field("adminConfig")) + /// ``` + /// + /// - Parameter secondMap: A required second `Expr` (evaluating to a Map) to merge. + /// - Parameter otherMaps: Optional additional `Expr` (evaluating to Maps) to merge. + /// - Returns: A new `FunctionExpr` representing the 'map_merge' operation. + func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr + + // MARK: Aggregations + + /// Creates an aggregation that counts the number of stage inputs where this expression evaluates + /// to a valid, non-null value. + /// + /// ```swift + /// // Count the total number of products with a 'productId' + /// Field("productId").count().alias("totalProducts") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'count' aggregation on this expression. + func count() -> AggregateFunction + + /// Creates an aggregation that calculates the sum of this numeric expression across multiple + /// stage inputs. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Calculate the total revenue from a set of orders + /// Field("orderAmount").sum().alias("totalRevenue") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'sum' aggregation. + func sum() -> AggregateFunction + + /// Creates an aggregation that calculates the average (mean) of this numeric expression across + /// multiple stage inputs. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Calculate the average age of users + /// Field("age").avg().alias("averageAge") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'avg' aggregation. + func avg() -> AggregateFunction + + /// Creates an aggregation that finds the minimum value of this expression across multiple stage + /// inputs. + /// + /// ```swift + /// // Find the lowest price of all products + /// Field("price").minimum().alias("lowestPrice") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'min' aggregation. + func minimum() -> AggregateFunction + + /// Creates an aggregation that finds the maximum value of this expression across multiple stage + /// inputs. + /// + /// ```swift + /// // Find the highest score in a leaderboard + /// Field("score").maximum().alias("highestScore") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'max' aggregation. + func maximum() -> AggregateFunction + + // MARK: Logical min/max + + /// Creates an expression that returns the larger value between this expression and other + /// expressions, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the largest of 'val1', 'val2', and 'val3' fields + /// Field("val1").logicalMaximum(Field("val2"), Field("val3")) + /// ``` + /// + /// - Parameter second: The second `Expr` to compare with. + /// - Parameter others: Optional additional `Expr` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical max operation. + func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that returns the larger value between this expression and other literal + /// values, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the largest of 'val1' (a field), 100, and 200.0 + /// Field("val1").logicalMaximum(100, 200.0) + /// ``` + /// + /// - Parameter second: The second literal `Sendable` value to compare with. + /// - Parameter others: Optional additional literal `Sendable` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical max operation. + func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr + + /// Creates an expression that returns the smaller value between this expression and other + /// expressions, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the smallest of 'val1', 'val2', and 'val3' fields + /// Field("val1").logicalMinimum(Field("val2"), Field("val3")) + /// ``` + /// + /// - Parameter second: The second `Expr` to compare with. + /// - Parameter others: Optional additional `Expr` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical min operation. + func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that returns the smaller value between this expression and other literal + /// values, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the smallest of 'val1' (a field), 0, and -5.5 + /// Field("val1").logicalMinimum(0, -5.5) + /// ``` + /// + /// - Parameter second: The second literal `Sendable` value to compare with. + /// - Parameter others: Optional additional literal `Sendable` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical min operation. + func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr + + // MARK: Vector Operations + + /// Creates an expression that calculates the length (number of dimensions) of this Firestore + /// Vector expression. + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Get the vector length (dimension) of the field 'embedding'. + /// Field("embedding").vectorLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length of the vector. + func vectorLength() -> FunctionExpr + + /// Calculates the cosine distance between this vector expression and another vector expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// ```swift + /// // Cosine distance between 'userVector' field and 'itemVector' field + /// Field("userVector").cosineDistance(Field("itemVector")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to compare against. + /// - Returns: A new `FunctionExpr` representing the cosine distance. + func cosineDistance(_ other: Expr) -> FunctionExpr + + /// Calculates the cosine distance between this vector expression and another vector literal + /// (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Cosine distance with a VectorValue + /// let targetVector = VectorValue(vector: [0.1, 0.2, 0.3]) + /// Field("docVector").cosineDistance(targetVector) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to compare against. + /// - Returns: A new `FunctionExpr` representing the cosine distance. + func cosineDistance(_ other: VectorValue) -> FunctionExpr + + /// Calculates the cosine distance between this vector expression and another vector literal + /// (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Cosine distance between 'location' field and a target location + /// Field("location").cosineDistance([37.7749, -122.4194]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to compare against. + /// - Returns: A new `FunctionExpr` representing the cosine distance. + func cosineDistance(_ other: [Double]) -> FunctionExpr + + /// Calculates the dot product between this vector expression and another vector expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// ```swift + /// // Dot product between 'vectorA' and 'vectorB' fields + /// Field("vectorA").dotProduct(Field("vectorB")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to calculate with. + /// - Returns: A new `FunctionExpr` representing the dot product. + func dotProduct(_ other: Expr) -> FunctionExpr + + /// Calculates the dot product between this vector expression and another vector literal + /// (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Dot product with a VectorValue + /// let weightVector = VectorValue(vector: [0.5, -0.5]) + /// Field("features").dotProduct(weightVector) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to calculate with. + /// - Returns: A new `FunctionExpr` representing the dot product. + func dotProduct(_ other: VectorValue) -> FunctionExpr + + /// Calculates the dot product between this vector expression and another vector literal + /// (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Dot product between a feature vector and a target vector literal + /// Field("features").dotProduct([0.5, 0.8, 0.2]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to calculate with. + /// - Returns: A new `FunctionExpr` representing the dot product. + func dotProduct(_ other: [Double]) -> FunctionExpr + + /// Calculates the Euclidean distance between this vector expression and another vector + /// expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// ```swift + /// // Euclidean distance between 'pointA' and 'pointB' fields + /// Field("pointA").euclideanDistance(Field("pointB")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to compare against. + /// - Returns: A new `FunctionExpr` representing the Euclidean distance. + func euclideanDistance(_ other: Expr) -> FunctionExpr + + /// Calculates the Euclidean distance between this vector expression and another vector literal + /// (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// let targetPoint = VectorValue(vector: [1.0, 2.0]) + /// Field("currentLocation").euclideanDistance(targetPoint) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to compare against. + /// - Returns: A new `FunctionExpr` representing the Euclidean distance. + func euclideanDistance(_ other: VectorValue) -> FunctionExpr + + /// Calculates the Euclidean distance between this vector expression and another vector literal + /// (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Euclidean distance between 'location' field and a target location literal + /// Field("location").euclideanDistance([37.7749, -122.4194]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to compare against. + /// - Returns: A new `FunctionExpr` representing the Euclidean distance. + func euclideanDistance(_ other: [Double]) -> FunctionExpr + + /// Calculates the Manhattan (L1) distance between this vector expression and another vector + /// expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Manhattan distance between 'vector1' field and 'vector2' field + /// Field("vector1").manhattanDistance(Field("vector2")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to compare against. + /// - Returns: A new `FunctionExpr` representing the Manhattan distance. + func manhattanDistance(_ other: Expr) -> FunctionExpr + + /// Calculates the Manhattan (L1) distance between this vector expression and another vector + /// literal (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// - Note: This API is in beta. + /// ```swift + /// let referencePoint = VectorValue(vector: [5.0, 10.0]) + /// Field("dataPoint").manhattanDistance(referencePoint) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to compare against. + /// - Returns: A new `FunctionExpr` representing the Manhattan distance. + func manhattanDistance(_ other: VectorValue) -> FunctionExpr + + /// Calculates the Manhattan (L1) distance between this vector expression and another vector + /// literal (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Manhattan distance between 'point' field and a target point + /// Field("point").manhattanDistance([10.0, 20.0]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to compare against. + /// - Returns: A new `FunctionExpr` representing the Manhattan distance. + func manhattanDistance(_ other: [Double]) -> FunctionExpr + + // MARK: Timestamp operations + + /// Creates an expression that interprets this expression (evaluating to a number) as microseconds + /// since the Unix epoch and returns a timestamp. + /// Assumes `self` evaluates to a number. + /// + /// ```swift + /// // Interpret 'microseconds' field as microseconds since epoch. + /// Field("microseconds").unixMicrosToTimestamp() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the timestamp. + func unixMicrosToTimestamp() -> FunctionExpr + + /// Creates an expression that converts this timestamp expression to the number of microseconds + /// since the Unix epoch. Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Convert 'timestamp' field to microseconds since epoch. + /// Field("timestamp").timestampToUnixMicros() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the number of microseconds. + func timestampToUnixMicros() -> FunctionExpr + + /// Creates an expression that interprets this expression (evaluating to a number) as milliseconds + /// since the Unix epoch and returns a timestamp. + /// Assumes `self` evaluates to a number. + /// + /// ```swift + /// // Interpret 'milliseconds' field as milliseconds since epoch. + /// Field("milliseconds").unixMillisToTimestamp() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the timestamp. + func unixMillisToTimestamp() -> FunctionExpr + + /// Creates an expression that converts this timestamp expression to the number of milliseconds + /// since the Unix epoch. Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Convert 'timestamp' field to milliseconds since epoch. + /// Field("timestamp").timestampToUnixMillis() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the number of milliseconds. + func timestampToUnixMillis() -> FunctionExpr + + /// Creates an expression that interprets this expression (evaluating to a number) as seconds + /// since the Unix epoch and returns a timestamp. + /// Assumes `self` evaluates to a number. + /// + /// ```swift + /// // Interpret 'seconds' field as seconds since epoch. + /// Field("seconds").unixSecondsToTimestamp() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the timestamp. + func unixSecondsToTimestamp() -> FunctionExpr + + /// Creates an expression that converts this timestamp expression to the number of seconds + /// since the Unix epoch. Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Convert 'timestamp' field to seconds since epoch. + /// Field("timestamp").timestampToUnixSeconds() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the number of seconds. + func timestampToUnixSeconds() -> FunctionExpr + + /// Creates an expression that adds a specified amount of time to this timestamp expression, + /// where unit and amount are provided as expressions. + /// Assumes `self` evaluates to a Timestamp, `unit` evaluates to a unit string, and `amount` + /// evaluates to an integer. + /// + /// ```swift + /// // Add duration from 'unitField'/'amountField' to 'timestamp' + /// Field("timestamp").timestampAdd(Field("unitField"), Field("amountField")) + /// ``` + /// + /// - Parameter unit: An `Expr` evaluating to the unit of time string (e.g., "day", "hour"). + /// Valid units are 'microsecond', 'millisecond', 'second', 'minute', 'hour', + /// 'day'. + /// - Parameter amount: An `Expr` evaluating to the amount (Int) of the unit to add. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. + func timestampAdd(_ unit: Expr, _ amount: Expr) -> FunctionExpr + + /// Creates an expression that adds a specified amount of time to this timestamp expression, + /// where unit and amount are provided as literals. + /// Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Add 1 day to the 'timestamp' field. + /// Field("timestamp").timestampAdd(.day, 1) + /// ``` + /// + /// - Parameter unit: The `TimeUnit` enum representing the unit of time. + /// - Parameter amount: The literal `Int` amount of the unit to add. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. + func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr + + /// Creates an expression that subtracts a specified amount of time from this timestamp + /// expression, + /// where unit and amount are provided as expressions. + /// Assumes `self` evaluates to a Timestamp, `unit` evaluates to a unit string, and `amount` + /// evaluates to an integer. + /// + /// ```swift + /// // Subtract duration from 'unitField'/'amountField' from 'timestamp' + /// Field("timestamp").timestampSub(Field("unitField"), Field("amountField")) + /// ``` + /// + /// - Parameter unit: An `Expr` evaluating to the unit of time string (e.g., "day", "hour"). + /// Valid units are 'microsecond', 'millisecond', 'second', 'minute', 'hour', + /// 'day'. + /// - Parameter amount: An `Expr` evaluating to the amount (Int) of the unit to subtract. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. + func timestampSub(_ unit: Expr, _ amount: Expr) -> FunctionExpr + + /// Creates an expression that subtracts a specified amount of time from this timestamp + /// expression, + /// where unit and amount are provided as literals. + /// Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Subtract 1 day from the 'timestamp' field. + /// Field("timestamp").timestampSub(.day, 1) + /// ``` + /// + /// - Parameter unit: The `TimeUnit` enum representing the unit of time. + /// - Parameter amount: The literal `Int` amount of the unit to subtract. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. + func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr + + // MARK: - Bitwise operations + + /// Creates an expression applying bitwise AND between this expression and an integer literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise AND of 'flags' field and 0xFF + /// Field("flags").bitAnd(0xFF) + /// ``` + /// + /// - Parameter otherBits: The integer literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise AND operation. + func bitAnd(_ otherBits: Int) -> FunctionExpr + + /// Creates an expression applying bitwise AND between this expression and a UInt8 literal (often + /// for byte masks). + /// Assumes `self` evaluates to an Integer or Bytes. + /// - Note: This API is in beta. + /// ```swift + /// // Bitwise AND of 'byteFlags' field and a byte mask + /// Field("byteFlags").bitAnd(0b00001111 as UInt8) + /// ``` + /// - Parameter otherBits: The UInt8 literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise AND operation. + func bitAnd(_ otherBits: UInt8) -> FunctionExpr + + /// Creates an expression applying bitwise AND between this expression and another expression. + /// Assumes `self` and `bitsExpression` evaluate to Integer or Bytes. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise AND of 'mask1' and 'mask2' fields + /// Field("mask1").bitAnd(Field("mask2")) + /// ``` + /// - Parameter bitsExpression: The other `Expr` operand. + /// - Returns: A new `FunctionExpr` representing the bitwise AND operation. + func bitAnd(_ bitsExpression: Expr) -> FunctionExpr + + /// Creates an expression applying bitwise OR between this expression and an integer literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise OR of 'flags' field and 0x01 + /// Field("flags").bitOr(0x01) + /// ``` + /// + /// - Parameter otherBits: The integer literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise OR operation. + func bitOr(_ otherBits: Int) -> FunctionExpr + + /// Creates an expression applying bitwise OR between this expression and a UInt8 literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// - Note: This API is in beta. + /// ```swift + /// // Set specific bits in 'controlByte' + /// Field("controlByte").bitOr(0b10000001 as UInt8) + /// ``` + /// - Parameter otherBits: The UInt8 literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise OR operation. + func bitOr(_ otherBits: UInt8) -> FunctionExpr + + /// Creates an expression applying bitwise OR between this expression and another expression. + /// Assumes `self` and `bitsExpression` evaluate to Integer or Bytes. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise OR of 'permissionSet1' and 'permissionSet2' fields + /// Field("permissionSet1").bitOr(Field("permissionSet2")) + /// ``` + /// - Parameter bitsExpression: The other `Expr` operand. + /// - Returns: A new `FunctionExpr` representing the bitwise OR operation. + func bitOr(_ bitsExpression: Expr) -> FunctionExpr + + /// Creates an expression applying bitwise XOR between this expression and an integer literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise XOR of 'toggle' field and 0xFFFF + /// Field("toggle").bitXor(0xFFFF) + /// ``` + /// + /// - Parameter otherBits: The integer literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise XOR operation. + func bitXor(_ otherBits: Int) -> FunctionExpr + + /// Creates an expression applying bitwise XOR between this expression and a UInt8 literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// - Note: This API is in beta. + /// ```swift + /// // Toggle bits in 'statusByte' using a XOR mask + /// Field("statusByte").bitXor(0b01010101 as UInt8) + /// ``` + /// - Parameter otherBits: The UInt8 literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise XOR operation. + func bitXor(_ otherBits: UInt8) -> FunctionExpr + + /// Creates an expression applying bitwise XOR between this expression and another expression. + /// Assumes `self` and `bitsExpression` evaluate to Integer or Bytes. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise XOR of 'key1' and 'key2' fields (assuming Bytes) + /// Field("key1").bitXor(Field("key2")) + /// ``` + /// - Parameter bitsExpression: The other `Expr` operand. + /// - Returns: A new `FunctionExpr` representing the bitwise XOR operation. + func bitXor(_ bitsExpression: Expr) -> FunctionExpr + + /// Creates an expression applying bitwise NOT to this expression. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise NOT of 'mask' field + /// Field("mask").bitNot() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the bitwise NOT operation. + func bitNot() -> FunctionExpr + + /// Creates an expression applying bitwise left shift to this expression by a literal number of + /// bits. + /// Assumes `self` evaluates to Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Left shift 'value' field by 2 bits + /// Field("value").bitLeftShift(2) + /// ``` + /// + /// - Parameter y: The number of bits (Int literal) to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise left shift operation. + func bitLeftShift(_ y: Int) -> FunctionExpr + + /// Creates an expression applying bitwise left shift to this expression by a number of bits + /// specified by an expression. + /// Assumes `self` evaluates to Integer or Bytes, and `numberExpr` evaluates to an Integer. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Left shift 'data' by number of bits in 'shiftCount' field + /// Field("data").bitLeftShift(Field("shiftCount")) + /// ``` + /// - Parameter numberExpr: An `Expr` (evaluating to an Int) for the number of bits to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise left shift operation. + func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr + + /// Creates an expression applying bitwise right shift to this expression by a literal number of + /// bits. + /// Assumes `self` evaluates to Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Right shift 'value' field by 4 bits + /// Field("value").bitRightShift(4) + /// ``` + /// + /// - Parameter y: The number of bits (Int literal) to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise right shift operation. + func bitRightShift(_ y: Int) -> FunctionExpr + + /// Creates an expression applying bitwise right shift to this expression by a number of bits + /// specified by an expression. + /// Assumes `self` evaluates to Integer or Bytes, and `numberExpr` evaluates to an Integer. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Right shift 'data' by number of bits in 'shiftCount' field + /// Field("data").bitRightShift(Field("shiftCount")) + /// ``` + /// - Parameter numberExpr: An `Expr` (evaluating to an Int) for the number of bits to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise right shift operation. + func bitRightShift(_ numberExpr: Expr) -> FunctionExpr + + /// Creates an expression that returns the result of `catchExpr` if this expression produces an + /// error during evaluation, + /// otherwise returns the result of this expression. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Try dividing 'a' by 'b', return field 'fallbackValue' on error (e.g., division by zero) + /// Field("a").divide(Field("b")).ifError(Field("fallbackValue")) + /// ``` + /// + /// - Parameter catchExpr: The `Expr` to evaluate and return if this expression errors. + /// - Returns: A new `FunctionExpr` representing the 'ifError' operation. + func ifError(_ catchExpr: Expr) -> FunctionExpr + + /// Creates an expression that returns the literal `catchValue` if this expression produces an + /// error during evaluation, + /// otherwise returns the result of this expression. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Get first item in 'title' array, or return "Default Title" if error (e.g., empty array) + /// Field("title").arrayOffset(0).ifError("Default Title") + /// ``` + /// + /// - Parameter catchValue: The literal `Sendable` value to return if this expression errors. + /// - Returns: A new `FunctionExpr` representing the 'ifError' operation. + func ifError(_ catchValue: Sendable) -> FunctionExpr + + // MARK: Sorting + + /// Creates an `Ordering` object that sorts documents in ascending order based on this expression. + /// + /// ```swift + /// // Sort documents by the 'name' field in ascending order + /// firestore.pipeline().collection("users") + /// .sort(Field("name").ascending()) + /// ``` + /// + /// - Returns: A new `Ordering` instance for ascending sorting. + func ascending() -> Ordering + + /// Creates an `Ordering` object that sorts documents in descending order based on this + /// expression. + /// + /// ```swift + /// // Sort documents by the 'createdAt' field in descending order + /// firestore.pipeline().collection("users") + /// .sort(Field("createdAt").descending()) + /// ``` + /// + /// - Returns: A new `Ordering` instance for descending sorting. + func descending() -> Ordering +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift new file mode 100644 index 00000000000..0d4f30fe463 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -0,0 +1,77 @@ +// Copyright 2025 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { + let bridge: ExprBridge + + let value: Any? + + // Initializer for optional values (including nil) + init(_ value: Any?) { + self.value = value + if value == nil { + bridge = ConstantBridge(NSNull()) + } else { + bridge = ConstantBridge(value!) + } + } + + // Initializer for numbers + public init(_ value: Double) { + self.init(value as Any) + } + + // Initializer for strings + public init(_ value: String) { + self.init(value as Any) + } + + // Initializer for boolean values + public init(_ value: Bool) { + self.init(value as Any) + } + + // Initializer for GeoPoint values + public init(_ value: GeoPoint) { + self.init(value as Any) + } + + // Initializer for Timestamp values + public init(_ value: Timestamp) { + self.init(value as Any) + } + + // Initializer for Date values + public init(_ value: Date) { + self.init(value as Any) + } + + // Initializer for DocumentReference + public init(_ value: DocumentReference) { + self.init(value as Any) + } + + // Initializer for vector values + public init(_ value: VectorValue) { + self.init(value as Any) + } + + public static let `nil` = Constant(nil) +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/DocumentId.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/DocumentId.swift new file mode 100644 index 00000000000..70c621d8cbd --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/DocumentId.swift @@ -0,0 +1,19 @@ +// Copyright 2025 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. + +public class DocumentId: Field, @unchecked Sendable { + public init() { + super.init("__name__") + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift new file mode 100644 index 00000000000..fa1dc7d7510 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift @@ -0,0 +1,32 @@ +// Copyright 2025 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. + +public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, SelectableWrapper, + @unchecked Sendable { + let bridge: ExprBridge + + var alias: String + + var expr: Expr { + return self + } + + public let fieldName: String + + public init(_ fieldName: String) { + self.fieldName = fieldName + alias = fieldName + bridge = FieldBridge(alias) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift new file mode 100644 index 00000000000..533f6a5ef51 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift @@ -0,0 +1,30 @@ +// Copyright 2025 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. + +public class FunctionExpr: Expr, BridgeWrapper, @unchecked Sendable { + let bridge: ExprBridge + + let functionName: String + let agrs: [Expr] + + public init(_ functionName: String, _ agrs: [Expr]) { + self.functionName = functionName + self.agrs = agrs + bridge = FunctionExprBridge( + name: functionName, + args: self.agrs.map { $0.toBridge() + } + ) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift new file mode 100644 index 00000000000..9826d1698c0 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift @@ -0,0 +1,33 @@ +// Copyright 2025 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. + +public class BooleanExpr: FunctionExpr, @unchecked Sendable { + override public init(_ functionName: String, _ agrs: [Expr]) { + super.init(functionName, agrs) + } + + public static func && (lhs: BooleanExpr, + rhs: @autoclosure () throws -> BooleanExpr) rethrows -> BooleanExpr { + try BooleanExpr("and", [lhs, rhs()]) + } + + public static func || (lhs: BooleanExpr, + rhs: @autoclosure () throws -> BooleanExpr) rethrows -> BooleanExpr { + try BooleanExpr("or", [lhs, rhs()]) + } + + public static prefix func ! (lhs: BooleanExpr) -> BooleanExpr { + return BooleanExpr("not", [lhs]) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift new file mode 100644 index 00000000000..247427f2fd8 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift @@ -0,0 +1,24 @@ +// Copyright 2025 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. + +public struct ExprWithAlias: Selectable, SelectableWrapper, Sendable { + public var alias: String + + public var expr: Expr + + init(_ expr: Expr, _ alias: String) { + self.alias = alias + self.expr = expr + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift new file mode 100644 index 00000000000..9659e95e682 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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. + */ + +public class Ordering: @unchecked Sendable { + let expr: Expr + let direction: Direction + let bridge: OrderingBridge + + init(expr: Expr, direction: Direction) { + self.expr = expr + self.direction = direction + bridge = OrderingBridge(expr: expr.toBridge(), direction: direction.rawValue) + } +} + +public struct Direction: Sendable, Equatable, Hashable { + let kind: Kind + let rawValue: String + + enum Kind: String { + case ascending + case descending + } + + public static var ascending: Direction { + return self.init(kind: .ascending, rawValue: "ascending") + } + + public static var descending: Direction { + return self.init(kind: .descending, rawValue: "descending") + } + + init(kind: Kind, rawValue: String) { + self.kind = kind + self.rawValue = rawValue + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift new file mode 100644 index 00000000000..4e49c97301b --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -0,0 +1,722 @@ +// Copyright 2025 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + +/// The `Pipeline` class provides a flexible and expressive framework for building complex data +/// transformation and query pipelines for Firestore. +/// +/// A pipeline takes data sources, such as Firestore collections or collection groups, and applies +/// a series of stages that are chained together. Each stage takes the output from the previous +/// stage +/// (or the data source) and produces an output for the next stage (or as the final output of the +/// pipeline). +/// +/// Expressions can be used within each stage to filter and transform data through the stage. +/// +/// NOTE: The chained stages do not prescribe exactly how Firestore will execute the pipeline. +/// Instead, Firestore only guarantees that the result is the same as if the chained stages were +/// executed in order. +/// +/// ## Usage Examples +/// +/// The following examples assume you have a `Firestore` instance named `db`. +/// +/// ```swift +/// import FirebaseFirestore +/// +/// // Example 1: Select specific fields and rename 'rating' to 'bookRating'. +/// // Assumes `Field("rating").as("bookRating")` is a valid `Selectable` expression. +/// do { +/// let results1 = try await db.pipeline().collection("books") +/// .select(Field("title"), Field("author"), Field("rating").as("bookRating")) +/// .execute() +/// print("Results 1: \(results1.documents)") +/// } catch { +/// print("Error in example 1: \(error)") +/// } +/// +/// // Example 2: Filter documents where 'genre' is "Science Fiction" and 'published' is after 1950. +/// // Assumes `Function.eq`, `Function.gt`, and `Function.and` create `BooleanExpr`. +/// do { +/// let results2 = try await db.pipeline().collection("books") +/// .where(Function.and( +/// Function.eq(Field("genre"), "Science Fiction"), +/// Function.gt(Field("published"), 1950) +/// )) +/// .execute() +/// print("Results 2: \(results2.documents)") +/// } catch { +/// print("Error in example 2: \(error)") +/// } +/// +/// // Example 3: Calculate the average rating of books published after 1980. +/// // Assumes `avg()` creates an `Accumulator` and `AggregateWithAlias` is used correctly. +/// do { +/// let results3 = try await db.pipeline().collection("books") +/// .where(Function.gt(Field("published"), 1980)) +/// .aggregate(AggregateWithas(avg(Field("rating")), alias: "averageRating")) +/// .execute() +/// print("Results 3: \(results3.documents)") +/// } catch { +/// print("Error in example 3: \(error)") +/// } +/// ``` +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct Pipeline: @unchecked Sendable { + private var stages: [Stage] + let bridge: PipelineBridge + let db: Firestore + + init(stages: [Stage], db: Firestore) { + self.stages = stages + self.db = db + bridge = PipelineBridge(stages: stages.map { $0.bridge }, db: db) + } + + /// Executes the defined pipeline and returns a `PipelineSnapshot` 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 + /// within a `PipelineSnapshot`. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume a pipeline is already configured. + /// do { + /// let snapshot = try await pipeline.execute() + /// // Process snapshot.documents + /// print("Pipeline executed successfully: \(snapshot.documents)") + /// } catch { + /// print("Pipeline execution failed: \(error)") + /// } + /// ``` + /// + /// - Throws: An error if the pipeline execution fails on the backend. + /// - Returns: A `PipelineSnapshot` containing the result of the pipeline execution. + public func execute() async throws -> PipelineSnapshot { + return try await withCheckedThrowingContinuation { continuation in + self.bridge.execute { result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: PipelineSnapshot(result!, pipeline: self)) + } + } + } + } + + /// Adds new fields to outputs from previous stages. + /// + /// This stage allows you to compute values on-the-fly based on existing data from previous + /// stages or constants. You can use this to create new fields or overwrite existing ones + /// (if there is a name overlap). + /// + /// The added fields are defined using `Selectable`s, which can be: + /// - `Field`: References an existing document field. + /// - `Function`: Performs a calculation using functions like `Function.add` or + /// `Function.multiply`, + /// typically with an assigned alias (e.g., `Function.multiply(Field("price"), + /// 1.1).as("priceWithTax")`). + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline from a collection. + /// let updatedPipeline = pipeline.addFields( + /// Field("rating").as("bookRating"), // Rename 'rating' to 'bookRating'. + /// Function.add(5, Field("quantity")).as("totalQuantityPlusFive") // Calculate + /// 'totalQuantityPlusFive'. + /// ) + /// // let results = try await updatedPipeline.execute() + /// ``` + /// + /// - Parameter field: The first field to add to the documents, specified as a `Selectable`. + /// - Parameter additionalFields: Optional additional fields to add, specified as `Selectable`s. + /// - Returns: A new `Pipeline` object with this stage appended. + public func addFields(_ field: Selectable, _ additionalFields: Selectable...) -> Pipeline { + let fields = [field] + additionalFields + return Pipeline(stages: stages + [AddFields(fields: fields)], db: db) + } + + /// Removes fields from outputs of previous stages. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let updatedPipeline = pipeline.removeFields(Field("confidentialData"), Field("internalNotes")) + /// // let results = try await updatedPipeline.execute() + /// ``` + /// + /// - Parameter field: The first field to remove, specified as a `Field` instance. + /// - Parameter additionalFields: Optional additional fields to remove. + /// - Returns: A new `Pipeline` object with this stage appended. + public func removeFields(_ field: Field, _ additionalFields: Field...) -> Pipeline { + return Pipeline( + stages: stages + [RemoveFieldsStage(fields: [field] + additionalFields)], + db: db + ) + } + + /// Removes fields from outputs of previous stages using field names. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Removes fields 'rating' and 'cost' from the previous stage outputs. + /// let updatedPipeline = pipeline.removeFields("rating", "cost") + /// // let results = try await updatedPipeline.execute() + /// ``` + /// + /// - Parameter field: The name of the first field to remove. + /// - Parameter additionalFields: Optional additional field names to remove. + /// - Returns: A new `Pipeline` object with this stage appended. + public func removeFields(_ field: String, _ additionalFields: String...) -> Pipeline { + return Pipeline( + stages: stages + [RemoveFieldsStage(fields: [field] + additionalFields)], + db: db + ) + } + + /// Selects or creates a set of fields from the outputs of previous stages. + /// + /// The selected fields are defined using `Selectable` expressions, which can be: + /// - `String`: Name of an existing field (implicitly converted to `Field`). + /// - `Field`: References an existing field. + /// - `Function`: Represents the result of a function with an assigned alias + /// (e.g., `Function.toUppercase(Field("address")).as("upperAddress")`). + /// + /// If no selections are provided, the output of this stage is typically empty. + /// Use `addFields` if only additions are desired without replacing the existing document + /// structure. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let projectedPipeline = pipeline.select( + /// Field("firstName"), + /// Field("lastName"), + /// Function.toUppercase(Field("address")).as("upperAddress") + /// ) + /// // let results = try await projectedPipeline.execute() + /// ``` + /// + /// - Parameter selection: The first field to include in the output documents, specified as a + /// `Selectable`. + /// - Parameter additionalSelections: Optional additional fields to include, specified as + /// `Selectable`s. + /// - Returns: A new `Pipeline` object with this stage appended. + public func select(_ selection: Selectable, _ additionalSelections: Selectable...) -> Pipeline { + let selections = [selection] + additionalSelections + return Pipeline( + stages: stages + [Select(selections: selections)], + db: db + ) + } + + /// Selects a set of fields from the outputs of previous stages using field names. + /// + /// The selected fields are specified by their names. If no selections are provided, + /// the output of this stage is typically empty. Use `addFields` if only additions are desired. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let projectedPipeline = pipeline.select("title", "author", "yearPublished") + /// // let results = try await projectedPipeline.execute() + /// ``` + /// + /// - Parameter selection: The name of the first field to include in the output documents. + /// - Parameter additionalSelections: Optional additional field names to include. + /// - Returns: A new `Pipeline` object with this stage appended. + public func select(_ selection: String, _ additionalSelections: String...) -> Pipeline { + let selections = ([selection] + additionalSelections).map { Field($0) } + return Pipeline( + stages: stages + [Select(selections: selections)], + db: db + ) + } + + /// Filters documents from previous stages, including only those matching the specified + /// `BooleanExpr`. + /// + /// This stage applies conditions similar to a "WHERE" clause in SQL. + /// Filter documents based on field values using `BooleanExpr` implementations, such as: + /// - Field comparators: `Function.eq`, `Function.lt` (less than), `Function.gt` (greater than). + /// - Logical operators: `Function.and`, `Function.or`, `Function.not`. + /// - Advanced functions: `Function.regexMatch`, `Function.arrayContains`. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let filteredPipeline = pipeline.where( + /// Field("rating").gt(4.0) // Rating greater than 4.0. + /// && Field("genre").eq("Science Fiction") // Genre is "Science Fiction". + /// ) + /// // let results = try await filteredPipeline.execute() + /// ``` + /// + /// - Parameter condition: The `BooleanExpr` to apply. + /// - Returns: A new `Pipeline` object with this stage appended. + public func `where`(_ condition: BooleanExpr) -> Pipeline { + return Pipeline(stages: stages + [Where(condition: condition)], db: db) + } + + /// Skips the first `offset` number of documents from the results of previous stages. + /// + /// A negative input number might count back from the end of the result set, + /// depending on backend behavior. This stage is useful for pagination, + /// typically used with `limit` to control page size. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline, possibly sorted. + /// // Retrieve the second page of 20 results (skip first 20, limit to next 20). + /// let pagedPipeline = pipeline + /// .sort(Ascending("published")) // Example sort. + /// .offset(20) // Skip the first 20 results. + /// .limit(20) // Take the next 20 results. + /// // let results = try await pagedPipeline.execute() + /// ``` + /// + /// - Parameter offset: The number of documents to skip (a `Int32` value). + /// - Returns: A new `Pipeline` object with this stage appended. + public func offset(_ offset: Int32) -> Pipeline { + return Pipeline(stages: stages + [Offset(offset)], db: db) + } + + /// Limits the maximum number of documents returned by previous stages to `limit`. + /// + /// A negative input number might count back from the end of the result set, + /// depending on backend behavior. This stage helps retrieve a controlled subset of data. + /// It's often used for: + /// - **Pagination:** With `offset` to retrieve specific pages. + /// - **Limiting Data Retrieval:** To improve performance with large collections. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Limit results to the top 10 highest-rated books. + /// let topTenPipeline = pipeline + /// .sort(Descending(Field("rating"))) + /// .limit(10) + /// // let results = try await topTenPipeline.execute() + /// ``` + /// + /// - Parameter limit: The maximum number of documents to return (a `Int32` value). + /// - Returns: A new `Pipeline` object with this stage appended. + public func limit(_ limit: Int32) -> Pipeline { + return Pipeline(stages: stages + [Limit(limit)], db: db) + } + + /// Returns a set of distinct documents based on specified grouping field names. + /// + /// This stage ensures that only unique combinations of values for the specified + /// group fields are included from the previous stage's output. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Get a list of unique author and genre combinations. + /// let distinctAuthorsGenresPipeline = pipeline.distinct("author", "genre") + /// // To further select only the author: + /// // .select("author") + /// // let results = try await distinctAuthorsGenresPipeline.execute() + /// ``` + /// + /// - Parameter group: The name of the first field for distinct value combinations. + /// - Parameter additionalGroups: Optional additional field names. + /// - Returns: A new `Pipeline` object with this stage appended. + public func distinct(_ group: String, _ additionalGroups: String...) -> Pipeline { + let selections = ([group] + additionalGroups).map { Field($0) } + return Pipeline(stages: stages + [Distinct(groups: selections)], db: db) + } + + /// Returns a set of distinct documents based on specified `Selectable` expressions. + /// + /// This stage ensures unique combinations of values from evaluated `Selectable` + /// expressions (e.g., `Field` or `Function` results). + /// + /// `Selectable` expressions can be: + /// - `Field`: A reference to an existing document field. + /// - `Function`: The result of a function with an alias (e.g., + /// `Function.toUppercase(Field("author")).as("authorName")`). + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Get unique uppercase author names and genre combinations. + /// let distinctPipeline = pipeline.distinct( + /// Field("author").uppercased().as("authorName"), + /// Field("genre") + /// ) + /// // To select only the transformed author name: + /// // .select(Field("authorName")) + /// // let results = try await distinctPipeline.execute() + /// ``` + /// + /// - Parameter group: The first `Selectable` expression to consider. + /// - Parameter additionalGroups: Optional additional `Selectable` expressions. + /// - Returns: A new `Pipeline` object with this stage appended. + public func distinct(_ group: Selectable, _ additionalGroups: Selectable...) -> Pipeline { + let groups = [group] + additionalGroups + return Pipeline(stages: stages + [Distinct(groups: groups)], db: db) + } + + /// Performs aggregation operations on all documents from previous stages. + /// + /// Computes aggregate values (e.g., sum, average, count) over the entire set of documents + /// from the previous stage. Aggregations are defined using `AggregateWithAlias`, + /// which pairs an `Accumulator` (e.g., `avg(Field("price"))`) with a result field name. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from a "books" collection. + /// // Calculate the average rating and total number of books. + /// let aggregatedPipeline = pipeline.aggregate( + /// AggregateWithas(aggregate: avg(Field("rating")), alias: "averageRating"), + /// AggregateWithas(aggregate: countAll(), alias: "totalBooks") + /// ) + /// // let results = try await aggregatedPipeline.execute() + /// // results.documents might be: [["averageRating": 4.2, "totalBooks": 150]] + /// ``` + /// + /// - Parameter accumulator: The first `AggregateWithAlias` expression. + /// - Parameter additionalAccumulators: Optional additional `AggregateWithAlias` expressions. + /// - Returns: A new `Pipeline` object with this stage appended. + public func aggregate(_ accumulator: AggregateWithAlias, + _ additionalAccumulators: AggregateWithAlias...) -> Pipeline { + return Pipeline( + stages: stages + [Aggregate( + accumulators: [accumulator] + additionalAccumulators, + groups: nil // No grouping: aggregate over all documents. + )], + db: db + ) + } + + /// Performs optionally grouped aggregation operations on documents from previous stages. + /// + /// Calculates aggregate values, optionally grouping documents by fields or `Selectable` + /// expressions. + /// - **Grouping:** Defined by the `groups` parameter. Each unique combination of values + /// from these `Selectable`s forms a group. If `groups` is `nil` or empty, + /// all documents form a single group. + /// - **Accumulators:** An array of `AggregateWithAlias` defining operations + /// (e.g., sum, average) within each group. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from "books" collection. + /// // Calculate the average rating for each genre. + /// let groupedAggregationPipeline = pipeline.aggregate( + /// [AggregateWithas(aggregate: avg(Field("rating")), alias: "avg_rating")], + /// groups: [Field("genre")] // Group by the "genre" field. + /// ) + /// // let results = try await groupedAggregationPipeline.execute() + /// // results.documents might be: + /// // [ + /// // ["genre": "SciFi", "avg_rating": 4.5], + /// // ["genre": "Fantasy", "avg_rating": 4.2] + /// // ] + /// ``` + /// + /// - Parameters: + /// - accumulator: An array of `AggregateWithAlias` expressions for calculations. + /// - groups: Optional array of `Selectable` expressions for grouping. If `nil` or empty, + /// aggregates across all documents. + /// - Returns: A new `Pipeline` object with this stage appended. + public func aggregate(_ accumulator: [AggregateWithAlias], + groups: [Selectable]? = nil) -> Pipeline { + return Pipeline(stages: stages + [Aggregate(accumulators: accumulator, groups: groups)], db: db) + } + + /// Performs optionally grouped aggregation operations using field names for grouping. + /// + /// Similar to the other `aggregate` method, but `groups` are specified as an array of `String` + /// field names. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from "books" collection. + /// // Count books for each publisher. + /// let groupedByPublisherPipeline = pipeline.aggregate( + /// [AggregateWithas(aggregate: countAll(), alias: "book_count")], + /// groups: ["publisher"] // Group by the "publisher" field name. + /// ) + /// // let results = try await groupedByPublisherPipeline.execute() + /// // results.documents might be: + /// // [ + /// // ["publisher": "Penguin", "book_count": 50], + /// // ["publisher": "HarperCollins", "book_count": 35] + /// // ] + /// ``` + /// + /// - Parameters: + /// - accumulator: An array of `AggregateWithAlias` expressions. + /// - groups: An optional array of `String` field names for grouping. + /// - Returns: A new `Pipeline` object with this stage appended. + public func aggregate(_ accumulator: [AggregateWithAlias], + groups: [String]? = nil) -> Pipeline { + let selectables = groups?.map { Field($0) } + return Pipeline( + stages: stages + [Aggregate(accumulators: accumulator, groups: selectables)], + db: db + ) + } + + /// Performs a vector similarity search, ordering results by similarity. + /// + /// Returns up to `limit` documents, from most to least similar based on vector embeddings. + /// The distance can optionally be included in a specified field. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from a collection with vector embeddings. + /// let queryVector: [Double] = [0.1, 0.2, ..., 0.8] // Example query vector. + /// let nearestNeighborsPipeline = pipeline.findNearest( + /// field: Field("embedding_field"), // Field containing the vector. + /// vectorValue: queryVector, // Query vector for comparison. + /// distanceMeasure: .COSINE, // Distance metric. + /// limit: 10, // Return top 10 nearest neighbors. + /// distanceField: "similarityScore" // Optional: field for distance score. + /// ) + /// // let results = try await nearestNeighborsPipeline.execute() + /// ``` + /// + /// - Parameters: + /// - field: The `Field` containing vector embeddings. + /// - vectorValue: An array of `Double` representing the query vector. + /// - distanceMeasure: The `DistanceMeasure` (e.g., `.EUCLIDEAN`, `.COSINE`) for comparison. + /// - limit: Optional. Maximum number of similar documents to return. + /// - distanceField: Optional. Name for a new field to store the calculated distance. + /// - Returns: A new `Pipeline` object with this stage appended. + public func findNearest(field: Field, + vectorValue: [Double], + distanceMeasure: DistanceMeasure, + limit: Int? = nil, + distanceField: String? = nil) -> Pipeline { + return Pipeline( + stages: stages + [ + FindNearest( + field: field, + vectorValue: vectorValue, + distanceMeasure: distanceMeasure, + limit: limit, + distanceField: distanceField + ), + ], + db: db + ) + } + + /// Sorts documents from previous stages based on one or more `Ordering` criteria. + /// + /// Specify multiple `Ordering` instances for multi-field sorting (ascending/descending). + /// If documents are equal by one criterion, the next is used. If all are equal, + /// relative order is unspecified. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Sort books by rating (descending), then by title (ascending). + /// let sortedPipeline = pipeline.sort( + /// Ascending("rating"), + /// Descending("title") // or Field("title").ascending() for ascending. + /// ) + /// // let results = try await sortedPipeline.execute() + /// ``` + /// + /// - Parameter ordering: The primary `Ordering` criterion. + /// - Parameter additionalOrdering: Optional additional `Ordering` criteria for secondary sorting, + /// etc. + /// - Returns: A new `Pipeline` object with this stage appended. + public func sort(_ ordering: Ordering, _ additionalOrdering: Ordering...) -> Pipeline { + let orderings = [ordering] + additionalOrdering + return Pipeline(stages: stages + [Sort(orderings: orderings)], db: db) + } + + /// Fully overwrites document fields with those from a nested map identified by an `Expr`. + /// + /// "Promotes" a map value (dictionary) from a field to become the new root document. + /// Each key-value pair from the map specified by `expr` becomes a field-value pair + /// in the output document, discarding original document fields. + /// + /// ```swift + /// // Assume input document: + /// // { "id": "user123", "profile": { "name": "Alex", "age": 30 }, "status": "active" } + /// // let pipeline: Pipeline = ... + /// + /// // Replace document with the contents of the 'profile' map. + /// let replacedPipeline = pipeline.replace(with: Field("profile")) + /// + /// // let results = try await replacedPipeline.execute() + /// // Output document would be: { "name": "Alex", "age": 30 } + /// ``` + /// + /// - Parameter expr: The `Expr` (typically a `Field`) that resolves to the nested map. + /// - Returns: A new `Pipeline` object with this stage appended. + public func replace(with expr: Expr) -> Pipeline { + return Pipeline(stages: stages + [ReplaceWith(expr: expr)], db: db) + } + + /// Fully overwrites document fields with those from a nested map identified by a field name. + /// + /// "Promotes" a map value (dictionary) from a field to become the new root document. + /// Each key-value pair from the map in `fieldName` becomes a field-value pair + /// in the output document, discarding original document fields. + /// + /// ```swift + /// // Assume input document: + /// // { "id": "user123", "details": { "role": "admin", "department": "tech" }, "joined": + /// "2023-01-15" } + /// // let pipeline: Pipeline = ... + /// + /// // Replace document with the contents of the 'details' map. + /// let replacedPipeline = pipeline.replace(with: "details") + /// + /// // let results = try await replacedPipeline.execute() + /// // Output document would be: { "role": "admin", "department": "tech" } + /// ``` + /// + /// - Parameter fieldName: The name of the field containing the nested map. + /// - Returns: A new `Pipeline` object with this stage appended. + public func replace(with fieldName: String) -> Pipeline { + return Pipeline(stages: stages + [ReplaceWith(expr: Field(fieldName))], db: db) + } + + /// Performs pseudo-random sampling of input documents, returning a specific count. + /// + /// Filters documents pseudo-randomly. `count` specifies the approximate number + /// to return. The actual number may vary and isn't guaranteed if the input set + /// is smaller than `count`. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from a large collection. + /// // Sample 25 books, if available. + /// let sampledPipeline = pipeline.sample(count: 25) + /// // let results = try await sampledPipeline.execute() + /// ``` + /// + /// - Parameter count: The target number of documents to sample (a `Int64` value). + /// - Returns: A new `Pipeline` object with this stage appended. + public func sample(count: Int64) -> Pipeline { + return Pipeline(stages: stages + [Sample(count: count)], db: db) + } + + /// Performs pseudo-random sampling of input documents, returning a percentage. + /// + /// Filters documents pseudo-randomly. `percentage` (0.0 to 1.0) specifies + /// the approximate fraction of documents to return from the input set. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Sample 50% of books. + /// let sampledPipeline = pipeline.sample(percentage: 0.5) + /// // let results = try await sampledPipeline.execute() + /// ``` + /// + /// - Parameter percentage: The percentage of documents to sample (e.g., 0.5 for 50%; a `Double` + /// value). + /// - Returns: A new `Pipeline` object with this stage appended. + public func sample(percentage: Double) -> Pipeline { + return Pipeline(stages: stages + [Sample(percentage: percentage)], db: db) + } + + /// Performs a union of all documents from this pipeline and another, including duplicates. + /// + /// Passes through documents from this pipeline's previous stage and also those from + /// the `other` pipeline's previous stage. The order of emitted documents is undefined. + /// Both pipelines should ideally have compatible document structures. + /// + /// ```swift + /// // let db: Firestore = ... + /// // let booksPipeline = db.collection("books").pipeline().select("title", "category") + /// // let magazinesPipeline = db.collection("magazines").pipeline().select("title", + /// Field("topic").as("category")) + /// + /// // Emit documents from both "books" and "magazines" collections. + /// let combinedPipeline = booksPipeline.union(magazinesPipeline) + /// // let results = try await combinedPipeline.execute() + /// ``` + /// + /// - Parameter other: The other `Pipeline` whose documents will be unioned. + /// - Returns: A new `Pipeline` object with this stage appended. + public func union(_ other: Pipeline) -> Pipeline { + return Pipeline(stages: stages + [Union(other: other)], db: db) + } + + /// Takes an array field from input documents and outputs a new document for each element. + /// + /// For each input document, this stage emits zero or more augmented documents based on + /// an array field specified by `field` (a `Selectable`). The `Selectable` for `field` + /// **must** have an alias; this alias becomes the field name in the output document + /// containing the unnested element. + /// + /// The original field containing the array is effectively replaced by the array element + /// under the new alias name in each output document. Other fields from the original document + /// are typically preserved. + /// + /// If `indexField` is provided, a new field with this name is added, containing the + /// zero-based index of the element within its original array. + /// + /// Behavior for non-array values or empty arrays depends on the backend. + /// + /// ```swift + /// // Assume input document: + /// // { "title": "The Hitchhiker's Guide", "authors": ["Douglas Adams", "Eoin Colfer"] } + /// // let pipeline: Pipeline = ... + /// + /// // Unnest 'authors'. Each author becomes a new document with the author in a "authorName" + /// field. + /// let unnestedPipeline = pipeline.unnest(Field("authors").as("authorName"), indexField: + /// "authorIndex") + /// + /// // let results = try await unnestedPipeline.execute() + /// // Possible Output (other fields like "title" are preserved): + /// // { "title": "The Hitchhiker's Guide", "authorName": "Douglas Adams", "authorIndex": 0 } + /// // { "title": "The Hitchhiker's Guide", "authorName": "Eoin Colfer", "authorIndex": 1 } + /// ``` + /// + /// - Parameters: + /// - field: A `Selectable` resolving to an array field. **Must include an alias** + /// (e.g., `Field("myArray").as("arrayElement")`) to name the output field. + /// - indexField: Optional. If provided, this string names a new field for the element's + /// zero-based index from the original array. + /// - Returns: A new `Pipeline` object with this stage appended. + public func unnest(_ field: Selectable, indexField: String? = nil) -> Pipeline { + return Pipeline(stages: stages + [Unnest(field: field, indexField: indexField)], db: db) + } + + /// Adds a generic stage to the pipeline by specifying its name and parameters. + /// + /// Use this to call backend-supported stages not yet strongly-typed in the SDK. + /// This method does not offer compile-time type safety for stage parameters; + /// the caller must ensure correct name, order, and types. + /// + /// Parameters in `params` and `options` are typically primitive types, `Field`, + /// `Function`, `Expr`, or arrays/dictionaries thereof. + /// + /// ```swift + /// // let pipeline: Pipeline = ... + /// // Example: Assuming a hypothetical backend stage "customFilterV2". + /// let genericPipeline = pipeline.genericStage( + /// name: "customFilterV2", + /// params: [Field("userScore"), 80], // Ordered parameters. + /// options: ["mode": "strict", "logLevel": 2] // Optional named parameters. + /// ) + /// // let results = try await genericPipeline.execute() + /// ``` + /// + /// - Parameters: + /// - name: The unique name of the stage (as recognized by the backend). + /// - params: An array of ordered, `Sendable` parameters for the stage. + /// - options: Optional dictionary of named, `Sendable` parameters. + /// - Returns: A new `Pipeline` object with this stage appended. + public func genericStage(name: String, params: [Sendable], + options: [String: Sendable]? = nil) -> Pipeline { + return Pipeline( + stages: stages + [GenericStage(name: name, params: params, options: options)], + db: db + ) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift new file mode 100644 index 00000000000..6e1d892f3cb --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -0,0 +1,70 @@ +// Copyright 2025 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct PipelineResult: @unchecked Sendable { + let bridge: __PipelineResultBridge + + init(_ bridge: __PipelineResultBridge) { + self.bridge = bridge + ref = self.bridge.reference + id = self.bridge.documentID + data = self.bridge.data() + createTime = self.bridge.create_time + updateTime = self.bridge.update_time + } + + /// The reference of the document, if the query returns the `__name__` field. + public let ref: DocumentReference? + + /// The ID of the document for which this `PipelineResult` contains data, if available. + public let id: String? + + /// The time the document was created, if available. + public let createTime: Timestamp? + + /// The time the document was last updated when the snapshot was generated. + public let updateTime: Timestamp? + + /// Retrieves all fields in the result as a dictionary. + public let data: [String: Sendable] + + /// Retrieves the field specified by `fieldPath`. + /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). + /// - Returns: The data at the specified field location or `nil` if no such field exists. + public func get(_ fieldName: String) -> Sendable? { + return bridge.get(fieldName) + } + + /// Retrieves the field specified by `fieldPath`. + /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). + /// - Returns: The data at the specified field location or `nil` if no such field exists. + public func get(_ fieldPath: FieldPath) -> Sendable? { + return bridge.get(fieldPath) + } + + /// Retrieves the field specified by `fieldPath`. + /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). + /// - Returns: The data at the specified field location or `nil` if no such field exists. + public func get(_ field: Field) -> Sendable? { + return bridge.get(field.fieldName) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift new file mode 100644 index 00000000000..e25191b8ad2 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift @@ -0,0 +1,45 @@ +// Copyright 2025 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct PipelineSnapshot: Sendable { + /// The Pipeline on which `execute()` was called to obtain this `PipelineSnapshot`. + public let pipeline: Pipeline + + /// An array of all the results in the `PipelineSnapshot`. + let results_cache: [PipelineResult] + + /// The time at which the pipeline producing this result was executed. + public let executionTime: Timestamp + + let bridge: __PipelineSnapshotBridge + + init(_ bridge: __PipelineSnapshotBridge, pipeline: Pipeline) { + self.bridge = bridge + self.pipeline = pipeline + executionTime = self.bridge.execution_time + results_cache = self.bridge.results.map { PipelineResult($0) } + } + + public func results() -> [PipelineResult] { + return results_cache + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift new file mode 100644 index 00000000000..da0b5b5b1b4 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -0,0 +1,56 @@ +// Copyright 2025 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. + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct PipelineSource: @unchecked Sendable { + let db: Firestore + + init(_ db: Firestore) { + self.db = db + } + + public func collection(_ path: String) -> Pipeline { + let normalizedPath = path.hasPrefix("/") ? path : "/" + path + return Pipeline(stages: [CollectionSource(collection: normalizedPath)], db: db) + } + + public func collectionGroup(_ collectionId: String) -> Pipeline { + return Pipeline( + stages: [CollectionGroupSource(collectionId: collectionId)], + db: db + ) + } + + public func database() -> Pipeline { + return Pipeline(stages: [DatabaseSource()], db: db) + } + + public func documents(_ docs: [DocumentReference]) -> Pipeline { + let paths = docs.map { $0.path.hasPrefix("/") ? $0.path : "/" + $0.path } + return Pipeline(stages: [DocumentsSource(paths: paths)], db: db) + } + + public func documents(_ paths: [String]) -> Pipeline { + let normalizedPaths = paths.map { $0.hasPrefix("/") ? $0 : "/" + $0 } + return Pipeline(stages: [DocumentsSource(paths: normalizedPaths)], db: db) + } + + public func create(from query: Query) -> Pipeline { + return Pipeline(stages: [QuerySource(query: query)], db: db) + } + + public func create(from aggregateQuery: AggregateQuery) -> Pipeline { + return Pipeline(stages: [AggregateQuerySource(aggregateQuery: aggregateQuery)], db: db) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift new file mode 100644 index 00000000000..de1a709d44d --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift @@ -0,0 +1,15 @@ +// Copyright 2025 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. + +public struct RealtimePipeline: @unchecked Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift new file mode 100644 index 00000000000..a9c655f4e6a --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift @@ -0,0 +1,15 @@ +// Copyright 2025 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. + +public protocol Selectable: Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift new file mode 100644 index 00000000000..0b8aa112db8 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift @@ -0,0 +1,37 @@ +// Copyright 2025 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. + +public struct TimeUnit: Sendable, Equatable, Hashable { + enum Kind: String { + case microsecond + case millisecond + case second + case minute + case hour + case day + } + + public static let microsecond = TimeUnit(kind: .microsecond) + public static let millisecond = TimeUnit(kind: .millisecond) + public static let second = TimeUnit(kind: .second) + public static let minute = TimeUnit(kind: .minute) + public static let hour = TimeUnit(kind: .hour) + public static let day = TimeUnit(kind: .day) + + public let rawValue: String + + init(kind: Kind) { + rawValue = kind.rawValue + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift deleted file mode 100644 index 00386d0c6dc..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2025 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 Foundation - -public struct PipelineSnapshot { - private let bridge: __PipelineSnapshotBridge - - init(_ bridge: __PipelineSnapshotBridge) { - self.bridge = bridge - } -} diff --git a/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift deleted file mode 100644 index ce84c0356ac..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 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 Foundation - -public class PipelineSource { - private let db: Firestore - public init(db: Firestore) { - self.db = db - } - - public func collection(path: String) -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: path)], db: db) - } -} diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index c5de0c00e52..65796af8471 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -14,18 +14,25 @@ * limitations under the License. */ -import FirebaseFirestoreInternal import Foundation +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) protocol Stage { var name: String { get } var bridge: StageBridge { get } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class CollectionSource: Stage { - var name: String = "collection" + let name: String = "collection" - var bridge: StageBridge + let bridge: StageBridge private var collection: String init(collection: String) { @@ -34,14 +41,314 @@ class CollectionSource: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class CollectionGroupSource: Stage { + let name: String = "collectionId" + + let bridge: StageBridge + private var collectionId: String + + init(collectionId: String) { + self.collectionId = collectionId + bridge = CollectionGroupSourceStageBridge(collectionId: collectionId) + } +} + +// Represents the entire database as a source. +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class DatabaseSource: Stage { + let name: String = "database" + let bridge: StageBridge + + init() { + bridge = DatabaseSourceStageBridge() + } +} + +// Represents a list of document references as a source. +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class DocumentsSource: Stage { + let name: String = "documents" + let bridge: StageBridge + private var references: [String] + + // Initialize with an array of String paths + init(paths: [String]) { + references = paths + bridge = DocumentsSourceStageBridge(documents: paths) + } +} + +// Represents an existing Query as a source. +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class QuerySource: Stage { + let name: String = "query" + let bridge: StageBridge + private var query: Query + + init(query: Query) { + self.query = query + bridge = DatabaseSourceStageBridge() + // TODO: bridge = QuerySourceStageBridge(query: query.query) + } +} + +// Represents an existing AggregateQuery as a source. +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class AggregateQuerySource: Stage { + let name: String = "aggregateQuery" + let bridge: StageBridge + private var aggregateQuery: AggregateQuery + + init(aggregateQuery: AggregateQuery) { + self.aggregateQuery = aggregateQuery + bridge = DatabaseSourceStageBridge() + // TODO: bridge = AggregateQuerySourceStageBridge(aggregateQuery: aggregateQuery.query) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Where: Stage { - var name: String = "where" + let name: String = "where" - var bridge: StageBridge - private var condition: Expr // TODO: should be FilterCondition + let bridge: StageBridge + private var condition: BooleanExpr - init(condition: Expr) { + init(condition: BooleanExpr) { self.condition = condition - bridge = WhereStageBridge(expr: (condition as! (Expr & BridgeWrapper)).bridge) + bridge = WhereStageBridge(expr: condition.toBridge()) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Limit: Stage { + let name: String = "limit" + + let bridge: StageBridge + private var limit: Int32 + + init(_ limit: Int32) { + self.limit = limit + bridge = LimitStageBridge(limit: NSInteger(limit)) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Offset: Stage { + let name: String = "offset" + + let bridge: StageBridge + private var offset: Int32 + + init(_ offset: Int32) { + self.offset = offset + bridge = OffsetStageBridge(offset: NSInteger(offset)) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class AddFields: Stage { + let name: String = "addFields" + let bridge: StageBridge + private var fields: [Selectable] + + init(fields: [Selectable]) { + self.fields = fields + let objc_accumulators = fields.reduce(into: [String: ExprBridge]()) { + result, + field + in + let seletable = field as! SelectableWrapper + result[seletable.alias] = seletable.expr.toBridge() + } + bridge = AddFieldsStageBridge(fields: objc_accumulators) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class RemoveFieldsStage: Stage { + let name: String = "removeFields" + let bridge: StageBridge + private var fields: [String] + + init(fields: [String]) { + self.fields = fields + bridge = RemoveFieldsStageBridge(fields: fields) + } + + init(fields: [Field]) { + self.fields = fields.map { $0.fieldName } + bridge = RemoveFieldsStageBridge(fields: self.fields) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Select: Stage { + let name: String = "select" + let bridge: StageBridge + private var selections: [Selectable] + + init(selections: [Selectable]) { + self.selections = selections + let map = Helper.selectablesToMap(selectables: selections) + bridge = SelectStageBridge(selections: map + .mapValues { Helper.sendableToExpr($0).toBridge() }) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Distinct: Stage { + let name: String = "distinct" + let bridge: StageBridge + private var groups: [Selectable] + + init(groups: [Selectable]) { + self.groups = groups + let map = Helper.selectablesToMap(selectables: groups) + bridge = DistinctStageBridge(groups: map + .mapValues { Helper.sendableToExpr($0).toBridge() }) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Aggregate: Stage { + let name: String = "aggregate" + let bridge: StageBridge + private var accumulators: [AggregateWithAlias] + private var groups: [String: Expr] = [:] + + init(accumulators: [AggregateWithAlias], groups: [Selectable]?) { + self.accumulators = accumulators + if groups != nil { + self.groups = Helper.selectablesToMap(selectables: groups!) + } + let map = accumulators + .reduce(into: [String: AggregateFunctionBridge]()) { result, accumulator in + result[accumulator.alias] = accumulator.aggregate.bridge + } + bridge = AggregateStageBridge( + accumulators: map, + groups: self.groups.mapValues { Helper.sendableToExpr($0).toBridge() } + ) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class FindNearest: Stage { + let name: String = "findNearest" + let bridge: StageBridge + private var field: Field + private var vectorValue: [Double] + private var distanceMeasure: DistanceMeasure + private var limit: Int? + private var distanceField: String? + + init(field: Field, + vectorValue: [Double], + distanceMeasure: DistanceMeasure, + limit: Int? = nil, + distanceField: String? = nil) { + self.field = field + self.vectorValue = vectorValue + self.distanceMeasure = distanceMeasure + self.limit = limit + self.distanceField = distanceField + bridge = FindNearestStageBridge( + field: field.bridge as! FieldBridge, + vectorValue: VectorValue(vectorValue), + distanceMeasure: distanceMeasure.kind.rawValue, + limit: limit as NSNumber?, + distanceField: distanceField + ) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Sort: Stage { + let name: String = "sort" + let bridge: StageBridge + private var orderings: [Ordering] + + init(orderings: [Ordering]) { + self.orderings = orderings + bridge = SortStageBridge(orderings: orderings.map { $0.bridge }) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class ReplaceWith: Stage { + let name: String = "replaceWith" + let bridge: StageBridge + private var expr: Expr + + init(expr: Expr) { + self.expr = expr + bridge = ReplaceWithStageBridge(expr: expr.toBridge()) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Sample: Stage { + let name: String = "sample" + let bridge: StageBridge + private var count: Int64? + private var percentage: Double? + + init(count: Int64) { + self.count = count + percentage = nil + bridge = SampleStageBridge(count: count) + } + + init(percentage: Double) { + self.percentage = percentage + count = nil + bridge = SampleStageBridge(percentage: percentage) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Union: Stage { + let name: String = "union" + let bridge: StageBridge + private var other: Pipeline + + init(other: Pipeline) { + self.other = other + bridge = UnionStageBridge(other: other.bridge) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class Unnest: Stage { + let name: String = "unnest" + let bridge: StageBridge + private var field: Selectable + private var indexField: String? + + init(field: Selectable, indexField: String? = nil) { + self.field = field + self.indexField = indexField + bridge = UnnestStageBridge( + field: Helper.sendableToExpr(field).toBridge(), + indexField: indexField + ) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class GenericStage: Stage { + let name: String + let bridge: StageBridge + private var params: [Sendable] + private var options: [String: Sendable]? + + init(name: String, params: [Sendable], options: [String: Sendable]? = nil) { + self.name = name + self.params = params + self.options = options + let bridgeParams = params.map { Helper.sendableToExpr($0).toBridge() } + let bridgeOptions = options?.mapValues { Helper.sendableToExpr($0).toBridge() } + bridge = GenericStageBridge(name: name, params: bridgeParams, options: bridgeOptions) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift new file mode 100644 index 00000000000..25ca1f09a6d --- /dev/null +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -0,0 +1,404 @@ +// Copyright 2025 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 Foundation +import XCTest + +import FirebaseFirestore + +final class PipelineTests: FSTIntegrationTestCase { + override func setUp() { + FSTIntegrationTestCase.switchToEnterpriseMode() + super.setUp() + } + + func testCreatePipeline() async throws { + let pipelineSource: PipelineSource = db.pipeline() + + let pipeline: Pipeline = pipelineSource.documents( + [db.collection("foo").document("bar"), db.document("foo/baz")] + ) + let _: Pipeline = pipelineSource.collection("foo") + let _: Pipeline = pipelineSource.collectionGroup("foo") + let _: Pipeline = pipelineSource.database() + + let query: Query = db.collection("foo").limit(to: 2) + let _: Pipeline = pipelineSource.create(from: query) + + let aggregateQuery = db.collection("foo").count + let _: Pipeline = pipelineSource.create(from: aggregateQuery) + + let _: PipelineSnapshot = try await pipeline.execute() + } + + func testWhereStage() async throws { + _ = db.pipeline().collection("books") + .where( + Field("rating").gt(4.0) && Field("genre").eq("Science Fiction") || ArrayContains( + fieldName: "fieldName", + values: "rating" + ) + ) + } + + func testAddFieldStage() async throws { + // Input + // { title: 'title1', price: 10, discount: 0.8 }, + // { title: 'title2', price: 12, discount: 1.0 }, + // { title: 'title3', price: 5, discount: 0.66 } + + // An expression that will compute price from the value of msrp field and discount field + let priceExpr: FunctionExpr = Field("msrp").multiply(Field("discount")) + + // An expression becomes a Selectable when given an alias. In this case + // the alias is 'salePrice' + let priceSelectableExpr: Selectable = priceExpr.as("salePrice") + + _ = db.pipeline().collection("books") + .addFields( + priceSelectableExpr // Add field `salePrice` based computed from msrp and discount + ) + + // We don't expect customers to separate the Expression definition from the + // Pipeline definition. This was shown above so readers of this doc can see + // the different types involved. The cleaner way to write the code above + // is to inline the Expr definition + _ = db.pipeline().collection("books") + .addFields( + Field("msrp").multiply(Field("discount")).as("salePrice"), + Field("author") + ) + + // Output + // { title: 'title1', price: 10, discount: 0.8, salePrice: 8.0}, + // { title: 'title2', price: 12, discount: 1.0, salePrice: 12.0 }, + // { title: 'title3', price: 5, discount: 0.66, salePrice: 3.30 } + } + + func testRemoveFieldsStage() async throws { + // removes field 'rating' and 'cost' from the previous stage outputs. + _ = db.pipeline().collection("books").removeFields("rating", "cost") + + // removes field 'rating'. + _ = db.pipeline().collection("books").removeFields(Field("rating")) + } + + func testSelectStage() async throws { + // Input + // { title: 'title1', price: 10, discount: 0.8 }, + // { title: 'title2', price: 12, discount: 1.0 }, + // { title: 'title3', price: 5, discount: 0.66 } + + // Overload for string and Selectable + _ = db.pipeline().collection("books") + .select( + Field("title"), // Field class inheritates Selectable + Field("msrp").multiply(Field("discount")).as("salePrice") + ) + + _ = db.pipeline().collection("books").select("title", "author") + + // Output + // { title: 'title1', salePrice: 8.0}, + // { title: 'title2', salePrice: 12.0 }, + // { title: 'title3', salePrice: 3.30 } + } + + func testSortStage() async throws { + // Sort books by rating in descending order, and then by title in ascending order for books + // with the same rating + _ = db.pipeline().collection("books") + .sort( + Field("rating").descending(), + Ascending("title") // alternative API offered + ) + } + + func testLimitStage() async throws { + // Limit the results to the top 10 highest-rated books + _ = db.pipeline().collection("books") + .sort(Field("rating").descending()) + .limit(10) + } + + func testOffsetStage() async throws { + // Retrieve the second page of 20 results + _ = db.pipeline().collection("books") + .sort(Field("published").descending()) + .offset(20) // Skip the first 20 results. Note that this must come + // before .limit(...) unlike in Query where the order did not matter. + .limit(20) // Take the next 20 results + } + + func testDistinctStage() async throws { + // Input + // { author: 'authorA', genre: 'genreA', title: 'title1' }, + // { author: 'authorb', genre: 'genreB', title: 'title2' }, + // { author: 'authorB', genre: 'genreB', title: 'title3' } + + // Get a list of unique author names in uppercase and genre combinations. + _ = db.pipeline().collection("books") + .distinct( + Field("author").uppercased().as("authorName"), + Field("genre") + ) + + // Output + // { authorName: 'AUTHORA', genre: 'genreA' }, + // { authorName: 'AUTHORB', genre: 'genreB' } + } + + func testAggregateStage() async throws { + // Input + // { genre: 'genreA', title: 'title1', rating: 5.0 }, + // { genre: 'genreB', title: 'title2', rating: 1.5 }, + // { genre: 'genreB', title: 'title3', rating: 2.5 } + + // Calculate the average rating and the total number of books + _ = db.pipeline().collection("books") + .aggregate( + Field("rating").avg().as("averageRating"), + CountAll().as("totalBooks") + ) + + // Output + // { totalBooks: 3, averageRating: 3.0 } + + // Input + // { genre: 'genreA', title: 'title1', rating: 5.0 }, + // { genre: 'genreB', title: 'title2', rating: 1.5 }, + // { genre: 'genreB', title: 'title3', rating: 2.5 } + + // Calculate the average rating and the total number of books and group by field 'genre' + _ = db.pipeline().collection("books") + .aggregate([ + Field("rating").avg().as("averageRating"), + CountAll().as("totalBooks"), + ], + groups: ["genre"]) + + // Output + // { genre: 'genreA', totalBooks: 1, averageRating: 5.0 } + // { genre: 'genreB', totalBooks: 2, averageRating: 2.0 } + } + + func testFindNearestStage() async throws { + _ = db.pipeline().collection("books").findNearest( + field: Field("embedding"), + vectorValue: [5.0], + distanceMeasure: .cosine, + limit: 3) + } + + func testReplaceStage() async throws { + // Input. + // { +// "name": "John Doe Jr.", +// "parents": { +// "father": "John Doe Sr.", +// "mother": "Jane Doe" +// } + // } + + // Emit field parents as the document. + _ = db.pipeline().collection("people") + .replace(with: Field("parents")) + + // Output + // { +// "father": "John Doe Sr.", +// "mother": "Jane Doe" + // } + } + + func testSampleStage() async throws { + // Sample 25 books, if the collection contains at least 25 documents + _ = db.pipeline().collection("books").sample(count: 10) + + // Sample 10 percent of the collection of books + _ = db.pipeline().collection("books").sample(percentage: 10) + } + + func testUnionStage() async throws { + // Emit documents from books collection and magazines collection. + _ = db.pipeline().collection("books") + .union(db.pipeline().collection("magazines")) + } + + func testUnnestStage() async throws { + // Input: + // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" + // ], ... } + + // Emit a book document for each tag of the book. + _ = db.pipeline().collection("books") + .unnest(Field("tags").as("tag")) + + // Output: + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", tags: [...], ... } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", tags: [...], ... } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", tags: [...], ... } + + // Emit a book document for each tag of the book mapped to its' index in the array. + _ = db.pipeline().collection("books") + .unnest(Field("tags").as("tag"), indexField: "index") + + // Output: + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", index: 0, tags: [...], + // ... } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", index: 1, tags: [...], ... + // } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", index: 2, tags: [...], + // ... } + } + + func testGenericStage() async throws { + // Assume we don't have a built-in "where" stage, the customer could still + // add this stage by calling genericStage, passing the name of the stage "where", + // and providing positional argument values. + _ = db.pipeline().collection("books") + .genericStage(name: "where", + params: [Field("published").lt(1900)]) + .select("title", "author") + + // In cases where the stage also supports named argument values, then these can be + // provided with a third argument that maps the argument name to value. + // Note that these named arguments are always optional in the stage definition. + _ = db.pipeline().collection("books") + .genericStage(name: "where", + params: [Field("published").lt(1900)], + options: ["someOptionalParamName": "the argument value for this param"]) + .select("title", "author") + } + + func testField() async throws { + // An expression that will return the value of the field `name` in the document + let nameField = Field("name") + + // An expression that will return the value of the field `description` in the document + // Field is a sub-type of Expr, so we can also declare our var of type Expr + let descriptionField: Expr = Field("description") + + // USAGE: anywhere an Expr type is accepted + // Use a field in a pipeline + _ = db.pipeline().collection("books") + .addFields( + Field("rating").as("bookRating") // Duplicate field 'rating' as 'bookRating' + ) + + // One special Field value is conveniently exposed as static function to help the user reference + // reserved field values of __name__. + _ = db.pipeline().collection("books") + .addFields( + DocumentId() + ) + } + + func testConstant() async throws { + // A constant for a number + let three = Constant(3) + + // A constant for a string + let name = Constant("Expressions API") + + // Const is a sub-type of Expr, so we can also declare our var of type Expr + let nothing: Expr = Constant.nil + + // USAGE: Anywhere an Expr type is accepted + // Add field `fromTheLibraryOf: 'Rafi'` to every document in the collection. + _ = db.pipeline().collection("books") + .addFields(Constant("Rafi").as("fromTheLibraryOf")) + } + + func testFunctionExpr() async throws { + let secondsField = Field("seconds") + + // Create a FunctionExpr using the multiply function to compute milliseconds + let milliseconds: FunctionExpr = secondsField.multiply(1000) + + // A firestore function is also a sub-type of Expr + let myExpr: Expr = milliseconds + } + + func testBooleanExpr() async throws { + let isApple: BooleanExpr = Field("type").eq("apple") + + // USAGE: stage where requires an expression of type BooleanExpr + let allAppleOptions: Pipeline = db.pipeline().collection("fruitOptions").where(isApple) + } + + func testSelectableExpr() async throws { + let secondsField = Field("seconds") + + // Create a selectable from our milliseconds expression. + let millisecondsSelectable: Selectable = secondsField.multiply(1000).as("milliseconds") + + // USAGE: stages addFields and select accept expressions of type Selectable + // Add (or overwrite) the 'milliseconds` field to each of our documents using the + // `.addFields(...)` stage. + _ = db.pipeline().collection("lapTimes") + .addFields(secondsField.multiply(1000).as("milliseconds")) + + // NOTE: Field implements Selectable, the alias is the same as the name + let secondsSelectable: Selectable = secondsField + } + + func testAggregateExpr() async throws { + let lapTimeSum: AggregateFunction = Field("seconds").sum() + + let lapTimeSumTarget: AggregateWithAlias = lapTimeSum.as("totalTrackTime") + + // USAGE: stage aggregate accepts expressions of type AggregateWithAlias + // A pipeline that will return one document with one field `totalTrackTime` that + // is the sum of all laps ever taken on the track. + _ = db.pipeline().collection("lapTimes") + .aggregate(lapTimeSum.as("totalTrackTime")) + } + + func testOrdering() async throws { + let fastestToSlowest: Ordering = Field("seconds").ascending() + + // USAGE: stage sort accepts objects of type Ordering + // Use this ordering to sort our lap times collection from fastest to slowest + _ = db.pipeline().collection("lapTimes").sort(fastestToSlowest) + } + + func testExpr() async throws { + // An expression that computes the area of a circle + // by chaining together two calls to the multiply function + let radiusField: Expr = Field("radius") + let radiusSq: Expr = radiusField.multiply(Field("radius")) + let areaExpr: Expr = radiusSq.multiply(3.14) + + // Or define this expression in one clean, fluent statement + let areaOfCircle: Selectable = Field("radius") + .multiply(Field("radius")) + .multiply(3.14) + .as("area") + + // And pass the expression to a Pipeline for evaluation + _ = db.pipeline().collection("circles").addFields(areaOfCircle) + } + + func testGeneric() async throws { + // This is the same of the logicalMin('price', 0)', if it did not exist + let myLm = FunctionExpr("logicalMin", [Field("price"), Constant(0)]) + + // Create a generic BooleanExpr for use where BooleanExpr is required + let myEq = BooleanExpr("eq", [Field("price"), Constant(10)]) + + // Create a generic AggregateFunction for use where AggregateFunction is required + let mySum = AggregateFunction("sum", [Field("price")]) + } +} diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index a2252488312..7bfdc8525a1 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -17,16 +17,152 @@ import FirebaseFirestore import Foundation +private let bookDocs: [String: [String: Any]] = [ + "book1": [ + "title": "The Hitchhiker's Guide to the Galaxy", + "author": "Douglas Adams", + "genre": "Science Fiction", + "published": 1979, + "rating": 4.2, + "tags": ["comedy", "space", "adventure"], // Array literal + "awards": ["hugo": true, "nebula": false], // Dictionary literal + "nestedField": ["level.1": ["level.2": true]], // Nested dictionary literal + ], + "book2": [ + "title": "Pride and Prejudice", + "author": "Jane Austen", + "genre": "Romance", + "published": 1813, + "rating": 4.5, + "tags": ["classic", "social commentary", "love"], + "awards": ["none": true], + ], + "book3": [ + "title": "One Hundred Years of Solitude", + "author": "Gabriel García Márquez", + "genre": "Magical Realism", + "published": 1967, + "rating": 4.3, + "tags": ["family", "history", "fantasy"], + "awards": ["nobel": true, "nebula": false], + ], + "book4": [ + "title": "The Lord of the Rings", + "author": "J.R.R. Tolkien", + "genre": "Fantasy", + "published": 1954, + "rating": 4.7, + "tags": ["adventure", "magic", "epic"], + "awards": ["hugo": false, "nebula": false], + ], + "book5": [ + "title": "The Handmaid's Tale", + "author": "Margaret Atwood", + "genre": "Dystopian", + "published": 1985, + "rating": 4.1, + "tags": ["feminism", "totalitarianism", "resistance"], + "awards": ["arthur c. clarke": true, "booker prize": false], + ], + "book6": [ + "title": "Crime and Punishment", + "author": "Fyodor Dostoevsky", + "genre": "Psychological Thriller", + "published": 1866, + "rating": 4.3, + "tags": ["philosophy", "crime", "redemption"], + "awards": ["none": true], + ], + "book7": [ + "title": "To Kill a Mockingbird", + "author": "Harper Lee", + "genre": "Southern Gothic", + "published": 1960, + "rating": 4.2, + "tags": ["racism", "injustice", "coming-of-age"], + "awards": ["pulitzer": true], + ], + "book8": [ + "title": "1984", + "author": "George Orwell", + "genre": "Dystopian", + "published": 1949, + "rating": 4.2, + "tags": ["surveillance", "totalitarianism", "propaganda"], + "awards": ["prometheus": true], + ], + "book9": [ + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "genre": "Modernist", + "published": 1925, + "rating": 4.0, + "tags": ["wealth", "american dream", "love"], + "awards": ["none": true], + ], + "book10": [ + "title": "Dune", + "author": "Frank Herbert", + "genre": "Science Fiction", + "published": 1965, + "rating": 4.6, + "tags": ["politics", "desert", "ecology"], + "awards": ["hugo": true, "nebula": true], + ], +] + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class PipelineIntegrationTests: FSTIntegrationTestCase { + override func setUp() { + FSTIntegrationTestCase.switchToEnterpriseMode() + super.setUp() + } + func testCount() async throws { try await firestore().collection("foo").document("bar").setData(["foo": "bar", "x": 42]) let snapshot = try await firestore() .pipeline() - .collection(path: "/foo") - .where(eq(field("foo"), constant("bar"))) + .collection("/foo") + .where(Field("foo").eq(Constant("bar"))) .execute() print(snapshot) } + + func testEmptyResults() async throws { + let collRef = collectionRef( + withDocuments: bookDocs + ) + let db = collRef.firestore + + let snapshot = try await db + .pipeline() + .collection(collRef.path) + .limit(0) + .execute() + + XCTAssertTrue(snapshot.results().isEmpty) + } + + func testFullResults() async throws { + let collRef = collectionRef( + withDocuments: bookDocs + ) + let db = collRef.firestore + + let snapshot = try await db + .pipeline() + .collection(collRef.path) + .execute() + + let results = snapshot.results() + XCTAssertEqual(results.count, 10) + + let actualIDs = Set(results.map { $0.id }) + let expectedIDs = Set([ + "book1", "book2", "book3", "book4", "book5", + "book6", "book7", "book8", "book9", "book10", + ]) + XCTAssertEqual(actualIDs, expectedIDs) + } } diff --git a/Firestore/core/src/api/aggregate_expressions.cc b/Firestore/core/src/api/aggregate_expressions.cc index 87fc69c368a..fb58918833c 100644 --- a/Firestore/core/src/api/aggregate_expressions.cc +++ b/Firestore/core/src/api/aggregate_expressions.cc @@ -22,7 +22,7 @@ namespace firebase { namespace firestore { namespace api { -google_firestore_v1_Value AggregateExpr::to_proto() const { +google_firestore_v1_Value AggregateFunction::to_proto() const { google_firestore_v1_Value result; result.which_value_type = google_firestore_v1_Value_function_value_tag; diff --git a/Firestore/core/src/api/aggregate_expressions.h b/Firestore/core/src/api/aggregate_expressions.h index 119198b2abd..fc19eacb0a5 100644 --- a/Firestore/core/src/api/aggregate_expressions.h +++ b/Firestore/core/src/api/aggregate_expressions.h @@ -29,12 +29,12 @@ namespace firebase { namespace firestore { namespace api { -class AggregateExpr { +class AggregateFunction { public: - AggregateExpr(std::string name, std::vector> params) + AggregateFunction(std::string name, std::vector> params) : name_(std::move(name)), params_(std::move(params)) { } - ~AggregateExpr() = default; + ~AggregateFunction() = default; google_firestore_v1_Value to_proto() const; diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h index 5b08a277e3b..fe6f4fde9c8 100644 --- a/Firestore/core/src/api/expressions.h +++ b/Firestore/core/src/api/expressions.h @@ -36,18 +36,12 @@ class Expr { virtual google_firestore_v1_Value to_proto() const = 0; }; -class Selectable : public Expr { - public: - virtual ~Selectable() = default; - virtual const std::string& alias() const = 0; -}; - -class Field : public Selectable { +class Field : public Expr { public: explicit Field(std::string name) : name_(std::move(name)) { } google_firestore_v1_Value to_proto() const override; - const std::string& alias() const override { + const std::string& alias() const { return name_; } diff --git a/Firestore/core/src/api/ordering.cc b/Firestore/core/src/api/ordering.cc index 6520cea5b6f..388280b532a 100644 --- a/Firestore/core/src/api/ordering.cc +++ b/Firestore/core/src/api/ordering.cc @@ -31,7 +31,7 @@ google_firestore_v1_Value Ordering::to_proto() const { result.map_value.fields = nanopb::MakeArray(2); result.map_value.fields[0].key = nanopb::MakeBytesArray("expression"); - result.map_value.fields[0].value = field_.to_proto(); + result.map_value.fields[0].value = expr_->to_proto(); result.map_value.fields[1].key = nanopb::MakeBytesArray("direction"); google_firestore_v1_Value direction; direction.which_value_type = google_firestore_v1_Value_string_value_tag; diff --git a/Firestore/core/src/api/ordering.h b/Firestore/core/src/api/ordering.h index 130dda12b19..2e4709d2af0 100644 --- a/Firestore/core/src/api/ordering.h +++ b/Firestore/core/src/api/ordering.h @@ -17,16 +17,17 @@ #ifndef FIRESTORE_CORE_SRC_API_ORDERING_H_ #define FIRESTORE_CORE_SRC_API_ORDERING_H_ +#include +#include #include #include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/util/exception.h" namespace firebase { namespace firestore { namespace api { -class UserDataReader; // forward declaration - class Ordering { public: enum Direction { @@ -34,14 +35,20 @@ class Ordering { DESCENDING, }; - Ordering(Field field, Direction direction) - : field_(std::move(field)), direction_(direction) { + static Direction DirectionFromString(const std::string& str) { + if (str == "ascending") return ASCENDING; + if (str == "descending") return DESCENDING; + util::ThrowInvalidArgument("Unknown direction: '%s' ", str); + } + + Ordering(std::shared_ptr expr, Direction direction) + : expr_(expr), direction_(direction) { } google_firestore_v1_Value to_proto() const; private: - Field field_; + std::shared_ptr expr_; Direction direction_; }; diff --git a/Firestore/core/src/api/pipeline.cc b/Firestore/core/src/api/pipeline.cc index 24c5109bd95..53d332f3259 100644 --- a/Firestore/core/src/api/pipeline.cc +++ b/Firestore/core/src/api/pipeline.cc @@ -40,6 +40,20 @@ void Pipeline::execute(util::StatusOrCallback callback) { this->firestore_->RunPipeline(*this, std::move(callback)); } +google_firestore_v1_Value Pipeline::to_proto() const { + google_firestore_v1_Value result; + + result.which_value_type = google_firestore_v1_Value_pipeline_value_tag; + result.pipeline_value = google_firestore_v1_Pipeline{}; + result.pipeline_value.stages_count = this->stages_.size(); + nanopb::SetRepeatedField( + &result.pipeline_value.stages, &result.pipeline_value.stages_count, + stages_, + [](const std::shared_ptr& arg) { return arg->to_proto(); }); + + return result; +} + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/api/pipeline.h b/Firestore/core/src/api/pipeline.h index 6103f366eda..edea35dce6d 100644 --- a/Firestore/core/src/api/pipeline.h +++ b/Firestore/core/src/api/pipeline.h @@ -47,6 +47,8 @@ class Pipeline { void execute(util::StatusOrCallback callback); + google_firestore_v1_Value to_proto() const; + private: std::vector> stages_; std::shared_ptr firestore_; diff --git a/Firestore/core/src/api/pipeline_result.h b/Firestore/core/src/api/pipeline_result.h index 53761752cdc..662ea721c6b 100644 --- a/Firestore/core/src/api/pipeline_result.h +++ b/Firestore/core/src/api/pipeline_result.h @@ -53,6 +53,14 @@ class PipelineResult { std::shared_ptr internal_value() const; absl::optional document_id() const; + absl::optional create_time() const { + return create_time_; + } + + absl::optional update_time() const { + return update_time_; + } + const absl::optional& internal_key() const { return internal_key_; } diff --git a/Firestore/core/src/api/pipeline_snapshot.h b/Firestore/core/src/api/pipeline_snapshot.h index 079f2d57375..2bb0a1e94d2 100644 --- a/Firestore/core/src/api/pipeline_snapshot.h +++ b/Firestore/core/src/api/pipeline_snapshot.h @@ -41,6 +41,10 @@ class PipelineSnapshot { return results_; } + model::SnapshotVersion execution_time() const { + return execution_time_; + } + const std::shared_ptr firestore() const { return firestore_; } diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index eaa19cb03bd..afa943c8cb0 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -20,6 +20,7 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/nanopb/message.h" #include "Firestore/core/src/nanopb/nanopb_util.h" @@ -88,8 +89,8 @@ google_firestore_v1_Pipeline_Stage DocumentsSource::to_proto() const { for (size_t i = 0; i < documents_.size(); ++i) { result.args[i].which_value_type = - google_firestore_v1_Value_string_value_tag; - result.args[i].string_value = nanopb::MakeBytesArray(documents_[i]); + google_firestore_v1_Value_reference_value_tag; + result.args[i].reference_value = nanopb::MakeBytesArray(documents_[i]); } result.options_count = 0; @@ -108,9 +109,9 @@ google_firestore_v1_Pipeline_Stage AddFields::to_proto() const { 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::shared_ptr& entry) { + fields_, [](const std::pair>& entry) { return _google_firestore_v1_MapValue_FieldsEntry{ - nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); result.options_count = 0; @@ -130,7 +131,8 @@ google_firestore_v1_Pipeline_Stage AggregateStage::to_proto() const { nanopb::SetRepeatedField( &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, this->accumulators_, - [](const std::pair>& entry) { + [](const std::pair>& + entry) { return _google_firestore_v1_MapValue_FieldsEntry{ nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); @@ -242,9 +244,9 @@ google_firestore_v1_Pipeline_Stage SelectStage::to_proto() const { 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::shared_ptr& entry) { + fields_, [](const std::pair>& entry) { return _google_firestore_v1_MapValue_FieldsEntry{ - nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); result.options_count = 0; @@ -260,7 +262,7 @@ google_firestore_v1_Pipeline_Stage SortStage::to_proto() const { result.args = nanopb::MakeArray(result.args_count); for (size_t i = 0; i < orders_.size(); ++i) { - result.args[i] = orders_[i].to_proto(); + result.args[i] = orders_[i]->to_proto(); } result.options_count = 0; @@ -278,9 +280,9 @@ google_firestore_v1_Pipeline_Stage DistinctStage::to_proto() const { 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, - groups_, [](const std::shared_ptr& entry) { + groups_, [](const std::pair>& entry) { return _google_firestore_v1_MapValue_FieldsEntry{ - nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); result.options_count = 0; @@ -304,6 +306,123 @@ google_firestore_v1_Pipeline_Stage RemoveFieldsStage::to_proto() const { return result; } +google_firestore_v1_Pipeline_Stage ReplaceWith::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("replace_with"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0] = expr_->to_proto(); + + result.options_count = 0; + result.options = nullptr; + return result; +} + +ReplaceWith::ReplaceWith(std::shared_ptr expr) : expr_(std::move(expr)) { +} + +Sample::Sample(std::string type, int64_t count, double percentage) + : type_(type), count_(count), percentage_(percentage) { +} + +google_firestore_v1_Pipeline_Stage Sample::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("sample"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + if (type_ == "count") { + result.args[0].which_value_type = + google_firestore_v1_Value_integer_value_tag; + result.args[0].integer_value = count_; + } else { + result.args[0].which_value_type = + google_firestore_v1_Value_double_value_tag; + result.args[0].double_value = percentage_; + } + + result.options_count = 0; + result.options = nullptr; + return result; +} + +Union::Union(std::shared_ptr other) : other_(std::move(other)) { +} + +google_firestore_v1_Pipeline_Stage Union::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("union"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0] = other_->to_proto(); + + result.options_count = 0; + result.options = nullptr; + return result; +} + +Unnest::Unnest(std::shared_ptr field, + absl::optional index_field) + : field_(std::move(field)), index_field_(std::move(index_field)) { +} + +google_firestore_v1_Pipeline_Stage Unnest::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("unnest"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0] = field_->to_proto(); + + if (index_field_.has_value()) { + result.options_count = 1; + result.options = + nanopb::MakeArray(1); + result.options[0].key = nanopb::MakeBytesArray("index_field"); + result.options[0].value.which_value_type = + google_firestore_v1_Value_string_value_tag; + result.options[0].value.string_value = + nanopb::MakeBytesArray(index_field_.value()); + } else { + result.options_count = 0; + result.options = nullptr; + } + + return result; +} + +GenericStage::GenericStage( + std::string name, + std::vector> params, + std::unordered_map> options) + : name_(std::move(name)), + params_(std::move(params)), + options_(std::move(options)) { +} + +google_firestore_v1_Pipeline_Stage GenericStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray(name_); + + result.args_count = static_cast(params_.size()); + result.args = nanopb::MakeArray(result.args_count); + + for (size_t i = 0; i < result.args_count; i++) { + result.args[i] = params_[i]->to_proto(); + } + + nanopb::SetRepeatedField( + &result.options, &result.options_count, options_, + [](const std::pair>& entry) { + return _google_firestore_v1_Pipeline_Stage_OptionsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; + }); + + 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 11534278002..a3c7303ae92 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -25,9 +25,11 @@ #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/core/src/api/aggregate_expressions.h" +#include "Firestore/core/src/api/api_fwd.h" #include "Firestore/core/src/api/expressions.h" #include "Firestore/core/src/api/ordering.h" #include "Firestore/core/src/nanopb/message.h" +#include "absl/types/optional.h" namespace firebase { namespace firestore { @@ -89,7 +91,8 @@ class DocumentsSource : public Stage { class AddFields : public Stage { public: - explicit AddFields(std::vector> fields) + explicit AddFields( + std::unordered_map> fields) : fields_(std::move(fields)) { } ~AddFields() override = default; @@ -97,27 +100,29 @@ class AddFields : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector> fields_; + std::unordered_map> fields_; }; class AggregateStage : public Stage { public: - AggregateStage(std::unordered_map> - accumulators, - std::unordered_map> groups) + AggregateStage( + std::unordered_map> + accumulators, + std::unordered_map> groups) : accumulators_(std::move(accumulators)), groups_(std::move(groups)) { } google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::unordered_map> accumulators_; + std::unordered_map> + accumulators_; std::unordered_map> groups_; }; class Where : public Stage { public: - explicit Where(std::shared_ptr expr) : expr_(std::move(expr)) { + explicit Where(std::shared_ptr expr) : expr_(expr) { } ~Where() override = default; @@ -169,14 +174,14 @@ class FindNearestStage : public Stage { class LimitStage : public Stage { public: - explicit LimitStage(int64_t limit) : limit_(limit) { + explicit LimitStage(int32_t limit) : limit_(limit) { } ~LimitStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: - int64_t limit_; + int32_t limit_; }; class OffsetStage : public Stage { @@ -193,7 +198,8 @@ class OffsetStage : public Stage { class SelectStage : public Stage { public: - explicit SelectStage(std::vector> fields) + explicit SelectStage( + std::unordered_map> fields) : fields_(std::move(fields)) { } ~SelectStage() override = default; @@ -201,12 +207,12 @@ class SelectStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector> fields_; + std::unordered_map> fields_; }; class SortStage : public Stage { public: - explicit SortStage(std::vector orders) + explicit SortStage(std::vector> orders) : orders_(std::move(orders)) { } ~SortStage() override = default; @@ -214,12 +220,13 @@ class SortStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector orders_; + std::vector> orders_; }; class DistinctStage : public Stage { public: - explicit DistinctStage(std::vector> groups) + explicit DistinctStage( + std::unordered_map> groups) : groups_(std::move(groups)) { } ~DistinctStage() override = default; @@ -227,7 +234,7 @@ class DistinctStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector> groups_; + std::unordered_map> groups_; }; class RemoveFieldsStage : public Stage { @@ -243,6 +250,63 @@ class RemoveFieldsStage : public Stage { std::vector fields_; }; +class ReplaceWith : public Stage { + public: + explicit ReplaceWith(std::shared_ptr expr); + ~ReplaceWith() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr expr_; +}; + +class Sample : public Stage { + public: + Sample(std::string type, int64_t count, double percentage); + ~Sample() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::string type_; + int64_t count_; + double percentage_; +}; + +class Union : public Stage { + public: + explicit Union(std::shared_ptr other); + ~Union() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr other_; +}; + +class Unnest : public Stage { + public: + Unnest(std::shared_ptr field, absl::optional index_field); + ~Unnest() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr field_; + absl::optional index_field_; +}; + +class GenericStage : public Stage { + public: + GenericStage(std::string name, + std::vector> params, + std::unordered_map> options); + ~GenericStage() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::string name_; + std::vector> params_; + std::unordered_map> options_; +}; + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/remote/serializer.cc b/Firestore/core/src/remote/serializer.cc index 2985f86d623..889767b84ee 100644 --- a/Firestore/core/src/remote/serializer.cc +++ b/Firestore/core/src/remote/serializer.cc @@ -1209,16 +1209,8 @@ Serializer::DecodeCursorValue(google_firestore_v1_Cursor& cursor) const { google_firestore_v1_StructuredPipeline Serializer::EncodePipeline( const api::Pipeline& pipeline) const { google_firestore_v1_StructuredPipeline result; - auto* stages = - MakeArray(pipeline.stages().size()); - size_t i = 0; - for (const auto& stage : pipeline.stages()) { - stages[i++] = stage->to_proto(); - } - - result.pipeline.stages_count = pipeline.stages().size(); - result.pipeline.stages = stages; + result.pipeline = pipeline.to_proto().pipeline_value; result.options_count = 0; result.options = nullptr; @@ -1514,7 +1506,8 @@ api::PipelineSnapshot Serializer::DecodePipelineResponse( const { auto execution_time = DecodeVersion(context, message->execution_time); - std::vector results(message->results_count); + std::vector results; + results.reserve(message->results_count); for (pb_size_t i = 0; i < message->results_count; ++i) { absl::optional key;