diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index 8567f74fe7d..72f21be674e 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -129,7 +129,8 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, '"${PODS_TARGET_SRCROOT}" ' + '"${PODS_TARGET_SRCROOT}/Firestore/Source/Public" ' + '"${PODS_ROOT}/nanopb" ' + - '"${PODS_TARGET_SRCROOT}/Firestore/Protos/nanopb"' + '"${PODS_TARGET_SRCROOT}/Firestore/Protos/nanopb" ' + + '"$(PODS_ROOT)/gRPC-C++/third_party/re2"' } s.compiler_flags = '$(inherited) -Wreorder -Werror=reorder -Wno-comma' diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index bd8caca6180..422fbd4a322 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 020AFD89BB40E5175838BB76 /* local_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F8043813A5D16963EC02B182 /* local_serializer_test.cc */; }; 022BA1619A576F6818B212C5 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; 02C953A7B0FA5EF87DB0361A /* FSTIntegrationTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */; }; + 02E1EA3818F4BEEA9CE40DAE /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 82DF854A7238D538FA53C908 /* timestamp_test.cc */; }; 02EB33CC2590E1484D462912 /* annotations.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */; }; 033A1FECDD47ED9B1891093B /* arithmetic_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 76EED4ED84056B623D92FE20 /* arithmetic_test.cc */; }; 035034AB3797D1E5E0112EC3 /* Validation_BloomFilterTest_MD5_1_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 3FDD0050CA08C8302400C5FB /* Validation_BloomFilterTest_MD5_1_1_bloom_filter_proto.json */; }; @@ -49,6 +50,7 @@ 064689971747DA312770AB7A /* collection_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4B0A3187AAD8B02135E80C2E /* collection_test.cc */; }; 06485D6DA8F64757D72636E1 /* leveldb_target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */; }; 06A3926F89C847846BE4D6BE /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; }; + 06B8A653BC26CB2C96024993 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 82DF854A7238D538FA53C908 /* timestamp_test.cc */; }; 06BCEB9C65DFAA142F3D3F0B /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 06D76CC82E034658BF7D4BE4 /* Validation_BloomFilterTest_MD5_1_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 3FDD0050CA08C8302400C5FB /* Validation_BloomFilterTest_MD5_1_1_bloom_filter_proto.json */; }; 06E0914D76667F1345EC17F5 /* Validation_BloomFilterTest_MD5_1_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C939D1789E38C09F9A0C1157 /* Validation_BloomFilterTest_MD5_1_0001_membership_test_result.json */; }; @@ -114,6 +116,7 @@ 0F5D0C58444564D97AF0C98E /* nanopb_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F5B6C1399F92FD60F2C582B /* nanopb_util_test.cc */; }; 0F99BB63CE5B3CFE35F9027E /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; }; 0FA4D5601BE9F0CB5EC2882C /* local_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F8043813A5D16963EC02B182 /* local_serializer_test.cc */; }; + 0FAAA0B65D64970AE296181A /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.cc */; }; 0FBDD5991E8F6CD5F8542474 /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; }; 0FC27212D6211ECC3D1DD2A1 /* leveldb_globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FC44D934D4A52C790659C8D6 /* leveldb_globals_cache_test.cc */; }; 10120B9B650091B49D3CF57B /* grpc_stream_tester.cc in Sources */ = {isa = PBXBuildFile; fileRef = 87553338E42B8ECA05BA987E /* grpc_stream_tester.cc */; }; @@ -177,6 +180,7 @@ 17ECB768DA44AE0F49647E22 /* memory_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6A33BC2D84233C355F1D0 /* memory_query_engine_test.cc */; }; 1817DEF8FF479D218381C541 /* FSTGoogleTestTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */; }; 185B0DF3E9396AA218E7A460 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4375BDCDBCA9938C7F086730 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json */; }; + 185C8B4D438F240B25E10D8D /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.cc */; }; 18638EAED9E126FC5D895B14 /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; }; 18CF41A17EA3292329E1119D /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; }; 18F644E6AA98E6D6F3F1F809 /* executor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4688208F9B9100554BA2 /* executor_test.cc */; }; @@ -189,6 +193,7 @@ 1A3D8028303B45FCBB21CAD3 /* aggregation_result.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D872D754B8AD88E28AF28B28 /* aggregation_result.pb.cc */; }; 1AE27A46DC082F28D9494599 /* bloom_filter.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7C0DCD2790019E66D8CC /* bloom_filter.pb.cc */; }; 1B4794A51F4266556CD0976B /* view_snapshot_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CC572A9168BBEF7B83E4BBC5 /* view_snapshot_test.cc */; }; + 1B4CDC4CC1C301D1B15168EE /* mirroring_semantics_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */; }; 1B6E74BA33B010D76DB1E2F9 /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; }; 1B730A4E8C4BD7B5B0FF9C7F /* collection_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4B0A3187AAD8B02135E80C2E /* collection_test.cc */; }; 1B816F48012524939CA57CB3 /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CCC9BD953F121B9E29F9AA42 /* user_test.cc */; }; @@ -257,8 +262,10 @@ 23C04A637090E438461E4E70 /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; }; 23EFC681986488B033C2B318 /* leveldb_opener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 75860CD13AF47EB1EA39EC2F /* leveldb_opener_test.cc */; }; 2403890A78D7AB099754A18C /* bloom_filter.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7C0DCD2790019E66D8CC /* bloom_filter.pb.cc */; }; + 2403D4FFF7D9E43FA9FDFF85 /* map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CB852EE6E7D301545700BFD8 /* map_test.cc */; }; 2428E92E063EBAEA44BA5913 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; }; 242BC62992ACC1A5B142CD4A /* FIRCompositeIndexQueryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */; }; + 245164AED462B0B8BE974293 /* mirroring_semantics_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */; }; 248DE4F56DD938F4DBCCF39B /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; }; 24B75C63BDCD5551B2F69901 /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; 24CB39421C63CD87242B31DF /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; }; @@ -412,9 +419,11 @@ 3BA4EEA6153B3833F86B8104 /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; }; 3BAFCABA851AE1865D904323 /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; }; 3C5D441E7D5C140F0FB14D91 /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; + 3C63B6ED2E494437BBAD82D7 /* mirroring_semantics_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */; }; 3C9DEC46FE7B3995A4EA629C /* memory_globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */; }; 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 */; }; + 3D1365A99984C2F86C2B8A82 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 82DF854A7238D538FA53C908 /* timestamp_test.cc */; }; 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 */; }; @@ -760,6 +769,7 @@ 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 */; }; 6161B5032047140C00A99DBB /* FIRFirestoreSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */; }; + 617B25F15686310041C967B3 /* map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CB852EE6E7D301545700BFD8 /* map_test.cc */; }; 618BBEA620B89AAC00B5BCE7 /* target.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7D20B89AAC00B5BCE7 /* target.pb.cc */; }; 618BBEA820B89AAC00B5BCE7 /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; }; 618BBEAE20B89AAC00B5BCE7 /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; }; @@ -834,12 +844,14 @@ 6C415868AE347DC4A26588C3 /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D22D4C211AC32E4F8B4883DA /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json */; }; 6C92AD45A3619A18ECCA5B1F /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; 6C941147D9DB62E1A845CAB7 /* debug_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6DBD8EDF0074DD0079ECCE6 /* debug_test.cc */; }; + 6D2FC59BAA15B54EF960D936 /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.cc */; }; 6D578695E8E03988820D401C /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CFC201A2EE200D97691 /* string_util_test.cc */; }; 6D7F70938662E8CA334F11C2 /* target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B5C37696557C81A6C2B7271A /* target_cache_test.cc */; }; 6DBB3DB3FD6B4981B7F26A55 /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; 6DCA8E54E652B78EFF3EEDAC /* XCTestCase+Await.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0372021401E00B64F25 /* XCTestCase+Await.mm */; }; 6DFD49CCE2281CE243FEBB63 /* thread_safe_memoizer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */; }; 6E10507432E1D7AE658D16BD /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; }; + 6E12265524DDD86F13797EF4 /* map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CB852EE6E7D301545700BFD8 /* map_test.cc */; }; 6E4854B19B120C6F0F8192CC /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; 6E59498D20F55BA800ECD9A5 /* FuzzingResources in Resources */ = {isa = PBXBuildFile; fileRef = 6ED6DEA120F5502700FC6076 /* FuzzingResources */; }; 6E6B8B8D61426E20495D9DF5 /* memory_globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */; }; @@ -955,6 +967,7 @@ 7EAB3129A58368EE4BD449ED /* leveldb_migrations_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */; }; 7EF540911720DAAF516BEDF0 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; }; 7EF56BA2A480026D62CCA35A /* logic_utils_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */; }; + 7F28DB0A713FE7AF1924595C /* map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CB852EE6E7D301545700BFD8 /* map_test.cc */; }; 7F5501F917A11DE4E11F5CC7 /* Validation_BloomFilterTest_MD5_50000_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3841925AA60E13A027F565E6 /* Validation_BloomFilterTest_MD5_50000_1_membership_test_result.json */; }; 7F6199159E24E19E2A3F5601 /* schedule_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9B0B005A79E765AF02793DCE /* schedule_test.cc */; }; 7F771EB980D9CFAAB4764233 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; @@ -1043,6 +1056,7 @@ 8F781F527ED72DC6C123689E /* autoid_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A521FC913E500713A1A /* autoid_test.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 */; }; + 90101123ABFB4DC13EC3EB0F /* mirroring_semantics_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */; }; 9012B0E121B99B9C7E54160B /* query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B8A853940305237AFDA8050B /* query_engine_test.cc */; }; 9016EF298E41456060578C90 /* field_transform_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7515B47C92ABEEC66864B55C /* field_transform_test.cc */; }; 906DB5C85F57EFCBD2027E60 /* grpc_unary_call_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */; }; @@ -1103,6 +1117,7 @@ 9AC604BF7A76CABDF26F8C8E /* cc_compilation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1B342370EAE3AA02393E33EB /* cc_compilation_test.cc */; }; 9B2C6A48A4DBD36080932B4E /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; 9B2CD4CBB1DFE8BC3C81A335 /* async_queue_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */; }; + 9B6A7DEDB98B7709D4621193 /* map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CB852EE6E7D301545700BFD8 /* map_test.cc */; }; 9B9BFC16E26BDE4AE0CDFF4B /* firebase_auth_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = F869D85E900E5AF6CD02E2FC /* firebase_auth_credentials_provider_test.mm */; }; 9BEC62D59EB2C68342F493CD /* credentials_provider_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2F4FA4576525144C5069A7A5 /* credentials_provider_test.cc */; }; 9C1F25177DC5753B075DCF65 /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; }; @@ -1141,6 +1156,7 @@ 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 */; }; + A405A976DB6444D3ED3FCAB2 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 82DF854A7238D538FA53C908 /* timestamp_test.cc */; }; A4757C171D2407F61332EA38 /* byte_stream_cpp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 01D10113ECC5B446DB35E96D /* byte_stream_cpp_test.cc */; }; A478FDD7C3F48FBFDDA7D8F5 /* leveldb_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */; }; A4AD189BDEF7A609953457A6 /* leveldb_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */; }; @@ -1189,6 +1205,7 @@ AB6D588EB21A2C8D40CEB408 /* byte_stream_cpp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 01D10113ECC5B446DB35E96D /* byte_stream_cpp_test.cc */; }; AB7BAB342012B519001E0872 /* geo_point_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB7BAB332012B519001E0872 /* geo_point_test.cc */; }; AB8209455BAA17850D5E196D /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; }; + AB958FA764741A41E532A540 /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.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 */; }; @@ -1288,6 +1305,7 @@ B6FDE6F91D3F81D045E962A0 /* bits_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D01201BC69F00D97691 /* bits_test.cc */; }; B743F4E121E879EF34536A51 /* leveldb_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 166CE73C03AB4366AAC5201C /* leveldb_index_manager_test.cc */; }; B7DD5FC63A78FF00E80332C0 /* grpc_stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */; }; + B7EFE1206B6A5A1712BD6745 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 82DF854A7238D538FA53C908 /* timestamp_test.cc */; }; B8062EBDB8E5B680E46A6DD1 /* geo_point_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB7BAB332012B519001E0872 /* geo_point_test.cc */; }; B81B6F327B5E3FE820DC3FB3 /* aggregation_result.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D872D754B8AD88E28AF28B28 /* aggregation_result.pb.cc */; }; B83A1416C3922E2F3EBA77FE /* grpc_stream_tester.cc in Sources */ = {isa = PBXBuildFile; fileRef = 87553338E42B8ECA05BA987E /* grpc_stream_tester.cc */; }; @@ -1328,6 +1346,7 @@ 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 */; }; + BDDAB87A7D76562BCB5D0BF8 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 82DF854A7238D538FA53C908 /* timestamp_test.cc */; }; 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 */; }; BE1D7C7E413449AFFBA21BCB /* overlay_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E1459FA70B8FC18DE4B80D0D /* overlay_test.cc */; }; @@ -1360,6 +1379,7 @@ C240DB0498C1C84C6AFA4C8D /* Validation_BloomFilterTest_MD5_50000_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B44DD11682C4803B73DCC34 /* Validation_BloomFilterTest_MD5_50000_01_bloom_filter_proto.json */; }; C25F321AC9BF8D1CFC8543AF /* reference_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 132E32997D781B896672D30A /* reference_set_test.cc */; }; C2E0C68B2EA6FA3683F4EE94 /* Validation_BloomFilterTest_MD5_50000_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3841925AA60E13A027F565E6 /* Validation_BloomFilterTest_MD5_50000_1_membership_test_result.json */; }; + C386EBE4B0EC1AE14AA89964 /* mirroring_semantics_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */; }; C393D6984614D8E4D8C336A2 /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; }; C39CBADA58F442C8D66C3DA2 /* FIRFieldPathTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04C202154AA00B64F25 /* FIRFieldPathTests.mm */; }; C3E4EE9615367213A71FEECF /* filesystem_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA02DA2FCD0001CFC6EB08DA /* filesystem_testing.cc */; }; @@ -1465,6 +1485,7 @@ 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 */; }; + D662D297663917AAA90F80A3 /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.cc */; }; D6962E598CEDABA312D87760 /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; }; D69B97FF4C065EACEDD91886 /* FSTSyncEngineTestDriver.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02E20213FFC00B64F25 /* FSTSyncEngineTestDriver.mm */; }; D6DE74259F5C0CCA010D6A0D /* grpc_stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */; }; @@ -1501,6 +1522,7 @@ DC0E186BDD221EAE9E4D2F41 /* sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4E20A36DBB00BCEB75 /* sorted_map_test.cc */; }; DC1C711290E12F8EF3601151 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; }; DC3351455F8753678905CF73 /* maybe_document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 28034BA61A7395543F1508B3 /* maybe_document.pb.cc */; }; + DC42BC2EF669EAFF5DBFE409 /* map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CB852EE6E7D301545700BFD8 /* map_test.cc */; }; DC48407370E87F2233D7AB7E /* statusor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352D20A3B3D7003E0143 /* statusor_test.cc */; }; DC6804424FC8F7B3044DD0BB /* random_access_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 014C60628830D95031574D15 /* random_access_queue_test.cc */; }; DCC8F3D4AA87C81AB3FD9491 /* md5_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3D050936A2D52257FD17FB6E /* md5_test.cc */; }; @@ -1541,6 +1563,7 @@ E1264B172412967A09993EC6 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; }; E15A05789FF01F44BCAE75EF /* fields_array_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA4CBA48204C9E25B56993BC /* fields_array_test.cc */; }; E186D002520881AD2906ADDB /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; }; + E1DB8E1A4CF3DCE2AE8454D8 /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.cc */; }; E21D819A06D9691A4B313440 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; E25DCFEF318E003B8B7B9DC8 /* index_backfiller_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1F50E872B3F117A674DA8E94 /* index_backfiller_test.cc */; }; E27C0996AF6EC6D08D91B253 /* document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D821C2DDC800EFB9CC /* document.pb.cc */; }; @@ -1609,6 +1632,7 @@ EC63BD5E46C8734B6D20312D /* Validation_BloomFilterTest_MD5_50000_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B44DD11682C4803B73DCC34 /* Validation_BloomFilterTest_MD5_50000_01_bloom_filter_proto.json */; }; EC7A44792A5513FBB6F501EE /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; EC80A217F3D66EB0272B36B0 /* FSTLevelDBSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */; }; + EC90E9E7C0B9AD601B343461 /* mirroring_semantics_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */; }; ECC433628575AE994C621C54 /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = CF39535F2C41AB0006FA6C0E /* create_noop_connectivity_monitor.cc */; }; ECED3B60C5718B085AAB14FB /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; }; ED14A67E34AEDF55232096EF /* Validation_BloomFilterTest_MD5_5000_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8582DFD74E8060C7072104B /* Validation_BloomFilterTest_MD5_5000_0001_membership_test_result.json */; }; @@ -2062,6 +2086,7 @@ 80B9DCD61D9C9A3793248509 /* Pods-Firestore_FuzzTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.release.xcconfig"; path = "Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.release.xcconfig"; sourceTree = ""; }; 81DFB7DE556603F7FDEDCA84 /* Pods-Firestore_Example_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS.debug.xcconfig"; path = "Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS.debug.xcconfig"; sourceTree = ""; }; 8294C2063C0096AE5E43F6DF /* Pods_Firestore_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 82DF854A7238D538FA53C908 /* timestamp_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; name = timestamp_test.cc; path = expressions/timestamp_test.cc; sourceTree = ""; }; 84076EADF6872C78CDAC7291 /* bundle_builder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bundle_builder.h; 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 = ""; }; @@ -2163,6 +2188,7 @@ C8FB22BCB9F454DA44BA80C8 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json; sourceTree = ""; }; C939D1789E38C09F9A0C1157 /* Validation_BloomFilterTest_MD5_1_0001_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_0001_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_0001_membership_test_result.json; sourceTree = ""; }; CB7B2D4691C380DE3EB59038 /* lru_garbage_collector_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = lru_garbage_collector_test.h; sourceTree = ""; }; + CB852EE6E7D301545700BFD8 /* map_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; name = map_test.cc; path = expressions/map_test.cc; sourceTree = ""; }; CC572A9168BBEF7B83E4BBC5 /* view_snapshot_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = view_snapshot_test.cc; sourceTree = ""; }; CCC9BD953F121B9E29F9AA42 /* user_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = user_test.cc; path = credentials/user_test.cc; sourceTree = ""; }; CD422AF3E4515FB8E9BE67A0 /* equals_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = equals_tester.h; sourceTree = ""; }; @@ -2210,6 +2236,7 @@ E3228F51DCDC2E90D5C58F97 /* ConditionalConformanceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConditionalConformanceTests.swift; sourceTree = ""; }; E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_target_cache_test.cc; sourceTree = ""; }; EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = thread_safe_memoizer_testing_test.cc; sourceTree = ""; }; + EEF23C7104A4D040C3A8CF9B /* string_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; name = string_test.cc; path = expressions/string_test.cc; sourceTree = ""; }; EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRVectorValueTests.mm; sourceTree = ""; }; EF6C285029E462A200A7D4F1 /* FIRAggregateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateTests.mm; sourceTree = ""; }; EF6C286C29E6D22200A7D4F1 /* AggregationIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregationIntegrationTests.swift; sourceTree = ""; }; @@ -2219,6 +2246,7 @@ F119BDDF2F06B3C0883B8297 /* firebase_app_check_credentials_provider_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = firebase_app_check_credentials_provider_test.mm; path = credentials/firebase_app_check_credentials_provider_test.mm; sourceTree = ""; }; F243090EDC079930C87D5F96 /* Pods-Firestore_Tests_tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_tvOS.debug.xcconfig"; path = "Target Support Files/Pods-Firestore_Tests_tvOS/Pods-Firestore_Tests_tvOS.debug.xcconfig"; sourceTree = ""; }; F339B5B848F79BBDB2133210 /* Pods-Firestore_Example_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_tvOS.release.xcconfig"; path = "Target Support Files/Pods-Firestore_Example_tvOS/Pods-Firestore_Example_tvOS.release.xcconfig"; sourceTree = ""; }; + F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; name = mirroring_semantics_test.cc; path = expressions/mirroring_semantics_test.cc; sourceTree = ""; }; F51619F8CFF13B0CDD13EDC3 /* logical_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; name = logical_test.cc; path = expressions/logical_test.cc; sourceTree = ""; }; F51859B394D01C0C507282F1 /* filesystem_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = filesystem_test.cc; sourceTree = ""; }; F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_local_store_test.cc; sourceTree = ""; }; @@ -3051,6 +3079,10 @@ F6DBD8EDF0074DD0079ECCE6 /* debug_test.cc */, 24F0F49F016E65823E0075DB /* field_test.cc */, F51619F8CFF13B0CDD13EDC3 /* logical_test.cc */, + CB852EE6E7D301545700BFD8 /* map_test.cc */, + F3704E3BF509EE783D0B0F08 /* mirroring_semantics_test.cc */, + EEF23C7104A4D040C3A8CF9B /* string_test.cc */, + 82DF854A7238D538FA53C908 /* timestamp_test.cc */, ); name = expressions; sourceTree = ""; @@ -4430,6 +4462,7 @@ F924DF3D9DCD2720C315A372 /* logic_utils_test.cc in Sources */, 477D5B6AB66340FEA10B6D23 /* logical_test.cc in Sources */, 3F6C9F8A993CF4B0CD51E7F0 /* lru_garbage_collector_test.cc in Sources */, + DC42BC2EF669EAFF5DBFE409 /* map_test.cc in Sources */, 1F6319D85C1AFC0D81394470 /* maybe_document.pb.cc in Sources */, 380E543B7BC6F648BBB250B4 /* md5_test.cc in Sources */, FE20E696E014CDCE918E91D6 /* md5_testing.cc in Sources */, @@ -4444,6 +4477,7 @@ A61BB461F3E5822175F81719 /* memory_remote_document_cache_test.cc in Sources */, C1237EE2A74F174A3DF5978B /* memory_target_cache_test.cc in Sources */, FB3D9E01547436163C456A3C /* message_test.cc in Sources */, + 1B4CDC4CC1C301D1B15168EE /* mirroring_semantics_test.cc in Sources */, C5F1E2220E30ED5EAC9ABD9E /* mutation.pb.cc in Sources */, 0DBD29A16030CDCD55E38CAB /* mutation_queue_test.cc in Sources */, 1CC9BABDD52B2A1E37E2698D /* mutation_test.cc in Sources */, @@ -4486,6 +4520,7 @@ 5EFBAD082CB0F86CD0711979 /* string_apple_test.mm in Sources */, 56D85436D3C864B804851B15 /* string_format_apple_test.mm in Sources */, 1F998DDECB54A66222CC66AA /* string_format_test.cc in Sources */, + 185C8B4D438F240B25E10D8D /* string_test.cc in Sources */, 8C39F6D4B3AA9074DF00CFB8 /* string_util_test.cc in Sources */, 229D1A9381F698D71F229471 /* string_win_test.cc in Sources */, 4A3FF3B16A39A5DC6B7EBA51 /* target.pb.cc in Sources */, @@ -4500,6 +4535,7 @@ 482D503CC826265FCEAB53DE /* thread_safe_memoizer_testing.cc in Sources */, 451EFFB413364E5A420F8B2D /* thread_safe_memoizer_testing_test.cc in Sources */, 5497CB78229DECDE000FB92F /* time_testing.cc in Sources */, + B7EFE1206B6A5A1712BD6745 /* timestamp_test.cc in Sources */, ACC9369843F5ED3BD2284078 /* timestamp_test.cc in Sources */, 2AAEABFD550255271E3BAC91 /* to_string_apple_test.mm in Sources */, 1E2AE064CF32A604DC7BFD4D /* to_string_test.cc in Sources */, @@ -4664,6 +4700,7 @@ 7EF56BA2A480026D62CCA35A /* logic_utils_test.cc in Sources */, E8911F2BCC97B0B1075D227B /* logical_test.cc in Sources */, 1F56F51EB6DF0951B1F4F85B /* lru_garbage_collector_test.cc in Sources */, + 6E12265524DDD86F13797EF4 /* map_test.cc in Sources */, DD175F74AC25CC419E874A1D /* maybe_document.pb.cc in Sources */, DCC8F3D4AA87C81AB3FD9491 /* md5_test.cc in Sources */, 169EDCF15637580BA79B61AD /* md5_testing.cc in Sources */, @@ -4678,6 +4715,7 @@ EADD28A7859FBB9BE4D913B0 /* memory_remote_document_cache_test.cc in Sources */, 0D124ED1B567672DD1BCEF05 /* memory_target_cache_test.cc in Sources */, ED9DF1EB20025227B38736EC /* message_test.cc in Sources */, + EC90E9E7C0B9AD601B343461 /* mirroring_semantics_test.cc in Sources */, 153F3E4E9E3A0174E29550B4 /* mutation.pb.cc in Sources */, 94BBB23B93E449D03FA34F87 /* mutation_queue_test.cc in Sources */, 5E6F9184B271F6D5312412FF /* mutation_test.cc in Sources */, @@ -4720,6 +4758,7 @@ 0087625FD31D76E1365C589E /* string_apple_test.mm in Sources */, 7A7EC216A0015D7620B4FF3E /* string_format_apple_test.mm in Sources */, 392F527F144BADDAC69C5485 /* string_format_test.cc in Sources */, + 0FAAA0B65D64970AE296181A /* string_test.cc in Sources */, E50187548B537DBCDBF7F9F0 /* string_util_test.cc in Sources */, 81D1B1D2B66BD8310AC5707F /* string_win_test.cc in Sources */, 81B23D2D4E061074958AF12F /* target.pb.cc in Sources */, @@ -4734,6 +4773,7 @@ 3D6AC48D6197E6539BBBD28F /* thread_safe_memoizer_testing.cc in Sources */, 7801E06BFFB08FCE7AB54AD6 /* thread_safe_memoizer_testing_test.cc in Sources */, 5497CB79229DECDE000FB92F /* time_testing.cc in Sources */, + 02E1EA3818F4BEEA9CE40DAE /* timestamp_test.cc in Sources */, 26CB3D7C871BC56456C6021E /* timestamp_test.cc in Sources */, 5BE49546D57C43DDFCDB6FBD /* to_string_apple_test.mm in Sources */, E500AB82DF2E7F3AFDB1AB3F /* to_string_test.cc in Sources */, @@ -4924,6 +4964,7 @@ 0595B5EBEB8F09952B72C883 /* logic_utils_test.cc in Sources */, 8DD012A04D143ABDBA86340D /* logical_test.cc in Sources */, 913F6E57AF18F84C5ECFD414 /* lru_garbage_collector_test.cc in Sources */, + 7F28DB0A713FE7AF1924595C /* map_test.cc in Sources */, 27B652E6288A9CD1B99E618F /* maybe_document.pb.cc in Sources */, 13ED75EFC2F6917951518A4B /* md5_test.cc in Sources */, E2AC3BDAAFFF9A45C916708B /* md5_testing.cc in Sources */, @@ -4938,6 +4979,7 @@ F7B1DF16A9DDFB664EA98EBB /* memory_remote_document_cache_test.cc in Sources */, 7E97B0F04E25610FF37E9259 /* memory_target_cache_test.cc in Sources */, 00F1CB487E8E0DA48F2E8FEC /* message_test.cc in Sources */, + C386EBE4B0EC1AE14AA89964 /* mirroring_semantics_test.cc in Sources */, BBDFE0000C4D7E529E296ED4 /* mutation.pb.cc in Sources */, C8A573895D819A92BF16B5E5 /* mutation_queue_test.cc in Sources */, F5A654E92FF6F3FF16B93E6B /* mutation_test.cc in Sources */, @@ -4980,6 +5022,7 @@ 62F86BBE7DDA5B295B57C8DA /* string_apple_test.mm in Sources */, BE92E16A9B9B7AD5EB072919 /* string_format_apple_test.mm in Sources */, E7CE4B1ECD008983FAB90F44 /* string_format_test.cc in Sources */, + AB958FA764741A41E532A540 /* string_test.cc in Sources */, 3FFFC1FE083D8BE9C4D9A148 /* string_util_test.cc in Sources */, 0BDC438E72D4DD44877BEDEE /* string_win_test.cc in Sources */, EC3331B17394886A3715CFD8 /* target.pb.cc in Sources */, @@ -4994,6 +5037,7 @@ 25D74F38A5EE96CC653ABB49 /* thread_safe_memoizer_testing.cc in Sources */, 688AC36AA9D0677E910D5A37 /* thread_safe_memoizer_testing_test.cc in Sources */, 6300709ECDE8E0B5A8645F8D /* time_testing.cc in Sources */, + A405A976DB6444D3ED3FCAB2 /* timestamp_test.cc in Sources */, 0CEE93636BA4852D3C5EC428 /* timestamp_test.cc in Sources */, 95DCD082374F871A86EF905F /* to_string_apple_test.mm in Sources */, 9E656F4FE92E8BFB7F625283 /* to_string_test.cc in Sources */, @@ -5184,6 +5228,7 @@ 0D6AE96565603226DB2E6838 /* logic_utils_test.cc in Sources */, BB07838C0EAB5E32CD0C75C6 /* logical_test.cc in Sources */, 95CE3F5265B9BB7297EE5A6B /* lru_garbage_collector_test.cc in Sources */, + 2403D4FFF7D9E43FA9FDFF85 /* map_test.cc in Sources */, 4F88E2D686CF4C150A29E84E /* maybe_document.pb.cc in Sources */, 211A60ECA3976D27C0BF59BB /* md5_test.cc in Sources */, E72A77095FF6814267DF0F6D /* md5_testing.cc in Sources */, @@ -5198,6 +5243,7 @@ 7281C2F04838AFFDF6A762DF /* memory_remote_document_cache_test.cc in Sources */, 7F9CE96304D413F7E7AA0DA0 /* memory_target_cache_test.cc in Sources */, 2A499CFB2831612A045977CD /* message_test.cc in Sources */, + 245164AED462B0B8BE974293 /* mirroring_semantics_test.cc in Sources */, 85D61BDC7FB99B6E0DD3AFCA /* mutation.pb.cc in Sources */, C06E54352661FCFB91968640 /* mutation_queue_test.cc in Sources */, 795A0E11B3951ACEA2859C8A /* mutation_test.cc in Sources */, @@ -5240,6 +5286,7 @@ 009F5174BD172716AFE9F20A /* string_apple_test.mm in Sources */, 7B0EA399F899537ACCC84E53 /* string_format_apple_test.mm in Sources */, 990EC10E92DADB7D86A4BEE3 /* string_format_test.cc in Sources */, + E1DB8E1A4CF3DCE2AE8454D8 /* string_test.cc in Sources */, 0AE084A7886BC11B8C305122 /* string_util_test.cc in Sources */, DC0B0E50DBAE916E6565AA18 /* string_win_test.cc in Sources */, B3E6F4CDB1663407F0980C7A /* target.pb.cc in Sources */, @@ -5254,6 +5301,7 @@ CF18D52A88F4F6F62C5495EF /* thread_safe_memoizer_testing.cc in Sources */, A7669E72BCED7FBADA4B1314 /* thread_safe_memoizer_testing_test.cc in Sources */, A25FF76DEF542E01A2DF3B0E /* time_testing.cc in Sources */, + BDDAB87A7D76562BCB5D0BF8 /* timestamp_test.cc in Sources */, 1E42CD0F60EB22A5D0C86D1F /* timestamp_test.cc in Sources */, F9705E595FC3818F13F6375A /* to_string_apple_test.mm in Sources */, 3BAFCABA851AE1865D904323 /* to_string_test.cc in Sources */, @@ -5428,6 +5476,7 @@ D156B9F19B5B29E77664FDFC /* logic_utils_test.cc in Sources */, 25202D64249BFE38AB8B8DA9 /* logical_test.cc in Sources */, 1290FA77A922B76503AE407C /* lru_garbage_collector_test.cc in Sources */, + 617B25F15686310041C967B3 /* map_test.cc in Sources */, 85ADFEB234EBE3D9CDFFCE12 /* maybe_document.pb.cc in Sources */, C86E85101352B5CDBF5909F9 /* md5_test.cc in Sources */, 723BBD713478BB26CEFA5A7D /* md5_testing.cc in Sources */, @@ -5442,6 +5491,7 @@ CEA91CE103B42533C54DBAD6 /* memory_remote_document_cache_test.cc in Sources */, FC1D22B6EC4E5F089AE39B8C /* memory_target_cache_test.cc in Sources */, 2B4D0509577E5CE0B0B8CEDF /* message_test.cc in Sources */, + 90101123ABFB4DC13EC3EB0F /* mirroring_semantics_test.cc in Sources */, 618BBEA820B89AAC00B5BCE7 /* mutation.pb.cc in Sources */, 1C4F88DDEFA6FA23E9E4DB4B /* mutation_queue_test.cc in Sources */, 32F022CB75AEE48CDDAF2982 /* mutation_test.cc in Sources */, @@ -5484,6 +5534,7 @@ 36FD4CE79613D18BC783C55B /* string_apple_test.mm in Sources */, 0535C1B65DADAE1CE47FA3CA /* string_format_apple_test.mm in Sources */, 54131E9720ADE679001DF3FF /* string_format_test.cc in Sources */, + 6D2FC59BAA15B54EF960D936 /* string_test.cc in Sources */, AB380CFE201A2F4500D97691 /* string_util_test.cc in Sources */, DD5976A45071455FF3FE74B8 /* string_win_test.cc in Sources */, 618BBEA620B89AAC00B5BCE7 /* target.pb.cc in Sources */, @@ -5498,6 +5549,7 @@ 8D67BAAD6D2F1913BACA6AC1 /* thread_safe_memoizer_testing.cc in Sources */, BD0882A40BD8AE042629C179 /* thread_safe_memoizer_testing_test.cc in Sources */, 5497CB77229DECDE000FB92F /* time_testing.cc in Sources */, + 3D1365A99984C2F86C2B8A82 /* timestamp_test.cc in Sources */, ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, B68B1E012213A765008977EF /* to_string_apple_test.mm in Sources */, B696858E2214B53900271095 /* to_string_test.cc in Sources */, @@ -5707,6 +5759,7 @@ 6FCC64A1937E286E76C294D0 /* logic_utils_test.cc in Sources */, 45070DD0F8428BB68E6895C6 /* logical_test.cc in Sources */, 4DF18D15AC926FB7A4888313 /* lru_garbage_collector_test.cc in Sources */, + 9B6A7DEDB98B7709D4621193 /* map_test.cc in Sources */, DC3351455F8753678905CF73 /* maybe_document.pb.cc in Sources */, E74D6C1056DE29969B5C4C62 /* md5_test.cc in Sources */, 1DCDED1F94EBC7F72FDBFC98 /* md5_testing.cc in Sources */, @@ -5721,6 +5774,7 @@ 31850B3D5232E8D3F8C4D90C /* memory_remote_document_cache_test.cc in Sources */, C7F3C6F569BBA904477F011C /* memory_target_cache_test.cc in Sources */, 26777815544F549DD18D87AF /* message_test.cc in Sources */, + 3C63B6ED2E494437BBAD82D7 /* mirroring_semantics_test.cc in Sources */, C393D6984614D8E4D8C336A2 /* mutation.pb.cc in Sources */, A7399FB3BEC50BBFF08EC9BA /* mutation_queue_test.cc in Sources */, D18DBCE3FE34BF5F14CF8ABD /* mutation_test.cc in Sources */, @@ -5763,6 +5817,7 @@ 623AA12C3481646B0715006D /* string_apple_test.mm in Sources */, A6D57EC3A0BF39060705ED29 /* string_format_apple_test.mm in Sources */, EB7BE7B43A99E0BC2B0A8077 /* string_format_test.cc in Sources */, + D662D297663917AAA90F80A3 /* string_test.cc in Sources */, 6D578695E8E03988820D401C /* string_util_test.cc in Sources */, 5B4391097A6DF86EC3801DEE /* string_win_test.cc in Sources */, 6FAC16B7FBD3B40D11A6A816 /* target.pb.cc in Sources */, @@ -5777,6 +5832,7 @@ D928302820891CCCAD0437DD /* thread_safe_memoizer_testing.cc in Sources */, C099AEC05D44976755BA32A2 /* thread_safe_memoizer_testing_test.cc in Sources */, 2D220B9ABFA36CD7AC43D0A7 /* time_testing.cc in Sources */, + 06B8A653BC26CB2C96024993 /* timestamp_test.cc in Sources */, D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */, 60260A06871DCB1A5F3448D3 /* to_string_apple_test.mm in Sources */, ECED3B60C5718B085AAB14FB /* to_string_test.cc in Sources */, diff --git a/Firestore/core/src/core/expressions_eval.cc b/Firestore/core/src/core/expressions_eval.cc index 04a687493a1..ecd23f1dd47 100644 --- a/Firestore/core/src/core/expressions_eval.cc +++ b/Firestore/core/src/core/expressions_eval.cc @@ -17,12 +17,18 @@ #include "Firestore/core/src/core/expressions_eval.h" #include // For std::reverse +#include #include +#include // Added for std::function #include +#include // For std::numeric_limits +#include #include +#include #include // For std::move #include // For std::vector +// Ensure timestamp proto is included #include "Firestore/core/src/api/expressions.h" #include "Firestore/core/src/api/stages.h" #include "Firestore/core/src/model/mutable_document.h" @@ -30,10 +36,13 @@ #include "Firestore/core/src/nanopb/message.h" // Added for MakeMessage #include "Firestore/core/src/remote/serializer.h" #include "Firestore/core/src/util/hard_assert.h" +#include "absl/strings/ascii.h" // For AsciiStrToLower/ToUpper (if needed later) +#include "absl/strings/internal/utf8.h" +#include "absl/strings/match.h" // For StartsWith, EndsWith, StrContains +#include "absl/strings/str_cat.h" // For StrAppend +#include "absl/strings/strip.h" // For StripAsciiWhitespace #include "absl/types/optional.h" - -// Forward declarations for logical expression classes -#include "Firestore/core/src/core/expressions_eval.h" +#include "re2/re2.h" namespace firebase { namespace firestore { @@ -122,16 +131,6 @@ absl::optional SafeMod(int64_t lhs, int64_t rhs) { return lhs % rhs; } -// Helper to get double value, converting integer if necessary. -absl::optional GetDoubleValue(const google_firestore_v1_Value& value) { - if (model::IsDouble(value)) { - return value.double_value; - } else if (model::IsInteger(value)) { - return static_cast(value.integer_value); - } - return absl::nullopt; -} - // Helper to create a Value proto from int64_t nanopb::Message IntValue(int64_t val) { google_firestore_v1_Value proto; @@ -148,92 +147,6 @@ nanopb::Message DoubleValue(double val) { return nanopb::MakeMessage(std::move(proto)); } -// Common evaluation logic for binary arithmetic operations -template -EvaluateResult EvaluateArithmetic(const api::FunctionExpr* expr, - const api::EvaluateContext& context, - const model::PipelineInputOutput& document, - IntOp int_op, - DoubleOp double_op) { - HARD_ASSERT(expr->params().size() >= 2, - "%s() function requires at least 2 params", expr->name()); - - EvaluateResult current_result = - expr->params()[0]->ToEvaluable()->Evaluate(context, document); - - for (size_t i = 1; i < expr->params().size(); ++i) { - if (current_result.IsErrorOrUnset()) { - return EvaluateResult::NewError(); - } - if (current_result.IsNull()) { - // Null propagates - return EvaluateResult::NewNull(); - } - - EvaluateResult next_operand = - expr->params()[i]->ToEvaluable()->Evaluate(context, document); - - if (next_operand.IsErrorOrUnset()) { - return EvaluateResult::NewError(); - } - if (next_operand.IsNull()) { - // Null propagates - return EvaluateResult::NewNull(); - } - - const google_firestore_v1_Value* left_val = current_result.value(); - const google_firestore_v1_Value* right_val = next_operand.value(); - - // Type checking - bool left_is_num = model::IsNumber(*left_val); - bool right_is_num = model::IsNumber(*right_val); - - if (!left_is_num || !right_is_num) { - return EvaluateResult::NewError(); // Type error - } - - // NaN propagation - if (model::IsNaNValue(*left_val) || model::IsNaNValue(*right_val)) { - current_result = - EvaluateResult::NewValue(nanopb::MakeMessage(model::NaNValue())); - continue; - } - - // Perform arithmetic - if (model::IsDouble(*left_val) || model::IsDouble(*right_val)) { - // Promote to double - absl::optional left_double = GetDoubleValue(*left_val); - absl::optional right_double = GetDoubleValue(*right_val); - // Should always succeed due to IsNumber check above - HARD_ASSERT(left_double.has_value() && right_double.has_value(), - "Failed to extract double values"); - - double result_double = - double_op(left_double.value(), right_double.value()); - current_result = EvaluateResult::NewValue(DoubleValue(result_double)); - - } else { - // Both are integers - absl::optional left_int = model::GetInteger(*left_val); - absl::optional right_int = model::GetInteger(*right_val); - // Should always succeed due to IsNumber check above - HARD_ASSERT(left_int.has_value() && right_int.has_value(), - "Failed to extract integer values"); - - absl::optional result_int = - int_op(left_int.value(), right_int.value()); - - if (!result_int.has_value()) { - // Overflow or division/mod by zero - return EvaluateResult::NewError(); - } - current_result = EvaluateResult::NewValue(IntValue(result_int.value())); - } - } - - return current_result; -} - } // anonymous namespace EvaluateResult::EvaluateResult( @@ -350,8 +263,52 @@ std::unique_ptr FunctionToEvaluable( return std::make_unique(function); } else if (function.name() == "logical_minimum") { return std::make_unique(function); + } else if (function.name() == "map_get") { + return std::make_unique(function); + } else if (function.name() == "byte_length") { + return std::make_unique(function); + } else if (function.name() == "char_length") { + return std::make_unique(function); + } else if (function.name() == "str_concat") { + return std::make_unique(function); + } else if (function.name() == "ends_with") { + return std::make_unique(function); + } else if (function.name() == "starts_with") { + return std::make_unique(function); + } else if (function.name() == "str_contains") { + return std::make_unique(function); + } else if (function.name() == "to_lower") { + return std::make_unique(function); + } else if (function.name() == "to_upper") { + return std::make_unique(function); + } else if (function.name() == "trim") { + return std::make_unique(function); + } else if (function.name() == "reverse") { + // Note: This handles string reverse. Array reverse is separate. + return std::make_unique(function); + } else if (function.name() == "regex_contains") { + return std::make_unique(function); + } else if (function.name() == "regex_match") { + return std::make_unique(function); + } else if (function.name() == "like") { + return std::make_unique(function); + } else if (function.name() == "unix_micros_to_timestamp") { + return std::make_unique(function); + } else if (function.name() == "unix_millis_to_timestamp") { + return std::make_unique(function); + } else if (function.name() == "unix_seconds_to_timestamp") { + return std::make_unique(function); + } else if (function.name() == "timestamp_to_unix_micros") { + return std::make_unique(function); + } else if (function.name() == "timestamp_to_unix_millis") { + return std::make_unique(function); + } else if (function.name() == "timestamp_to_unix_seconds") { + return std::make_unique(function); + } else if (function.name() == "timestamp_add") { + return std::make_unique(function); + } else if (function.name() == "timestamp_sub") { + return std::make_unique(function); } - // TODO(wuandy): Add other non-array/logical functions HARD_FAIL("Unsupported function name: %s", function.name()); } @@ -407,17 +364,27 @@ EvaluateResult ComparisonBase::Evaluate( std::unique_ptr left_evaluable = expr_->params()[0]->ToEvaluable(); - std::unique_ptr right_evaluable = - expr_->params()[1]->ToEvaluable(); - EvaluateResult left = left_evaluable->Evaluate(context, document); - if (left.IsErrorOrUnset()) { - return left; // Propagate Error or Unset + + switch (left.type()) { + case EvaluateResult::ResultType::kError: + case EvaluateResult::ResultType::kUnset: { + return EvaluateResult::NewError(); + } + default: + break; } + std::unique_ptr right_evaluable = + expr_->params()[1]->ToEvaluable(); EvaluateResult right = right_evaluable->Evaluate(context, document); - if (right.IsErrorOrUnset()) { - return right; // Propagate Error or Unset + switch (right.type()) { + case EvaluateResult::ResultType::kError: + case EvaluateResult::ResultType::kUnset: { + return EvaluateResult::NewError(); + } + default: + break; } // Comparisons involving Null propagate Null @@ -528,97 +495,726 @@ EvaluateResult CoreGt::CompareToResult(const EvaluateResult& left, return EvaluateResult::NewValue(nanopb::MakeMessage(model::FalseValue())); } - bool result = model::Compare(*left.value(), *right.value()) == - util::ComparisonResult::Descending; - return EvaluateResult::NewValue( - nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); + bool result = model::Compare(*left.value(), *right.value()) == + util::ComparisonResult::Descending; + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +EvaluateResult CoreGte::CompareToResult(const EvaluateResult& left, + const EvaluateResult& right) const { + // Type mismatch always results in false + if (model::GetTypeOrder(*left.value()) != + model::GetTypeOrder(*right.value())) { + return EvaluateResult::NewValue(nanopb::MakeMessage(model::FalseValue())); + } + // NaN compared to anything is false + if (model::IsNaNValue(*left.value()) || model::IsNaNValue(*right.value())) { + return EvaluateResult::NewValue(nanopb::MakeMessage(model::FalseValue())); + } + + // Check for equality first using StrictEquals + if (model::StrictEquals(*left.value(), *right.value()) == + model::StrictEqualsResult::kEq) { + return EvaluateResult::NewValue(nanopb::MakeMessage(model::TrueValue())); + } + + // If not equal, perform standard comparison + bool result = model::Compare(*left.value(), *right.value()) == + util::ComparisonResult::Descending; + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +// --- String Expression Implementations --- + +namespace { + +/** + * @brief Validates a string as UTF-8 and process the Unicode code points. + * + * Iterates through the byte sequence of the input string, performing + * full UTF-8 validation checks: + * - Correct number of continuation bytes. + * - Correct format of continuation bytes (10xxxxxx). + * - No overlong encodings (e.g., encoding '/' as 2 bytes). + * - Decoded code points are within the valid Unicode range + * (U+0000-U+D7FF and U+E000-U+10FFFF), excluding surrogates. + * + * @tparam T The type of the result accumulator. + * @param s The input string (byte sequence) to validate. + * @param result A pointer to the result accumulator, updated by `func`. + * @param func A function `void(T* result, uint32_t code_point, + * absl::string_view utf8_bytes)` called for each valid code point, providing + * the code point and its UTF-8 byte representation. + * @return `true` if the string is valid UTF-8, `false` otherwise. + */ +template +bool ProcessUtf8(const std::string& s, + T* result, + std::function func) { + int i = 0; + const int len = s.size(); + const unsigned char* data = reinterpret_cast(s.data()); + + while (i < len) { + uint32_t code_point = 0; // To store the decoded code point + int num_bytes = 0; + const unsigned char start_byte = data[i]; + + // 1. Determine expected sequence length and initial code point bits + if ((start_byte & 0x80) == 0) { // 1-byte sequence (ASCII 0xxxxxxx) + num_bytes = 1; + code_point = start_byte; + // Overlong check: Not possible for 1-byte sequences + // Range check: ASCII is always valid (0x00-0x7F) + } else if ((start_byte & 0xE0) == 0xC0) { // 2-byte sequence (110xxxxx) + num_bytes = 2; + code_point = start_byte & 0x1F; // Mask out 110xxxxx + // Overlong check: Must not represent code points < 0x80 + // Also, C0 and C1 are specifically invalid start bytes + if (start_byte < 0xC2) { + return false; // C0, C1 are invalid starts + } + } else if ((start_byte & 0xF0) == 0xE0) { // 3-byte sequence (1110xxxx) + num_bytes = 3; + code_point = start_byte & 0x0F; // Mask out 1110xxxx + } else if ((start_byte & 0xF8) == 0xF0) { // 4-byte sequence (11110xxx) + num_bytes = 4; + code_point = + start_byte & 0x07; // Mask out 11110xxx + // Overlong check: Must not represent code points + // < 0x10000 Range check: Must not represent code + // points > 0x10FFFF F4 90.. BF.. is > 0x10FFFF + if (start_byte > 0xF4) { + return false; + } + } else { + return false; // Invalid start byte (e.g., 10xxxxxx or > F4) + } + + // 2. Check for incomplete sequence + if (i + num_bytes > len) { + return false; // Sequence extends beyond string end + } + + // 3. Check and process continuation bytes (if any) + for (int j = 1; j < num_bytes; ++j) { + const unsigned char continuation_byte = data[i + j]; + if ((continuation_byte & 0xC0) != 0x80) { + return false; // Not a valid continuation byte (10xxxxxx) + } + // Combine bits into the code point + code_point = (code_point << 6) | (continuation_byte & 0x3F); + } + + // 4. Perform Overlong and Range Checks based on the fully decoded + // code_point + if (num_bytes == 2 && code_point < 0x80) { + return false; // Overlong encoding (should have been 1 byte) + } + if (num_bytes == 3 && code_point < 0x800) { + // Specific check for 0xE0 0x80..0x9F .. sequences (overlong) + if (start_byte == 0xE0 && (data[i + 1] & 0xFF) < 0xA0) { + return false; + } + return false; // Overlong encoding (should have been 1 or 2 bytes) + } + if (num_bytes == 4 && code_point < 0x10000) { + // Specific check for 0xF0 0x80..0x8F .. sequences (overlong) + if (start_byte == 0xF0 && (data[i + 1] & 0xFF) < 0x90) { + return false; + } + return false; // Overlong encoding (should have been 1, 2 or 3 bytes) + } + + // Check for surrogates (U+D800 to U+DFFF) + if (code_point >= 0xD800 && code_point <= 0xDFFF) { + return false; + } + + // Check for code points beyond the Unicode maximum (U+10FFFF) + if (code_point > 0x10FFFF) { + // Specific check for 0xF4 90..BF .. sequences (> U+10FFFF) + if (start_byte == 0xF4 && (data[i + 1] & 0xFF) > 0x8F) { + return false; + } + return false; + } + + // 5. If all checks passed, call the function and advance index + absl::string_view utf8_bytes(s.data() + i, num_bytes); + func(result, code_point, utf8_bytes); + i += num_bytes; + } + + return true; // String is valid UTF-8 +} + +// Helper function to convert SQL LIKE patterns to RE2 regex patterns. +// Handles % (matches any sequence of zero or more characters) +// and _ (matches any single character). +// Escapes other regex special characters. +std::string LikeToRegex(const std::string& like_pattern) { + std::string regex_pattern = "^"; // Anchor at the start + for (char c : like_pattern) { + switch (c) { + case '%': + regex_pattern += ".*"; + break; + case '_': + regex_pattern += "."; + break; + // Escape RE2 special characters + case '\\': + case '.': + case '*': + case '+': + case '?': + case '(': + case ')': + case '|': + case '{': + case '}': + case '[': + case ']': + case '^': + case '$': + regex_pattern += '\\'; + regex_pattern += c; + break; + default: + regex_pattern += c; + break; + } + } + regex_pattern += '$'; // Anchor at the end + return regex_pattern; +} + +} // anonymous namespace + +EvaluateResult StringSearchBase::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 2, + "%s() function requires exactly 2 params", expr_->name()); + + bool has_null = false; + EvaluateResult op1 = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + switch (op1.type()) { + case EvaluateResult::ResultType::kString: { + break; + } + case EvaluateResult::ResultType::kNull: { + has_null = true; + break; + } + default: { + return EvaluateResult::NewError(); + } + } + + EvaluateResult op2 = + expr_->params()[1]->ToEvaluable()->Evaluate(context, document); + switch (op2.type()) { + case EvaluateResult::ResultType::kString: { + break; + } + case EvaluateResult::ResultType::kNull: { + has_null = true; + break; + } + default: { + return EvaluateResult::NewError(); + } + } + + // Null propagation + if (has_null) { + return EvaluateResult::NewNull(); + } + + // Both operands are valid strings, perform the specific search + std::string value_str = nanopb::MakeString(op1.value()->string_value); + std::string search_str = nanopb::MakeString(op2.value()->string_value); + + return PerformSearch(value_str, search_str); +} + +EvaluateResult CoreRegexContains::PerformSearch( + const std::string& value, const std::string& search) const { + re2::RE2 re(search); + if (!re.ok()) { + // TODO(wuandy): Log warning about invalid regex? + return EvaluateResult::NewError(); + } + bool result = RE2::PartialMatch(value, re); + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +EvaluateResult CoreRegexMatch::PerformSearch(const std::string& value, + const std::string& search) const { + re2::RE2 re(search); + if (!re.ok()) { + // TODO(wuandy): Log warning about invalid regex? + return EvaluateResult::NewError(); + } + bool result = RE2::FullMatch(value, re); + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +EvaluateResult CoreLike::PerformSearch(const std::string& value, + const std::string& search) const { + std::string regex_pattern = LikeToRegex(search); + re2::RE2 re(regex_pattern); + // LikeToRegex should ideally produce valid regex, but check anyway. + if (!re.ok()) { + // TODO(wuandy): Log warning about failed LIKE conversion? + return EvaluateResult::NewError(); + } + // LIKE implies matching the entire string + bool result = RE2::FullMatch(value, re); + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +EvaluateResult CoreByteLength::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, + "byte_length() requires exactly 1 param"); + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kString: { + const auto str = nanopb::MakeString(evaluated.value()->string_value); + // Validate UTF-8 using the generic function with a no-op lambda + bool dummy_result = false; // Result accumulator not needed here + bool is_valid_utf8 = ProcessUtf8( + str, &dummy_result, + [](bool*, uint32_t, absl::string_view) { /* no-op */ }); + + if (is_valid_utf8) { + return EvaluateResult::NewValue(IntValue(str.size())); + } else { + return EvaluateResult::NewError(); // Invalid UTF-8 + } + } + case EvaluateResult::ResultType::kBytes: { + const size_t len = evaluated.value()->bytes_value == nullptr + ? 0 + : evaluated.value()->bytes_value->size; + return EvaluateResult::NewValue(IntValue(len)); + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + return EvaluateResult::NewError(); // Type mismatch or Error/Unset + } +} + +EvaluateResult CoreCharLength::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, + "char_length() requires exactly 1 param"); + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kString: { + const auto str = nanopb::MakeString(evaluated.value()->string_value); + // Count codepoints using the generic function + int char_count = 0; + bool is_valid_utf8 = ProcessUtf8( + str, &char_count, + [](int* count, uint32_t, absl::string_view) { (*count)++; }); + + if (is_valid_utf8) { + return EvaluateResult::NewValue(IntValue(char_count)); + } else { + return EvaluateResult::NewError(); // Invalid UTF-8 + } + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + return EvaluateResult::NewError(); // Type mismatch or Error/Unset + } +} + +EvaluateResult CoreStrConcat::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + std::string result_string; + + bool found_null = false; + for (const auto& param : expr_->params()) { + EvaluateResult evaluated = + param->ToEvaluable()->Evaluate(context, document); + switch (evaluated.type()) { + case EvaluateResult::ResultType::kString: { + absl::StrAppend(&result_string, + nanopb::MakeString(evaluated.value()->string_value)); + break; + } + case EvaluateResult::ResultType::kNull: { + found_null = true; + break; + } + default: + return EvaluateResult::NewError(); // Type mismatch or Error/Unset + } + } + + if (found_null) { + return EvaluateResult::NewNull(); + } + + return EvaluateResult::NewValue(model::StringValue(result_string)); +} + +EvaluateResult CoreEndsWith::PerformSearch(const std::string& value, + const std::string& search) const { + // Use absl::EndsWith + bool result = absl::EndsWith(value, search); + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +EvaluateResult CoreStartsWith::PerformSearch(const std::string& value, + const std::string& search) const { + // Use absl::StartsWith + bool result = absl::StartsWith(value, search); + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +EvaluateResult CoreStrContains::PerformSearch(const std::string& value, + const std::string& search) const { + // Use absl::StrContains + bool result = absl::StrContains(value, search); + return EvaluateResult::NewValue( + nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); +} + +EvaluateResult CoreToLower::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, + "to_lower() requires exactly 1 param"); + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kString: { + std::locale locale{"en_US.UTF-8"}; + std::string str = nanopb::MakeString(evaluated.value()->string_value); + std::transform(str.begin(), str.end(), str.begin(), + [&locale](char c) { return std::tolower(c, locale); }); + return EvaluateResult::NewValue(model::StringValue(str)); + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + return EvaluateResult::NewError(); // Type mismatch or Error/Unset + } +} +EvaluateResult CoreToUpper::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, + "to_upper() requires exactly 1 param"); + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kString: { + std::locale locale{"en_US.UTF-8"}; + std::string str = nanopb::MakeString(evaluated.value()->string_value); + std::transform(str.begin(), str.end(), str.begin(), + [&locale](char c) { return std::toupper(c, locale); }); + return EvaluateResult::NewValue(model::StringValue(str)); + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + return EvaluateResult::NewError(); // Type mismatch or Error/Unset + } +} + +EvaluateResult CoreTrim::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, "trim() requires exactly 1 param"); + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kString: { + absl::string_view trimmed_view = absl::StripAsciiWhitespace( + nanopb::MakeString(evaluated.value()->string_value)); + return EvaluateResult::NewValue( + model::StringValue(std::move(trimmed_view))); + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + return EvaluateResult::NewError(); // Type mismatch or Error/Unset + } +} + +EvaluateResult CoreReverse::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, + "reverse() requires exactly 1 param"); + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kString: { + std::string reversed; + bool is_valid_utf8 = ProcessUtf8( + nanopb::MakeString(evaluated.value()->string_value), &reversed, + [](std::string* reversed_str, uint32_t /*code_point*/, + absl::string_view utf8_bytes) { + reversed_str->insert(0, utf8_bytes.data(), utf8_bytes.size()); + }); + + if (is_valid_utf8) { + return EvaluateResult::NewValue(model::StringValue(reversed)); + } + + return EvaluateResult::NewError(); + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + return EvaluateResult::NewError(); // Type mismatch or Error/Unset + } +} + +// --- Map Expression Implementations --- + +EvaluateResult CoreMapGet::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 2, + "map_get() function requires exactly 2 params (map and key)"); + + // Evaluate the map operand (param 0) + std::unique_ptr map_evaluable = + expr_->params()[0]->ToEvaluable(); + EvaluateResult map_result = map_evaluable->Evaluate(context, document); + + switch (map_result.type()) { + case EvaluateResult::ResultType::kUnset: { + // If the map itself is unset, the result is unset + return EvaluateResult::NewUnset(); + } + case EvaluateResult::ResultType::kMap: { + // Expected type, continue + break; + } + default: { + // Any other type (including Null, Error) is an error + return EvaluateResult::NewError(); + } + } + + // Evaluate the key operand (param 1) + std::unique_ptr key_evaluable = + expr_->params()[1]->ToEvaluable(); + EvaluateResult key_result = key_evaluable->Evaluate(context, document); + + absl::optional key_string; + switch (key_result.type()) { + case EvaluateResult::ResultType::kString: { + key_string = nanopb::MakeString(key_result.value()->string_value); + HARD_ASSERT(key_string.has_value(), "Failed to extract string key"); + break; + } + default: { + // Key must be a string, otherwise it's an error + return EvaluateResult::NewError(); + } + } + + // Look up the field in the map value + const auto* entry = model::FindEntry(*map_result.value(), key_string.value()); + + if (entry != nullptr) { + // Key found, return a deep clone of the value + return EvaluateResult::NewValue(model::DeepClone(entry->value)); + } else { + // Key not found, return Unset + return EvaluateResult::NewUnset(); + } +} + +// --- Arithmetic Implementations --- +EvaluateResult ArithmeticBase::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() >= 2, + "%s() function requires at least 2 params", expr_->name()); + + EvaluateResult current_result = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + for (size_t i = 1; i < expr_->params().size(); ++i) { + // Check current accumulated result before evaluating next operand + if (current_result.IsErrorOrUnset()) { + // Propagate error immediately if accumulated result is error/unset + // Note: Unset is treated as Error in arithmetic according to TS logic + return EvaluateResult::NewError(); + } + // Null check happens inside ApplyOperation + + EvaluateResult next_operand = + expr_->params()[i]->ToEvaluable()->Evaluate(context, document); + + // Apply the operation + current_result = ApplyOperation(current_result, next_operand); + + // If ApplyOperation resulted in error or unset, propagate immediately as + // error + if (current_result.IsErrorOrUnset()) { + // Treat Unset from ApplyOperation as Error for propagation + return EvaluateResult::NewError(); + } + // Null is handled within the loop by ApplyOperation in the next iteration + } + + return current_result; +} + +inline EvaluateResult ArithmeticBase::ApplyOperation( + const EvaluateResult& left, const EvaluateResult& right) const { + // Mirroring TypeScript logic: + // 1. Check for Error/Unset first + if (left.IsErrorOrUnset() || right.IsErrorOrUnset()) { + return EvaluateResult::NewError(); + } + // 2. Check for Null + if (left.IsNull() || right.IsNull()) { + return EvaluateResult::NewNull(); + } + + // 3. Type check: Both must be numbers + const google_firestore_v1_Value* left_val = left.value(); + const google_firestore_v1_Value* right_val = right.value(); + if (!model::IsNumber(*left_val) || !model::IsNumber(*right_val)) { + return EvaluateResult::NewError(); // Type error + } + + // 4. Determine operation type (Integer or Double) + if (model::IsDouble(*left_val) || model::IsDouble(*right_val)) { + // Promote to double + double left_double_val = model::IsDouble(*left_val) + ? left_val->double_value + : static_cast(left_val->integer_value); + double right_double_val = + model::IsDouble(*right_val) + ? right_val->double_value + : static_cast(right_val->integer_value); + + // NaN propagation and specific error handling (like div/mod by zero) + // are handled within PerformDoubleOperation. + return PerformDoubleOperation(left_double_val, right_double_val); + + } else { + // Both are integers + absl::optional left_int_opt = model::GetInteger(*left_val); + absl::optional right_int_opt = model::GetInteger(*right_val); + // These should always succeed because we already checked IsNumber and + // excluded IsDouble. + HARD_ASSERT(left_int_opt.has_value() && right_int_opt.has_value(), + "Failed to extract integer values after IsNumber check"); + + return PerformIntegerOperation(left_int_opt.value(), right_int_opt.value()); + } } -EvaluateResult CoreGte::CompareToResult(const EvaluateResult& left, - const EvaluateResult& right) const { - // Type mismatch always results in false - if (model::GetTypeOrder(*left.value()) != - model::GetTypeOrder(*right.value())) { - return EvaluateResult::NewValue(nanopb::MakeMessage(model::FalseValue())); +EvaluateResult CoreAdd::PerformIntegerOperation(int64_t l, int64_t r) const { + auto const result = SafeAdd(l, r); + if (result.has_value()) { + return EvaluateResult::NewValue(IntValue(result.value())); } - // NaN compared to anything is false - if (model::IsNaNValue(*left.value()) || model::IsNaNValue(*right.value())) { - return EvaluateResult::NewValue(nanopb::MakeMessage(model::FalseValue())); + + return EvaluateResult::NewError(); +} + +EvaluateResult CoreAdd::PerformDoubleOperation(double l, double r) const { + return EvaluateResult::NewValue(DoubleValue(l + r)); +} + +EvaluateResult CoreSubtract::PerformIntegerOperation(int64_t l, + int64_t r) const { + auto const result = SafeSubtract(l, r); + if (result.has_value()) { + return EvaluateResult::NewValue(IntValue(result.value())); } - // Check for equality first using StrictEquals - if (model::StrictEquals(*left.value(), *right.value()) == - model::StrictEqualsResult::kEq) { - return EvaluateResult::NewValue(nanopb::MakeMessage(model::TrueValue())); + return EvaluateResult::NewError(); +} + +EvaluateResult CoreSubtract::PerformDoubleOperation(double l, double r) const { + return EvaluateResult::NewValue(DoubleValue(l - r)); +} + +EvaluateResult CoreMultiply::PerformIntegerOperation(int64_t l, + int64_t r) const { + auto const result = SafeMultiply(l, r); + if (result.has_value()) { + return EvaluateResult::NewValue(IntValue(result.value())); } - // If not equal, perform standard comparison - bool result = model::Compare(*left.value(), *right.value()) == - util::ComparisonResult::Descending; - return EvaluateResult::NewValue( - nanopb::MakeMessage(result ? model::TrueValue() : model::FalseValue())); + return EvaluateResult::NewError(); } -// --- Arithmetic Implementations --- +EvaluateResult CoreMultiply::PerformDoubleOperation(double l, double r) const { + return EvaluateResult::NewValue(DoubleValue(l * r)); +} -EvaluateResult CoreAdd::Evaluate( - const api::EvaluateContext& context, - const model::PipelineInputOutput& document) const { - return EvaluateArithmetic( - expr_.get(), context, document, - [](int64_t l, int64_t r) { return SafeAdd(l, r); }, - [](double l, double r) { return l + r; }); +EvaluateResult CoreDivide::PerformIntegerOperation(int64_t l, int64_t r) const { + auto const result = SafeDivide(l, r); + if (result.has_value()) { + return EvaluateResult::NewValue(IntValue(result.value())); + } + + return EvaluateResult::NewError(); } -EvaluateResult CoreSubtract::Evaluate( - const api::EvaluateContext& context, - const model::PipelineInputOutput& document) const { - return EvaluateArithmetic( - expr_.get(), context, document, - [](int64_t l, int64_t r) { return SafeSubtract(l, r); }, - [](double l, double r) { return l - r; }); +EvaluateResult CoreDivide::PerformDoubleOperation(double l, double r) const { + // C++ double division handles signed zero correctly according to IEEE + // 754. +x / +0 -> +Inf -x / +0 -> -Inf +x / -0 -> -Inf -x / -0 -> +Inf + // 0 / 0 -> NaN + return EvaluateResult::NewValue(DoubleValue(l / r)); } -EvaluateResult CoreMultiply::Evaluate( - const api::EvaluateContext& context, - const model::PipelineInputOutput& document) const { - return EvaluateArithmetic( - expr_.get(), context, document, - [](int64_t l, int64_t r) { return SafeMultiply(l, r); }, - [](double l, double r) { return l * r; }); +EvaluateResult CoreMod::PerformIntegerOperation(int64_t l, int64_t r) const { + auto const result = SafeMod(l, r); + if (result.has_value()) { + return EvaluateResult::NewValue(IntValue(result.value())); + } + + return EvaluateResult::NewError(); } -EvaluateResult CoreDivide::Evaluate( - const api::EvaluateContext& context, - const model::PipelineInputOutput& document) const { - return EvaluateArithmetic( - expr_.get(), context, document, - // Integer division - [](int64_t l, int64_t r) { return SafeDivide(l, r); }, - // Double division - [](double l, double r) { - // C++ double division handles signed zero correctly according to IEEE - // 754. +x / +0 -> +Inf -x / +0 -> -Inf +x / -0 -> -Inf -x / -0 -> +Inf - // 0 / 0 -> NaN - return l / r; - }); -} - -EvaluateResult CoreMod::Evaluate( - const api::EvaluateContext& context, - const model::PipelineInputOutput& document) const { - return EvaluateArithmetic( - expr_.get(), context, document, - // Integer modulo - [](int64_t l, int64_t r) { return SafeMod(l, r); }, - // Double modulo - [](double l, double r) { - if (r == 0.0) { - return std::numeric_limits::quiet_NaN(); - } - // Use std::fmod for double modulo, matches C++ and Firestore semantics - return std::fmod(l, r); - }); +EvaluateResult CoreMod::PerformDoubleOperation(double l, double r) const { + if (r == 0.0) { + return EvaluateResult::NewValue( + DoubleValue(std::numeric_limits::quiet_NaN())); + } + // Use std::fmod for double modulo, matches C++ and Firestore semantics + return EvaluateResult::NewValue(DoubleValue(std::fmod(l, r))); } // --- Array Expression Implementations --- @@ -1344,6 +1940,560 @@ EvaluateResult CoreNot::Evaluate( } } +namespace { +// timestamp utilities + +// --- Timestamp Constants --- +// 0001-01-01T00:00:00Z +constexpr int64_t kTimestampMinSeconds = -62135596800LL; +// 9999-12-31T23:59:59Z (max seconds part) +constexpr int64_t kTimestampMaxSeconds = 253402300799LL; +// Max nanoseconds part +constexpr int32_t kTimestampMaxNanos = 999999999; + +constexpr int64_t kMillisecondsPerSecond = 1000LL; +constexpr int64_t kMicrosecondsPerSecond = 1000000LL; +constexpr int64_t kNanosecondsPerMicrosecond = 1000LL; +constexpr int64_t kNanosecondsPerMillisecond = 1000000LL; +constexpr int64_t kNanosecondsPerSecond = 1000000000LL; + +// 0001-01-01T00:00:00.000Z +constexpr int64_t kTimestampMinMilliseconds = + kTimestampMinSeconds * kMillisecondsPerSecond; +// 9999-12-31T23:59:59.999Z +constexpr int64_t kTimestampMaxMilliseconds = + kTimestampMaxSeconds * kMillisecondsPerSecond + 999LL; + +// 0001-01-01T00:00:00.000000Z +constexpr int64_t kTimestampMinMicroseconds = + kTimestampMinSeconds * kMicrosecondsPerSecond; +// 9999-12-31T23:59:59.999999Z +constexpr int64_t kTimestampMaxMicroseconds = + kTimestampMaxSeconds * kMicrosecondsPerSecond + 999999LL; + +// --- Timestamp Helper Functions --- + +bool IsMicrosInBounds(int64_t micros) { + return micros >= kTimestampMinMicroseconds && + micros <= kTimestampMaxMicroseconds; +} + +bool IsMillisInBounds(int64_t millis) { + return millis >= kTimestampMinMilliseconds && + millis <= kTimestampMaxMilliseconds; +} + +bool IsSecondsInBounds(int64_t seconds) { + return seconds >= kTimestampMinSeconds && seconds <= kTimestampMaxSeconds; +} + +// Checks if a google_protobuf_Timestamp is within the valid Firestore range. +bool IsTimestampInBounds(const google_protobuf_Timestamp& ts) { + if (ts.seconds < kTimestampMinSeconds || ts.seconds > kTimestampMaxSeconds) { + return false; + } + // Nanos must be non-negative and less than 1 second. + if (ts.nanos < 0 || ts.nanos >= kNanosecondsPerSecond) { + return false; + } + // Additional checks for min/max boundaries. + if (ts.seconds == kTimestampMinSeconds && ts.nanos != 0) { + return false; // Min timestamp must have 0 nanos. + } + if (ts.seconds == kTimestampMaxSeconds && ts.nanos > kTimestampMaxNanos) { + return false; // Max timestamp allows up to 999,999,999 nanos. + } + return true; +} + +// Converts a google_protobuf_Timestamp to total microseconds since epoch. +// Returns nullopt if the timestamp is out of bounds or calculation overflows. +absl::optional TimestampToMicros(const google_protobuf_Timestamp& ts) { + if (!IsTimestampInBounds(ts)) { + return absl::nullopt; + } + + absl::optional seconds_part_micros = + SafeMultiply(ts.seconds, kMicrosecondsPerSecond); + if (!seconds_part_micros.has_value()) { + return absl::nullopt; // Overflow multiplying seconds + } + + // Integer division truncates towards zero. + int64_t nanos_part_micros = ts.nanos / kNanosecondsPerMicrosecond; + + absl::optional total_micros = + SafeAdd(seconds_part_micros.value(), nanos_part_micros); + + // Final check to ensure the result is within the representable microsecond + // range. + if (!total_micros.has_value() || !IsMicrosInBounds(total_micros.value())) { + return absl::nullopt; + } + + return total_micros; +} + +// Enum for time units used in timestamp arithmetic. +enum class TimeUnit { + kMicrosecond, + kMillisecond, + kSecond, + kMinute, + kHour, + kDay +}; + +// Parses a string representation of a time unit into the TimeUnit enum. +absl::optional ParseTimeUnit(const std::string& unit_str) { + if (unit_str == "microsecond") return TimeUnit::kMicrosecond; + if (unit_str == "millisecond") return TimeUnit::kMillisecond; + if (unit_str == "second") return TimeUnit::kSecond; + if (unit_str == "minute") return TimeUnit::kMinute; + if (unit_str == "hour") return TimeUnit::kHour; + if (unit_str == "day") return TimeUnit::kDay; + return absl::nullopt; // Invalid unit string +} + +// Calculates the total microseconds for a given unit and amount. +// Returns nullopt on overflow. +absl::optional MicrosFromUnitAndAmount(TimeUnit unit, int64_t amount) { + switch (unit) { + case TimeUnit::kMicrosecond: + return amount; // No multiplication needed, no overflow possible here. + case TimeUnit::kMillisecond: + return SafeMultiply( + amount, kNanosecondsPerMillisecond / kNanosecondsPerMicrosecond); + case TimeUnit::kSecond: + return SafeMultiply(amount, kMicrosecondsPerSecond); + case TimeUnit::kMinute: + return SafeMultiply(amount, 60LL * kMicrosecondsPerSecond); + case TimeUnit::kHour: + return SafeMultiply(amount, 3600LL * kMicrosecondsPerSecond); + case TimeUnit::kDay: + return SafeMultiply(amount, 86400LL * kMicrosecondsPerSecond); + default: + // Should not happen if ParseTimeUnit is used correctly. + HARD_FAIL("Invalid TimeUnit enum value"); + return absl::nullopt; + } +} + +// Helper to create a google_protobuf_Timestamp from seconds and nanos. +// Assumes inputs are already validated to be within bounds. +google_protobuf_Timestamp CreateTimestampProto(int64_t seconds, int32_t nanos) { + google_protobuf_Timestamp ts; + // Use direct member assignment for protobuf fields + ts.seconds = seconds; + ts.nanos = nanos; + return ts; +} + +// Helper function to adjust timestamp for negative nanoseconds. +// Returns the adjusted {seconds, nanos} pair. Returns nullopt if adjusting +// seconds underflows. +absl::optional> AdjustTimestamp(int64_t seconds, + int32_t nanos) { + if (nanos < 0) { + absl::optional adjusted_seconds = SafeSubtract(seconds, 1); + if (!adjusted_seconds.has_value()) { + return absl::nullopt; // Underflow during adjustment + } + // Ensure nanos is within [-1e9 + 1, -1] before adding 1e9. + // The modulo operation should guarantee this range for negative results. + return std::make_pair(adjusted_seconds.value(), + nanos + kNanosecondsPerSecond); + } + // No adjustment needed, return original values. + return std::make_pair(seconds, nanos); +} + +} // anonymous namespace + +// --- Timestamp Expression Implementations --- + +EvaluateResult UnixToTimestampBase::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, + "%s() function requires exactly 1 param", expr_->name()); + + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kInt: { + absl::optional value = model::GetInteger(*evaluated.value()); + HARD_ASSERT(value.has_value(), "Integer value extraction failed"); + return ToTimestamp(value.value()); + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + // Type error (not integer or null) + return EvaluateResult::NewError(); + } +} + +EvaluateResult TimestampToUnixBase::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT(expr_->params().size() == 1, + "%s() function requires exactly 1 param", expr_->name()); + + EvaluateResult evaluated = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + + switch (evaluated.type()) { + case EvaluateResult::ResultType::kTimestamp: { + // Check if the input timestamp is within valid bounds before conversion. + if (!IsTimestampInBounds(evaluated.value()->timestamp_value)) { + return EvaluateResult::NewError(); + } + return ToUnix(evaluated.value()->timestamp_value); + } + case EvaluateResult::ResultType::kNull: + return EvaluateResult::NewNull(); + default: + // Type error (not timestamp or null) + return EvaluateResult::NewError(); + } +} + +EvaluateResult TimestampArithmeticBase::Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const { + HARD_ASSERT( + expr_->params().size() == 3, + "%s() function requires exactly 3 params (timestamp, unit, amount)", + expr_->name()); + + bool has_null = false; + + // 1. Evaluate Timestamp operand + EvaluateResult timestamp_result = + expr_->params()[0]->ToEvaluable()->Evaluate(context, document); + switch (timestamp_result.type()) { + case EvaluateResult::ResultType::kTimestamp: + // Check initial timestamp bounds + if (!IsTimestampInBounds(timestamp_result.value()->timestamp_value)) { + return EvaluateResult::NewError(); + } + break; + case EvaluateResult::ResultType::kNull: + has_null = true; + break; + default: + return EvaluateResult::NewError(); // Type error + } + + // 2. Evaluate Unit operand (must be string) + EvaluateResult unit_result = + expr_->params()[1]->ToEvaluable()->Evaluate(context, document); + absl::optional time_unit; + switch (unit_result.type()) { + case EvaluateResult::ResultType::kString: { + std::string unit_str = + nanopb::MakeString(unit_result.value()->string_value); + time_unit = ParseTimeUnit(unit_str); + if (!time_unit.has_value()) { + return EvaluateResult::NewError(); // Invalid unit string + } + break; + } + case EvaluateResult::ResultType::kNull: + has_null = true; + break; + default: + return EvaluateResult::NewError(); // Type error + } + + // 3. Evaluate Amount operand (must be integer) + EvaluateResult amount_result = + expr_->params()[2]->ToEvaluable()->Evaluate(context, document); + absl::optional amount; + switch (amount_result.type()) { + case EvaluateResult::ResultType::kInt: + amount = model::GetInteger(*amount_result.value()); + HARD_ASSERT(amount.has_value(), "Integer value extraction failed"); + break; + case EvaluateResult::ResultType::kNull: + has_null = true; + break; + default: + return EvaluateResult::NewError(); // Type error + } + + // Null propagation + if (has_null) { + return EvaluateResult::NewNull(); + } + + // Calculate initial micros and micros to operate + absl::optional initial_micros = + TimestampToMicros(timestamp_result.value()->timestamp_value); + if (!initial_micros.has_value()) { + // Should have been caught by IsTimestampInBounds earlier, but double-check. + return EvaluateResult::NewError(); + } + + absl::optional micros_to_operate = + MicrosFromUnitAndAmount(time_unit.value(), amount.value()); + if (!micros_to_operate.has_value()) { + return EvaluateResult::NewError(); // Overflow calculating micros delta + } + + // Perform the specific arithmetic (add or subtract) + absl::optional new_micros_opt = + PerformArithmetic(initial_micros.value(), micros_to_operate.value()); + if (!new_micros_opt.has_value()) { + return EvaluateResult::NewError(); // Arithmetic overflow/error + } + int64_t new_micros = new_micros_opt.value(); + + // Check final microsecond bounds + if (!IsMicrosInBounds(new_micros)) { + return EvaluateResult::NewError(); + } + + // Convert back to seconds and nanos + // Use SafeDivide to handle potential INT64_MIN / -1 edge case, though + // unlikely here. + absl::optional new_seconds_opt = + SafeDivide(new_micros, kMicrosecondsPerSecond); + if (!new_seconds_opt.has_value()) { + return EvaluateResult::NewError(); // Should not happen if IsMicrosInBounds + // passed + } + int64_t new_seconds = new_seconds_opt.value(); + int64_t nanos_remainder_micros = new_micros % kMicrosecondsPerSecond; + + // Adjust seconds and calculate nanos based on remainder sign + int32_t new_nanos; + if (nanos_remainder_micros < 0) { + // If remainder is negative, adjust seconds down and make nanos positive. + absl::optional adjusted_seconds_opt = SafeSubtract(new_seconds, 1); + if (!adjusted_seconds_opt.has_value()) + return EvaluateResult::NewError(); // Overflow + new_seconds = adjusted_seconds_opt.value(); + new_nanos = + static_cast((nanos_remainder_micros + kMicrosecondsPerSecond) * + kNanosecondsPerMicrosecond); + } else { + new_nanos = static_cast(nanos_remainder_micros * + kNanosecondsPerMicrosecond); + } + + // Create the final timestamp proto + google_protobuf_Timestamp result_ts = + CreateTimestampProto(new_seconds, new_nanos); + + // Final check on calculated timestamp bounds + if (!IsTimestampInBounds(result_ts)) { + return EvaluateResult::NewError(); + } + + // Wrap in Value proto and return + google_firestore_v1_Value result_value; + result_value.which_value_type = google_firestore_v1_Value_timestamp_value_tag; + result_value.timestamp_value = result_ts; // Copy the timestamp proto + return EvaluateResult::NewValue(nanopb::MakeMessage(std::move(result_value))); +} + +// --- Specific Timestamp Function Implementations --- + +// Define constructors declared in the header +CoreUnixMicrosToTimestamp::CoreUnixMicrosToTimestamp( + const api::FunctionExpr& expr) + : UnixToTimestampBase(expr) { +} +CoreUnixMillisToTimestamp::CoreUnixMillisToTimestamp( + const api::FunctionExpr& expr) + : UnixToTimestampBase(expr) { +} +CoreUnixSecondsToTimestamp::CoreUnixSecondsToTimestamp( + const api::FunctionExpr& expr) + : UnixToTimestampBase(expr) { +} +CoreTimestampToUnixMicros::CoreTimestampToUnixMicros( + const api::FunctionExpr& expr) + : TimestampToUnixBase(expr) { +} +CoreTimestampToUnixMillis::CoreTimestampToUnixMillis( + const api::FunctionExpr& expr) + : TimestampToUnixBase(expr) { +} +CoreTimestampToUnixSeconds::CoreTimestampToUnixSeconds( + const api::FunctionExpr& expr) + : TimestampToUnixBase(expr) { +} +CoreTimestampAdd::CoreTimestampAdd(const api::FunctionExpr& expr) + : TimestampArithmeticBase(expr) { +} +CoreTimestampSub::CoreTimestampSub(const api::FunctionExpr& expr) + : TimestampArithmeticBase(expr) { +} + +// Define member function implementations +EvaluateResult CoreUnixMicrosToTimestamp::ToTimestamp(int64_t micros) const { + if (!IsMicrosInBounds(micros)) { + return EvaluateResult::NewError(); + } + + // Use SafeDivide to handle potential INT64_MIN / -1 edge case, though + // unlikely here. + absl::optional seconds_opt = + SafeDivide(micros, kMicrosecondsPerSecond); + if (!seconds_opt.has_value()) return EvaluateResult::NewError(); + int64_t initial_seconds = seconds_opt.value(); + // Calculate initial nanos directly from the remainder. + int32_t initial_nanos = static_cast( + (micros % kMicrosecondsPerSecond) * kNanosecondsPerMicrosecond); + + // Adjust for negative nanoseconds using the helper function. + absl::optional> adjusted_ts = + AdjustTimestamp(initial_seconds, initial_nanos); + + if (!adjusted_ts.has_value()) { + return EvaluateResult::NewError(); // Overflow during adjustment + } + + int64_t final_seconds = adjusted_ts.value().first; + int32_t final_nanos = adjusted_ts.value().second; + + google_firestore_v1_Value result_value; + result_value.which_value_type = google_firestore_v1_Value_timestamp_value_tag; + result_value.timestamp_value = + CreateTimestampProto(final_seconds, final_nanos); + + // Final bounds check after adjustment. + if (!IsTimestampInBounds(result_value.timestamp_value)) { + return EvaluateResult::NewError(); + } + + return EvaluateResult::NewValue(nanopb::MakeMessage(std::move(result_value))); +} + +EvaluateResult CoreUnixMillisToTimestamp::ToTimestamp(int64_t millis) const { + if (!IsMillisInBounds(millis)) { + return EvaluateResult::NewError(); + } + + absl::optional seconds_opt = + SafeDivide(millis, kMillisecondsPerSecond); + if (!seconds_opt.has_value()) return EvaluateResult::NewError(); + int64_t initial_seconds = seconds_opt.value(); + // Calculate initial nanos directly from the remainder. + int32_t initial_nanos = static_cast( + (millis % kMillisecondsPerSecond) * kNanosecondsPerMillisecond); + + // Adjust for negative nanoseconds using the helper function. + absl::optional> adjusted_ts = + AdjustTimestamp(initial_seconds, initial_nanos); + + if (!adjusted_ts.has_value()) { + return EvaluateResult::NewError(); // Overflow during adjustment + } + + int64_t final_seconds = adjusted_ts.value().first; + int32_t final_nanos = adjusted_ts.value().second; + + google_firestore_v1_Value result_value; + result_value.which_value_type = google_firestore_v1_Value_timestamp_value_tag; + result_value.timestamp_value = + CreateTimestampProto(final_seconds, final_nanos); + + // Final bounds check after adjustment. + if (!IsTimestampInBounds(result_value.timestamp_value)) { + return EvaluateResult::NewError(); + } + + return EvaluateResult::NewValue(nanopb::MakeMessage(std::move(result_value))); +} + +EvaluateResult CoreUnixSecondsToTimestamp::ToTimestamp(int64_t seconds) const { + if (!IsSecondsInBounds(seconds)) { + return EvaluateResult::NewError(); + } + + google_firestore_v1_Value result_value; + result_value.which_value_type = google_firestore_v1_Value_timestamp_value_tag; + result_value.timestamp_value = + CreateTimestampProto(seconds, 0); // Nanos are always 0 + + // Bounds check is implicitly handled by IsSecondsInBounds + return EvaluateResult::NewValue(nanopb::MakeMessage(std::move(result_value))); +} + +EvaluateResult CoreTimestampToUnixMicros::ToUnix( + const google_protobuf_Timestamp& ts) const { + absl::optional micros = TimestampToMicros(ts); + // Check if the resulting micros are within representable bounds (already done + // in TimestampToMicros) + if (!micros.has_value()) { + return EvaluateResult::NewError(); + } + return EvaluateResult::NewValue(IntValue(micros.value())); +} + +EvaluateResult CoreTimestampToUnixMillis::ToUnix( + const google_protobuf_Timestamp& ts) const { + absl::optional micros_opt = TimestampToMicros(ts); + if (!micros_opt.has_value()) { + return EvaluateResult::NewError(); + } + int64_t micros = micros_opt.value(); + + // Perform division, truncating towards zero. + absl::optional millis_opt = SafeDivide(micros, 1000LL); + if (!millis_opt.has_value()) { + // This should ideally not happen if micros were in bounds, but check + // anyway. + return EvaluateResult::NewError(); + } + int64_t millis = millis_opt.value(); + + // Adjust for negative timestamps where truncation differs from floor + // division. If micros is negative and not perfectly divisible by 1000, + // subtract 1 from millis. + if (micros < 0 && (micros % 1000LL != 0)) { + absl::optional adjusted_millis_opt = SafeSubtract(millis, 1); + if (!adjusted_millis_opt.has_value()) + return EvaluateResult::NewError(); // Overflow check + millis = adjusted_millis_opt.value(); + } + + // Check if the resulting millis are within representable bounds + if (!IsMillisInBounds(millis)) { + return EvaluateResult::NewError(); + } + + return EvaluateResult::NewValue(IntValue(millis)); +} + +EvaluateResult CoreTimestampToUnixSeconds::ToUnix( + const google_protobuf_Timestamp& ts) const { + // Seconds are directly available and already checked by IsTimestampInBounds + // in base class. + int64_t seconds = ts.seconds; + // Check if the resulting seconds are within representable bounds (redundant + // but safe) + if (!IsSecondsInBounds(seconds)) { + return EvaluateResult::NewError(); + } + return EvaluateResult::NewValue(IntValue(seconds)); +} + +absl::optional CoreTimestampAdd::PerformArithmetic( + int64_t initial_micros, int64_t micros_to_operate) const { + return SafeAdd(initial_micros, micros_to_operate); +} + +absl::optional CoreTimestampSub::PerformArithmetic( + int64_t initial_micros, int64_t micros_to_operate) const { + return SafeSubtract(initial_micros, micros_to_operate); +} + } // namespace core } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/core/expressions_eval.h b/Firestore/core/src/core/expressions_eval.h index 17a8eae7242..29f2beb3147 100644 --- a/Firestore/core/src/core/expressions_eval.h +++ b/Firestore/core/src/core/expressions_eval.h @@ -19,15 +19,22 @@ #include +#include // For std::move #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" + #include "Firestore/core/src/api/expressions.h" #include "Firestore/core/src/api/stages.h" +#include "Firestore/core/src/model/value_util.h" #include "Firestore/core/src/nanopb/message.h" +#include "Firestore/core/src/util/hard_assert.h" +#include "absl/types/optional.h" namespace firebase { namespace firestore { namespace core { +// Forward declaration removed, definition moved below + /** Represents the result of evaluating an expression. */ class EvaluateResult { public: @@ -217,12 +224,103 @@ class CoreGte : public ComparisonBase { const EvaluateResult& right) const override; }; -class CoreAdd : public EvaluableExpr { +// --- Base Class for Arithmetic Operations --- +class ArithmeticBase : public EvaluableExpr { public: - explicit CoreAdd(const api::FunctionExpr& expr) + explicit ArithmeticBase(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } + ~ArithmeticBase() override = default; + // Implementation is inline below + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + + protected: + // Performs the specific integer operation (e.g., add, subtract). + // Returns Error result on overflow or invalid operation (like div/mod by + // zero). + virtual EvaluateResult PerformIntegerOperation(int64_t lhs, + int64_t rhs) const = 0; + + // Performs the specific double operation. + // Returns Error result on invalid operation (like div/mod by zero). + virtual EvaluateResult PerformDoubleOperation(double lhs, + double rhs) const = 0; + + // Applies the arithmetic operation between two evaluated results. + // Mirrors the logic from TypeScript's applyArithmetics. + // Implementation is inline below + EvaluateResult ApplyOperation(const EvaluateResult& left, + const EvaluateResult& right) const; + + std::unique_ptr expr_; +}; +// --- End Base Class for Arithmetic Operations --- + +class CoreAdd : public ArithmeticBase { + public: + explicit CoreAdd(const api::FunctionExpr& expr) : ArithmeticBase(expr) { + } + + protected: + EvaluateResult PerformIntegerOperation(int64_t lhs, + int64_t rhs) const override; + EvaluateResult PerformDoubleOperation(double lhs, double rhs) const override; +}; + +class CoreSubtract : public ArithmeticBase { + public: + explicit CoreSubtract(const api::FunctionExpr& expr) : ArithmeticBase(expr) { + } + + protected: + EvaluateResult PerformIntegerOperation(int64_t lhs, + int64_t rhs) const override; + EvaluateResult PerformDoubleOperation(double lhs, double rhs) const override; +}; + +class CoreMultiply : public ArithmeticBase { + public: + explicit CoreMultiply(const api::FunctionExpr& expr) : ArithmeticBase(expr) { + } + + protected: + EvaluateResult PerformIntegerOperation(int64_t lhs, + int64_t rhs) const override; + EvaluateResult PerformDoubleOperation(double lhs, double rhs) const override; +}; + +class CoreDivide : public ArithmeticBase { + public: + explicit CoreDivide(const api::FunctionExpr& expr) : ArithmeticBase(expr) { + } + + protected: + EvaluateResult PerformIntegerOperation(int64_t lhs, + int64_t rhs) const override; + EvaluateResult PerformDoubleOperation(double lhs, double rhs) const override; +}; + +class CoreMod : public ArithmeticBase { + public: + explicit CoreMod(const api::FunctionExpr& expr) : ArithmeticBase(expr) { + } + + protected: + EvaluateResult PerformIntegerOperation(int64_t lhs, + int64_t rhs) const override; + EvaluateResult PerformDoubleOperation(double lhs, double rhs) const override; +}; + +// --- Array Expressions --- + +class CoreArrayReverse : public EvaluableExpr { + public: + explicit CoreArrayReverse(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } EvaluateResult Evaluate( const api::EvaluateContext& context, const model::PipelineInputOutput& document) const override; @@ -231,12 +329,24 @@ class CoreAdd : public EvaluableExpr { std::unique_ptr expr_; }; -class CoreSubtract : public EvaluableExpr { +class CoreArrayContains : public EvaluableExpr { public: - explicit CoreSubtract(const api::FunctionExpr& expr) + explicit CoreArrayContains(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + + private: + std::unique_ptr expr_; +}; +class CoreArrayContainsAll : public EvaluableExpr { + public: + explicit CoreArrayContainsAll(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } EvaluateResult Evaluate( const api::EvaluateContext& context, const model::PipelineInputOutput& document) const override; @@ -245,12 +355,24 @@ class CoreSubtract : public EvaluableExpr { std::unique_ptr expr_; }; -class CoreMultiply : public EvaluableExpr { +class CoreArrayContainsAny : public EvaluableExpr { public: - explicit CoreMultiply(const api::FunctionExpr& expr) + explicit CoreArrayContainsAny(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + private: + std::unique_ptr expr_; +}; + +class CoreArrayLength : public EvaluableExpr { + public: + explicit CoreArrayLength(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } EvaluateResult Evaluate( const api::EvaluateContext& context, const model::PipelineInputOutput& document) const override; @@ -259,9 +381,13 @@ class CoreMultiply : public EvaluableExpr { std::unique_ptr expr_; }; -class CoreDivide : public EvaluableExpr { +// --- String Expressions --- + +/** Base class for binary string search functions (starts_with, ends_with, + * str_contains). */ +class StringSearchBase : public EvaluableExpr { public: - explicit CoreDivide(const api::FunctionExpr& expr) + explicit StringSearchBase(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } @@ -269,16 +395,48 @@ class CoreDivide : public EvaluableExpr { const api::EvaluateContext& context, const model::PipelineInputOutput& document) const override; + protected: + /** + * Performs the specific string search logic after operands have been + * evaluated and basic checks (Error, Unset, Null, Type) have passed. + */ + virtual EvaluateResult PerformSearch(const std::string& value, + const std::string& search) const = 0; + + std::unique_ptr expr_; +}; + +class CoreByteLength : public EvaluableExpr { + public: + explicit CoreByteLength(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + private: std::unique_ptr expr_; }; -class CoreMod : public EvaluableExpr { +class CoreCharLength : public EvaluableExpr { public: - explicit CoreMod(const api::FunctionExpr& expr) + explicit CoreCharLength(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + private: + std::unique_ptr expr_; +}; + +class CoreStrConcat : public EvaluableExpr { + public: + explicit CoreStrConcat(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } EvaluateResult Evaluate( const api::EvaluateContext& context, const model::PipelineInputOutput& document) const override; @@ -287,11 +445,42 @@ class CoreMod : public EvaluableExpr { std::unique_ptr expr_; }; -// --- Array Expressions --- +class CoreEndsWith : public StringSearchBase { + public: + explicit CoreEndsWith(const api::FunctionExpr& expr) + : StringSearchBase(expr) { + } -class CoreArrayReverse : public EvaluableExpr { + protected: + EvaluateResult PerformSearch(const std::string& value, + const std::string& search) const override; +}; + +class CoreStartsWith : public StringSearchBase { public: - explicit CoreArrayReverse(const api::FunctionExpr& expr) + explicit CoreStartsWith(const api::FunctionExpr& expr) + : StringSearchBase(expr) { + } + + protected: + EvaluateResult PerformSearch(const std::string& value, + const std::string& search) const override; +}; + +class CoreStrContains : public StringSearchBase { + public: + explicit CoreStrContains(const api::FunctionExpr& expr) + : StringSearchBase(expr) { + } + + protected: + EvaluateResult PerformSearch(const std::string& value, + const std::string& search) const override; +}; + +class CoreToLower : public EvaluableExpr { + public: + explicit CoreToLower(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } EvaluateResult Evaluate( @@ -302,9 +491,9 @@ class CoreArrayReverse : public EvaluableExpr { std::unique_ptr expr_; }; -class CoreArrayContains : public EvaluableExpr { +class CoreToUpper : public EvaluableExpr { public: - explicit CoreArrayContains(const api::FunctionExpr& expr) + explicit CoreToUpper(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } EvaluateResult Evaluate( @@ -315,9 +504,9 @@ class CoreArrayContains : public EvaluableExpr { std::unique_ptr expr_; }; -class CoreArrayContainsAll : public EvaluableExpr { +class CoreTrim : public EvaluableExpr { public: - explicit CoreArrayContainsAll(const api::FunctionExpr& expr) + explicit CoreTrim(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } EvaluateResult Evaluate( @@ -328,9 +517,9 @@ class CoreArrayContainsAll : public EvaluableExpr { std::unique_ptr expr_; }; -class CoreArrayContainsAny : public EvaluableExpr { +class CoreReverse : public EvaluableExpr { public: - explicit CoreArrayContainsAny(const api::FunctionExpr& expr) + explicit CoreReverse(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } EvaluateResult Evaluate( @@ -341,9 +530,43 @@ class CoreArrayContainsAny : public EvaluableExpr { std::unique_ptr expr_; }; -class CoreArrayLength : public EvaluableExpr { +class CoreRegexContains : public StringSearchBase { public: - explicit CoreArrayLength(const api::FunctionExpr& expr) + explicit CoreRegexContains(const api::FunctionExpr& expr) + : StringSearchBase(expr) { + } + + protected: + EvaluateResult PerformSearch(const std::string& value, + const std::string& search) const override; +}; + +class CoreRegexMatch : public StringSearchBase { + public: + explicit CoreRegexMatch(const api::FunctionExpr& expr) + : StringSearchBase(expr) { + } + + protected: + EvaluateResult PerformSearch(const std::string& value, + const std::string& search) const override; +}; + +class CoreLike : public StringSearchBase { + public: + explicit CoreLike(const api::FunctionExpr& expr) : StringSearchBase(expr) { + } + + protected: + EvaluateResult PerformSearch(const std::string& value, + const std::string& search) const override; +}; + +// --- Map Expressions --- + +class CoreMapGet : public EvaluableExpr { + public: + explicit CoreMapGet(const api::FunctionExpr& expr) : expr_(std::make_unique(expr)) { } EvaluateResult Evaluate( @@ -553,6 +776,134 @@ class CoreNot : public EvaluableExpr { std::unique_ptr expr_; }; +// --- Timestamp Expressions --- + +/** Base class for converting Unix time (micros/millis/seconds) to Timestamp. */ +class UnixToTimestampBase : public EvaluableExpr { + public: + explicit UnixToTimestampBase(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } + + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + + protected: + /** Performs the specific conversion logic after input validation. */ + virtual EvaluateResult ToTimestamp(int64_t value) const = 0; + + std::unique_ptr expr_; +}; + +// Note: Implementations are in expressions_eval.cc +class CoreUnixMicrosToTimestamp : public UnixToTimestampBase { + public: + explicit CoreUnixMicrosToTimestamp(const api::FunctionExpr& expr); + + protected: + EvaluateResult ToTimestamp(int64_t value) const override; +}; + +class CoreUnixMillisToTimestamp : public UnixToTimestampBase { + public: + explicit CoreUnixMillisToTimestamp(const api::FunctionExpr& expr); + + protected: + EvaluateResult ToTimestamp(int64_t value) const override; +}; + +class CoreUnixSecondsToTimestamp : public UnixToTimestampBase { + public: + explicit CoreUnixSecondsToTimestamp(const api::FunctionExpr& expr); + + protected: + EvaluateResult ToTimestamp(int64_t value) const override; +}; + +/** Base class for converting Timestamp to Unix time (micros/millis/seconds). */ +class TimestampToUnixBase : public EvaluableExpr { + public: + explicit TimestampToUnixBase(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } + + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + + protected: + /** Performs the specific conversion logic after input validation. */ + virtual EvaluateResult ToUnix( + const google_protobuf_Timestamp& ts) const = 0; // Use protobuf type + + std::unique_ptr expr_; +}; + +// Note: Implementations are in expressions_eval.cc +class CoreTimestampToUnixMicros : public TimestampToUnixBase { + public: + explicit CoreTimestampToUnixMicros(const api::FunctionExpr& expr); + + protected: + EvaluateResult ToUnix(const google_protobuf_Timestamp& ts) const override; +}; + +class CoreTimestampToUnixMillis : public TimestampToUnixBase { + public: + explicit CoreTimestampToUnixMillis(const api::FunctionExpr& expr); + + protected: + EvaluateResult ToUnix(const google_protobuf_Timestamp& ts) const override; +}; + +class CoreTimestampToUnixSeconds : public TimestampToUnixBase { + public: + explicit CoreTimestampToUnixSeconds(const api::FunctionExpr& expr); + + protected: + EvaluateResult ToUnix(const google_protobuf_Timestamp& ts) const override; +}; + +/** Base class for timestamp arithmetic (add/sub). */ +class TimestampArithmeticBase : public EvaluableExpr { + public: + explicit TimestampArithmeticBase(const api::FunctionExpr& expr) + : expr_(std::make_unique(expr)) { + } + + EvaluateResult Evaluate( + const api::EvaluateContext& context, + const model::PipelineInputOutput& document) const override; + + protected: + /** Performs the specific arithmetic operation. */ + // Return optional as int128 is not needed and adds complexity + virtual absl::optional PerformArithmetic( + int64_t initial_micros, int64_t micros_to_operate) const = 0; + + std::unique_ptr expr_; +}; + +// Note: Implementations are in expressions_eval.cc +class CoreTimestampAdd : public TimestampArithmeticBase { + public: + explicit CoreTimestampAdd(const api::FunctionExpr& expr); + + protected: + absl::optional PerformArithmetic( + int64_t initial_micros, int64_t micros_to_operate) const override; +}; + +class CoreTimestampSub : public TimestampArithmeticBase { + public: + explicit CoreTimestampSub(const api::FunctionExpr& expr); + + protected: + absl::optional PerformArithmetic( + int64_t initial_micros, int64_t micros_to_operate) const override; +}; + /** * Converts a high-level expression representation into an evaluable one. */ diff --git a/Firestore/core/src/model/object_value.cc b/Firestore/core/src/model/object_value.cc index 3b812fe535d..eeae6539a8b 100644 --- a/Firestore/core/src/model/object_value.cc +++ b/Firestore/core/src/model/object_value.cc @@ -50,40 +50,6 @@ using nanopb::Message; using nanopb::ReleaseFieldOwnership; using nanopb::SetRepeatedField; -struct MapEntryKeyCompare { - bool operator()(const google_firestore_v1_MapValue_FieldsEntry& entry, - absl::string_view segment) const { - return nanopb::MakeStringView(entry.key) < segment; - } - bool operator()(absl::string_view segment, - const google_firestore_v1_MapValue_FieldsEntry& entry) const { - return segment < nanopb::MakeStringView(entry.key); - } -}; - -/** - * Finds an entry by key in the provided map value. Returns `nullptr` if the - * entry does not exist. - */ -google_firestore_v1_MapValue_FieldsEntry* FindEntry( - const google_firestore_v1_Value& value, absl::string_view segment) { - if (!IsMap(value)) { - return nullptr; - } - const google_firestore_v1_MapValue& map_value = value.map_value; - - // MapValues in iOS are always stored in sorted order. - auto found = std::equal_range(map_value.fields, - map_value.fields + map_value.fields_count, - segment, MapEntryKeyCompare()); - - if (found.first == found.second) { - return nullptr; - } - - return found.first; -} - size_t CalculateSizeOfUnion( const google_firestore_v1_MapValue& map_value, const std::map>& upserts, diff --git a/Firestore/core/src/model/value_util.cc b/Firestore/core/src/model/value_util.cc index 7b6a15732a2..e2020d5a87d 100644 --- a/Firestore/core/src/model/value_util.cc +++ b/Firestore/core/src/model/value_util.cc @@ -955,6 +955,20 @@ Message RefValue( return result; } +Message StringValue(const std::string& value) { + Message result; + result->which_value_type = google_firestore_v1_Value_string_value_tag; + result->reference_value = nanopb::MakeBytesArray(value); + return result; +} + +Message StringValue(absl::string_view value) { + Message result; + result->which_value_type = google_firestore_v1_Value_string_value_tag; + result->reference_value = nanopb::MakeBytesArray(value.data(), value.size()); + return result; +} + Message ArrayValue( std::vector> values) { google_firestore_v1_Value result; @@ -1037,6 +1051,38 @@ absl::optional GetInteger(const google_firestore_v1_Value& value) { return absl::nullopt; } +namespace { +struct MapEntryKeyCompare { + bool operator()(const google_firestore_v1_MapValue_FieldsEntry& entry, + absl::string_view segment) const { + return nanopb::MakeStringView(entry.key) < segment; + } + bool operator()(absl::string_view segment, + const google_firestore_v1_MapValue_FieldsEntry& entry) const { + return segment < nanopb::MakeStringView(entry.key); + } +}; +} // namespace + +google_firestore_v1_MapValue_FieldsEntry* FindEntry( + const google_firestore_v1_Value& value, absl::string_view field) { + if (!IsMap(value)) { + return nullptr; + } + const google_firestore_v1_MapValue& map_value = value.map_value; + + // MapValues in iOS are always stored in sorted order. + auto found = std::equal_range(map_value.fields, + map_value.fields + map_value.fields_count, + field, MapEntryKeyCompare()); + + if (found.first == found.second) { + return nullptr; + } + + return found.first; +} + namespace { StrictEqualsResult StrictArrayEquals( diff --git a/Firestore/core/src/model/value_util.h b/Firestore/core/src/model/value_util.h index 12079e9498f..6c82bf80d8e 100644 --- a/Firestore/core/src/model/value_util.h +++ b/Firestore/core/src/model/value_util.h @@ -248,6 +248,17 @@ google_firestore_v1_Value MinMap(); nanopb::Message RefValue( const DatabaseId& database_id, const DocumentKey& document_key); +/** + * Returns a Protobuf string value. + * + * The returned value might point to heap allocated memory that is owned by + * this function. To take ownership of this memory, call `DeepClone`. + */ +nanopb::Message StringValue( + const std::string& value); + +nanopb::Message StringValue(absl::string_view value); + /** * Returns a Protobuf array value representing the given values. * @@ -303,6 +314,13 @@ inline bool IsMap(const absl::optional& value) { */ absl::optional GetInteger(const google_firestore_v1_Value& value); +/** + * Finds an entry by key in the provided map value. Returns `nullptr` if the + * entry does not exist. + */ +google_firestore_v1_MapValue_FieldsEntry* FindEntry( + const google_firestore_v1_Value& value, absl::string_view field); + } // namespace model inline bool operator==(const google_firestore_v1_Value& lhs, diff --git a/Firestore/core/test/unit/core/expressions/comparison_test.cc b/Firestore/core/test/unit/core/expressions/comparison_test.cc index 5ea9c40fa96..7c07d021493 100644 --- a/Firestore/core/test/unit/core/expressions/comparison_test.cc +++ b/Firestore/core/test/unit/core/expressions/comparison_test.cc @@ -54,7 +54,6 @@ using testutil::RefConstant; using testutil::Returns; using testutil::ReturnsError; using testutil::ReturnsNull; -using testutil::ReturnsUnset; using testutil::SharedConstant; // Base fixture for common setup @@ -150,7 +149,7 @@ TEST_F(EqFunctionTest, NullOperandReturnsNull) { EXPECT_THAT( EvaluateExpr(*EqExpr({SharedConstant(model::NullValue()), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } // Corresponds to eq.nan tests in typescript @@ -251,26 +250,26 @@ TEST_F(EqFunctionTest, ErrorHandling) { for (const auto& val : ComparisonValueTestData::AllSupportedComparableValues()) { EXPECT_THAT(EvaluateExpr(*EqExpr({error_expr, val}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT(EvaluateExpr(*EqExpr({val, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); } EXPECT_THAT(EvaluateExpr(*EqExpr({error_expr, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*EqExpr({error_expr, SharedConstant(model::NullValue())}), non_map_input), - ReturnsUnset()); + ReturnsError()); } -TEST_F(EqFunctionTest, MissingFieldReturnsUnset) { +TEST_F(EqFunctionTest, MissingFieldReturnsError) { EXPECT_THAT(EvaluateExpr(*EqExpr({std::make_shared("nonexistent"), SharedConstant(testutil::Value(1LL))})), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*EqExpr({SharedConstant(testutil::Value(1LL)), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } // --- Neq (!=) Tests --- @@ -331,7 +330,7 @@ TEST_F(NeqFunctionTest, NullOperandReturnsNull) { EXPECT_THAT( EvaluateExpr(*NeqExpr({SharedConstant(model::NullValue()), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } // Corresponds to neq.nan tests @@ -393,27 +392,27 @@ TEST_F(NeqFunctionTest, ErrorHandling) { for (const auto& val : ComparisonValueTestData::AllSupportedComparableValues()) { EXPECT_THAT(EvaluateExpr(*NeqExpr({error_expr, val}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT(EvaluateExpr(*NeqExpr({val, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); } EXPECT_THAT(EvaluateExpr(*NeqExpr({error_expr, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*NeqExpr({error_expr, SharedConstant(model::NullValue())}), non_map_input), - ReturnsUnset()); + ReturnsError()); } -TEST_F(NeqFunctionTest, MissingFieldReturnsUnset) { +TEST_F(NeqFunctionTest, MissingFieldReturnsError) { EXPECT_THAT( EvaluateExpr(*NeqExpr({std::make_shared("nonexistent"), SharedConstant(testutil::Value(1LL))})), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*NeqExpr({SharedConstant(testutil::Value(1LL)), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } // --- Lt (<) Tests --- @@ -475,7 +474,7 @@ TEST_F(LtFunctionTest, NullOperandReturnsNull) { EXPECT_THAT( EvaluateExpr(*LtExpr({SharedConstant(model::NullValue()), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } TEST_F(LtFunctionTest, NaNComparisonsReturnFalse) { @@ -524,26 +523,26 @@ TEST_F(LtFunctionTest, ErrorHandling) { for (const auto& val : ComparisonValueTestData::AllSupportedComparableValues()) { EXPECT_THAT(EvaluateExpr(*LtExpr({error_expr, val}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT(EvaluateExpr(*LtExpr({val, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); } EXPECT_THAT(EvaluateExpr(*LtExpr({error_expr, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*LtExpr({error_expr, SharedConstant(model::NullValue())}), non_map_input), - ReturnsUnset()); + ReturnsError()); } -TEST_F(LtFunctionTest, MissingFieldReturnsUnset) { +TEST_F(LtFunctionTest, MissingFieldReturnsError) { EXPECT_THAT(EvaluateExpr(*LtExpr({std::make_shared("nonexistent"), SharedConstant(testutil::Value(1LL))})), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*LtExpr({SharedConstant(testutil::Value(1LL)), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } // --- Lte (<=) Tests --- @@ -600,7 +599,7 @@ TEST_F(LteFunctionTest, NullOperandReturnsNull) { EXPECT_THAT( EvaluateExpr(*LteExpr({SharedConstant(model::NullValue()), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } TEST_F(LteFunctionTest, NaNComparisonsReturnFalse) { @@ -649,27 +648,27 @@ TEST_F(LteFunctionTest, ErrorHandling) { for (const auto& val : ComparisonValueTestData::AllSupportedComparableValues()) { EXPECT_THAT(EvaluateExpr(*LteExpr({error_expr, val}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT(EvaluateExpr(*LteExpr({val, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); } EXPECT_THAT(EvaluateExpr(*LteExpr({error_expr, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*LteExpr({error_expr, SharedConstant(model::NullValue())}), non_map_input), - ReturnsUnset()); + ReturnsError()); } -TEST_F(LteFunctionTest, MissingFieldReturnsUnset) { +TEST_F(LteFunctionTest, MissingFieldReturnsError) { EXPECT_THAT( EvaluateExpr(*LteExpr({std::make_shared("nonexistent"), SharedConstant(testutil::Value(1LL))})), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*LteExpr({SharedConstant(testutil::Value(1LL)), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } // --- Gt (>) Tests --- @@ -732,7 +731,7 @@ TEST_F(GtFunctionTest, NullOperandReturnsNull) { EXPECT_THAT( EvaluateExpr(*GtExpr({SharedConstant(model::NullValue()), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } TEST_F(GtFunctionTest, NaNComparisonsReturnFalse) { @@ -781,26 +780,26 @@ TEST_F(GtFunctionTest, ErrorHandling) { for (const auto& val : ComparisonValueTestData::AllSupportedComparableValues()) { EXPECT_THAT(EvaluateExpr(*GtExpr({error_expr, val}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT(EvaluateExpr(*GtExpr({val, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); } EXPECT_THAT(EvaluateExpr(*GtExpr({error_expr, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*GtExpr({error_expr, SharedConstant(model::NullValue())}), non_map_input), - ReturnsUnset()); + ReturnsError()); } -TEST_F(GtFunctionTest, MissingFieldReturnsUnset) { +TEST_F(GtFunctionTest, MissingFieldReturnsError) { EXPECT_THAT(EvaluateExpr(*GtExpr({std::make_shared("nonexistent"), SharedConstant(testutil::Value(1LL))})), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*GtExpr({SharedConstant(testutil::Value(1LL)), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } // --- Gte (>=) Tests --- @@ -857,7 +856,7 @@ TEST_F(GteFunctionTest, NullOperandReturnsNull) { EXPECT_THAT( EvaluateExpr(*GteExpr({SharedConstant(model::NullValue()), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } TEST_F(GteFunctionTest, NaNComparisonsReturnFalse) { @@ -906,27 +905,27 @@ TEST_F(GteFunctionTest, ErrorHandling) { for (const auto& val : ComparisonValueTestData::AllSupportedComparableValues()) { EXPECT_THAT(EvaluateExpr(*GteExpr({error_expr, val}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT(EvaluateExpr(*GteExpr({val, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); } EXPECT_THAT(EvaluateExpr(*GteExpr({error_expr, error_expr}), non_map_input), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*GteExpr({error_expr, SharedConstant(model::NullValue())}), non_map_input), - ReturnsUnset()); + ReturnsError()); } -TEST_F(GteFunctionTest, MissingFieldReturnsUnset) { +TEST_F(GteFunctionTest, MissingFieldReturnsError) { EXPECT_THAT( EvaluateExpr(*GteExpr({std::make_shared("nonexistent"), SharedConstant(testutil::Value(1LL))})), - ReturnsUnset()); + ReturnsError()); EXPECT_THAT( EvaluateExpr(*GteExpr({SharedConstant(testutil::Value(1LL)), std::make_shared("nonexistent")})), - ReturnsUnset()); + ReturnsError()); } } // namespace core diff --git a/Firestore/core/test/unit/core/expressions/map_test.cc b/Firestore/core/test/unit/core/expressions/map_test.cc new file mode 100644 index 00000000000..5dc03e738c2 --- /dev/null +++ b/Firestore/core/test/unit/core/expressions/map_test.cc @@ -0,0 +1,90 @@ +/* + * 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. + */ + +#include +#include + +#include "Firestore/core/src/api/expressions.h" // For api::Expr, api::MapGet +#include "Firestore/core/src/core/expressions_eval.h" +#include "Firestore/core/src/model/value_util.h" // For value constants +#include "Firestore/core/test/unit/testutil/expression_test_util.h" // For test helpers +#include "Firestore/core/test/unit/testutil/testutil.h" // For test helpers like Value, Map +#include "gmock/gmock.h" // For matchers like Returns +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace core { + +using api::Expr; +// using api::MapGet; // Removed incorrect using +using api::FunctionExpr; // Added for creating map_get +using testutil::EvaluateExpr; +using testutil::Map; +using testutil::Returns; +using testutil::ReturnsError; +using testutil::ReturnsUnset; +using testutil::SharedConstant; +using testutil::Value; + +// Fixture for MapGet function tests +class MapGetTest : public ::testing::Test {}; + +// Helper to create a MapGet expression +inline std::shared_ptr MapGetExpr(std::shared_ptr map_expr, + std::shared_ptr key_expr) { + return std::make_shared( + "map_get", std::vector>{std::move(map_expr), + std::move(key_expr)}); +} + +TEST_F(MapGetTest, GetExistingKeyReturnsValue) { + auto map_expr = + SharedConstant(Map("a", Value(1LL), "b", Value(2LL), "c", Value(3LL))); + auto key_expr = SharedConstant("b"); + EXPECT_THAT(EvaluateExpr(*MapGetExpr(map_expr, key_expr)), + Returns(Value(2LL))); +} + +TEST_F(MapGetTest, GetMissingKeyReturnsUnset) { + auto map_expr = + SharedConstant(Map("a", Value(1LL), "b", Value(2LL), "c", Value(3LL))); + auto key_expr = SharedConstant("d"); + EXPECT_THAT(EvaluateExpr(*MapGetExpr(map_expr, key_expr)), ReturnsUnset()); +} + +TEST_F(MapGetTest, GetEmptyMapReturnsUnset) { + auto map_expr = SharedConstant(Map()); + auto key_expr = SharedConstant("d"); + EXPECT_THAT(EvaluateExpr(*MapGetExpr(map_expr, key_expr)), ReturnsUnset()); +} + +TEST_F(MapGetTest, GetWrongMapTypeReturnsError) { + auto map_expr = + SharedConstant("not a map"); // Pass a string instead of a map + auto key_expr = SharedConstant("d"); + EXPECT_THAT(EvaluateExpr(*MapGetExpr(map_expr, key_expr)), ReturnsError()); +} + +TEST_F(MapGetTest, GetWrongKeyTypeReturnsError) { + auto map_expr = SharedConstant(Map()); + auto key_expr = SharedConstant(false); + EXPECT_THAT(EvaluateExpr(*MapGetExpr(map_expr, key_expr)), ReturnsError()); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/unit/core/expressions/mirroring_semantics_test.cc b/Firestore/core/test/unit/core/expressions/mirroring_semantics_test.cc new file mode 100644 index 00000000000..02a66579b84 --- /dev/null +++ b/Firestore/core/test/unit/core/expressions/mirroring_semantics_test.cc @@ -0,0 +1,243 @@ +/* + * 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. + */ + +#include // For std::function +#include // For std::numeric_limits +#include // For std::shared_ptr +#include +#include // For std::move +#include + +#include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/core/expressions_eval.h" +#include "Firestore/core/src/model/field_path.h" // Correct include for FieldPath +#include "Firestore/core/src/util/string_format.h" // Include for StringFormat +#include "Firestore/core/test/unit/testutil/expression_test_util.h" +#include "Firestore/core/test/unit/testutil/testutil.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace core { + +using api::Expr; +using api::Field; // Correct expression type for field access +using api::FunctionExpr; +using model::FieldPath; // Use FieldPath model type +using testing::_; +using testutil::AddExpr; +using testutil::ArrayContainsAllExpr; +using testutil::ArrayContainsAnyExpr; +using testutil::ArrayContainsExpr; +using testutil::ArrayLengthExpr; +using testutil::ByteLengthExpr; +using testutil::CharLengthExpr; +using testutil::DivideExpr; +using testutil::EndsWithExpr; +using testutil::EqAnyExpr; +using testutil::EqExpr; +using testutil::EvaluateExpr; +using testutil::GteExpr; +using testutil::GtExpr; +using testutil::IsNanExpr; +using testutil::IsNotNanExpr; +using testutil::LikeExpr; +using testutil::LteExpr; +using testutil::LtExpr; +using testutil::ModExpr; +using testutil::MultiplyExpr; +using testutil::NeqExpr; +using testutil::NotEqAnyExpr; +using testutil::RegexContainsExpr; +using testutil::RegexMatchExpr; +using testutil::Returns; +using testutil::ReturnsError; +using testutil::ReturnsNull; +using testutil::ReverseExpr; +using testutil::SharedConstant; +using testutil::StartsWithExpr; +using testutil::StrConcatExpr; +using testutil::StrContainsExpr; +using testutil::SubtractExpr; +using testutil::TimestampToUnixMicrosExpr; +using testutil::TimestampToUnixMillisExpr; +using testutil::TimestampToUnixSecondsExpr; +using testutil::ToLowerExpr; +using testutil::ToUpperExpr; +using testutil::TrimExpr; +using testutil::UnixMicrosToTimestampExpr; +using testutil::UnixMillisToTimestampExpr; +using testutil::UnixSecondsToTimestampExpr; +using testutil::Value; +using util::StringFormat; // Using declaration for StringFormat + +// Base fixture for mirroring semantics tests +class MirroringSemanticsTest : public ::testing::Test { + protected: + // Define common input expressions + const std::shared_ptr NULL_INPUT = SharedConstant(nullptr); + // Error: Integer division by zero + const std::shared_ptr ERROR_INPUT = + DivideExpr({SharedConstant(1LL), SharedConstant(0LL)}); + // Unset: Field that doesn't exist in the default test document + const std::shared_ptr UNSET_INPUT = + std::make_shared("non-existent-field"); + // Valid: A simple valid input for binary tests + const std::shared_ptr VALID_INPUT = SharedConstant(42LL); +}; + +// --- Unary Function Tests --- + +TEST_F(MirroringSemanticsTest, UnaryFunctionInputMirroring) { + using UnaryBuilder = + std::function(std::shared_ptr)>; + + const std::vector unary_function_builders = { + [](auto v) { return IsNanExpr(v); }, + [](auto v) { return IsNotNanExpr(v); }, + [](auto v) { return ArrayLengthExpr(v); }, + [](auto v) { return ReverseExpr(v); }, + [](auto v) { return CharLengthExpr(v); }, + [](auto v) { return ByteLengthExpr(v); }, + [](auto v) { return ToLowerExpr(v); }, + [](auto v) { return ToUpperExpr(v); }, + [](auto v) { return TrimExpr(v); }, + [](auto v) { return UnixMicrosToTimestampExpr(v); }, + [](auto v) { return TimestampToUnixMicrosExpr(v); }, + [](auto v) { return UnixMillisToTimestampExpr(v); }, + [](auto v) { return TimestampToUnixMillisExpr(v); }, + [](auto v) { return UnixSecondsToTimestampExpr(v); }, + [](auto v) { return TimestampToUnixSecondsExpr(v); }}; + + struct TestCase { + std::shared_ptr input_expr; + testing::Matcher expected_matcher; + std::string description; + }; + + const std::vector test_cases = { + {NULL_INPUT, ReturnsNull(), "NULL"}, + {ERROR_INPUT, ReturnsError(), "ERROR"}, + {UNSET_INPUT, ReturnsError(), "UNSET"} // Unary ops expect resolved args + }; + + for (const auto& builder : unary_function_builders) { + // Get function name for better error messages (requires a dummy call) + std::string func_name = "unknown"; + auto dummy_expr = builder(SharedConstant("dummy")); + if (auto func_expr = std::dynamic_pointer_cast(dummy_expr)) { + func_name = func_expr->name(); + } + + for (const auto& test_case : test_cases) { + SCOPED_TRACE(StringFormat("Function: %s, Input: %s", func_name, + test_case.description)); + + std::shared_ptr expr_to_evaluate; + expr_to_evaluate = builder(test_case.input_expr); + EXPECT_THAT(EvaluateExpr(*expr_to_evaluate), test_case.expected_matcher); + } + } +} + +// --- Binary Function Tests --- + +TEST_F(MirroringSemanticsTest, BinaryFunctionInputMirroring) { + using BinaryBuilder = std::function( + std::shared_ptr, std::shared_ptr)>; + + // Note: Variadic functions like add, multiply, str_concat are tested + // with their base binary case here. + const std::vector binary_function_builders = { + // Arithmetic (Variadic, base is binary) + [](auto v1, auto v2) { return AddExpr({v1, v2}); }, + [](auto v1, auto v2) { return SubtractExpr({v1, v2}); }, + [](auto v1, auto v2) { return MultiplyExpr({v1, v2}); }, + [](auto v1, auto v2) { return DivideExpr({v1, v2}); }, + [](auto v1, auto v2) { return ModExpr({v1, v2}); }, + // Comparison + [](auto v1, auto v2) { return EqExpr({v1, v2}); }, + [](auto v1, auto v2) { return NeqExpr({v1, v2}); }, + [](auto v1, auto v2) { return LtExpr({v1, v2}); }, + [](auto v1, auto v2) { return LteExpr({v1, v2}); }, + [](auto v1, auto v2) { return GtExpr({v1, v2}); }, + [](auto v1, auto v2) { return GteExpr({v1, v2}); }, + // Array + [](auto v1, auto v2) { return ArrayContainsExpr({v1, v2}); }, + [](auto v1, auto v2) { return ArrayContainsAllExpr({v1, v2}); }, + [](auto v1, auto v2) { return ArrayContainsAnyExpr({v1, v2}); }, + [](auto v1, auto v2) { return EqAnyExpr(v1, v2); }, + [](auto v1, auto v2) { return NotEqAnyExpr(v1, v2); }, + // String + [](auto v1, auto v2) { return LikeExpr(v1, v2); }, + [](auto v1, auto v2) { return RegexContainsExpr(v1, v2); }, + [](auto v1, auto v2) { return RegexMatchExpr(v1, v2); }, + [](auto v1, auto v2) { return StrContainsExpr(v1, v2); }, + [](auto v1, auto v2) { return StartsWithExpr(v1, v2); }, + [](auto v1, auto v2) { return EndsWithExpr(v1, v2); }, + [](auto v1, auto v2) { return StrConcatExpr({v1, v2}); } + // TODO(b/351084804): mapGet is not implemented yet + }; + + struct BinaryTestCase { + std::shared_ptr left; + std::shared_ptr right; + testing::Matcher expected_matcher; + std::string description; + }; + + const std::vector test_cases = { + // Rule 1: NULL, NULL -> NULL + {NULL_INPUT, NULL_INPUT, ReturnsNull(), "NULL, NULL -> NULL"}, + // Rule 2: Error/Unset propagation + {NULL_INPUT, ERROR_INPUT, ReturnsError(), "NULL, ERROR -> ERROR"}, + {ERROR_INPUT, NULL_INPUT, ReturnsError(), "ERROR, NULL -> ERROR"}, + {NULL_INPUT, UNSET_INPUT, ReturnsError(), "NULL, UNSET -> ERROR"}, + {UNSET_INPUT, NULL_INPUT, ReturnsError(), "UNSET, NULL -> ERROR"}, + {ERROR_INPUT, ERROR_INPUT, ReturnsError(), "ERROR, ERROR -> ERROR"}, + {ERROR_INPUT, UNSET_INPUT, ReturnsError(), "ERROR, UNSET -> ERROR"}, + {UNSET_INPUT, ERROR_INPUT, ReturnsError(), "UNSET, ERROR -> ERROR"}, + {UNSET_INPUT, UNSET_INPUT, ReturnsError(), "UNSET, UNSET -> ERROR"}, + {VALID_INPUT, ERROR_INPUT, ReturnsError(), "VALID, ERROR -> ERROR"}, + {ERROR_INPUT, VALID_INPUT, ReturnsError(), "ERROR, VALID -> ERROR"}, + {VALID_INPUT, UNSET_INPUT, ReturnsError(), "VALID, UNSET -> ERROR"}, + {UNSET_INPUT, VALID_INPUT, ReturnsError(), "UNSET, VALID -> ERROR"}}; + + for (const auto& builder : binary_function_builders) { + // Get function name for better error messages (requires a dummy call) + std::string func_name = "unknown"; + auto dummy_expr = + builder(SharedConstant("dummy1"), SharedConstant("dummy2")); + if (auto func_expr = std::dynamic_pointer_cast(dummy_expr)) { + func_name = func_expr->name(); + } + + for (const auto& test_case : test_cases) { + SCOPED_TRACE(StringFormat("Function: %s, Case: %s", func_name, + test_case.description)); + + std::shared_ptr expr_to_evaluate; + expr_to_evaluate = builder(test_case.left, test_case.right); + + EXPECT_THAT(EvaluateExpr(*expr_to_evaluate), test_case.expected_matcher); + } + } +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/unit/core/expressions/string_test.cc b/Firestore/core/test/unit/core/expressions/string_test.cc new file mode 100644 index 00000000000..17ca21fd914 --- /dev/null +++ b/Firestore/core/test/unit/core/expressions/string_test.cc @@ -0,0 +1,814 @@ +/* + * 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. + */ + +#include +#include +#include +#include + +#include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/core/expressions_eval.h" +#include "Firestore/core/src/model/value_util.h" +#include "Firestore/core/test/unit/testutil/expression_test_util.h" +#include "Firestore/core/test/unit/testutil/testutil.h" // For Value, Bytes etc. +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace core { + +using api::Expr; +using api::FunctionExpr; +using testutil::ByteLengthExpr; +using testutil::Bytes; +using testutil::CharLengthExpr; +using testutil::EndsWithExpr; +using testutil::EvaluateExpr; +using testutil::Field; +using testutil::LikeExpr; +using testutil::Map; // Added Map helper +using testutil::RegexContainsExpr; +using testutil::RegexMatchExpr; +using testutil::Returns; +using testutil::ReturnsError; +using testutil::ReturnsNull; // If needed for string functions +using testutil::ReverseExpr; +using testutil::SharedConstant; +using testutil::StartsWithExpr; +using testutil::StrConcatExpr; +using testutil::StrContainsExpr; +using testutil::ToLowerExpr; +using testutil::ToUpperExpr; +using testutil::TrimExpr; +using testutil::Value; + +// Fixtures for different string functions +class ByteLengthTest : public ::testing::Test {}; +class CharLengthTest : public ::testing::Test {}; +class StrConcatTest : public ::testing::Test {}; +class EndsWithTest : public ::testing::Test {}; +class LikeTest : public ::testing::Test {}; +class RegexContainsTest : public ::testing::Test {}; +class RegexMatchTest : public ::testing::Test {}; +class StartsWithTest : public ::testing::Test {}; +class StrContainsTest : public ::testing::Test {}; +class ToLowerTest : public ::testing::Test {}; +class ToUpperTest : public ::testing::Test {}; +class TrimTest : public ::testing::Test {}; +class ReverseTest : public ::testing::Test {}; + +// --- ByteLength Tests --- +TEST_F(ByteLengthTest, EmptyString) { + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(""))), + Returns(Value(0LL))); +} + +TEST_F(ByteLengthTest, EmptyByte) { + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(Value(Bytes({}))))), + Returns(Value(0LL))); +} + +TEST_F(ByteLengthTest, NonStringOrBytesReturnsError) { + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(123LL))), + ReturnsError()); + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(true))), + ReturnsError()); + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr( + SharedConstant(Value(Bytes({0x01, 0x02, 0x03}))))), + Returns(Value(3LL))); +} + +TEST_F(ByteLengthTest, HighSurrogateOnly) { + // UTF-8 encoding of a lone high surrogate is invalid. + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant( + u"\xED\xA0\xBC"))), // U+D83C encoded incorrectly + ReturnsError()); // Expect error for invalid UTF-8 +} + +TEST_F(ByteLengthTest, LowSurrogateOnly) { + // UTF-8 encoding of a lone low surrogate is invalid. + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant( + u"\xED\xBD\x93"))), // U+DF53 encoded incorrectly + ReturnsError()); // Expect error for invalid UTF-8 +} + +TEST_F(ByteLengthTest, LowAndHighSurrogateSwapped) { + // Invalid sequence + EXPECT_THAT(EvaluateExpr( + *ByteLengthExpr(SharedConstant(u"\xED\xBD\x93\xED\xA0\xBC"))), + ReturnsError()); // Expect error for invalid UTF-8 +} + +TEST_F(ByteLengthTest, WrongContinuation) { + std::vector invalids{ + // 1. Invalid Start Byte (0xFF is not a valid start byte) + // UTF-8 start bytes must be in the patterns 0xxxxxxx, 110xxxxx, + // 1110xxxx, or 11110xxx. + // Bytes 0xC0, 0xC1, and 0xF5 to 0xFF are always invalid. + "Start \xFF End", + + // 2. Missing Continuation Byte(s) + // 0xE2 requires two continuation bytes (10xxxxxx), but only one is + // provided before 'E'. + "Incomplete \xE2\x82 End", // Needs one more byte after \x82 + + // 0xF0 requires three continuation bytes, but none are provided before + // 'E'. + "Incomplete \xF0 End", // Needs three bytes after \xF0 + + // 3. Invalid Continuation Byte + // 0xE2 indicates a 3-byte sequence, expecting two bytes starting with + // 10xxxxxx. + // However, the second byte is 0x20 (' '), which is ASCII and doesn't + // start with 10. + "Bad follow byte \xE2\x82\x20 End", // 0x20 is not 10xxxxxx + + // 4. Overlong Encoding (ASCII character '/' encoded using 2 bytes) + // The code point U+002F ('/') should be encoded as just 0x2F in UTF-8. + // Encoding it as 0xC0 0xAF is invalid (overlong). Note: 0xC0/0xC1 are + // always invalid starts. + // Let's use a different example: encoding U+00A9 (©) as 3 bytes when + // it should be 2. + // Correct: 0xC2 0xA9 + // Invalid Overlong Example (hypothetical, often caught by decoders): + // Trying to encode NULL (0x00) as 0xC0 0x80 + "Overlong NULL \xC0\x80", // Invalid way to encode U+0000 + "Overlong Slash \xC0\xAF", // Invalid way to encode U+002F ('/') + + // 5. Sequence Decodes to Invalid Code Point (Surrogate Half) + // UTF-8 must not encode code points in the surrogate range U+D800 to + // U+DFFF. + // The sequence 0xED 0xA0 0x80 decodes to U+D800, which is an invalid + // surrogate. + "Surrogate \xED\xA0\x80", // Decodes to U+D800 + + // 6. Sequence Decodes to Code Point > U+10FFFF + // Unicode code points only go up to U+10FFFF. + // This sequence (if interpreted loosely) might represent a value + // outside the valid range. + // For example, 0xF4 0x90 0x80 0x80 decodes to U+110000. + "Too high \xF4\x90\x80\x80" // Decodes to U+110000 + }; + + for (const auto& invalid : invalids) { + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(invalid.c_str()))), + ReturnsError()); + } +} + +TEST_F(ByteLengthTest, Ascii) { + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant("abc"))), + Returns(Value(3LL))); + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant("1234"))), + Returns(Value(4LL))); + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant("abc123!@"))), + Returns(Value(8LL))); +} + +TEST_F(ByteLengthTest, LargeString) { + std::string large_a(1500, 'a'); + std::string large_ab(3000, ' '); // Preallocate + for (int i = 0; i < 1500; ++i) { + large_ab[2 * i] = 'a'; + large_ab[2 * i + 1] = 'b'; + } + + // Use .c_str() for std::string variables + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(large_a.c_str()))), + Returns(Value(1500LL))); + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(large_ab.c_str()))), + Returns(Value(3000LL))); +} + +TEST_F(ByteLengthTest, TwoBytesPerCharacter) { + // UTF-8: é=2, ç=2, ñ=2, ö=2, ü=2 => 10 bytes + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant("éçñöü"))), + Returns(Value(10LL))); + EXPECT_THAT( + EvaluateExpr(*ByteLengthExpr(SharedConstant(Value(Bytes( + {0xc3, 0xa9, 0xc3, 0xa7, 0xc3, 0xb1, 0xc3, 0xb6, 0xc3, 0xbc}))))), + Returns(Value(10LL))); +} + +TEST_F(ByteLengthTest, ThreeBytesPerCharacter) { + // UTF-8: 你=3, 好=3, 世=3, 界=3 => 12 bytes + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant("你好世界"))), + Returns(Value(12LL))); + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant( + Value(Bytes({0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd, 0xe4, 0xb8, + 0x96, 0xe7, 0x95, 0x8c}))))), + Returns(Value(12LL))); +} + +TEST_F(ByteLengthTest, FourBytesPerCharacter) { + // UTF-8: 🀘=4, 🂡=4 => 8 bytes (U+1F018, U+1F0A1) + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant("🀘🂡"))), + Returns(Value(8LL))); + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant(Value( + Bytes({0xF0, 0x9F, 0x80, 0x98, 0xF0, 0x9F, 0x82, 0xA1}))))), + Returns(Value(8LL))); +} + +TEST_F(ByteLengthTest, MixOfDifferentEncodedLengths) { + // a=1, é=2, 好=3, 🂡=4 => 10 bytes + EXPECT_THAT(EvaluateExpr(*ByteLengthExpr(SharedConstant("aé好🂡"))), + Returns(Value(10LL))); + EXPECT_THAT( + EvaluateExpr(*ByteLengthExpr(SharedConstant(Value(Bytes( + {0x61, 0xc3, 0xa9, 0xe5, 0xa5, 0xbd, 0xF0, 0x9F, 0x82, 0xA1}))))), + Returns(Value(10LL))); +} + +// --- CharLength Tests --- +TEST_F(CharLengthTest, EmptyString) { + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant(""))), + Returns(Value(0LL))); +} + +TEST_F(CharLengthTest, BytesTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*CharLengthExpr( + SharedConstant(Value(Bytes({'a', 'b', 'c'}))))), + ReturnsError()); +} + +TEST_F(CharLengthTest, BaseCaseBmp) { + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("abc"))), + Returns(Value(3LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("1234"))), + Returns(Value(4LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("abc123!@"))), + Returns(Value(8LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("你好世界"))), + Returns(Value(4LL))); // Each char is 1 code point + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("cafétéria"))), + Returns(Value(9LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("абвгд"))), + Returns(Value(5LL))); + EXPECT_THAT( + EvaluateExpr(*CharLengthExpr(SharedConstant("¡Hola! ¿Cómo estás?"))), + Returns(Value(19LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("☺"))), // U+263A + Returns(Value(1LL))); +} + +TEST_F(CharLengthTest, Spaces) { + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant(""))), + Returns(Value(0LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant(" "))), + Returns(Value(1LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant(" "))), + Returns(Value(2LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("a b"))), + Returns(Value(3LL))); +} + +TEST_F(CharLengthTest, SpecialCharacters) { + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("\n"))), + Returns(Value(1LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("\t"))), + Returns(Value(1LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("\\"))), + Returns(Value(1LL))); +} + +TEST_F(CharLengthTest, BmpSmpMix) { + // Hello = 5, Smiling Face Emoji (U+1F60A) = 1 => 6 code points + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("Hello😊"))), + Returns(Value(6LL))); +} + +TEST_F(CharLengthTest, Smp) { + // Strawberry (U+1F353) = 1, Peach (U+1F351) = 1 => 2 code points + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant("🍓🍑"))), + Returns(Value(2LL))); +} + +// Note: C++ char_length likely counts code points correctly, unlike JS which +// might count UTF-16 code units for lone surrogates. Assuming C++ counts code +// points. +TEST_F(CharLengthTest, HighSurrogateOnly) { + // Lone high surrogate U+D83C is 1 code point (though invalid sequence) + EXPECT_THAT( + EvaluateExpr( + *CharLengthExpr(SharedConstant("\xED\xA0\xBC"))), // Invalid UTF-8 + ReturnsError()); // Expect error if implementation validates UTF-8 + // Returns(Value(1LL))); // Or returns 1 if it counts invalid points +} + +TEST_F(CharLengthTest, LowSurrogateOnly) { + // Lone low surrogate U+DF53 is 1 code point (though invalid sequence) + EXPECT_THAT( + EvaluateExpr( + *CharLengthExpr(SharedConstant("\xED\xBD\x93"))), // Invalid UTF-8 + ReturnsError()); // Expect error if implementation validates UTF-8 + // Returns(Value(1LL))); // Or returns 1 if it counts invalid points +} + +TEST_F(CharLengthTest, LowAndHighSurrogateSwapped) { + // Swapped surrogates are 2 code points (though invalid sequence) + EXPECT_THAT( + EvaluateExpr(*CharLengthExpr( + SharedConstant("\xED\xBD\x93\xED\xA0\xBC"))), // Invalid UTF-8 + ReturnsError()); // Expect error if implementation validates UTF-8 + // Returns(Value(2LL))); // Or returns 2 if it counts invalid points +} + +TEST_F(CharLengthTest, LargeString) { + std::string large_a(1500, 'a'); + std::string large_ab(3000, ' '); // Preallocate + for (int i = 0; i < 1500; ++i) { + large_ab[2 * i] = 'a'; + large_ab[2 * i + 1] = 'b'; + } + + // Use .c_str() for std::string variables + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant(large_a.c_str()))), + Returns(Value(1500LL))); + EXPECT_THAT(EvaluateExpr(*CharLengthExpr(SharedConstant(large_ab.c_str()))), + Returns(Value(3000LL))); +} + +// --- StrConcat Tests --- +TEST_F(StrConcatTest, MultipleStringChildrenReturnsCombination) { + EXPECT_THAT( + EvaluateExpr(*StrConcatExpr( + {SharedConstant("foo"), SharedConstant(" "), SharedConstant("bar")})), + Returns(Value("foo bar"))); +} + +TEST_F(StrConcatTest, MultipleNonStringChildrenReturnsError) { + EXPECT_THAT( + EvaluateExpr(*StrConcatExpr({SharedConstant("foo"), SharedConstant(42LL), + SharedConstant("bar")})), + ReturnsError()); +} + +TEST_F(StrConcatTest, MultipleCalls) { + auto func = StrConcatExpr( + {SharedConstant("foo"), SharedConstant(" "), SharedConstant("bar")}); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value("foo bar"))); + EXPECT_THAT(EvaluateExpr(*func), + Returns(Value("foo bar"))); // Ensure expression is reusable + EXPECT_THAT(EvaluateExpr(*func), Returns(Value("foo bar"))); +} + +TEST_F(StrConcatTest, LargeNumberOfInputs) { + std::vector> args; + std::string expected_result = ""; + args.reserve(500); + for (int i = 0; i < 500; ++i) { + args.push_back(SharedConstant("a")); + expected_result += "a"; + } + // Need to construct FunctionExpr with vector directly + auto func = StrConcatExpr(std::move(args)); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(expected_result))); +} + +TEST_F(StrConcatTest, LargeStrings) { + std::string a500(500, 'a'); + std::string b500(500, 'b'); + std::string c500(500, 'c'); + // Use .c_str() for std::string variables + auto func = + StrConcatExpr({SharedConstant(a500.c_str()), SharedConstant(b500.c_str()), + SharedConstant(c500.c_str())}); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(a500 + b500 + c500))); +} + +// --- EndsWith Tests --- +TEST_F(EndsWithTest, GetNonStringValueIsError) { + EXPECT_THAT(EvaluateExpr(*EndsWithExpr(SharedConstant(42LL), + SharedConstant("search"))), + ReturnsError()); +} + +TEST_F(EndsWithTest, GetNonStringSuffixIsError) { + EXPECT_THAT(EvaluateExpr(*EndsWithExpr(SharedConstant("search"), + SharedConstant(42LL))), + ReturnsError()); +} + +TEST_F(EndsWithTest, GetEmptyInputsReturnsTrue) { + EXPECT_THAT( + EvaluateExpr(*EndsWithExpr(SharedConstant(""), SharedConstant(""))), + Returns(Value(true))); +} + +TEST_F(EndsWithTest, GetEmptyValueReturnsFalse) { + EXPECT_THAT( + EvaluateExpr(*EndsWithExpr(SharedConstant(""), SharedConstant("v"))), + Returns(Value(false))); +} + +TEST_F(EndsWithTest, GetEmptySuffixReturnsTrue) { + EXPECT_THAT( + EvaluateExpr(*EndsWithExpr(SharedConstant("value"), SharedConstant(""))), + Returns(Value(true))); +} + +TEST_F(EndsWithTest, GetReturnsTrue) { + EXPECT_THAT(EvaluateExpr(*EndsWithExpr(SharedConstant("search"), + SharedConstant("rch"))), + Returns(Value(true))); +} + +TEST_F(EndsWithTest, GetReturnsFalse) { + EXPECT_THAT(EvaluateExpr(*EndsWithExpr(SharedConstant("search"), + SharedConstant("rcH"))), + Returns(Value(false))); // Case-sensitive +} + +TEST_F(EndsWithTest, GetLargeSuffixReturnsFalse) { + EXPECT_THAT(EvaluateExpr(*EndsWithExpr(SharedConstant("val"), + SharedConstant("a very long suffix"))), + Returns(Value(false))); +} + +// --- Like Tests --- +TEST_F(LikeTest, GetNonStringLikeIsError) { + EXPECT_THAT( + EvaluateExpr(*LikeExpr(SharedConstant(42LL), SharedConstant("search"))), + ReturnsError()); +} + +TEST_F(LikeTest, GetNonStringValueIsError) { + EXPECT_THAT( + EvaluateExpr(*LikeExpr(SharedConstant("ear"), SharedConstant(42LL))), + ReturnsError()); +} + +TEST_F(LikeTest, GetStaticLike) { + auto func = LikeExpr(SharedConstant("yummy food"), SharedConstant("%food")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(true))); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(true))); // Reusable + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(true))); +} + +TEST_F(LikeTest, GetEmptySearchString) { + auto func = LikeExpr(SharedConstant(""), SharedConstant("%hi%")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(false))); +} + +TEST_F(LikeTest, GetEmptyLike) { + auto func = LikeExpr(SharedConstant("yummy food"), SharedConstant("")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(false))); +} + +TEST_F(LikeTest, GetEscapedLike) { + auto func = + LikeExpr(SharedConstant("yummy food??"), SharedConstant("%food??")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(true))); +} + +TEST_F(LikeTest, GetDynamicLike) { + // Construct FunctionExpr directly for mixed types + auto func = std::make_shared( + "like", + std::vector>{ + SharedConstant("yummy food"), std::make_shared("regex")}); + EXPECT_THAT(EvaluateExpr(*func, testutil::Doc("coll/doc1", 1, + Map("regex", Value("yummy%")))), + Returns(Value(true))); + EXPECT_THAT(EvaluateExpr(*func, testutil::Doc("coll/doc2", 1, + Map("regex", Value("food%")))), + Returns(Value(false))); + EXPECT_THAT( + EvaluateExpr(*func, testutil::Doc("coll/doc3", 1, + Map("regex", Value("yummy_food")))), + Returns(Value(true))); +} + +// --- RegexContains Tests --- +TEST_F(RegexContainsTest, GetNonStringRegexIsError) { + EXPECT_THAT(EvaluateExpr(*RegexContainsExpr(SharedConstant(42LL), + SharedConstant("search"))), + ReturnsError()); +} + +TEST_F(RegexContainsTest, GetNonStringValueIsError) { + EXPECT_THAT(EvaluateExpr(*RegexContainsExpr(SharedConstant("ear"), + SharedConstant(42LL))), + ReturnsError()); +} + +TEST_F(RegexContainsTest, GetInvalidRegexIsError) { + // Assuming C++ uses RE2 or similar, backreferences might be + // invalid/unsupported + auto func = + RegexContainsExpr(SharedConstant("abcabc"), SharedConstant("(abc)\\1")); + EXPECT_THAT(EvaluateExpr(*func), ReturnsError()); +} + +TEST_F(RegexContainsTest, GetStaticRegex) { + auto func = + RegexContainsExpr(SharedConstant("yummy food"), SharedConstant(".*oo.*")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(true))); +} + +TEST_F(RegexContainsTest, GetSubStringLiteral) { + auto func = RegexContainsExpr(SharedConstant("yummy good food"), + SharedConstant("good")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(true))); +} + +TEST_F(RegexContainsTest, GetSubStringRegex) { + auto func = RegexContainsExpr(SharedConstant("yummy good food"), + SharedConstant("go*d")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(true))); +} + +TEST_F(RegexContainsTest, GetDynamicRegex) { + // Construct FunctionExpr directly for mixed types + auto func = std::make_shared( + "regex_contains", + std::vector>{ + SharedConstant("yummy food"), std::make_shared("regex")}); + EXPECT_THAT( + EvaluateExpr(*func, testutil::Doc("coll/doc1", 1, + Map("regex", Value("^yummy.*")))), + Returns(Value(true))); + EXPECT_THAT( + EvaluateExpr( + *func, testutil::Doc("coll/doc2", 1, Map("regex", Value("fooood$")))), + Returns(Value(false))); + EXPECT_THAT(EvaluateExpr(*func, testutil::Doc("coll/doc3", 1, + Map("regex", Value(".*")))), + Returns(Value(true))); +} + +// --- RegexMatch Tests --- +TEST_F(RegexMatchTest, GetNonStringRegexIsError) { + EXPECT_THAT(EvaluateExpr(*RegexMatchExpr(SharedConstant(42LL), + SharedConstant("search"))), + ReturnsError()); +} + +TEST_F(RegexMatchTest, GetNonStringValueIsError) { + EXPECT_THAT(EvaluateExpr( + *RegexMatchExpr(SharedConstant("ear"), SharedConstant(42LL))), + ReturnsError()); +} + +TEST_F(RegexMatchTest, GetInvalidRegexIsError) { + // Assuming C++ uses RE2 or similar, backreferences might be + // invalid/unsupported + auto func = + RegexMatchExpr(SharedConstant("abcabc"), SharedConstant("(abc)\\1")); + EXPECT_THAT(EvaluateExpr(*func), ReturnsError()); +} + +TEST_F(RegexMatchTest, GetStaticRegex) { + auto func = + RegexMatchExpr(SharedConstant("yummy food"), SharedConstant(".*oo.*")); + EXPECT_THAT(EvaluateExpr(*func), + Returns(Value(true))); // Matches because .* matches whole string +} + +TEST_F(RegexMatchTest, GetSubStringLiteral) { + // regex_match requires full match + auto func = + RegexMatchExpr(SharedConstant("yummy good food"), SharedConstant("good")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(false))); +} + +TEST_F(RegexMatchTest, GetSubStringRegex) { + // regex_match requires full match + auto func = + RegexMatchExpr(SharedConstant("yummy good food"), SharedConstant("go*d")); + EXPECT_THAT(EvaluateExpr(*func), Returns(Value(false))); +} + +TEST_F(RegexMatchTest, GetDynamicRegex) { + // Construct FunctionExpr directly for mixed types + auto func = std::make_shared( + "regex_match", + std::vector>{ + SharedConstant("yummy food"), std::make_shared("regex")}); + EXPECT_THAT( + EvaluateExpr(*func, testutil::Doc("coll/doc1", 1, + Map("regex", Value("^yummy.*")))), + Returns(Value(true))); // Matches full string + EXPECT_THAT( + EvaluateExpr( + *func, testutil::Doc("coll/doc2", 1, Map("regex", Value("fooood$")))), + Returns(Value(false))); + EXPECT_THAT(EvaluateExpr(*func, testutil::Doc("coll/doc3", 1, + Map("regex", Value(".*")))), + Returns(Value(true))); // Matches full string + EXPECT_THAT(EvaluateExpr(*func, testutil::Doc("coll/doc4", 1, + Map("regex", Value("yummy")))), + Returns(Value(false))); // Does not match full string +} + +// --- StartsWith Tests --- +TEST_F(StartsWithTest, GetNonStringValueIsError) { + EXPECT_THAT(EvaluateExpr(*StartsWithExpr(SharedConstant(42LL), + SharedConstant("search"))), + ReturnsError()); +} + +TEST_F(StartsWithTest, GetNonStringPrefixIsError) { + EXPECT_THAT(EvaluateExpr(*StartsWithExpr(SharedConstant("search"), + SharedConstant(42LL))), + ReturnsError()); +} + +TEST_F(StartsWithTest, GetEmptyInputsReturnsTrue) { + EXPECT_THAT( + EvaluateExpr(*StartsWithExpr(SharedConstant(""), SharedConstant(""))), + Returns(Value(true))); +} + +TEST_F(StartsWithTest, GetEmptyValueReturnsFalse) { + EXPECT_THAT( + EvaluateExpr(*StartsWithExpr(SharedConstant(""), SharedConstant("v"))), + Returns(Value(false))); +} + +TEST_F(StartsWithTest, GetEmptyPrefixReturnsTrue) { + EXPECT_THAT(EvaluateExpr( + *StartsWithExpr(SharedConstant("value"), SharedConstant(""))), + Returns(Value(true))); +} + +TEST_F(StartsWithTest, GetReturnsTrue) { + EXPECT_THAT(EvaluateExpr(*StartsWithExpr(SharedConstant("search"), + SharedConstant("sea"))), + Returns(Value(true))); +} + +TEST_F(StartsWithTest, GetReturnsFalse) { + EXPECT_THAT(EvaluateExpr(*StartsWithExpr(SharedConstant("search"), + SharedConstant("Sea"))), + Returns(Value(false))); // Case-sensitive +} + +TEST_F(StartsWithTest, GetLargePrefixReturnsFalse) { + EXPECT_THAT(EvaluateExpr(*StartsWithExpr( + SharedConstant("val"), SharedConstant("a very long prefix"))), + Returns(Value(false))); +} + +// --- StrContains Tests --- +TEST_F(StrContainsTest, ValueNonStringIsError) { + EXPECT_THAT(EvaluateExpr(*StrContainsExpr(SharedConstant(42LL), + SharedConstant("value"))), + ReturnsError()); +} + +TEST_F(StrContainsTest, SubStringNonStringIsError) { + EXPECT_THAT(EvaluateExpr(*StrContainsExpr(SharedConstant("search space"), + SharedConstant(42LL))), + ReturnsError()); +} + +TEST_F(StrContainsTest, ExecuteTrue) { + EXPECT_THAT(EvaluateExpr( + *StrContainsExpr(SharedConstant("abc"), SharedConstant("c"))), + Returns(Value(true))); + EXPECT_THAT(EvaluateExpr(*StrContainsExpr(SharedConstant("abc"), + SharedConstant("bc"))), + Returns(Value(true))); + EXPECT_THAT(EvaluateExpr(*StrContainsExpr(SharedConstant("abc"), + SharedConstant("abc"))), + Returns(Value(true))); + EXPECT_THAT( + EvaluateExpr(*StrContainsExpr(SharedConstant("abc"), SharedConstant(""))), + Returns(Value(true))); + EXPECT_THAT( + EvaluateExpr(*StrContainsExpr(SharedConstant(""), SharedConstant(""))), + Returns(Value(true))); + EXPECT_THAT(EvaluateExpr( + *StrContainsExpr(SharedConstant("☃☃☃"), SharedConstant("☃"))), + Returns(Value(true))); +} + +TEST_F(StrContainsTest, ExecuteFalse) { + EXPECT_THAT(EvaluateExpr(*StrContainsExpr(SharedConstant("abc"), + SharedConstant("abcd"))), + Returns(Value(false))); + EXPECT_THAT(EvaluateExpr( + *StrContainsExpr(SharedConstant("abc"), SharedConstant("d"))), + Returns(Value(false))); + EXPECT_THAT( + EvaluateExpr(*StrContainsExpr(SharedConstant(""), SharedConstant("a"))), + Returns(Value(false))); + EXPECT_THAT(EvaluateExpr(*StrContainsExpr(SharedConstant(""), + SharedConstant("abcde"))), + Returns(Value(false))); +} + +// --- ToLower Tests --- +TEST_F(ToLowerTest, Basic) { + EXPECT_THAT(EvaluateExpr(*ToLowerExpr(SharedConstant("FOO Bar"))), + Returns(Value("foo bar"))); +} + +TEST_F(ToLowerTest, Empty) { + EXPECT_THAT(EvaluateExpr(*ToLowerExpr(SharedConstant(""))), + Returns(Value(""))); +} + +TEST_F(ToLowerTest, NonString) { + EXPECT_THAT(EvaluateExpr(*ToLowerExpr(SharedConstant(123LL))), + ReturnsError()); +} + +TEST_F(ToLowerTest, Null) { + EXPECT_THAT(EvaluateExpr(*ToLowerExpr(SharedConstant(nullptr))), + ReturnsNull()); +} + +// --- ToUpper Tests --- +TEST_F(ToUpperTest, Basic) { + EXPECT_THAT(EvaluateExpr(*ToUpperExpr(SharedConstant("foo Bar"))), + Returns(Value("FOO BAR"))); +} + +TEST_F(ToUpperTest, Empty) { + EXPECT_THAT(EvaluateExpr(*ToUpperExpr(SharedConstant(""))), + Returns(Value(""))); +} + +TEST_F(ToUpperTest, NonString) { + EXPECT_THAT(EvaluateExpr(*ToUpperExpr(SharedConstant(123LL))), + ReturnsError()); +} + +TEST_F(ToUpperTest, Null) { + EXPECT_THAT(EvaluateExpr(*ToUpperExpr(SharedConstant(nullptr))), + ReturnsNull()); +} + +// --- Trim Tests --- +TEST_F(TrimTest, Basic) { + EXPECT_THAT(EvaluateExpr(*TrimExpr(SharedConstant(" foo bar "))), + Returns(Value("foo bar"))); +} + +TEST_F(TrimTest, NoTrimNeeded) { + EXPECT_THAT(EvaluateExpr(*TrimExpr(SharedConstant("foo bar"))), + Returns(Value("foo bar"))); +} + +TEST_F(TrimTest, OnlyWhitespace) { + EXPECT_THAT(EvaluateExpr(*TrimExpr(SharedConstant(" \t\n "))), + Returns(Value(""))); +} + +TEST_F(TrimTest, Empty) { + EXPECT_THAT(EvaluateExpr(*TrimExpr(SharedConstant(""))), Returns(Value(""))); +} + +TEST_F(TrimTest, NonString) { + EXPECT_THAT(EvaluateExpr(*TrimExpr(SharedConstant(123LL))), ReturnsError()); +} + +TEST_F(TrimTest, Null) { + EXPECT_THAT(EvaluateExpr(*TrimExpr(SharedConstant(nullptr))), ReturnsNull()); +} + +// --- Reverse Tests --- +TEST_F(ReverseTest, Basic) { + EXPECT_THAT(EvaluateExpr(*ReverseExpr(SharedConstant("abc"))), + Returns(Value("cba"))); +} + +TEST_F(ReverseTest, Empty) { + EXPECT_THAT(EvaluateExpr(*ReverseExpr(SharedConstant(""))), + Returns(Value(""))); +} + +TEST_F(ReverseTest, Unicode) { + EXPECT_THAT(EvaluateExpr(*ReverseExpr(SharedConstant("aé好🂡"))), + Returns(Value("🂡好éa"))); +} + +TEST_F(ReverseTest, NonString) { + EXPECT_THAT(EvaluateExpr(*ReverseExpr(SharedConstant(123LL))), + ReturnsError()); +} + +TEST_F(ReverseTest, Null) { + EXPECT_THAT(EvaluateExpr(*ReverseExpr(SharedConstant(nullptr))), + ReturnsNull()); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/unit/core/expressions/timestamp_test.cc b/Firestore/core/test/unit/core/expressions/timestamp_test.cc new file mode 100644 index 00000000000..b91dbbff7db --- /dev/null +++ b/Firestore/core/test/unit/core/expressions/timestamp_test.cc @@ -0,0 +1,638 @@ +/* + * 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. + */ + +#include +#include + +#include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/test/unit/testutil/expression_test_util.h" +#include "Firestore/core/test/unit/testutil/testutil.h" +#include "gmock/gmock.h" // Include gMock +#include "gtest/gtest.h" // Include gTest + +namespace firebase { +namespace firestore { +namespace core { + +using ::firebase::Timestamp; // Correct namespace +using testutil::EvaluateExpr; +using testutil::Returns; +// using testutil::ReturnsError; // Remove using declaration +using testutil::SharedConstant; +using testutil::SubtractExpr; // Needed for overflow tests +using testutil::UnixMicrosToTimestampExpr; +using testutil::Value; + +// Base fixture for common setup (if needed later) +class TimestampExpressionsTest : public ::testing::Test {}; + +// Fixture for UnixMicrosToTimestamp function tests +class UnixMicrosToTimestampTest : public TimestampExpressionsTest {}; + +TEST_F(UnixMicrosToTimestampTest, StringTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant("abc"))), + testutil::ReturnsError()); // Fully qualify +} + +TEST_F(UnixMicrosToTimestampTest, ZeroValueReturnsTimestampEpoch) { + EXPECT_THAT(EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant(0LL))), + Returns(Value(Timestamp(0, 0)))); +} + +TEST_F(UnixMicrosToTimestampTest, IntTypeReturnsTimestamp) { + EXPECT_THAT( + EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant(1000000LL))), + Returns(Value(Timestamp(1, 0)))); +} + +TEST_F(UnixMicrosToTimestampTest, LongTypeReturnsTimestamp) { + EXPECT_THAT( + EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant(9876543210LL))), + Returns(Value(Timestamp(9876, 543210000)))); +} + +TEST_F(UnixMicrosToTimestampTest, LongTypeNegativeReturnsTimestamp) { + // -10000 micros = -0.01 seconds = -10,000,000 nanos + google_firestore_v1_Value timestamp; + timestamp.which_value_type = google_firestore_v1_Value_timestamp_value_tag; + timestamp.timestamp_value.seconds = -1; + timestamp.timestamp_value.nanos = 990000000; + EXPECT_THAT( + EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant(-10000LL))), + Returns(nanopb::MakeMessage(timestamp))); +} + +TEST_F(UnixMicrosToTimestampTest, LongTypeNegativeOverflowReturnsError) { + // Min representable timestamp: seconds=-62135596800, nanos=0 + // Corresponds to micros: -62135596800 * 1,000,000 = -62135596800000000 + const int64_t min_micros = -62135596800000000LL; + + // Test the boundary value + EXPECT_THAT( + EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant(min_micros))), + Returns(Value(Timestamp(-62135596800LL, 0)))); + + // Test value just below the boundary (using subtraction) + auto below_min_expr = + SubtractExpr({SharedConstant(min_micros), SharedConstant(1LL)}); + EXPECT_THAT( + EvaluateExpr(*UnixMicrosToTimestampExpr(std::move(below_min_expr))), + testutil::ReturnsError()); // Fully qualify +} + +TEST_F(UnixMicrosToTimestampTest, LongTypePositiveOverflowReturnsError) { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + // Corresponds to micros: 253402300799 * 1,000,000 + 999999 + // = 253402300799000000 + 999999 = 253402300799999999 + const int64_t max_micros = 253402300799999999LL; + + // Test the boundary value + EXPECT_THAT( + EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant(max_micros))), + Returns(Value(Timestamp(253402300799LL, 999999000)))); // Nanos truncated + + // Test value just above the boundary + // max_micros + 1 = 253402300800000000 + EXPECT_THAT( + EvaluateExpr(*UnixMicrosToTimestampExpr(SharedConstant(max_micros + 1))), + testutil::ReturnsError()); // Fully qualify +} + +// Fixture for UnixMillisToTimestamp function tests +class UnixMillisToTimestampTest : public TimestampExpressionsTest {}; + +using testutil::UnixMillisToTimestampExpr; // Add using declaration for this + // fixture + +TEST_F(UnixMillisToTimestampTest, StringTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant("abc"))), + testutil::ReturnsError()); +} + +TEST_F(UnixMillisToTimestampTest, ZeroValueReturnsTimestampEpoch) { + EXPECT_THAT(EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(0LL))), + Returns(Value(Timestamp(0, 0)))); +} + +TEST_F(UnixMillisToTimestampTest, IntTypeReturnsTimestamp) { + EXPECT_THAT(EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(1000LL))), + Returns(Value(Timestamp(1, 0)))); +} + +TEST_F(UnixMillisToTimestampTest, LongTypeReturnsTimestamp) { + EXPECT_THAT( + EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(9876543210LL))), + Returns(Value(Timestamp(9876543, 210000000)))); +} + +TEST_F(UnixMillisToTimestampTest, LongTypeNegativeReturnsTimestamp) { + EXPECT_THAT( + EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(-10000LL))), + Returns(Value(Timestamp(-10, 0)))); +} + +TEST_F(UnixMillisToTimestampTest, LongTypeNegativeOverflowReturnsError) { + // Min representable timestamp: seconds=-62135596800, nanos=0 + // Corresponds to millis: -62135596800 * 1000 = -62135596800000 + const int64_t min_millis = -62135596800000LL; + + // Test the boundary value + EXPECT_THAT( + EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(min_millis))), + Returns(Value(Timestamp(-62135596800LL, 0)))); + + // Test value just below the boundary + EXPECT_THAT( + EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(min_millis - 1))), + testutil::ReturnsError()); +} + +TEST_F(UnixMillisToTimestampTest, LongTypePositiveOverflowReturnsError) { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + // Corresponds to millis: 253402300799 * 1000 + 999 = 253402300799999 + const int64_t max_millis = 253402300799999LL; + + // Test the boundary value + EXPECT_THAT( + EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(max_millis))), + Returns(Value(Timestamp(253402300799LL, 999000000)))); + + // Test value just above the boundary + EXPECT_THAT( + EvaluateExpr(*UnixMillisToTimestampExpr(SharedConstant(max_millis + 1))), + testutil::ReturnsError()); +} + +// Fixture for UnixSecondsToTimestamp function tests +class UnixSecondsToTimestampTest : public TimestampExpressionsTest {}; + +using testutil::UnixSecondsToTimestampExpr; // Add using declaration + +TEST_F(UnixSecondsToTimestampTest, StringTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*UnixSecondsToTimestampExpr(SharedConstant("abc"))), + testutil::ReturnsError()); +} + +TEST_F(UnixSecondsToTimestampTest, ZeroValueReturnsTimestampEpoch) { + EXPECT_THAT(EvaluateExpr(*UnixSecondsToTimestampExpr(SharedConstant(0LL))), + Returns(Value(Timestamp(0, 0)))); +} + +TEST_F(UnixSecondsToTimestampTest, IntTypeReturnsTimestamp) { + EXPECT_THAT(EvaluateExpr(*UnixSecondsToTimestampExpr(SharedConstant(1LL))), + Returns(Value(Timestamp(1, 0)))); +} + +TEST_F(UnixSecondsToTimestampTest, LongTypeReturnsTimestamp) { + EXPECT_THAT( + EvaluateExpr(*UnixSecondsToTimestampExpr(SharedConstant(9876543210LL))), + Returns(Value(Timestamp(9876543210LL, 0)))); +} + +TEST_F(UnixSecondsToTimestampTest, LongTypeNegativeReturnsTimestamp) { + EXPECT_THAT( + EvaluateExpr(*UnixSecondsToTimestampExpr(SharedConstant(-10000LL))), + Returns(Value(Timestamp(-10000LL, 0)))); +} + +TEST_F(UnixSecondsToTimestampTest, LongTypeNegativeOverflowReturnsError) { + // Min representable timestamp: seconds=-62135596800, nanos=0 + const int64_t min_seconds = -62135596800LL; + + // Test the boundary value + EXPECT_THAT( + EvaluateExpr(*UnixSecondsToTimestampExpr(SharedConstant(min_seconds))), + Returns(Value(Timestamp(min_seconds, 0)))); + + // Test value just below the boundary + EXPECT_THAT(EvaluateExpr( + *UnixSecondsToTimestampExpr(SharedConstant(min_seconds - 1))), + testutil::ReturnsError()); +} + +TEST_F(UnixSecondsToTimestampTest, LongTypePositiveOverflowReturnsError) { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + const int64_t max_seconds = 253402300799LL; + + // Test the boundary value (max seconds, zero nanos) + EXPECT_THAT( + EvaluateExpr(*UnixSecondsToTimestampExpr(SharedConstant(max_seconds))), + Returns(Value(Timestamp(max_seconds, 0)))); + + // Test value just above the boundary + EXPECT_THAT(EvaluateExpr( + *UnixSecondsToTimestampExpr(SharedConstant(max_seconds + 1))), + testutil::ReturnsError()); +} + +// Fixture for TimestampToUnixMicros function tests +class TimestampToUnixMicrosTest : public TimestampExpressionsTest {}; + +using testutil::TimestampToUnixMicrosExpr; // Add using declaration + +TEST_F(TimestampToUnixMicrosTest, NonTimestampTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(123LL))), + testutil::ReturnsError()); +} + +TEST_F(TimestampToUnixMicrosTest, TimestampReturnsMicros) { + Timestamp ts(347068800, 0); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(ts))), + Returns(Value(347068800000000LL))); +} + +TEST_F(TimestampToUnixMicrosTest, EpochTimestampReturnsMicros) { + Timestamp ts(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(ts))), + Returns(Value(0LL))); +} + +TEST_F(TimestampToUnixMicrosTest, CurrentTimestampReturnsMicros) { + // Note: C++ doesn't have a direct equivalent to JS Timestamp.now() easily + // accessible here. We'll test with a known value instead. + Timestamp now(1678886400, + 123456000); // Example: March 15, 2023 12:00:00.123456 UTC + int64_t expected_micros = 1678886400LL * 1000000LL + 123456LL; + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(now))), + Returns(Value(expected_micros))); +} + +TEST_F(TimestampToUnixMicrosTest, MaxTimestampReturnsMicros) { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + Timestamp max_ts(253402300799LL, 999999999); + // Expected micros: 253402300799 * 1,000,000 + 999999 = 253402300799999999 + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(max_ts))), + Returns(Value(253402300799999999LL))); +} + +TEST_F(TimestampToUnixMicrosTest, MinTimestampReturnsMicros) { + // Min representable timestamp: seconds=-62135596800, nanos=0 + Timestamp min_ts(-62135596800LL, 0); + // Expected micros: -62135596800 * 1,000,000 = -62135596800000000 + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(min_ts))), + Returns(Value(-62135596800000000LL))); +} + +TEST_F(TimestampToUnixMicrosTest, TimestampOverflowReturnsError) { + // Create a timestamp value slightly outside the representable int64_t range + // for microseconds. This requires constructing the Value proto directly. + // Using MAX_SAFE_INTEGER from JS isn't directly applicable, focus on int64 + // limits. A timestamp with seconds > INT64_MAX / 1,000,000 will overflow. + // Let's use a value known to be problematic. + // Note: The original JS test uses MAX_SAFE_INTEGER which is ~2^53. C++ + // int64_t is 2^63. The actual overflow check happens internally based on + // int64_t limits for micros. We expect the internal conversion to fail if the + // result exceeds int64 limits. Let's test with a timestamp whose microsecond + // equivalent *would* overflow int64_t. Example: seconds slightly larger than + // INT64_MAX / 1,000,000 + google_firestore_v1_Value timestamp_proto; + timestamp_proto.timestamp_value.seconds = + 9223372036855LL; // > INT64_MAX / 1M + timestamp_proto.timestamp_value.nanos = 0; + timestamp_proto.which_value_type = + google_firestore_v1_Value_timestamp_value_tag; + + EXPECT_THAT( + EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(timestamp_proto))), + testutil::ReturnsError()); +} + +TEST_F(TimestampToUnixMicrosTest, TimestampTruncatesToMicros) { + // Timestamp: seconds=-1, nanos=999999999 + // Micros: -1 * 1,000,000 + 999999 = -1 + Timestamp ts(-1, 999999999); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMicrosExpr(SharedConstant(ts))), + Returns(Value(-1LL))); +} + +// Fixture for TimestampToUnixMillis function tests +class TimestampToUnixMillisTest : public TimestampExpressionsTest {}; + +using testutil::TimestampToUnixMillisExpr; // Add using declaration + +TEST_F(TimestampToUnixMillisTest, NonTimestampTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(123LL))), + testutil::ReturnsError()); +} + +TEST_F(TimestampToUnixMillisTest, TimestampReturnsMillis) { + Timestamp ts(347068800, 0); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(ts))), + Returns(Value(347068800000LL))); +} + +TEST_F(TimestampToUnixMillisTest, EpochTimestampReturnsMillis) { + Timestamp ts(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(ts))), + Returns(Value(0LL))); +} + +TEST_F(TimestampToUnixMillisTest, CurrentTimestampReturnsMillis) { + // Test with a known value + Timestamp now(1678886400, + 123000000); // Example: March 15, 2023 12:00:00.123 UTC + int64_t expected_millis = 1678886400LL * 1000LL + 123LL; + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(now))), + Returns(Value(expected_millis))); +} + +TEST_F(TimestampToUnixMillisTest, MaxTimestampReturnsMillis) { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + // Millis calculation truncates nanos part: 999999999 / 1,000,000 = 999 + Timestamp max_ts(253402300799LL, + 999000000); // Use nanos divisible by 1M for clarity + // Expected millis: 253402300799 * 1000 + 999 = 253402300799999 + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(max_ts))), + Returns(Value(253402300799999LL))); +} + +TEST_F(TimestampToUnixMillisTest, MinTimestampReturnsMillis) { + // Min representable timestamp: seconds=-62135596800, nanos=0 + Timestamp min_ts(-62135596800LL, 0); + // Expected millis: -62135596800 * 1000 = -62135596800000 + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(min_ts))), + Returns(Value(-62135596800000LL))); +} + +TEST_F(TimestampToUnixMillisTest, TimestampTruncatesToMillis) { + // Timestamp: seconds=-1, nanos=999999999 + // Millis: -1 * 1000 + 999 = -1 + Timestamp ts(-1, 999999999); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(ts))), + Returns(Value(-1LL))); +} + +TEST_F(TimestampToUnixMillisTest, TimestampOverflowReturnsError) { + // Test with a timestamp whose millisecond equivalent would overflow int64_t. + // Example: seconds slightly larger than INT64_MAX / 1000 + google_firestore_v1_Value timestamp_proto; + // INT64_MAX is approx 9.22e18. INT64_MAX / 1000 is approx 9.22e15. + timestamp_proto.timestamp_value.seconds = + 9223372036854776LL; // > INT64_MAX / 1000 + timestamp_proto.timestamp_value.nanos = 0; + timestamp_proto.which_value_type = + google_firestore_v1_Value_timestamp_value_tag; + + EXPECT_THAT( + EvaluateExpr(*TimestampToUnixMillisExpr(SharedConstant(timestamp_proto))), + testutil::ReturnsError()); +} + +// Fixture for TimestampToUnixSeconds function tests +class TimestampToUnixSecondsTest : public TimestampExpressionsTest {}; + +using testutil::TimestampToUnixSecondsExpr; // Add using declaration + +TEST_F(TimestampToUnixSecondsTest, NonTimestampTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr(SharedConstant(123LL))), + testutil::ReturnsError()); +} + +TEST_F(TimestampToUnixSecondsTest, TimestampReturnsSeconds) { + Timestamp ts(347068800, 0); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr(SharedConstant(ts))), + Returns(Value(347068800LL))); +} + +TEST_F(TimestampToUnixSecondsTest, EpochTimestampReturnsSeconds) { + Timestamp ts(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr(SharedConstant(ts))), + Returns(Value(0LL))); +} + +TEST_F(TimestampToUnixSecondsTest, CurrentTimestampReturnsSeconds) { + // Test with a known value + Timestamp now(1678886400, + 123456789); // Example: March 15, 2023 12:00:00.123456789 UTC + int64_t expected_seconds = 1678886400LL; // Truncates nanos + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr(SharedConstant(now))), + Returns(Value(expected_seconds))); +} + +TEST_F(TimestampToUnixSecondsTest, MaxTimestampReturnsSeconds) { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + Timestamp max_ts(253402300799LL, 999999999); + // Expected seconds: 253402300799 + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr(SharedConstant(max_ts))), + Returns(Value(253402300799LL))); +} + +TEST_F(TimestampToUnixSecondsTest, MinTimestampReturnsSeconds) { + // Min representable timestamp: seconds=-62135596800, nanos=0 + Timestamp min_ts(-62135596800LL, 0); + // Expected seconds: -62135596800 + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr(SharedConstant(min_ts))), + Returns(Value(-62135596800LL))); +} + +TEST_F(TimestampToUnixSecondsTest, TimestampTruncatesToSeconds) { + // Timestamp: seconds=-1, nanos=999999999 + // Seconds: -1 + Timestamp ts(-1, 999999999); + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr(SharedConstant(ts))), + Returns(Value(-1LL))); +} + +TEST_F(TimestampToUnixSecondsTest, TimestampOverflowReturnsError) { + google_firestore_v1_Value timestamp_proto_max; + timestamp_proto_max.timestamp_value.seconds = + std::numeric_limits::max(); + timestamp_proto_max.timestamp_value.nanos = 999999999; + timestamp_proto_max.which_value_type = + google_firestore_v1_Value_timestamp_value_tag; + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr( + SharedConstant(timestamp_proto_max))), + testutil::ReturnsError()); + + google_firestore_v1_Value timestamp_proto_min; + timestamp_proto_min.timestamp_value.seconds = + std::numeric_limits::min(); + timestamp_proto_min.timestamp_value.nanos = 0; + timestamp_proto_min.which_value_type = + google_firestore_v1_Value_timestamp_value_tag; + EXPECT_THAT(EvaluateExpr(*TimestampToUnixSecondsExpr( + SharedConstant(timestamp_proto_min))), + testutil::ReturnsError()); +} + +// Fixture for TimestampAdd function tests +class TimestampAddTest : public TimestampExpressionsTest {}; + +using testutil::ReturnsNull; // Add using declaration for null checks +using testutil::TimestampAddExpr; // Add using declaration + +TEST_F(TimestampAddTest, TimestampAddStringTypeReturnsError) { + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant("abc"), + SharedConstant("second"), + SharedConstant(1LL))), + testutil::ReturnsError()); +} + +TEST_F(TimestampAddTest, TimestampAddZeroValueReturnsTimestampEpoch) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("second"), + SharedConstant(0LL))), + Returns(Value(epoch))); +} + +TEST_F(TimestampAddTest, TimestampAddIntTypeReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("second"), + SharedConstant(1LL))), + Returns(Value(Timestamp(1, 0)))); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("second"), + SharedConstant(9876543210LL))), + Returns(Value(Timestamp(9876543210LL, 0)))); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeNegativeReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("second"), + SharedConstant(-10000LL))), + Returns(Value(Timestamp(-10000LL, 0)))); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeNegativeOverflowReturnsError) { + Timestamp min_ts(-62135596800LL, 0); + // Test adding 0 (boundary) + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(min_ts), + SharedConstant("second"), + SharedConstant(0LL))), + Returns(Value(min_ts))); + // Test adding -1 (overflow) + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(min_ts), + SharedConstant("second"), + SharedConstant(-1LL))), + testutil::ReturnsError()); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypePositiveOverflowReturnsError) { + Timestamp max_ts(253402300799LL, 999999000); + // Test adding 0 (boundary) + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr( + SharedConstant(max_ts), + SharedConstant("microsecond"), // Smallest unit + SharedConstant(0LL))), + Returns(Value(max_ts))); // Expect the same max timestamp + + // Test adding 1 microsecond (should overflow) + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(max_ts), + SharedConstant("microsecond"), + SharedConstant(1LL))), + testutil::ReturnsError()); + + // Test adding 1 second to a timestamp close to max + Timestamp near_max_ts(253402300799LL, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(near_max_ts), + SharedConstant("second"), + SharedConstant(0LL))), + Returns(Value(near_max_ts))); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(near_max_ts), + SharedConstant("second"), + SharedConstant(1LL))), + testutil::ReturnsError()); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeMinuteReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("minute"), + SharedConstant(1LL))), + Returns(Value(Timestamp(60, 0)))); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeHourReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT( + EvaluateExpr(*TimestampAddExpr( + SharedConstant(epoch), SharedConstant("hour"), SharedConstant(1LL))), + Returns(Value(Timestamp(3600, 0)))); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeDayReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT( + EvaluateExpr(*TimestampAddExpr( + SharedConstant(epoch), SharedConstant("day"), SharedConstant(1LL))), + Returns(Value(Timestamp(86400, 0)))); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeMillisecondReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("millisecond"), + SharedConstant(1LL))), + Returns(Value(Timestamp(0, 1000000)))); +} + +TEST_F(TimestampAddTest, TimestampAddLongTypeMicrosecondReturnsTimestamp) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("microsecond"), + SharedConstant(1LL))), + Returns(Value(Timestamp(0, 1000)))); +} + +TEST_F(TimestampAddTest, TimestampAddInvalidTimeUnitReturnsError) { + Timestamp epoch(0, 0); + EXPECT_THAT( + EvaluateExpr(*TimestampAddExpr( + SharedConstant(epoch), SharedConstant("abc"), SharedConstant(1LL))), + testutil::ReturnsError()); +} + +TEST_F(TimestampAddTest, TimestampAddInvalidAmountReturnsError) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("second"), + SharedConstant("abc"))), + testutil::ReturnsError()); +} + +TEST_F(TimestampAddTest, TimestampAddNullAmountReturnsNull) { + Timestamp epoch(0, 0); + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(epoch), + SharedConstant("second"), + SharedConstant(nullptr))), + ReturnsNull()); +} + +TEST_F(TimestampAddTest, TimestampAddNullTimeUnitReturnsNull) { + Timestamp epoch(0, 0); + EXPECT_THAT( + EvaluateExpr(*TimestampAddExpr( + SharedConstant(epoch), SharedConstant(nullptr), SharedConstant(1LL))), + ReturnsNull()); +} + +TEST_F(TimestampAddTest, TimestampAddNullTimestampReturnsNull) { + EXPECT_THAT(EvaluateExpr(*TimestampAddExpr(SharedConstant(nullptr), + SharedConstant("second"), + SharedConstant(1LL))), + ReturnsNull()); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/unit/testutil/expression_test_util.h b/Firestore/core/test/unit/testutil/expression_test_util.h index 387e0a348e4..459b43ee6d3 100644 --- a/Firestore/core/test/unit/testutil/expression_test_util.h +++ b/Firestore/core/test/unit/testutil/expression_test_util.h @@ -156,6 +156,59 @@ inline std::shared_ptr ModExpr( "mod", std::vector>(params)); } +// --- Timestamp Expression Helpers --- + +inline std::shared_ptr UnixMicrosToTimestampExpr( + std::shared_ptr operand) { + return std::make_shared( + "unix_micros_to_timestamp", + std::vector>{std::move(operand)}); +} + +inline std::shared_ptr UnixMillisToTimestampExpr( + std::shared_ptr operand) { + return std::make_shared( + "unix_millis_to_timestamp", + std::vector>{std::move(operand)}); +} + +inline std::shared_ptr UnixSecondsToTimestampExpr( + std::shared_ptr operand) { + return std::make_shared( + "unix_seconds_to_timestamp", + std::vector>{std::move(operand)}); +} + +inline std::shared_ptr TimestampToUnixMicrosExpr( + std::shared_ptr operand) { + return std::make_shared( + "timestamp_to_unix_micros", + std::vector>{std::move(operand)}); +} + +inline std::shared_ptr TimestampToUnixMillisExpr( + std::shared_ptr operand) { + return std::make_shared( + "timestamp_to_unix_millis", + std::vector>{std::move(operand)}); +} + +inline std::shared_ptr TimestampToUnixSecondsExpr( + std::shared_ptr operand) { + return std::make_shared( + "timestamp_to_unix_seconds", + std::vector>{std::move(operand)}); +} + +inline std::shared_ptr TimestampAddExpr(std::shared_ptr timestamp, + std::shared_ptr unit, + std::shared_ptr amount) { + return std::make_shared( + "timestamp_add", + std::vector>{std::move(timestamp), std::move(unit), + std::move(amount)}); +} + // --- Comparison Expression Helpers --- inline std::shared_ptr EqExpr( @@ -225,8 +278,8 @@ inline std::shared_ptr ArrayLengthExpr(std::shared_ptr array_expr) { "array_length", std::vector>{array_expr}); } -// TODO(wuandy): Add ArrayConcatExpr, ArrayReverseExpr, ArrayElementExpr when -// needed. +// TODO(b/351084804): Add ArrayConcatExpr, ArrayReverseExpr, ArrayElementExpr +// when needed. // --- Logical Expression Helpers --- @@ -588,6 +641,88 @@ inline testing::Matcher Returns( new ReturnsMatcherImpl(std::move(expected_value))); } +// --- String Expression Helpers --- + +inline std::shared_ptr CharLengthExpr(std::shared_ptr operand) { + return std::make_shared( + "char_length", std::vector>{std::move(operand)}); +} + +inline std::shared_ptr ByteLengthExpr(std::shared_ptr operand) { + return std::make_shared( + "byte_length", std::vector>{std::move(operand)}); +} + +inline std::shared_ptr ToLowerExpr(std::shared_ptr operand) { + return std::make_shared( + "to_lower", std::vector>{std::move(operand)}); +} + +inline std::shared_ptr ToUpperExpr(std::shared_ptr operand) { + return std::make_shared( + "to_upper", std::vector>{std::move(operand)}); +} + +inline std::shared_ptr ReverseExpr(std::shared_ptr operand) { + return std::make_shared( + "reverse", std::vector>{std::move(operand)}); +} + +inline std::shared_ptr TrimExpr(std::shared_ptr operand) { + return std::make_shared( + "trim", std::vector>{std::move(operand)}); +} + +inline std::shared_ptr LikeExpr(std::shared_ptr value, + std::shared_ptr pattern) { + return std::make_shared( + "like", + std::vector>{std::move(value), std::move(pattern)}); +} + +inline std::shared_ptr RegexContainsExpr(std::shared_ptr value, + std::shared_ptr regex) { + return std::make_shared( + "regex_contains", + std::vector>{std::move(value), std::move(regex)}); +} + +inline std::shared_ptr RegexMatchExpr(std::shared_ptr value, + std::shared_ptr regex) { + return std::make_shared( + "regex_match", + std::vector>{std::move(value), std::move(regex)}); +} + +inline std::shared_ptr StrContainsExpr(std::shared_ptr value, + std::shared_ptr search) { + return std::make_shared( + "str_contains", + std::vector>{std::move(value), std::move(search)}); +} + +inline std::shared_ptr StartsWithExpr(std::shared_ptr value, + std::shared_ptr prefix) { + return std::make_shared( + "starts_with", + std::vector>{std::move(value), std::move(prefix)}); +} + +inline std::shared_ptr EndsWithExpr(std::shared_ptr value, + std::shared_ptr suffix) { + return std::make_shared( + "ends_with", + std::vector>{std::move(value), std::move(suffix)}); +} + +inline std::shared_ptr StrConcatExpr( + std::vector> operands) { + return std::make_shared("str_concat", std::move(operands)); +} + +// --- Vector Expression Helpers --- +// TODO(b/351084804): Add vector helpers when supported. + } // namespace testutil } // namespace firestore } // namespace firebase