diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index a0a881ed7e2..5a2266b6bba 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -89,6 +89,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, abseil_version = '0.20200225.0' s.dependency 'abseil/algorithm', abseil_version s.dependency 'abseil/base', abseil_version + s.dependency 'abseil/container/flat_hash_map', abseil_version s.dependency 'abseil/memory', abseil_version s.dependency 'abseil/meta', abseil_version s.dependency 'abseil/strings/strings', abseil_version diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index e4d54e70a49..27b24829770 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -745,6 +745,7 @@ 91AEFFEE35FBE15FEC42A1F4 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; }; 920B6ABF76FDB3547F1CCD84 /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; 925BE64990449E93242A00A2 /* memory_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 74FBEFA4FE4B12C435011763 /* memory_mutation_queue_test.cc */; }; + 92C3733EF8D9E4596B43010E /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 050C1BC58C2A6C470C9E0F35 /* value_util_test.cc */; }; 92D7081085679497DC112EDB /* persistence_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9113B6F513D0473AEABBAF1F /* persistence_testing.cc */; }; 92EFF0CC2993B43CBC7A61FF /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; }; 9382BE7190E7750EE7CCCE7C /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; }; @@ -956,6 +957,7 @@ C21B3A1CCB3AD42E57EA14FC /* Pods_Firestore_Tests_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 759E964B6A03E6775C992710 /* Pods_Firestore_Tests_macOS.framework */; }; C23552A6D9FB0557962870C2 /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; }; C25F321AC9BF8D1CFC8543AF /* reference_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 132E32997D781B896672D30A /* reference_set_test.cc */; }; + C343CA82954A852D899619B6 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 050C1BC58C2A6C470C9E0F35 /* value_util_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 */; }; @@ -1064,6 +1066,7 @@ DE17D9D0C486E1817E9E11F9 /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; }; DE435F33CE563E238868D318 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; }; DE50F1D39D34F867BC750957 /* grpc_stream_tester.cc in Sources */ = {isa = PBXBuildFile; fileRef = 87553338E42B8ECA05BA987E /* grpc_stream_tester.cc */; }; + DE7B9A2DE0427C4F4221B4EA /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 050C1BC58C2A6C470C9E0F35 /* value_util_test.cc */; }; DE8C47B973526A20D88F785D /* token_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7DF2023A3EF00BA84F0 /* token_test.cc */; }; DEF4BF5FAA83C37100408F89 /* bundle_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 79EAA9F7B1B9592B5F053923 /* bundle_spec_test.json */; }; DF27137C8EA7D095D68851B4 /* field_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E8551D6C6FB0B1BACE9E5BAD /* field_filter_test.cc */; }; @@ -1121,6 +1124,7 @@ EC80A217F3D66EB0272B36B0 /* FSTLevelDBSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */; }; 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 */; }; + ED3DF8288703E89B701AB163 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 050C1BC58C2A6C470C9E0F35 /* value_util_test.cc */; }; ED420D8F49DA5C41EEF93913 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; }; ED4E2AC80CAF2A8FDDAC3DEE /* field_mask_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */; }; ED9DF1EB20025227B38736EC /* message_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CE37875365497FFA8687B745 /* message_test.cc */; }; @@ -1139,6 +1143,7 @@ F3261CBFC169DB375A0D9492 /* FSTMockDatastore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */; }; F386012CAB7F0C0A5564016A /* credentials_provider_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D9342023966E000A432D /* credentials_provider_test.cc */; }; F3F09BC931A717CEFF4E14B9 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; + F3FC69D44288F28DA3A1B7A6 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 050C1BC58C2A6C470C9E0F35 /* value_util_test.cc */; }; F481368DB694B3B4D0C8E4A2 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; }; F4F00BF4E87D7F0F0F8831DB /* FSTEventAccumulator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */; }; F4FAC5A7D40A0A9A3EA77998 /* FSTLevelDBSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */; }; @@ -1148,6 +1153,7 @@ F6079BFC9460B190DA85C2E6 /* pretty_printing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB323F9553050F4F6490F9FF /* pretty_printing_test.cc */; }; F609600E9A88A4D44FD1FCEB /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; }; F660788F69B4336AC6CD2720 /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; }; + F685114BCCE28435948110B8 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 050C1BC58C2A6C470C9E0F35 /* value_util_test.cc */; }; F696B7467E80E370FDB3EAA7 /* remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7EB299CF85034F09CFD6F3FD /* remote_document_cache_test.cc */; }; F72DF72447EA7AB9D100816A /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; F731A0CCD0220B370BC1BE8B /* BasicCompileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* BasicCompileTests.swift */; }; @@ -1243,6 +1249,7 @@ 014C60628830D95031574D15 /* random_access_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = random_access_queue_test.cc; sourceTree = ""; }; 045D39C4A7D52AF58264240F /* remote_document_cache_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = remote_document_cache_test.h; sourceTree = ""; }; 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = ordered_code_benchmark.cc; sourceTree = ""; }; + 050C1BC58C2A6C470C9E0F35 /* value_util_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = value_util_test.cc; sourceTree = ""; }; 0840319686A223CC4AD3FAB1 /* leveldb_remote_document_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_remote_document_cache_test.cc; sourceTree = ""; }; 0EE5300F8233D14025EF0456 /* string_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_test.mm; sourceTree = ""; }; 11984BA0A99D7A7ABA5B0D90 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = ""; }; @@ -2233,6 +2240,7 @@ B686F2B02024FFD70028D6BE /* resource_path_test.cc */, ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */, 33607A3AE91548BD219EC9C6 /* transform_operation_test.cc */, + 050C1BC58C2A6C470C9E0F35 /* value_util_test.cc */, ); path = model; sourceTree = ""; @@ -3451,6 +3459,7 @@ 5D51D8B166D24EFEF73D85A2 /* transform_operation_test.cc in Sources */, 5F19F66D8B01BA2B97579017 /* tree_sorted_map_test.cc in Sources */, 16F52ECC6FA8A0587CD779EB /* user_test.cc in Sources */, + F3FC69D44288F28DA3A1B7A6 /* value_util_test.cc in Sources */, A9A9994FB8042838671E8506 /* view_snapshot_test.cc in Sources */, AD8F0393B276B2934D251AAC /* view_test.cc in Sources */, 2D65D31D71A75B046C47B0EB /* view_testing.cc in Sources */, @@ -3629,6 +3638,7 @@ 5EE21E86159A1911E9503BC1 /* transform_operation_test.cc in Sources */, 627253FDEC6BB5549FE77F4E /* tree_sorted_map_test.cc in Sources */, 596C782EFB68131380F8EEF8 /* user_test.cc in Sources */, + C343CA82954A852D899619B6 /* value_util_test.cc in Sources */, 1B4794A51F4266556CD0976B /* view_snapshot_test.cc in Sources */, C1F196EC5A7C112D2F7C7724 /* view_test.cc in Sources */, 3451DC1712D7BF5D288339A2 /* view_testing.cc in Sources */, @@ -3819,6 +3829,7 @@ 15BF63DFF3A7E9A5376C4233 /* transform_operation_test.cc in Sources */, 54B91B921DA757C64CC67C90 /* tree_sorted_map_test.cc in Sources */, 8D5A9E6E43B6F47431841FE2 /* user_test.cc in Sources */, + ED3DF8288703E89B701AB163 /* value_util_test.cc in Sources */, 3A307F319553A977258BB3D6 /* view_snapshot_test.cc in Sources */, 89C71AEAA5316836BB1D5A01 /* view_test.cc in Sources */, 06BCEB9C65DFAA142F3D3F0B /* view_testing.cc in Sources */, @@ -4009,6 +4020,7 @@ 44EAF3E6EAC0CC4EB2147D16 /* transform_operation_test.cc in Sources */, 3D22F56C0DE7C7256C75DC06 /* tree_sorted_map_test.cc in Sources */, 918E3D35942CE493690C45CE /* user_test.cc in Sources */, + DE7B9A2DE0427C4F4221B4EA /* value_util_test.cc in Sources */, 81A6B241E63540900F205817 /* view_snapshot_test.cc in Sources */, A5B8C273593D1BB6E8AE4CBA /* view_test.cc in Sources */, 7F771EB980D9CFAAB4764233 /* view_testing.cc in Sources */, @@ -4197,6 +4209,7 @@ D3CB03747E34D7C0365638F1 /* transform_operation_test.cc in Sources */, 549CCA5120A36DBC00BCEB75 /* tree_sorted_map_test.cc in Sources */, ABC1D7DE2023A05300BA84F0 /* user_test.cc in Sources */, + 92C3733EF8D9E4596B43010E /* value_util_test.cc in Sources */, 340987A77D72C80A3E0FDADF /* view_snapshot_test.cc in Sources */, 17473086EBACB98CDC3CC65C /* view_test.cc in Sources */, DDDE74C752E65DE7D39A7166 /* view_testing.cc in Sources */, @@ -4406,6 +4419,7 @@ 60186935E36CF79E48A0B293 /* transform_operation_test.cc in Sources */, 5DA343D28AE05B0B2FE9FFB3 /* tree_sorted_map_test.cc in Sources */, D43F7601F3F3DE3125346D42 /* user_test.cc in Sources */, + F685114BCCE28435948110B8 /* value_util_test.cc in Sources */, 59E89A97A476790E89AFC7E7 /* view_snapshot_test.cc in Sources */, B63D84B2980C7DEE7E6E4708 /* view_test.cc in Sources */, 48D1B38B93D34F1B82320577 /* view_testing.cc in Sources */, diff --git a/Firestore/core/CMakeLists.txt b/Firestore/core/CMakeLists.txt index 1c7b5590d36..3f11d0aeed0 100644 --- a/Firestore/core/CMakeLists.txt +++ b/Firestore/core/CMakeLists.txt @@ -131,11 +131,12 @@ target_compile_definitions( target_link_libraries( firestore_util PUBLIC - absl_base - absl_memory - absl_meta - absl_optional - absl_strings + absl::base + absl::flat_hash_map + absl::memory + absl::meta + absl::optional + absl::strings ) if(HAVE_OPENSSL_RAND_H) @@ -232,11 +233,12 @@ target_include_directories( target_link_libraries( firestore_core PUBLIC LevelDB::LevelDB - absl_base - absl_memory - absl_meta - absl_optional - absl_strings + absl::base + absl::flat_hash_map + absl::memory + absl::meta + absl::optional + absl::strings firestore_nanopb firestore_protos_nanopb firestore_util diff --git a/Firestore/core/src/model/server_timestamp_util.cc b/Firestore/core/src/model/server_timestamp_util.cc new file mode 100644 index 00000000000..683953a4602 --- /dev/null +++ b/Firestore/core/src/model/server_timestamp_util.cc @@ -0,0 +1,68 @@ +/* + * Copyright 2021 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 "Firestore/core/src/model/server_timestamp_util.h" +#include "Firestore/core/src/nanopb/nanopb_util.h" +#include "Firestore/core/src/util/hard_assert.h" +#include "absl/strings/string_view.h" + +namespace firebase { +namespace firestore { +namespace model { + +const char kTypeKey[] = "__type__"; +const char kLocalWriteTimeKey[] = "__local_write_time__"; +const char kServerTimestampSentinel[] = "server_timestamp"; + +bool IsServerTimestamp(const google_firestore_v1_Value& value) { + if (value.which_value_type != google_firestore_v1_Value_map_value_tag) { + return false; + } + + if (value.map_value.fields_count > 3) { + return false; + } + + for (size_t i = 0; i < value.map_value.fields_count; ++i) { + const auto& field = value.map_value.fields[i]; + absl::string_view key = nanopb::MakeStringView(field.key); + if (key == kTypeKey) { + return field.value.which_value_type == + google_firestore_v1_Value_string_value_tag && + nanopb::MakeStringView(field.value.string_value) == + kServerTimestampSentinel; + } + } + + return false; +} + +const google_firestore_v1_Value& GetLocalWriteTime( + const firebase::firestore::google_firestore_v1_Value& value) { + for (size_t i = 0; i < value.map_value.fields_count; ++i) { + const auto& field = value.map_value.fields[i]; + absl::string_view key = nanopb::MakeStringView(field.key); + if (key == kLocalWriteTimeKey) { + return field.value; + } + } + + HARD_FAIL("LocalWriteTime not found"); +} + +} // namespace model +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/model/server_timestamp_util.h b/Firestore/core/src/model/server_timestamp_util.h new file mode 100644 index 00000000000..b5c79f55c3d --- /dev/null +++ b/Firestore/core/src/model/server_timestamp_util.h @@ -0,0 +1,45 @@ +/* + * Copyright 2021 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. + */ + +#ifndef FIRESTORE_CORE_SRC_MODEL_SERVER_TIMESTAMP_UTIL_H_ +#define FIRESTORE_CORE_SRC_MODEL_SERVER_TIMESTAMP_UTIL_H_ + +#include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" + +namespace firebase { +namespace firestore { +namespace model { + +// Utility methods to handle ServerTimestamps, which are stored using special +// sentinel fields in MapValues. + +/** + * Returns whether the provided value is a field map that contains the + * sentinel values of a ServerTimestamp. + */ +bool IsServerTimestamp(const google_firestore_v1_Value& value); + +/** + * Returns the local time at which the timestamp was written to the document. + */ +const google_firestore_v1_Value& GetLocalWriteTime( + const google_firestore_v1_Value& value); + +} // namespace model +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_MODEL_SERVER_TIMESTAMP_UTIL_H_ diff --git a/Firestore/core/src/model/value_util.cc b/Firestore/core/src/model/value_util.cc new file mode 100644 index 00000000000..2afb906a4f5 --- /dev/null +++ b/Firestore/core/src/model/value_util.cc @@ -0,0 +1,491 @@ +/* + * Copyright 2021 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 "Firestore/core/src/model/value_util.h" + +#include +#include +#include +#include + +#include "Firestore/core/src/model/server_timestamp_util.h" +#include "Firestore/core/src/nanopb/nanopb_util.h" +#include "Firestore/core/src/util/comparison.h" +#include "Firestore/core/src/util/hard_assert.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" + +namespace firebase { +namespace firestore { +namespace model { + +using util::ComparisonResult; + +TypeOrder GetTypeOrder(const google_firestore_v1_Value& value) { + switch (value.which_value_type) { + case google_firestore_v1_Value_null_value_tag: + return TypeOrder::kNull; + + case google_firestore_v1_Value_boolean_value_tag: + return TypeOrder::kBoolean; + + case google_firestore_v1_Value_integer_value_tag: + case google_firestore_v1_Value_double_value_tag: + return TypeOrder::kNumber; + + case google_firestore_v1_Value_timestamp_value_tag: + return TypeOrder::kTimestamp; + + case google_firestore_v1_Value_string_value_tag: + return TypeOrder::kString; + + case google_firestore_v1_Value_bytes_value_tag: + return TypeOrder::kBlob; + + case google_firestore_v1_Value_reference_value_tag: + return TypeOrder::kReference; + + case google_firestore_v1_Value_geo_point_value_tag: + return TypeOrder::kGeoPoint; + + case google_firestore_v1_Value_array_value_tag: + return TypeOrder::kArray; + + case google_firestore_v1_Value_map_value_tag: { + if (IsServerTimestamp(value)) { + return TypeOrder::kServerTimestamp; + } + return TypeOrder::kMap; + } + + default: + HARD_FAIL("Invalid type value: %s", value.which_value_type); + } +} + +ComparisonResult CompareNumbers(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + if (left.which_value_type == google_firestore_v1_Value_double_value_tag) { + double left_double = left.double_value; + if (right.which_value_type == google_firestore_v1_Value_double_value_tag) { + return util::Compare(left_double, right.double_value); + } else { + return util::CompareMixedNumber(left_double, right.integer_value); + } + } else { + int64_t left_long = left.integer_value; + if (right.which_value_type == google_firestore_v1_Value_integer_value_tag) { + return util::Compare(left_long, right.integer_value); + } else { + return util::ReverseOrder( + util::CompareMixedNumber(right.double_value, left_long)); + } + } +} + +ComparisonResult CompareTimestamps(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + ComparisonResult cmp = util::Compare(left.timestamp_value.seconds, + right.timestamp_value.seconds); + if (cmp != ComparisonResult::Same) { + return cmp; + } + return util::Compare(left.timestamp_value.nanos, right.timestamp_value.nanos); +} + +ComparisonResult CompareStrings(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + absl::string_view left_string = nanopb::MakeStringView(left.string_value); + absl::string_view right_string = nanopb::MakeStringView(right.string_value); + return util::Compare(left_string, right_string); +} + +ComparisonResult CompareBlobs(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + if (left.bytes_value && right.bytes_value) { + size_t size = std::min(left.bytes_value->size, right.bytes_value->size); + int cmp = + std::memcmp(left.bytes_value->bytes, right.bytes_value->bytes, size); + return cmp != 0 + ? util::ComparisonResultFromInt(cmp) + : util::Compare(left.bytes_value->size, right.bytes_value->size); + } else { + // An empty blob is represented by a nullptr + return util::Compare(left.bytes_value != nullptr, + right.bytes_value != nullptr); + } +} + +ComparisonResult CompareReferences(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + std::vector left_segments = absl::StrSplit( + nanopb::MakeStringView(left.reference_value), '/', absl::SkipEmpty()); + std::vector right_segments = absl::StrSplit( + nanopb::MakeStringView(right.reference_value), '/', absl::SkipEmpty()); + + int min_length = std::min(left_segments.size(), right_segments.size()); + for (int i = 0; i < min_length; ++i) { + ComparisonResult cmp = util::Compare(left_segments[i], right_segments[i]); + if (cmp != ComparisonResult::Same) { + return cmp; + } + } + return util::Compare(left_segments.size(), right_segments.size()); +} + +ComparisonResult CompareGeoPoints(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + ComparisonResult cmp = util::Compare(left.geo_point_value.latitude, + right.geo_point_value.latitude); + if (cmp != ComparisonResult::Same) { + return cmp; + } + return util::Compare(left.geo_point_value.longitude, + right.geo_point_value.longitude); +} + +ComparisonResult CompareArrays(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + int min_length = + std::min(left.array_value.values_count, right.array_value.values_count); + for (int i = 0; i < min_length; ++i) { + ComparisonResult cmp = + Compare(left.array_value.values[i], right.array_value.values[i]); + if (cmp != ComparisonResult::Same) { + return cmp; + } + } + return util::Compare(left.array_value.values_count, + right.array_value.values_count); +} + +ComparisonResult CompareObjects(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + google_firestore_v1_MapValue left_map = left.map_value; + google_firestore_v1_MapValue right_map = right.map_value; + + // Create a sorted mapping of field key to index. This is then used to walk + // both maps in sorted order. + std::map left_key_to_value_index; + for (size_t i = 0; i < left_map.fields_count; ++i) { + left_key_to_value_index.emplace(nanopb::MakeString(left_map.fields[i].key), + i); + } + std::map right_key_to_value_index; + for (size_t i = 0; i < right_map.fields_count; ++i) { + right_key_to_value_index.emplace( + nanopb::MakeString(right_map.fields[i].key), i); + } + + for (auto left_it = left_key_to_value_index.begin(), + right_it = right_key_to_value_index.begin(); + left_it != left_key_to_value_index.end() && + right_it != right_key_to_value_index.end(); + ++left_it, ++right_it) { + ComparisonResult key_cmp = util::Compare(left_it->first, right_it->first); + if (key_cmp != ComparisonResult::Same) { + return key_cmp; + } + + ComparisonResult value_cmp = + Compare(left.map_value.fields[left_it->second].value, + right.map_value.fields[right_it->second].value); + if (value_cmp != ComparisonResult::Same) { + return value_cmp; + } + } + + return util::Compare(left_map.fields_count, right_map.fields_count); +} + +ComparisonResult Compare(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + TypeOrder left_type = GetTypeOrder(left); + TypeOrder right_type = GetTypeOrder(right); + + if (left_type != right_type) { + return util::Compare(left_type, right_type); + } + + switch (left_type) { + case TypeOrder::kNull: + return ComparisonResult::Same; + + case TypeOrder::kBoolean: + return util::Compare(left.boolean_value, right.boolean_value); + + case TypeOrder::kNumber: + return CompareNumbers(left, right); + + case TypeOrder::kTimestamp: + return CompareTimestamps(left, right); + + case TypeOrder::kServerTimestamp: + return CompareTimestamps(GetLocalWriteTime(left), + GetLocalWriteTime(right)); + + case TypeOrder::kString: + return CompareStrings(left, right); + + case TypeOrder::kBlob: + return CompareBlobs(left, right); + + case TypeOrder::kReference: + return CompareReferences(left, right); + + case TypeOrder::kGeoPoint: + return CompareGeoPoints(left, right); + + case TypeOrder::kArray: + return CompareArrays(left, right); + + case TypeOrder::kMap: + return CompareObjects(left, right); + + default: + HARD_FAIL("Invalid type value: %s", left_type); + } +} + +bool NumberEquals(const firebase::firestore::google_firestore_v1_Value& left, + const firebase::firestore::google_firestore_v1_Value& right) { + if (left.which_value_type == google_firestore_v1_Value_integer_value_tag && + right.which_value_type == google_firestore_v1_Value_integer_value_tag) { + return left.integer_value == right.integer_value; + } else if (left.which_value_type == + google_firestore_v1_Value_double_value_tag && + right.which_value_type == + google_firestore_v1_Value_double_value_tag) { + return util::DoubleBitwiseEquals(left.double_value, right.double_value); + } + return false; +} + +bool ArrayEquals(const firebase::firestore::google_firestore_v1_Value& left, + const firebase::firestore::google_firestore_v1_Value& right) { + const google_firestore_v1_ArrayValue& left_array = left.array_value; + const google_firestore_v1_ArrayValue& right_array = right.array_value; + + if (left_array.values_count != right_array.values_count) { + return false; + } + + for (size_t i = 0; i < left_array.values_count; ++i) { + if (left_array.values[i] != right_array.values[i]) { + return false; + } + } + + return true; +} + +bool ObjectEquals(const firebase::firestore::google_firestore_v1_Value& left, + const firebase::firestore::google_firestore_v1_Value& right) { + google_firestore_v1_MapValue left_map = left.map_value; + google_firestore_v1_MapValue right_map = right.map_value; + + if (left_map.fields_count != right_map.fields_count) { + return false; + } + + // Create a map of field names to index for one of the maps. This is then used + // look up the corresponding value for the other map's fields. + absl::flat_hash_map key_to_value_index; + for (size_t i = 0; i < left_map.fields_count; ++i) { + key_to_value_index.emplace(nanopb::MakeStringView(left_map.fields[i].key), + i); + } + + for (size_t i = 0; i < right_map.fields_count; ++i) { + absl::string_view key = nanopb::MakeStringView(right_map.fields[i].key); + auto left_index_it = key_to_value_index.find(key); + + if (left_index_it == key_to_value_index.end()) { + return false; + } + + if (left_map.fields[left_index_it->second].value != + right_map.fields[i].value) { + return false; + } + } + + return true; +} + +bool operator==(const google_firestore_v1_Value& lhs, + const google_firestore_v1_Value& rhs) { + TypeOrder left_type = GetTypeOrder(lhs); + TypeOrder right_type = GetTypeOrder(rhs); + if (left_type != right_type) { + return false; + } + + switch (left_type) { + case TypeOrder::kNull: + return true; + + case TypeOrder::kBoolean: + return lhs.boolean_value == rhs.boolean_value; + + case TypeOrder::kNumber: + return NumberEquals(lhs, rhs); + + case TypeOrder::kTimestamp: + return lhs.timestamp_value.seconds == rhs.timestamp_value.seconds && + lhs.timestamp_value.nanos == rhs.timestamp_value.nanos; + + case TypeOrder::kServerTimestamp: + return GetLocalWriteTime(lhs) == GetLocalWriteTime(rhs); + + case TypeOrder::kString: + return nanopb::MakeStringView(lhs.string_value) == + nanopb::MakeStringView(rhs.string_value); + + case TypeOrder::kBlob: + return CompareBlobs(lhs, rhs) == ComparisonResult::Same; + + case TypeOrder::kReference: + return nanopb::MakeStringView(lhs.reference_value) == + nanopb::MakeStringView(rhs.reference_value); + + case TypeOrder::kGeoPoint: + return lhs.geo_point_value.latitude == rhs.geo_point_value.latitude && + lhs.geo_point_value.longitude == rhs.geo_point_value.longitude; + + case TypeOrder::kArray: + return ArrayEquals(lhs, rhs); + + case TypeOrder::kMap: + return ObjectEquals(lhs, rhs); + + default: + HARD_FAIL("Invalid type value: %s", left_type); + } +} + +std::string CanonifyTimestamp(const google_firestore_v1_Value& value) { + return absl::StrFormat("time(%d,%d)", value.timestamp_value.seconds, + value.timestamp_value.nanos); +} + +std::string CanonifyBlob(const google_firestore_v1_Value& value) { + return absl::BytesToHexString(nanopb::MakeStringView(value.bytes_value)); +} + +std::string CanonifyReference(const google_firestore_v1_Value& value) { + std::vector segments = absl::StrSplit( + nanopb::MakeStringView(value.reference_value), '/', absl::SkipEmpty()); + HARD_ASSERT(segments.size() >= 5, + "Reference values should have at least 5 components"); + return absl::StrJoin(segments.begin() + 5, segments.end(), "/"); +} + +std::string CanonifyGeoPoint(const google_firestore_v1_Value& value) { + return absl::StrFormat("geo(%.1f,%.1f)", value.geo_point_value.latitude, + value.geo_point_value.longitude); +} + +std::string CanonifyArray(const google_firestore_v1_Value& value) { + const auto& array = value.array_value; + + std::string result = "["; + for (size_t i = 0; i < array.values_count; ++i) { + absl::StrAppend(&result, CanonicalId(array.values[i])); + if (i != array.values_count - 1) { + absl::StrAppend(&result, ","); + } + } + result += "]"; + return result; +} + +std::string CanonifyObject(const google_firestore_v1_Value& value) { + const auto& fields = value.map_value.fields; + + // Even though MapValue are likely sorted correctly based on their insertion + // order (e.g. when received from the backend), local modifications can bring + // elements out of order. We need to re-sort the elements to ensure that + // canonical IDs are independent of insertion order. + std::map sorted_keys_to_index; + for (size_t i = 0; i < value.map_value.fields_count; ++i) { + sorted_keys_to_index.emplace(nanopb::MakeString(fields[i].key), i); + } + + std::string result = "{"; + bool first = true; + for (const auto& entry : sorted_keys_to_index) { + if (!first) { + absl::StrAppend(&result, ","); + } else { + first = false; + } + + absl::StrAppend(&result, entry.first, ":", + CanonicalId(fields[entry.second].value)); + } + result += "}"; + + return result; +} + +std::string CanonicalId(const google_firestore_v1_Value& value) { + switch (value.which_value_type) { + case google_firestore_v1_Value_null_value_tag: + return "null"; + + case google_firestore_v1_Value_boolean_value_tag: + return value.boolean_value ? "true" : "false"; + + case google_firestore_v1_Value_integer_value_tag: + return std::to_string(value.integer_value); + + case google_firestore_v1_Value_double_value_tag: + return absl::StrFormat("%.1f", value.double_value); + + case google_firestore_v1_Value_timestamp_value_tag: + return CanonifyTimestamp(value); + + case google_firestore_v1_Value_string_value_tag: + return nanopb::MakeString(value.string_value); + + case google_firestore_v1_Value_bytes_value_tag: + return CanonifyBlob(value); + + case google_firestore_v1_Value_reference_value_tag: + return CanonifyReference(value); + + case google_firestore_v1_Value_geo_point_value_tag: + return CanonifyGeoPoint(value); + + case google_firestore_v1_Value_array_value_tag: + return CanonifyArray(value); + + case google_firestore_v1_Value_map_value_tag: { + return CanonifyObject(value); + } + + default: + HARD_FAIL("Invalid type value: %s", value.which_value_type); + } +} + +} // namespace model +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/model/value_util.h b/Firestore/core/src/model/value_util.h new file mode 100644 index 00000000000..bec17b9f9c5 --- /dev/null +++ b/Firestore/core/src/model/value_util.h @@ -0,0 +1,75 @@ +/* + * Copyright 2021 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. + */ + +#ifndef FIRESTORE_CORE_SRC_MODEL_VALUE_UTIL_H_ +#define FIRESTORE_CORE_SRC_MODEL_VALUE_UTIL_H_ + +#include + +#include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" + +namespace firebase { +namespace firestore { + +namespace util { +enum class ComparisonResult; +} + +namespace model { + +/** + * The order of types in Firestore. This order is based on the backend's + * ordering, but modified to support server timestamps. + */ +enum class TypeOrder { + kNull = 0, + kBoolean = 1, + kNumber = 2, + kTimestamp = 3, + kServerTimestamp = 4, + kString = 5, + kBlob = 6, + kReference = 7, + kGeoPoint = 8, + kArray = 9, + kMap = 10 +}; + +/** Returns the backend's type order of the given Value type. */ +TypeOrder GetTypeOrder(const google_firestore_v1_Value& value); + +util::ComparisonResult Compare(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right); + +/** + * Generate the canonical ID for the provided field value (as used in Target + * serialization). + */ +std::string CanonicalId(const google_firestore_v1_Value& value); + +bool operator==(const google_firestore_v1_Value& lhs, + const google_firestore_v1_Value& rhs); + +inline bool operator!=(const google_firestore_v1_Value& lhs, + const google_firestore_v1_Value& rhs) { + return !(lhs == rhs); +} + +} // namespace model +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_MODEL_VALUE_UTIL_H_ diff --git a/Firestore/core/src/remote/serializer.h b/Firestore/core/src/remote/serializer.h index e07e4b20766..de3223cd89f 100644 --- a/Firestore/core/src/remote/serializer.h +++ b/Firestore/core/src/remote/serializer.h @@ -249,6 +249,15 @@ class Serializer { nanopb::Reader* reader, const google_firestore_v1_StructuredQuery_Filter& proto) const; + /** + * Encodes a database ID and resource path into the following form: + * /projects/$project_id/database/$database_id/documents/$path + * + * Does not verify that the database_id matches the current instance. + */ + pb_bytes_array_t* EncodeResourceName(const model::DatabaseId& database_id, + const model::ResourcePath& path) const; + private: google_firestore_v1_Value EncodeNull() const; google_firestore_v1_Value EncodeBoolean(bool value) const; @@ -272,13 +281,6 @@ class Serializer { model::ResourcePath DecodeQueryPath(nanopb::Reader* reader, absl::string_view name) const; - /** - * Encodes a database ID and resource path into the following form: - * /projects/$project_id/database/$database_id/documents/$path - */ - pb_bytes_array_t* EncodeResourceName(const model::DatabaseId& database_id, - const model::ResourcePath& path) const; - /** * Decodes a fully qualified resource name into a resource path and validates * that there is a project and database encoded in the path. There are no diff --git a/Firestore/core/src/util/comparison.cc b/Firestore/core/src/util/comparison.cc index 15fd788708d..296d5c44c3a 100644 --- a/Firestore/core/src/util/comparison.cc +++ b/Firestore/core/src/util/comparison.cc @@ -29,18 +29,6 @@ namespace util { using std::isnan; -/** - * Creates a ComparisonResult from a typical integer return value, where - * 0 means "same", less than zero means "ascending", and greater than zero - * means "descending". - */ -constexpr ComparisonResult ComparisonResultFromInt(int value) { - // TODO(c++14): convert this to an if statement. - return value < 0 ? ComparisonResult::Ascending - : (value > 0 ? ComparisonResult::Descending - : ComparisonResult::Same); -} - ComparisonResult Comparator::Compare( absl::string_view left, absl::string_view right) const { return ComparisonResultFromInt(left.compare(right)); diff --git a/Firestore/core/src/util/comparison.h b/Firestore/core/src/util/comparison.h index c256f94cc3f..9aa234b0f6a 100644 --- a/Firestore/core/src/util/comparison.h +++ b/Firestore/core/src/util/comparison.h @@ -72,6 +72,18 @@ constexpr bool Descending(ComparisonResult result) noexcept { return result == ComparisonResult::Descending; } +/** + * Creates a ComparisonResult from a typical integer return value, where + * 0 means "same", less than zero means "ascending", and greater than zero + * means "descending". + */ +constexpr ComparisonResult ComparisonResultFromInt(int value) { + // TODO(c++14): convert this to an if statement. + return value < 0 ? ComparisonResult::Ascending + : (value > 0 ? ComparisonResult::Descending + : ComparisonResult::Same); +} + /** * Returns the reverse order (i.e. Ascending => Descending) etc. */ diff --git a/Firestore/core/test/unit/model/value_util_test.cc b/Firestore/core/test/unit/model/value_util_test.cc new file mode 100644 index 00000000000..89fc2b8bf93 --- /dev/null +++ b/Firestore/core/test/unit/model/value_util_test.cc @@ -0,0 +1,327 @@ +/* + * Copyright 2021 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 "Firestore/core/src/model/value_util.h" +#include "Firestore/core/src/model/database_id.h" +#include "Firestore/core/src/model/field_value.h" +#include "Firestore/core/src/remote/serializer.h" +#include "Firestore/core/src/util/comparison.h" +#include "Firestore/core/test/unit/testutil/equals_tester.h" +#include "Firestore/core/test/unit/testutil/testutil.h" +#include "Firestore/core/test/unit/testutil/time_testing.h" +#include "absl/base/casts.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace model { +namespace { + +using testutil::Array; +using testutil::BlobValue; +using testutil::DbId; +using testutil::Key; +using testutil::Map; +using testutil::time_point; +using testutil::Value; +using util::ComparisonResult; + +double ToDouble(uint64_t value) { + return absl::bit_cast(value); +} + +const uint64_t kNanBits = 0x7fff000000000000ULL; + +const time_point kDate1 = testutil::MakeTimePoint(2016, 5, 20, 10, 20, 0); +const Timestamp kTimestamp1{1463739600, 0}; + +const time_point kDate2 = testutil::MakeTimePoint(2016, 10, 21, 15, 32, 0); +const Timestamp kTimestamp2{1477063920, 0}; + +class ValueUtilTest : public ::testing::Test { + public: + template + google_firestore_v1_Value Wrap(T input) { + model::FieldValue fv = Value(input); + return serializer.EncodeFieldValue(fv); + } + + template + google_firestore_v1_Value WrapObject(Args&&... key_value_pairs) { + FieldValue fv = + testutil::WrapObject(std::forward(key_value_pairs)...); + return serializer.EncodeFieldValue(fv); + } + + template + google_firestore_v1_Value WrapArray(Args&&... values) { + std::vector contents{ + Value(std::forward(values))...}; + FieldValue fv = FieldValue::FromArray(std::move(contents)); + return serializer.EncodeFieldValue(fv); + } + + google_firestore_v1_Value WrapReference(const DatabaseId& database_id, + const DocumentKey& key) { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_reference_value_tag; + result.reference_value = + serializer.EncodeResourceName(database_id, key.path()); + return result; + } + + google_firestore_v1_Value WrapServerTimestamp( + const model::FieldValue& input) { + // TODO(mrschmidt): Replace with EncodeFieldValue encoding when available + return WrapObject("__type__", "server_timestamp", "__local_write_time__", + input.server_timestamp_value().local_write_time()); + } + + template + void Add(std::vector>& groups, + Args... values) { + std::vector group{std::forward(values)...}; + groups.emplace_back(group); + } + + void VerifyEquality(std::vector& left, + std::vector& right, + bool expected_equals) { + for (const auto& val1 : left) { + for (const auto& val2 : right) { + EXPECT_EQ(expected_equals, val1 == val2) + << "Equality check failed for '" << CanonicalId(val1) << "' and '" + << CanonicalId(val2) << "' (expected " << expected_equals << ")"; + } + } + } + + void VerifyOrdering(std::vector& left, + std::vector& right, + ComparisonResult expected_result) { + for (const auto& val1 : left) { + for (const auto& val2 : right) { + EXPECT_EQ(expected_result, Compare(val1, val2)) + << "Order check failed for '" << CanonicalId(val1) << "' and '" + << CanonicalId(val2) << "' (expected " + << static_cast(expected_result) << ")"; + EXPECT_EQ(util::ReverseOrder(expected_result), Compare(val2, val1)) + << "Reverse order check failed for '" << CanonicalId(val1) + << "' and '" << CanonicalId(val2) << "' (expected " + << static_cast(util::ReverseOrder(expected_result)) << ")"; + } + } + } + + void VerifyCanonicalId(const google_firestore_v1_Value& value, + const std::string& expected_canonical_id) { + std::string actual_canonical_id = CanonicalId(value); + EXPECT_EQ(expected_canonical_id, actual_canonical_id); + } + + private: + remote::Serializer serializer{DbId()}; +}; + +TEST_F(ValueUtilTest, Equality) { + // Create a matrix that defines an equality group. The outer vector has + // multiple rows and each row can have an arbitrary number of entries. + // The elements within a row must equal each other, but not be equal + // to all elements of other rows. + std::vector> equals_group; + + Add(equals_group, Wrap(nullptr), Wrap(nullptr)); + Add(equals_group, Wrap(false), Wrap(false)); + Add(equals_group, Wrap(true), Wrap(true)); + Add(equals_group, Wrap(std::numeric_limits::quiet_NaN()), + Wrap(ToDouble(kCanonicalNanBits)), Wrap(ToDouble(kNanBits)), + Wrap(std::nan("1")), Wrap(std::nan("2"))); + // -0.0 and 0.0 compare the same but are not equal. + Add(equals_group, Wrap(-0.0)); + Add(equals_group, Wrap(0.0)); + Add(equals_group, Wrap(1), Wrap(1LL)); + // Doubles and Longs aren't equal (even though they compare same). + Add(equals_group, Wrap(1.0), Wrap(1.0)); + Add(equals_group, Wrap(1.1), Wrap(1.1)); + Add(equals_group, Wrap(BlobValue(0, 1, 1))); + Add(equals_group, Wrap(BlobValue(0, 1))); + Add(equals_group, Wrap("string"), Wrap("string")); + Add(equals_group, Wrap("strin")); + Add(equals_group, Wrap(std::string("strin\0", 6))); + // latin small letter e + combining acute accent + Add(equals_group, Wrap("e\u0301b")); + // latin small letter e with acute accent + Add(equals_group, Wrap("\u00e9a")); + Add(equals_group, Wrap(Timestamp::FromTimePoint(kDate1)), Wrap(kTimestamp1)); + Add(equals_group, Wrap(Timestamp::FromTimePoint(kDate2)), Wrap(kTimestamp2)); + // NOTE: ServerTimestampValues can't be parsed via Wrap(). + Add(equals_group, + WrapServerTimestamp(FieldValue::FromServerTimestamp(kTimestamp1)), + WrapServerTimestamp(FieldValue::FromServerTimestamp(kTimestamp1))); + Add(equals_group, + WrapServerTimestamp(FieldValue::FromServerTimestamp(kTimestamp2))); + Add(equals_group, Wrap(GeoPoint(0, 1)), Wrap(GeoPoint(0, 1))); + Add(equals_group, Wrap(GeoPoint(1, 0))); + Add(equals_group, WrapReference(DbId(), Key("coll/doc1")), + WrapReference(DbId(), Key("coll/doc1"))); + Add(equals_group, WrapReference(DbId(), Key("coll/doc2"))); + Add(equals_group, WrapReference(DbId("project/baz"), Key("coll/doc2"))); + Add(equals_group, WrapArray("foo", "bar"), WrapArray("foo", "bar")); + Add(equals_group, WrapArray("foo", "bar", "baz")); + Add(equals_group, WrapArray("foo")); + Add(equals_group, WrapObject("bar", 1, "foo", 2), + WrapObject("foo", 2, "bar", 1)); + Add(equals_group, WrapObject("bar", 2, "foo", 1)); + Add(equals_group, WrapObject("bar", 1)); + Add(equals_group, WrapObject("foo", 1)); + + for (size_t i = 0; i < equals_group.size(); ++i) { + for (size_t j = i; j < equals_group.size(); ++j) { + VerifyEquality(equals_group[i], equals_group[j], + /* expected_equals= */ i == j); + } + } +} + +TEST_F(ValueUtilTest, Ordering) { + // Create a matrix that defines a comparison group. The outer vector has + // multiple rows and each row can have an arbitrary number of entries. + // The elements within a row must compare equal to each other, but order after + // all elements in previous groups and before all elements in later groups. + std::vector> comparison_groups; + + // null first + Add(comparison_groups, Wrap(nullptr)); + + // booleans + Add(comparison_groups, Wrap(false)); + Add(comparison_groups, Wrap(true)); + + // numbers + Add(comparison_groups, Wrap(-1e20)); + Add(comparison_groups, Wrap(LLONG_MIN)); + Add(comparison_groups, Wrap(-0.1)); + // Zeros all compare the same. + Add(comparison_groups, Wrap(-0.0), Wrap(0.0), Wrap(0L)); + Add(comparison_groups, Wrap(0.1)); + // Doubles and longs Compare() the same. + Add(comparison_groups, Wrap(1.0), Wrap(1L)); + Add(comparison_groups, Wrap(LLONG_MAX)); + Add(comparison_groups, Wrap(1e20)); + + // dates + Add(comparison_groups, Wrap(kTimestamp1)); + Add(comparison_groups, Wrap(kTimestamp2)); + + // server timestamps come after all concrete timestamps. + // NOTE: server timestamps can't be parsed with Wrap(). + Add(comparison_groups, + WrapServerTimestamp(FieldValue::FromServerTimestamp(kTimestamp1))); + Add(comparison_groups, + WrapServerTimestamp(FieldValue::FromServerTimestamp(kTimestamp2))); + + // strings + Add(comparison_groups, Wrap("")); + Add(comparison_groups, Wrap("\001\ud7ff\ue000\uffff")); + Add(comparison_groups, Wrap("(╯°□°)╯︵ ┻━┻")); + Add(comparison_groups, Wrap("a")); + Add(comparison_groups, Wrap(std::string("abc\0 def", 8))); + Add(comparison_groups, Wrap("abc def")); + // latin small letter e + combining acute accent + latin small letter b + Add(comparison_groups, Wrap("e\u0301b")); + Add(comparison_groups, Wrap("æ")); + // latin small letter e with acute accent + latin small letter a + Add(comparison_groups, Wrap("\u00e9a")); + + // blobs + Add(comparison_groups, Wrap(BlobValue())); + Add(comparison_groups, Wrap(BlobValue(0))); + Add(comparison_groups, Wrap(BlobValue(0, 1, 2, 3, 4))); + Add(comparison_groups, Wrap(BlobValue(0, 1, 2, 4, 3))); + Add(comparison_groups, Wrap(BlobValue(255))); + + // resource names + Add(comparison_groups, WrapReference(DbId("p1/d1"), Key("c1/doc1"))); + Add(comparison_groups, WrapReference(DbId("p1/d1"), Key("c1/doc2"))); + Add(comparison_groups, WrapReference(DbId("p1/d1"), Key("c10/doc1"))); + Add(comparison_groups, WrapReference(DbId("p1/d1"), Key("c2/doc1"))); + Add(comparison_groups, WrapReference(DbId("p1/d2"), Key("c1/doc1"))); + Add(comparison_groups, WrapReference(DbId("p2/d1"), Key("c1/doc1"))); + + // geo points + Add(comparison_groups, Wrap(GeoPoint(-90, -180))); + Add(comparison_groups, Wrap(GeoPoint(-90, 0))); + Add(comparison_groups, Wrap(GeoPoint(-90, 180))); + Add(comparison_groups, Wrap(GeoPoint(0, -180))); + Add(comparison_groups, Wrap(GeoPoint(0, 0))); + Add(comparison_groups, Wrap(GeoPoint(0, 180))); + Add(comparison_groups, Wrap(GeoPoint(1, -180))); + Add(comparison_groups, Wrap(GeoPoint(1, 0))); + Add(comparison_groups, Wrap(GeoPoint(1, 180))); + Add(comparison_groups, Wrap(GeoPoint(90, -180))); + Add(comparison_groups, Wrap(GeoPoint(90, 0))); + Add(comparison_groups, Wrap(GeoPoint(90, 180))); + + // arrays + Add(comparison_groups, WrapArray("bar")); + Add(comparison_groups, WrapArray("foo", 1)); + Add(comparison_groups, WrapArray("foo", 2)); + Add(comparison_groups, WrapArray("foo", "0")); + + // objects + Add(comparison_groups, WrapObject("bar", 0)); + Add(comparison_groups, WrapObject("bar", 0, "foo", 1)); + Add(comparison_groups, WrapObject("foo", 1)); + Add(comparison_groups, WrapObject("foo", 2)); + Add(comparison_groups, WrapObject("foo", "0")); + + for (size_t i = 0; i < comparison_groups.size(); ++i) { + for (size_t j = i; j < comparison_groups.size(); ++j) { + VerifyOrdering( + comparison_groups[i], comparison_groups[j], + i == j ? ComparisonResult::Same : ComparisonResult::Ascending); + } + } +} + +TEST_F(ValueUtilTest, CanonicalId) { + VerifyCanonicalId(Wrap(nullptr), "null"); + VerifyCanonicalId(Wrap(true), "true"); + VerifyCanonicalId(Wrap(false), "false"); + VerifyCanonicalId(Wrap(1), "1"); + VerifyCanonicalId(Wrap(1.0), "1.0"); + VerifyCanonicalId(Wrap(Timestamp(30, 1000)), "time(30,1000)"); + VerifyCanonicalId(Wrap("a"), "a"); + VerifyCanonicalId(Wrap(std::string("a\0b", 3)), std::string("a\0b", 3)); + VerifyCanonicalId(Wrap(BlobValue(1, 2, 3)), "010203"); + VerifyCanonicalId(WrapReference(DbId("p1/d1"), Key("c1/doc1")), "c1/doc1"); + VerifyCanonicalId(Wrap(GeoPoint(30, 60)), "geo(30.0,60.0)"); + VerifyCanonicalId(WrapArray(1, 2, 3), "[1,2,3]"); + VerifyCanonicalId(WrapObject("a", 1, "b", 2, "c", "3"), "{a:1,b:2,c:3}"); + VerifyCanonicalId(WrapObject("a", Array("b", Map("c", GeoPoint(30, 60)))), + "{a:[b,{c:geo(30.0,60.0)}]}"); +} + +TEST_F(ValueUtilTest, CanonicalIdIgnoresSortOrder) { + VerifyCanonicalId(WrapObject("a", 1, "b", 2, "c", "3"), "{a:1,b:2,c:3}"); + VerifyCanonicalId(WrapObject("c", 3, "b", 2, "a", "1"), "{a:1,b:2,c:3}"); +} + +} // namespace + +} // namespace model +} // namespace firestore +} // namespace firebase