diff --git a/FirebaseFirestoreInternal/FirebaseFirestore/FIRDistanceMeasure.h b/FirebaseFirestoreInternal/FirebaseFirestore/FIRDistanceMeasure.h new file mode 100644 index 00000000000..e2cd4ba65d5 --- /dev/null +++ b/FirebaseFirestoreInternal/FirebaseFirestore/FIRDistanceMeasure.h @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import diff --git a/FirebaseFirestoreInternal/FirebaseFirestore/FIRFindNearestOptions.h b/FirebaseFirestoreInternal/FirebaseFirestore/FIRFindNearestOptions.h new file mode 100644 index 00000000000..c671ef0828a --- /dev/null +++ b/FirebaseFirestoreInternal/FirebaseFirestore/FIRFindNearestOptions.h @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import diff --git a/FirebaseFirestoreInternal/FirebaseFirestore/FIRVectorSource.h b/FirebaseFirestoreInternal/FirebaseFirestore/FIRVectorSource.h new file mode 100644 index 00000000000..ac85c462d20 --- /dev/null +++ b/FirebaseFirestoreInternal/FirebaseFirestore/FIRVectorSource.h @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import diff --git a/FirebaseFirestoreInternal/FirebaseFirestore/FIRVectorValue.h b/FirebaseFirestoreInternal/FirebaseFirestore/FIRVectorValue.h new file mode 100644 index 00000000000..ac85c462d20 --- /dev/null +++ b/FirebaseFirestoreInternal/FirebaseFirestore/FIRVectorValue.h @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index fbfc61ba933..6a4973c7b02 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,6 @@ +# 11.1.0 +- [feature] Add `VectorValue` type support. + # 11.0.0 - [removed] **Breaking change**: The deprecated `FirebaseFirestoreSwift` module has been removed. See diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index c3db9e2da77..7478ab8332d 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -1536,6 +1536,9 @@ EF79998EBE4C72B97AB1880E /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 40F9D09063A07F710811A84F /* value_util_test.cc */; }; EF8C005DC4BEA6256D1DBC6F /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CCC9BD953F121B9E29F9AA42 /* user_test.cc */; }; EFD682178A87513A5F1AEFD9 /* memory_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6A33BC2D84233C355F1D0 /* memory_query_engine_test.cc */; }; + EFF22EAA2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */; }; + EFF22EAB2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */; }; + EFF22EAC2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */; }; F05B277F16BDE6A47FE0F943 /* local_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F8043813A5D16963EC02B182 /* local_serializer_test.cc */; }; F08DA55D31E44CB5B9170CCE /* limbo_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */; }; F091532DEE529255FB008E25 /* snapshot_version_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */; }; @@ -1687,7 +1690,7 @@ 132E32997D781B896672D30A /* reference_set_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = reference_set_test.cc; sourceTree = ""; }; 166CE73C03AB4366AAC5201C /* leveldb_index_manager_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_index_manager_test.cc; sourceTree = ""; }; 1A7D48A017ECB54FD381D126 /* Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json; sourceTree = ""; }; - 1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = thread_safe_memoizer_test.cc; sourceTree = ""; }; + 1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = thread_safe_memoizer_test.cc; sourceTree = ""; }; 1B342370EAE3AA02393E33EB /* cc_compilation_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = cc_compilation_test.cc; path = api/cc_compilation_test.cc; sourceTree = ""; }; 1B9F95EC29FAD3F100EEC075 /* FIRAggregateQueryUnitTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateQueryUnitTests.mm; sourceTree = ""; }; 1C01D8CE367C56BB2624E299 /* index.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = index.pb.h; path = admin/index.pb.h; sourceTree = ""; }; @@ -1744,7 +1747,7 @@ 4BD051DBE754950FEAC7A446 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json; sourceTree = ""; }; 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_benchmark.mm; sourceTree = ""; }; 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnapshotListenerSourceTests.swift; sourceTree = ""; }; - 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; path = listen_source_spec_test.json; sourceTree = ""; }; + 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; path = listen_source_spec_test.json; sourceTree = ""; }; 4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bundle_builder.cc; sourceTree = ""; }; 526D755F65AC676234F57125 /* target_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_test.cc; sourceTree = ""; }; 52756B7624904C36FBB56000 /* fake_target_metadata_provider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = fake_target_metadata_provider.h; sourceTree = ""; }; @@ -1898,7 +1901,7 @@ 62E54B832A9E910A003347C8 /* IndexingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexingTests.swift; sourceTree = ""; }; 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_index_matcher_test.cc; sourceTree = ""; }; 64AA92CFA356A2360F3C5646 /* filesystem_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = filesystem_testing.h; sourceTree = ""; }; - 65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; path = FIRCompositeIndexQueryTests.mm; sourceTree = ""; }; + 65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRCompositeIndexQueryTests.mm; sourceTree = ""; }; 67786C62C76A740AEDBD8CD3 /* FSTTestingHooks.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = FSTTestingHooks.h; sourceTree = ""; }; 69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; sourceTree = ""; }; 6A7A30A2DB3367E08939E789 /* bloom_filter.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bloom_filter.pb.h; sourceTree = ""; }; @@ -2083,6 +2086,7 @@ 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 = ""; }; EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_migrations_test.cc; sourceTree = ""; }; + EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorIntegrationTests.swift; sourceTree = ""; }; F02F734F272C3C70D1307076 /* filter_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = filter_test.cc; sourceTree = ""; }; 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 = ""; }; F354C0FE92645B56A6C6FD44 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; sourceTree = ""; }; @@ -2226,6 +2230,7 @@ 62E54B832A9E910A003347C8 /* IndexingTests.swift */, 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */, 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */, + EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */, ); path = Integration; sourceTree = ""; @@ -4581,6 +4586,7 @@ 62E54B862A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */, + EFF22EAC2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, 4D42E5C756229C08560DD731 /* XCTestCase+Await.mm in Sources */, 09BE8C01EC33D1FD82262D5D /* aggregate_query_test.cc in Sources */, 0EC3921AE220410F7394729B /* aggregation_result.pb.cc in Sources */, @@ -4821,6 +4827,7 @@ 62E54B852A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */, + EFF22EAB2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, 736C4E82689F1CA1859C4A3F /* XCTestCase+Await.mm in Sources */, 412BE974741729A6683C386F /* aggregate_query_test.cc in Sources */, DF983A9C1FBF758AF3AF110D /* aggregation_result.pb.cc in Sources */, @@ -5307,6 +5314,7 @@ 62E54B842A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */, + EFF22EAA2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, 5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */, B04E4FE20930384DF3A402F9 /* aggregate_query_test.cc in Sources */, 1A3D8028303B45FCBB21CAD3 /* aggregation_result.pb.cc in Sources */, diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests_iOS.xcscheme index b4ac9bed6dd..98b55265c5e 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests_iOS.xcscheme @@ -26,8 +26,17 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - enableASanStackUseAfterReturn = "YES" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + enableASanStackUseAfterReturn = "YES"> + + + + @@ -40,17 +49,6 @@ - - - - - - - - *)array { + return [[FIRVectorValue alloc] initWithArray:array]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRVectorValue.mm b/Firestore/Source/API/FIRVectorValue.mm new file mode 100644 index 00000000000..d4f25b39ac3 --- /dev/null +++ b/Firestore/Source/API/FIRVectorValue.mm @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#include + +#include "Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRVectorValue + +@synthesize array = _internalValue; + +- (instancetype)initWithArray:(NSArray *)array { + if (self = [super init]) { + _internalValue = [NSArray arrayWithArray:array]; + } + return self; +} + +- (BOOL)isEqual:(nullable id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRVectorValue class]]) { + return NO; + } + + FIRVectorValue *otherVector = ((FIRVectorValue *)object); + + return [self.array isEqualToArray:otherVector.array]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataReader.mm b/Firestore/Source/API/FSTUserDataReader.mm index 14a06b90e48..4720c78d493 100644 --- a/Firestore/Source/API/FSTUserDataReader.mm +++ b/Firestore/Source/API/FSTUserDataReader.mm @@ -25,6 +25,7 @@ #import "Firestore/Source/API/FSTUserDataReader.h" #import "FIRGeoPoint.h" +#import "FIRVectorValue.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRFieldPath+Internal.h" @@ -341,6 +342,42 @@ - (ParsedUpdateData)parsedUpdateData:(id)input { return std::move(result); } +- (Message)parseVectorValue:(FIRVectorValue *)vectorValue + context:(ParseContext &&)context { + __block Message result; + result->which_value_type = google_firestore_v1_Value_map_value_tag; + result->map_value = {}; + + result->map_value.fields_count = 2; + result->map_value.fields = nanopb::MakeArray(2); + + result->map_value.fields[0].key = nanopb::CopyBytesArray(model::kTypeValueFieldKey); + result->map_value.fields[0].value = *[self encodeStringValue:MakeString(@"__vector__")].release(); + + NSArray *vectorArray = vectorValue.array; + + __block Message arrayMessage; + arrayMessage->which_value_type = google_firestore_v1_Value_array_value_tag; + arrayMessage->array_value.values_count = CheckedSize([vectorArray count]); + arrayMessage->array_value.values = + nanopb::MakeArray(arrayMessage->array_value.values_count); + + [vectorArray enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *) { + if (![entry isKindOfClass:[NSNumber class]]) { + ThrowInvalidArgument("VectorValues must only contain numeric values.", + context.FieldDescription()); + } + + // Vector values must always use Double encoding + arrayMessage->array_value.values[idx] = *[self encodeDouble:[entry doubleValue]].release(); + }]; + + result->map_value.fields[1].key = nanopb::CopyBytesArray(model::kVectorValueFieldKey); + result->map_value.fields[1].value = *arrayMessage.release(); + + return std::move(result); +} + - (Message)parseArray:(NSArray *)array context:(ParseContext &&)context { __block Message result; @@ -529,7 +566,9 @@ - (void)parseSentinelFieldValue:(FIRFieldValue *)fieldValue context:(ParseContex _databaseID.database_id(), context.FieldDescription()); } return [self encodeReference:_databaseID key:reference.key]; - + } else if ([input isKindOfClass:[FIRVectorValue class]]) { + FIRVectorValue *vector = input; + return [self parseVectorValue:vector context:std::move(context)]; } else { ThrowInvalidArgument("Unsupported type: %s%s", NSStringFromClass([input class]), context.FieldDescription()); diff --git a/Firestore/Source/API/FSTUserDataWriter.mm b/Firestore/Source/API/FSTUserDataWriter.mm index c561efe1353..1e170531782 100644 --- a/Firestore/Source/API/FSTUserDataWriter.mm +++ b/Firestore/Source/API/FSTUserDataWriter.mm @@ -21,6 +21,7 @@ #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/Source/API/FIRDocumentReference+Internal.h" +#include "Firestore/Source/API/FIRFieldValue+Internal.h" #include "Firestore/Source/API/converters.h" #include "Firestore/core/include/firebase/firestore/geo_point.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" @@ -105,6 +106,8 @@ - (id)convertedValue:(const google_firestore_v1_Value &)value { case TypeOrder::kGeoPoint: return MakeFIRGeoPoint( GeoPoint(value.geo_point_value.latitude, value.geo_point_value.longitude)); + case TypeOrder::kVector: + return [self convertedVector:value.map_value]; case TypeOrder::kMaxValue: // It is not possible for users to construct a kMaxValue manually. break; @@ -123,6 +126,18 @@ - (id)convertedValue:(const google_firestore_v1_Value &)value { return result; } +- (FIRVectorValue *)convertedVector:(const google_firestore_v1_MapValue &)mapValue { + for (pb_size_t i = 0; i < mapValue.fields_count; ++i) { + absl::string_view key = MakeStringView(mapValue.fields[i].key); + const google_firestore_v1_Value &value = mapValue.fields[i].value; + if ((0 == key.compare(absl::string_view("value"))) && + value.which_value_type == google_firestore_v1_Value_array_value_tag) { + return [FIRFieldValue vectorWithArray:[self convertedArray:value.array_value]]; + } + } + return [FIRFieldValue vectorWithArray:@[]]; +} + - (NSArray *)convertedArray:(const google_firestore_v1_ArrayValue &)arrayValue { NSMutableArray *result = [NSMutableArray arrayWithCapacity:arrayValue.values_count]; for (pb_size_t i = 0; i < arrayValue.values_count; ++i) { diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRDistanceMeasure.h b/Firestore/Source/Public/FirebaseFirestore/FIRDistanceMeasure.h new file mode 100644 index 00000000000..e35173a4dcb --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRDistanceMeasure.h @@ -0,0 +1,14 @@ +// +// FIRFirestoreDistanceMeasure.h +// FirebaseFirestoreInternal +// +// Created by Mark Duckworth on 7/25/24. +// + +#import + +typedef NS_ENUM(NSUInteger, FIRDistanceMeasure) { + FIRDistanceMeasureCosine, + FIRDistanceMeasureEuclidean, + FIRDistanceMeasureDotProduct +} NS_SWIFT_NAME(FirestoreDistanceMeasure); diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h b/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h index 9f64fbdc99d..781c3e19b6b 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h @@ -36,7 +36,8 @@ NS_SWIFT_NAME(FieldPath) * @param fieldNames A list of field names. * @return A `FieldPath` that points to a field location in a document. */ -- (instancetype)initWithFields:(NSArray *)fieldNames NS_SWIFT_NAME(init(_:)); +- (instancetype)initWithFields:(NSArray *)fieldNames + NS_SWIFT_NAME(init(_:)) NS_DESIGNATED_INITIALIZER; /** * A special sentinel `FieldPath` to refer to the ID of a document. It can be used in queries to diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h b/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h index 269735570d9..8add3dec1fa 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h @@ -17,6 +17,7 @@ #import NS_ASSUME_NONNULL_BEGIN +@class FIRVectorValue; /** * Sentinel values that can be used when writing document fields with `setData()` or `updateData()`. @@ -90,6 +91,14 @@ NS_SWIFT_NAME(FieldValue) */ + (instancetype)fieldValueForIntegerIncrement:(int64_t)l NS_SWIFT_NAME(increment(_:)); +/** + * Creates a new `VectorValue` constructed with a copy of the given array of NSNumbers. + * + * @param array Create a `VectorValue` instance with a copy of this array of NSNumbers. + * @return A new `VectorValue` constructed with a copy of the given array of NSNumbers. + */ ++ (FIRVectorValue *)vectorWithArray:(NSArray *)array NS_REFINED_FOR_SWIFT; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRFindNearestOptions.h b/Firestore/Source/Public/FirebaseFirestore/FIRFindNearestOptions.h new file mode 100644 index 00000000000..2ccd4b8f6d7 --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRFindNearestOptions.h @@ -0,0 +1,37 @@ +// +// FIRFindNearestOptions.h +// FirebaseFirestoreInternal +// +// Created by Mark Duckworth on 7/25/24. +// +#import + +#import "FIRFieldPath.h" + +@class FIRAggregateQuery; +@class FIRAggregateField; +@class FIRFieldPath; +@class FIRFirestore; +@class FIRFilter; +@class FIRQuerySnapshot; +@class FIRDocumentSnapshot; +@class FIRVectorQuery; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(FindNearestOptions) +@interface FIRFindNearestOptions : NSObject +@property(nonatomic, readonly) FIRFieldPath *distanceResultFieldPath; +@property(nonatomic, readonly) NSNumber *distanceThreshold NS_REFINED_FOR_SWIFT; + +- (nonnull instancetype)init NS_DESIGNATED_INITIALIZER; + +- (nonnull FIRFindNearestOptions *)optionsWithDistanceResultFieldPath: + (FIRFieldPath *)distanceResultFieldPath; + +- (nonnull FIRFindNearestOptions *)optionsWithDistanceThreshold:(NSNumber *)distanceThreshold + NS_REFINED_FOR_SWIFT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h index c75952876a2..04912f9ab7c 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h @@ -16,6 +16,8 @@ #import +#import "FIRDistanceMeasure.h" +#import "FIRFindNearestOptions.h" #import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" #import "FIRSnapshotListenOptions.h" @@ -27,6 +29,8 @@ @class FIRFilter; @class FIRQuerySnapshot; @class FIRDocumentSnapshot; +@class FIRVectorQuery; +@class FIRVectorValue; NS_ASSUME_NONNULL_BEGIN @@ -49,6 +53,13 @@ NS_SWIFT_NAME(Query) /** The `Firestore` instance that created this query (useful for performing transactions, etc.). */ @property(nonatomic, strong, readonly) FIRFirestore *firestore; +- (nonnull FIRVectorQuery *)findNearestWithFieldPath:(nonnull FIRFieldPath *)fieldPath + queryVectorValue:(nonnull FIRVectorValue *)queryVectorValue + limit:(int64_t)limit + distanceMeasure:(FIRDistanceMeasure)distanceMeasure + options:(nonnull FIRFindNearestOptions *)options + NS_REFINED_FOR_SWIFT; + #pragma mark - Retrieving Data /** * Reads the documents matching this query. diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRVectorQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRVectorQuery.h new file mode 100644 index 00000000000..caea1c002b7 --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRVectorQuery.h @@ -0,0 +1,41 @@ +// +// FIRVectorQuery.h +// FirebaseFirestoreInternal +// +// Created by Mark Duckworth on 7/25/24. +// +#import + +#import "FIRVectorQuerySnapshot.h" +#import "FIRVectorSource.h" + +@class FIRAggregateQuery; +@class FIRAggregateField; +@class FIRFieldPath; +@class FIRFirestore; +@class FIRFilter; +@class FIRVectorQuerySnapshot; +@class FIRDocumentSnapshot; +@class FIRVectorQuery; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(VectorQuery) +@interface FIRVectorQuery : NSObject + +@property(nonatomic, strong, readonly) FIRQuery *query; + +/** + * Executes this query. + * + * @param source The source from which to acquire the VectorQuery results. + * @param completion a block to execute once the results have been successfully read. + * snapshot will be `nil` only if error is `non-nil`. + */ +- (void)getDocumentsWithSource:(FIRVectorSource)source + completion:(void (^)(FIRVectorQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))completion NS_REFINED_FOR_SWIFT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRVectorQuerySnapshot.h b/Firestore/Source/Public/FirebaseFirestore/FIRVectorQuerySnapshot.h new file mode 100644 index 00000000000..7ecdfcc1662 --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRVectorQuerySnapshot.h @@ -0,0 +1,33 @@ +// +// FIRVectorQuerySnapshot.h +// FirebaseFirestoreInternal +// +// Created by Mark Duckworth on 7/25/24. +// +#import + +NS_ASSUME_NONNULL_BEGIN + +@class FIRVectorQuery; +@class FIRAggregateQuery; +@class FIRAggregateField; +@class FIRFieldPath; +@class FIRFirestore; +@class FIRFilter; +@class FIRQuerySnapshot; +@class FIRDocumentSnapshot; + +NS_SWIFT_NAME(VectorQuerySnapshot) +@interface FIRVectorQuerySnapshot : NSObject +@property(nonatomic, strong, readonly) FIRVectorQuery *query; +@property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata; +@property(nonatomic, readonly, getter=isEmpty) BOOL empty; +@property(nonatomic, readonly) NSInteger count; +@property(nonatomic, strong, readonly) NSArray *documents; +@property(nonatomic, strong, readonly) NSArray *documentChanges; +- (NSArray *)documentChangesWithIncludeMetadataChanges: + (BOOL)includeMetadataChanges NS_SWIFT_NAME(documentChanges(includeMetadataChanges:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRVectorSource.h b/Firestore/Source/Public/FirebaseFirestore/FIRVectorSource.h new file mode 100644 index 00000000000..5d4e995f10b --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRVectorSource.h @@ -0,0 +1,12 @@ +// +// FIRFirestoreVectorSource.h +// FirebaseFirestoreInternal +// +// Created by Mark Duckworth on 7/25/24. +// + +#import + +typedef NS_ENUM(NSUInteger, FIRVectorSource) { + FIRVectorSourceServer, +} NS_SWIFT_NAME(VectorSource); diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h b/Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h new file mode 100644 index 00000000000..2468379937c --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represent a vector type in Firestore documents. + * Create an instance with `@link `FieldValue.vector(...)`. + */ +NS_SWIFT_NAME(VectorValue) +@interface FIRVectorValue : NSObject + +/** Returns a copy of the raw number array that represents the vector. */ +@property(readonly) NSArray *array NS_REFINED_FOR_SWIFT; + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a `VectorValue` constructed with a copy of the given array of NSNumbrers. + * @param array An array of NSNumbers that represents a vector. + */ +- (instancetype)initWithArray:(NSArray *)array NS_REFINED_FOR_SWIFT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift index 8322bd0ae0a..20f5a6e8b2b 100644 --- a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift +++ b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift @@ -31,6 +31,7 @@ struct FirestorePassthroughTypes: StructureCodingPassthroughTypeResolver { t is GeoPoint || t is Timestamp || t is FieldValue || - t is DocumentReference + t is DocumentReference || + t is VectorValue } } diff --git a/Firestore/Swift/Source/Codable/VectorValue+Codable.swift b/Firestore/Swift/Source/Codable/VectorValue+Codable.swift new file mode 100644 index 00000000000..57d544af3b7 --- /dev/null +++ b/Firestore/Swift/Source/Codable/VectorValue+Codable.swift @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +/** + * A protocol describing the encodable properties of a VectorValue. + */ +private protocol CodableVectorValue: Codable { + var array: [Double] { get } + + init(__array: [NSNumber]) +} + +/** The keys in a Timestamp. Must match the properties of CodableTimestamp. */ +private enum VectorValueKeys: String, CodingKey { + case array +} + +/** + * An extension of VectorValue that implements the behavior of the Codable protocol. + * + * Note: this is implemented manually here because the Swift compiler can't synthesize these methods + * when declaring an extension to conform to Codable. + */ +extension CodableVectorValue { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: VectorValueKeys.self) + let data = try container.decode([Double].self, forKey: .array) + + let array = data.map { double in + NSNumber(value: double) + } + self.init(__array: array) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: VectorValueKeys.self) + try container.encode(array, forKey: .array) + } +} + +/** Extends VectorValue to conform to Codable. */ +extension VectorValue: CodableVectorValue {} diff --git a/Firestore/Swift/Source/SwiftAPI/FIeldPath+Expressible.swift b/Firestore/Swift/Source/SwiftAPI/FIeldPath+Expressible.swift new file mode 100644 index 00000000000..35c5cb3d157 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/FIeldPath+Expressible.swift @@ -0,0 +1,38 @@ +// +// FIeldPath+Expressible.swift +// FirebaseFirestore +// +// Created by Mark Duckworth on 8/2/24. +// + +import Foundation + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +// extension FieldPath : ExpressibleByStringLiteral { +// public required convenience init(stringLiteral: String) { +// self.init([stringLiteral]) +// } +// } + +private protocol ExpressibleByStringLiteralFieldPath: ExpressibleByStringLiteral { + init(_: [String]) +} + +/** + * An extension of VectorValue that implements the behavior of the Codable protocol. + * + * Note: this is implemented manually here because the Swift compiler can't synthesize these methods + * when declaring an extension to conform to Codable. + */ +extension ExpressibleByStringLiteralFieldPath { + public init(stringLiteral: String) { + self.init([stringLiteral]) + } +} + +/** Extends VectorValue to conform to Codable. */ +extension FieldPath: ExpressibleByStringLiteralFieldPath {} diff --git a/Firestore/Swift/Source/SwiftAPI/FieldValue+Swift.swift b/Firestore/Swift/Source/SwiftAPI/FieldValue+Swift.swift new file mode 100644 index 00000000000..ccab6238267 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/FieldValue+Swift.swift @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +public extension FieldValue { + /// Creates a new `VectorValue` constructed with a copy of the given array of Doubles. + /// - Parameter array: An array of Doubles. + /// - Returns: A new `VectorValue` constructed with a copy of the given array of Doubles. + static func vector(_ array: [Double]) -> VectorValue { + let nsNumbers = array.map { double in + NSNumber(value: double) + } + return FieldValue.__vector(with: nsNumbers) + } + + /// Creates a new `VectorValue` constructed with a copy of the given array of Floats. + /// - Parameter array: An array of Floats. + /// - Returns: A new `VectorValue` constructed with a copy of the given array of Floats. + static func vector(_ array: [Float]) -> VectorValue { + let nsNumbers = array.map { float in + NSNumber(value: float) + } + return FieldValue.__vector(with: nsNumbers) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/FindNearestOptions+Swift.swift b/Firestore/Swift/Source/SwiftAPI/FindNearestOptions+Swift.swift new file mode 100644 index 00000000000..9c48d90ed81 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/FindNearestOptions+Swift.swift @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +public extension FindNearestOptions { + var distanceThreshold: Double { + return __distanceThreshold.doubleValue + } + + func withDistanceThreshold(_ distanceThreshold: Double) -> FindNearestOptions { + return __withDistanceThreshold(NSNumber(value: distanceThreshold)) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Query+Swift.swift b/Firestore/Swift/Source/SwiftAPI/Query+Swift.swift new file mode 100644 index 00000000000..f54e3189110 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Query+Swift.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +public extension Query { + func findNearest(fieldPath: FieldPath, + queryVector: VectorValue, + limit: Int64, + distanceMeasure: FirestoreDistanceMeasure, + options: FindNearestOptions = FindNearestOptions()) -> VectorQuery { + fatalError("not implemented") + } + + func findNearest(fieldPath: FieldPath, + queryVector: [Double], + limit: Int64, + distanceMeasure: FirestoreDistanceMeasure, + options: FindNearestOptions = FindNearestOptions()) -> VectorQuery { + fatalError("not implemented") + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/VectorQuery+Swift.swift b/Firestore/Swift/Source/SwiftAPI/VectorQuery+Swift.swift new file mode 100644 index 00000000000..558c05bea9a --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/VectorQuery+Swift.swift @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +public extension VectorQuery { + func getDocuments(source: VectorSource) async throws -> VectorQuerySnapshot { + fatalError("not implemented") + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/VectorValue+Swift.swift b/Firestore/Swift/Source/SwiftAPI/VectorValue+Swift.swift new file mode 100644 index 00000000000..dffb35eb811 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/VectorValue+Swift.swift @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +public extension VectorValue { + convenience init(_ array: [Double]) { + let nsNumbers = array.map { float in + NSNumber(value: float) + } + + self.init(__array: nsNumbers) + } + + /// Returns a raw number array representation of the vector. + /// - Returns: An array of Double values representing the vector. + var array: [Double] { + return __array.map { Double(truncating: $0) } + } +} diff --git a/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift b/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift index 47aec91c4f6..406fb823a2a 100644 --- a/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift +++ b/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift @@ -83,13 +83,15 @@ class CodableIntegrationTests: FSTIntegrationTestCase { var ts: Timestamp var geoPoint: GeoPoint var docRef: DocumentReference + var vector: VectorValue } let docToWrite = documentRef() let model = Model(name: "test", age: 42, ts: Timestamp(seconds: 987_654_321, nanoseconds: 0), geoPoint: GeoPoint(latitude: 45, longitude: 54), - docRef: docToWrite) + docRef: docToWrite, + vector: FieldValue.vector([0.7, 0.6])) for flavor in allFlavors { try setData(from: model, forDocument: docToWrite, withFlavor: flavor) @@ -185,6 +187,31 @@ class CodableIntegrationTests: FSTIntegrationTestCase { } } + func testVectorValue() throws { + struct Model: Codable { + var name: String + var embedding: VectorValue + } + let model = Model( + name: "name", + embedding: VectorValue([0.1, 0.3, 0.4]) + ) + + let docToWrite = documentRef() + + for flavor in allFlavors { + try setData(from: model, forDocument: docToWrite, withFlavor: flavor) + + let data = try readDocument(forRef: docToWrite).data(as: Model.self) + + XCTAssertEqual( + data.embedding, + VectorValue([0.1, 0.3, 0.4]), + "Failed with flavor \(flavor)" + ) + } + } + func testDataBlob() throws { struct Model: Encodable { var name: String diff --git a/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift b/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift index 7b5c7812a20..61b4da23530 100644 --- a/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift +++ b/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift @@ -673,4 +673,84 @@ class SnapshotListenerSourceTests: FSTIntegrationTestCase { defaultRegistration.remove() cacheRegistration.remove() } + + func testListenToDocumentsWithVectors() throws { + let collection = collectionRef() + let doc = collection.document() + + let registration = collection.whereField("purpose", isEqualTo: "vector tests") + .addSnapshotListener(eventAccumulator.valueEventHandler) + + var querySnap = eventAccumulator.awaitEvent(withName: "snapshot") as! QuerySnapshot + XCTAssertEqual(querySnap.isEmpty, true) + + doc.setData([ + "purpose": "vector tests", + "vector0": FieldValue.vector([0.0]), + "vector1": FieldValue.vector([1, 2, 3.99]), + ]) + + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") as! QuerySnapshot + XCTAssertEqual(querySnap.isEmpty, false) + XCTAssertEqual( + querySnap.documents[0].data()["vector0"] as! VectorValue, + FieldValue.vector([0.0]) + ) + XCTAssertEqual( + querySnap.documents[0].data()["vector1"] as! VectorValue, + FieldValue.vector([1, 2, 3.99]) + ) + + doc.setData([ + "purpose": "vector tests", + "vector0": FieldValue.vector([0.0]), + "vector1": FieldValue.vector([1, 2, 3.99]), + "vector2": FieldValue.vector([0.0, 0, 0]), + ]) + + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") as! QuerySnapshot + XCTAssertEqual(querySnap.isEmpty, false) + XCTAssertEqual( + querySnap.documents[0].data()["vector0"] as! VectorValue, + FieldValue.vector([0.0]) + ) + XCTAssertEqual( + querySnap.documents[0].data()["vector1"] as! VectorValue, + FieldValue.vector([1, 2, 3.99]) + ) + XCTAssertEqual( + querySnap.documents[0].data()["vector2"] as! VectorValue, + FieldValue.vector([0.0, 0, 0]) + ) + + doc.updateData([ + "vector3": FieldValue.vector([-1, -200, -999.0]), + ]) + + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") as! QuerySnapshot + XCTAssertEqual(querySnap.isEmpty, false) + XCTAssertEqual( + querySnap.documents[0].data()["vector0"] as! VectorValue, + FieldValue.vector([0.0]) + ) + XCTAssertEqual( + querySnap.documents[0].data()["vector1"] as! VectorValue, + FieldValue.vector([1, 2, 3.99]) + ) + XCTAssertEqual( + querySnap.documents[0].data()["vector2"] as! VectorValue, + FieldValue.vector([0.0, 0, 0]) + ) + XCTAssertEqual( + querySnap.documents[0].data()["vector3"] as! VectorValue, + FieldValue.vector([-1, -200, -999.0]) + ) + + doc.delete() + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") as! QuerySnapshot + XCTAssertEqual(querySnap.isEmpty, true) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } } diff --git a/Firestore/Swift/Tests/Integration/VectorIntegrationTests.swift b/Firestore/Swift/Tests/Integration/VectorIntegrationTests.swift new file mode 100644 index 00000000000..22740e8058d --- /dev/null +++ b/Firestore/Swift/Tests/Integration/VectorIntegrationTests.swift @@ -0,0 +1,296 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Combine +import FirebaseFirestore +import Foundation + +// iOS 15 required for test implementation, not vector feature +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class VectorIntegrationTests: FSTIntegrationTestCase { + func exampleFindNearest() async throws { + let collection = collectionRef() + + let vectorQuery = collection.findNearest( + fieldPath: "embedding", + queryVector: [1.0, 2.0, 3.0], + limit: 10, + distanceMeasure: FirestoreDistanceMeasure.cosine + ) + + try await vectorQuery.getDocuments(source: VectorSource.server) + } + + func exampleFindNearestWithOptions() async throws { + let collection = collectionRef() + + let vectorQuery = collection.findNearest( + fieldPath: "embedding", + queryVector: [1.0, 2.0, 3.0], + limit: 10, + distanceMeasure: FirestoreDistanceMeasure.cosine, + options: FindNearestOptions().withDistanceResultFieldPath("distance") + .withDistanceThreshold(0.5) + ) + + try await vectorQuery.getDocuments(source: VectorSource.server) + } + + func testWriteAndReadVectorEmbeddings() async throws { + let collection = collectionRef() + + let ref = try await collection.addDocument(data: [ + "vector0": FieldValue.vector([0.0]), + "vector1": FieldValue.vector([1, 2, 3.99]), + ]) + + try await ref.setData([ + "vector0": FieldValue.vector([0.0]), + "vector1": FieldValue.vector([1, 2, 3.99]), + "vector2": FieldValue.vector([0, 0, 0] as [Double]), + ]) + + try await ref.updateData([ + "vector3": FieldValue.vector([-1, -200, -999] as [Double]), + ]) + + let snapshot = try await ref.getDocument() + XCTAssertEqual(snapshot.get("vector0") as? VectorValue, FieldValue.vector([0.0])) + XCTAssertEqual(snapshot.get("vector1") as? VectorValue, FieldValue.vector([1, 2, 3.99])) + XCTAssertEqual( + snapshot.get("vector2") as? VectorValue, + FieldValue.vector([0, 0, 0] as [Double]) + ) + XCTAssertEqual( + snapshot.get("vector3") as? VectorValue, + FieldValue.vector([-1, -200, -999] as [Double]) + ) + } + + @available(iOS 15, tvOS 15, macOS 12.0, macCatalyst 13, watchOS 7, *) + func testSdkOrdersVectorFieldSameWayAsBackend() async throws { + let collection = collectionRef() + + let docsInOrder: [[String: Any]] = [ + ["embedding": [1, 2, 3, 4, 5, 6]], + ["embedding": [100]], + ["embedding": FieldValue.vector([Double.infinity * -1])], + ["embedding": FieldValue.vector([-100.0])], + ["embedding": FieldValue.vector([100.0])], + ["embedding": FieldValue.vector([Double.infinity])], + ["embedding": FieldValue.vector([1, 2.0])], + ["embedding": FieldValue.vector([2, 2.0])], + ["embedding": FieldValue.vector([1, 2, 3.0])], + ["embedding": FieldValue.vector([1, 2, 3, 4.0])], + ["embedding": FieldValue.vector([1, 2, 3, 4, 5.0])], + ["embedding": FieldValue.vector([1, 2, 100, 4, 4.0])], + ["embedding": FieldValue.vector([100, 2, 3, 4, 5.0])], + ["embedding": ["HELLO": "WORLD"]], + ["embedding": ["hello": "world"]], + ] + + var docs: [[String: Any]] = [] + for data in docsInOrder { + let docRef = try await collection.addDocument(data: data) + docs.append(["id": docRef.documentID, "value": data]) + } + + // We validate that the SDK orders the vector field the same way as the backend + // by comparing the sort order of vector fields from getDocsFromServer and + // onSnapshot. onSnapshot will return sort order of the SDK, + // and getDocsFromServer will return sort order of the backend. + + let orderedQuery = collection.order(by: "embedding") + + let watchSnapshot = try await Future() { promise in + orderedQuery.addSnapshotListener { snapshot, error in + if let error { + promise(Result.failure(error)) + } + if let snapshot { + promise(Result.success(snapshot)) + } + } + }.value + + let getSnapshot = try await orderedQuery.getDocuments(source: .server) + + // Compare the snapshot (including sort order) of a snapshot + // from Query.onSnapshot() to an actual snapshot from Query.get() + XCTAssertEqual(watchSnapshot.count, getSnapshot.count) + for i in 0 ..< min(watchSnapshot.count, getSnapshot.count) { + XCTAssertEqual( + watchSnapshot.documents[i].documentID, + getSnapshot.documents[i].documentID + ) + } + + // Compare the snapshot (including sort order) of a snapshot + // from Query.onSnapshot() to the expected sort order from + // the backend. + XCTAssertEqual(watchSnapshot.count, docs.count) + for i in 0 ..< min(watchSnapshot.count, docs.count) { + XCTAssertEqual(watchSnapshot.documents[i].documentID, docs[i]["id"] as! String) + } + } + + func testSdkOrdersVectorFieldSameWayOnlineAndOffline() async throws { + let collection = collectionRef() + + let docsInOrder: [[String: Any]] = [ + ["embedding": [1, 2, 3, 4, 5, 6]], + ["embedding": [100]], + ["embedding": FieldValue.vector([Double.infinity * -1])], + ["embedding": FieldValue.vector([-100.0])], + ["embedding": FieldValue.vector([100.0])], + ["embedding": FieldValue.vector([Double.infinity])], + ["embedding": FieldValue.vector([1, 2.0])], + ["embedding": FieldValue.vector([2, 2.0])], + ["embedding": FieldValue.vector([1, 2, 3.0])], + ["embedding": FieldValue.vector([1, 2, 3, 4.0])], + ["embedding": FieldValue.vector([1, 2, 3, 4, 5.0])], + ["embedding": FieldValue.vector([1, 2, 100, 4, 4.0])], + ["embedding": FieldValue.vector([100, 2, 3, 4, 5.0])], + ["embedding": ["HELLO": "WORLD"]], + ["embedding": ["hello": "world"]], + ] + + var docIds: [String] = [] + for data in docsInOrder { + let docRef = try await collection.addDocument(data: data) + docIds.append(docRef.documentID) + } + + checkOnlineAndOfflineQuery(collection.order(by: "embedding"), matchesResult: docIds) + } + + func testSdkFiltersVectorFieldSameWayOnlineAndOffline() async throws { + let collection = collectionRef() + + let docsInOrder: [[String: Any]] = [ + ["embedding": [1, 2, 3, 4, 5, 6]], + ["embedding": [100]], + ["embedding": FieldValue.vector([Double.infinity * -1])], + ["embedding": FieldValue.vector([-100.0])], + ["embedding": FieldValue.vector([100.0])], + ["embedding": FieldValue.vector([Double.infinity])], + ["embedding": FieldValue.vector([1, 2.0])], + ["embedding": FieldValue.vector([2, 2.0])], + ["embedding": FieldValue.vector([1, 2, 3.0])], + ["embedding": FieldValue.vector([1, 2, 3, 4.0])], + ["embedding": FieldValue.vector([1, 2, 3, 4, 5.0])], + ["embedding": FieldValue.vector([1, 2, 100, 4, 4.0])], + ["embedding": FieldValue.vector([100, 2, 3, 4, 5.0])], + ["embedding": ["HELLO": "WORLD"]], + ["embedding": ["hello": "world"]], + ] + + var docIds: [String] = [] + for data in docsInOrder { + let docRef = try await collection.addDocument(data: data) + docIds.append(docRef.documentID) + } + + checkOnlineAndOfflineQuery( + collection.order(by: "embedding") + .whereField("embedding", isLessThan: FieldValue.vector([1, 2, 100, 4, 4.0])), + matchesResult: Array(docIds[2 ... 10]) + ) + checkOnlineAndOfflineQuery( + collection.order(by: "embedding") + .whereField("embedding", isGreaterThanOrEqualTo: FieldValue.vector([1, 2, 100, 4, 4.0])), + matchesResult: Array(docIds[11 ... 12]) + ) + } + + func testQueryVectorValueWrittenByCodable() async throws { + let collection = collectionRef() + + struct Model: Codable { + var name: String + var embedding: VectorValue + } + let model = Model( + name: "name", + embedding: FieldValue.vector([0.1, 0.3, 0.4]) + ) + + try collection.document().setData(from: model) + + let querySnap: QuerySnapshot = try await collection.whereField( + "embedding", + isEqualTo: FieldValue.vector([0.1, 0.3, 0.4]) + ).getDocuments() + + XCTAssertEqual(1, querySnap.count) + + let returnedModel: Model = try querySnap.documents[0].data(as: Model.self) + XCTAssertEqual(returnedModel.embedding, VectorValue([0.1, 0.3, 0.4])) + + let vectorData: [Double] = returnedModel.embedding.array + XCTAssertEqual(vectorData, [0.1, 0.3, 0.4]) + } + + func testQueryVectorValueWrittenByCodableClass() async throws { + let collection = collectionRef() + + struct Model: Codable { + var name: String + var embedding: VectorValue + } + + struct ModelWithDistance: Codable { + var name: String + var embedding: VectorValue + var distance: Double + } + + struct WithDistance: Decodable { + var distance: Double + var data: T + + private enum CodingKeys: String, CodingKey { + case distance + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + distance = try container.decode(Double.self, forKey: .distance) + data = try T(from: decoder) + } + } + + let model = ModelWithDistance( + name: "name", + embedding: FieldValue.vector([0.1, 0.3, 0.4]), + distance: 0.2 + ) + + try collection.document().setData(from: model) + + let querySnap: QuerySnapshot = try await collection.getDocuments() + + XCTAssertEqual(1, querySnap.count) + + let returnedModel: WithDistance = + try querySnap.documents[0].data(as: WithDistance.self) + XCTAssertEqual(returnedModel.data.embedding, VectorValue([0.1, 0.3, 0.4])) + XCTAssertEqual(returnedModel.distance, 0.2) + + let vectorData: [Double] = returnedModel.data.embedding.array + XCTAssertEqual(vectorData, [0.1, 0.3, 0.4]) + } +} diff --git a/Firestore/core/src/core/target.cc b/Firestore/core/src/core/target.cc index 76b9c625f57..3002a955da1 100644 --- a/Firestore/core/src/core/target.cc +++ b/Firestore/core/src/core/target.cc @@ -219,8 +219,7 @@ Target::IndexBoundValue Target::GetAscendingBound( switch (field_filter.op()) { case FieldFilter::Operator::LessThan: case FieldFilter::Operator::LessThanOrEqual: - filter_value = - model::GetLowerBound(field_filter.value().which_value_type); + filter_value = model::GetLowerBound(field_filter.value()); break; case FieldFilter::Operator::Equal: case FieldFilter::Operator::In: @@ -284,8 +283,7 @@ Target::IndexBoundValue Target::GetDescendingBound( switch (field_filter.op()) { case FieldFilter::Operator::GreaterThanOrEqual: case FieldFilter::Operator::GreaterThan: - filter_value = - model::GetUpperBound(field_filter.value().which_value_type); + filter_value = model::GetUpperBound(field_filter.value()); filter_inclusive = false; break; case FieldFilter::Operator::Equal: diff --git a/Firestore/core/src/index/firestore_index_value_writer.cc b/Firestore/core/src/index/firestore_index_value_writer.cc index 97a80351a35..4587844b930 100644 --- a/Firestore/core/src/index/firestore_index_value_writer.cc +++ b/Firestore/core/src/index/firestore_index_value_writer.cc @@ -21,6 +21,7 @@ #include #include "Firestore/core/src/model/resource_path.h" +#include "Firestore/core/src/model/value_util.h" #include "Firestore/core/src/nanopb/nanopb_util.h" namespace firebase { @@ -46,6 +47,7 @@ enum IndexType { kReference = 37, kGeopoint = 45, kArray = 50, + kVector = 53, kMap = 55, kReferenceSegment = 60, // A terminator that indicates that a truncatable value was not truncated. @@ -105,6 +107,31 @@ void WriteIndexArray(const google_firestore_v1_ArrayValue& array_index_value, } } +void WriteIndexVector(const google_firestore_v1_MapValue& map_index_value, + DirectionalIndexByteEncoder* encoder) { + WriteValueTypeLabel(encoder, IndexType::kVector); + + absl::optional valueIndex = + model::IndexOfKey(map_index_value, model::kRawVectorValueFieldKey, + model::kVectorValueFieldKey); + + if (!valueIndex.has_value() || + map_index_value.fields[valueIndex.value()].value.which_value_type != + google_firestore_v1_Value_array_value_tag) { + return WriteIndexArray(model::MinArray().array_value, encoder); + } + + auto value = map_index_value.fields[valueIndex.value()].value; + + // Vectors sort first by length + WriteValueTypeLabel(encoder, IndexType::kNumber); + encoder->WriteLong(value.array_value.values_count); + + // Vectors then sort by position value + WriteIndexString(model::kVectorValueFieldKey, encoder); + WriteIndexValueAux(value, encoder); +} + void WriteIndexMap(google_firestore_v1_MapValue map_index_value, DirectionalIndexByteEncoder* encoder) { WriteValueTypeLabel(encoder, IndexType::kMap); @@ -183,6 +210,9 @@ void WriteIndexValueAux(const google_firestore_v1_Value& index_value, if (model::IsMaxValue(index_value)) { WriteValueTypeLabel(encoder, std::numeric_limits::max()); break; + } else if (model::IsVectorValue(index_value)) { + WriteIndexVector(index_value.map_value, encoder); + break; } WriteIndexMap(index_value.map_value, encoder); WriteTruncationMarker(encoder); diff --git a/Firestore/core/src/model/value_util.cc b/Firestore/core/src/model/value_util.cc index 61c4a8c865f..f363d2d7090 100644 --- a/Firestore/core/src/model/value_util.cc +++ b/Firestore/core/src/model/value_util.cc @@ -38,26 +38,33 @@ namespace firebase { namespace firestore { namespace model { -namespace { + +using nanopb::Message; +using util::ComparisonResult; /** The smallest reference value. */ pb_bytes_array_s* kMinimumReferenceValue = nanopb::MakeBytesArray("projects//databases//documents/"); -/** The field type of a maximum proto value. */ -const char* kRawMaxValueFieldKey = "__type__"; -pb_bytes_array_s* kMaxValueFieldKey = - nanopb::MakeBytesArray(kRawMaxValueFieldKey); +/** The field type of a special object type. */ +const char* kRawTypeValueFieldKey = "__type__"; +pb_bytes_array_s* kTypeValueFieldKey = + nanopb::MakeBytesArray(kRawTypeValueFieldKey); /** The field value of a maximum proto value. */ const char* kRawMaxValueFieldValue = "__max__"; pb_bytes_array_s* kMaxValueFieldValue = nanopb::MakeBytesArray(kRawMaxValueFieldValue); -} // namespace +/** The type of a VectorValue proto. */ +const char* kRawVectorTypeFieldValue = "__vector__"; +pb_bytes_array_s* kVectorTypeFieldValue = + nanopb::MakeBytesArray(kRawVectorTypeFieldValue); -using nanopb::Message; -using util::ComparisonResult; +/** The value key of a VectorValue proto. */ +const char* kRawVectorValueFieldKey = "value"; +pb_bytes_array_s* kVectorValueFieldKey = + nanopb::MakeBytesArray(kRawVectorValueFieldKey); TypeOrder GetTypeOrder(const google_firestore_v1_Value& value) { switch (value.which_value_type) { @@ -94,6 +101,8 @@ TypeOrder GetTypeOrder(const google_firestore_v1_Value& value) { return TypeOrder::kServerTimestamp; } else if (IsMaxValue(value)) { return TypeOrder::kMaxValue; + } else if (IsVectorValue(value)) { + return TypeOrder::kVector; } return TypeOrder::kMap; } @@ -253,6 +262,43 @@ ComparisonResult CompareMaps(const google_firestore_v1_MapValue& left, return util::Compare(left_map->fields_count, right_map->fields_count); } +ComparisonResult CompareVectors(const google_firestore_v1_Value& left, + const google_firestore_v1_Value& right) { + HARD_ASSERT(IsVectorValue(left) && IsVectorValue(right), + "Cannot compare non-vector values as vectors."); + + absl::optional leftIndex = + IndexOfKey(left.map_value, kRawVectorValueFieldKey, kVectorValueFieldKey); + absl::optional rightIndex = IndexOfKey( + right.map_value, kRawVectorValueFieldKey, kVectorValueFieldKey); + + pb_size_t leftArrayLength = 0; + google_firestore_v1_Value leftArray; + if (leftIndex.has_value()) { + leftArray = left.map_value.fields[leftIndex.value()].value; + leftArrayLength = leftArray.array_value.values_count; + } + + pb_size_t rightArrayLength = 0; + google_firestore_v1_Value rightArray; + if (leftIndex.has_value()) { + rightArray = right.map_value.fields[rightIndex.value()].value; + rightArrayLength = rightArray.array_value.values_count; + } + + if (leftArrayLength == 0 && rightArrayLength == 0) { + return ComparisonResult::Same; + } + + ComparisonResult lengthCompare = + util::Compare(leftArrayLength, rightArrayLength); + if (lengthCompare != ComparisonResult::Same) { + return lengthCompare; + } + + return CompareArrays(leftArray, rightArray); +} + ComparisonResult Compare(const google_firestore_v1_Value& left, const google_firestore_v1_Value& right) { TypeOrder left_type = GetTypeOrder(left); @@ -297,6 +343,9 @@ ComparisonResult Compare(const google_firestore_v1_Value& left, case TypeOrder::kMap: return CompareMaps(left.map_value, right.map_value); + case TypeOrder::kVector: + return CompareVectors(left, right); + case TypeOrder::kMaxValue: return util::ComparisonResult::Same; @@ -425,6 +474,7 @@ bool Equals(const google_firestore_v1_Value& lhs, case TypeOrder::kArray: return ArrayEquals(lhs.array_value, rhs.array_value); + case TypeOrder::kVector: case TypeOrder::kMap: return MapValueEquals(lhs.map_value, rhs.map_value); @@ -539,106 +589,87 @@ std::string CanonicalId(const google_firestore_v1_ArrayValue& value) { return CanonifyArray(value); } -google_firestore_v1_Value GetLowerBound(pb_size_t value_tag) { - switch (value_tag) { +google_firestore_v1_Value GetLowerBound( + const google_firestore_v1_Value& value) { + switch (value.which_value_type) { case google_firestore_v1_Value_null_value_tag: return NullValue(); case google_firestore_v1_Value_boolean_value_tag: { - google_firestore_v1_Value value; - value.which_value_type = value_tag; - value.boolean_value = false; - return value; + return MinBoolean(); } case google_firestore_v1_Value_integer_value_tag: case google_firestore_v1_Value_double_value_tag: { - return NaNValue(); + return MinNumber(); } case google_firestore_v1_Value_timestamp_value_tag: { - google_firestore_v1_Value value; - value.which_value_type = value_tag; - value.timestamp_value.seconds = std::numeric_limits::min(); - value.timestamp_value.nanos = 0; - return value; + return MinTimestamp(); } case google_firestore_v1_Value_string_value_tag: { - google_firestore_v1_Value value; - value.which_value_type = value_tag; - value.string_value = nullptr; - return value; + return MinString(); } case google_firestore_v1_Value_bytes_value_tag: { - google_firestore_v1_Value value; - value.which_value_type = value_tag; - value.bytes_value = nullptr; - return value; + return MinBytes(); } case google_firestore_v1_Value_reference_value_tag: { - google_firestore_v1_Value result; - result.which_value_type = google_firestore_v1_Value_reference_value_tag; - result.reference_value = kMinimumReferenceValue; - return result; + return MinReference(); } case google_firestore_v1_Value_geo_point_value_tag: { - google_firestore_v1_Value value; - value.which_value_type = value_tag; - value.geo_point_value.latitude = -90.0; - value.geo_point_value.longitude = -180.0; - return value; + return MinGeoPoint(); } case google_firestore_v1_Value_array_value_tag: { - google_firestore_v1_Value value; - value.which_value_type = value_tag; - value.array_value.values = nullptr; - value.array_value.values_count = 0; - return value; + return MinArray(); } case google_firestore_v1_Value_map_value_tag: { - google_firestore_v1_Value value; - value.which_value_type = value_tag; - value.map_value.fields = nullptr; - value.map_value.fields_count = 0; - return value; + if (IsVectorValue(value)) { + return MinVector(); + } + + return MinMap(); } default: - HARD_FAIL("Invalid type value: %s", value_tag); + HARD_FAIL("Invalid type value: %s", value.which_value_type); } } -google_firestore_v1_Value GetUpperBound(pb_size_t value_tag) { - switch (value_tag) { +google_firestore_v1_Value GetUpperBound( + const google_firestore_v1_Value& value) { + switch (value.which_value_type) { case google_firestore_v1_Value_null_value_tag: - return GetLowerBound(google_protobuf_BoolValue_value_tag); + return MinBoolean(); case google_firestore_v1_Value_boolean_value_tag: - return GetLowerBound(google_firestore_v1_Value_integer_value_tag); + return MinNumber(); case google_firestore_v1_Value_integer_value_tag: case google_firestore_v1_Value_double_value_tag: - return GetLowerBound(google_firestore_v1_Value_timestamp_value_tag); + return MinTimestamp(); case google_firestore_v1_Value_timestamp_value_tag: - return GetLowerBound(google_firestore_v1_Value_string_value_tag); + return MinString(); case google_firestore_v1_Value_string_value_tag: - return GetLowerBound(google_firestore_v1_Value_bytes_value_tag); + return MinBytes(); case google_firestore_v1_Value_bytes_value_tag: - return GetLowerBound(google_firestore_v1_Value_reference_value_tag); + return MinReference(); case google_firestore_v1_Value_reference_value_tag: - return GetLowerBound(google_firestore_v1_Value_geo_point_value_tag); + return MinGeoPoint(); case google_firestore_v1_Value_geo_point_value_tag: - return GetLowerBound(google_firestore_v1_Value_array_value_tag); + return MinArray(); case google_firestore_v1_Value_array_value_tag: - return GetLowerBound(google_firestore_v1_Value_map_value_tag); + return MinVector(); case google_firestore_v1_Value_map_value_tag: + if (IsVectorValue(value)) { + return MinMap(); + } return MaxValue(); default: - HARD_FAIL("Invalid type value: %s", value_tag); + HARD_FAIL("Invalid type value: %s", value.which_value_type); } } @@ -693,7 +724,7 @@ google_firestore_v1_Value MaxValue() { "google_firestore_v1_MapValue_FieldsEntry should be " "trivially-destructible; otherwise, it should use NoDestructor below."); static google_firestore_v1_MapValue_FieldsEntry field_entry; - field_entry.key = kMaxValueFieldKey; + field_entry.key = kTypeValueFieldKey; field_entry.value = value; google_firestore_v1_MapValue map_value; @@ -718,9 +749,9 @@ bool IsMaxValue(const google_firestore_v1_Value& value) { // Comparing the pointer address, then actual content if addresses are // different. - if (value.map_value.fields[0].key != kMaxValueFieldKey && + if (value.map_value.fields[0].key != kTypeValueFieldKey && nanopb::MakeStringView(value.map_value.fields[0].key) != - kRawMaxValueFieldKey) { + kRawTypeValueFieldKey) { return false; } @@ -736,6 +767,65 @@ bool IsMaxValue(const google_firestore_v1_Value& value) { kRawMaxValueFieldValue; } +absl::optional IndexOfKey( + const google_firestore_v1_MapValue& mapValue, + const char* kRawTypeValueFieldKey, + pb_bytes_array_s* kTypeValueFieldKey) { + for (pb_size_t i = 0; i < mapValue.fields_count; i++) { + if (mapValue.fields[i].key == kTypeValueFieldKey || + nanopb::MakeStringView(mapValue.fields[i].key) == + kRawTypeValueFieldKey) { + return i; + } + } + + return absl::nullopt; +} + +bool IsVectorValue(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 < 2) { + return false; + } + + absl::optional typeFieldIndex = + IndexOfKey(value.map_value, kRawTypeValueFieldKey, kTypeValueFieldKey); + if (!typeFieldIndex.has_value()) { + return false; + } + + if (value.map_value.fields[typeFieldIndex.value()].value.which_value_type != + google_firestore_v1_Value_string_value_tag) { + return false; + } + + // Comparing the pointer address, then actual content if addresses are + // different. + if (value.map_value.fields[typeFieldIndex.value()].value.string_value != + kVectorTypeFieldValue && + nanopb::MakeStringView( + value.map_value.fields[typeFieldIndex.value()].value.string_value) != + kRawVectorTypeFieldValue) { + return false; + } + + absl::optional valueFieldIndex = IndexOfKey( + value.map_value, kRawVectorValueFieldKey, kVectorValueFieldKey); + if (!valueFieldIndex.has_value()) { + return false; + } + + if (value.map_value.fields[valueFieldIndex.value()].value.which_value_type != + google_firestore_v1_Value_array_value_tag) { + return false; + } + + return true; +} + google_firestore_v1_Value NaNValue() { google_firestore_v1_Value nan_value; nan_value.which_value_type = google_firestore_v1_Value_double_value_tag; @@ -748,6 +838,98 @@ bool IsNaNValue(const google_firestore_v1_Value& value) { std::isnan(value.double_value); } +google_firestore_v1_Value MinBoolean() { + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_boolean_value_tag; + lowerBound.boolean_value = false; + return lowerBound; +} + +google_firestore_v1_Value MinNumber() { + return NaNValue(); +} + +google_firestore_v1_Value MinTimestamp() { + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_timestamp_value_tag; + lowerBound.timestamp_value.seconds = std::numeric_limits::min(); + lowerBound.timestamp_value.nanos = 0; + return lowerBound; +} + +google_firestore_v1_Value MinString() { + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_string_value_tag; + lowerBound.string_value = nullptr; + return lowerBound; +} + +google_firestore_v1_Value MinBytes() { + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_bytes_value_tag; + lowerBound.bytes_value = nullptr; + return lowerBound; +} + +google_firestore_v1_Value MinReference() { + google_firestore_v1_Value result; + result.which_value_type = google_firestore_v1_Value_reference_value_tag; + result.reference_value = kMinimumReferenceValue; + return result; +} + +google_firestore_v1_Value MinGeoPoint() { + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_geo_point_value_tag; + lowerBound.geo_point_value.latitude = -90.0; + lowerBound.geo_point_value.longitude = -180.0; + return lowerBound; +} + +google_firestore_v1_Value MinArray() { + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_array_value_tag; + lowerBound.array_value.values = nullptr; + lowerBound.array_value.values_count = 0; + return lowerBound; +} + +google_firestore_v1_Value MinVector() { + google_firestore_v1_Value typeValue; + typeValue.which_value_type = google_firestore_v1_Value_string_value_tag; + typeValue.string_value = kVectorTypeFieldValue; + + google_firestore_v1_MapValue_FieldsEntry* field_entries = + nanopb::MakeArray(2); + field_entries[0].key = kTypeValueFieldKey; + field_entries[0].value = typeValue; + + google_firestore_v1_Value arrayValue; + arrayValue.which_value_type = google_firestore_v1_Value_array_value_tag; + arrayValue.array_value.values = nullptr; + arrayValue.array_value.values_count = 0; + field_entries[1].key = kVectorValueFieldKey; + field_entries[1].value = arrayValue; + + google_firestore_v1_MapValue map_value; + map_value.fields_count = 2; + map_value.fields = field_entries; + + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_map_value_tag; + lowerBound.map_value = map_value; + + return lowerBound; +} + +google_firestore_v1_Value MinMap() { + google_firestore_v1_Value lowerBound; + lowerBound.which_value_type = google_firestore_v1_Value_map_value_tag; + lowerBound.map_value.fields = nullptr; + lowerBound.map_value.fields_count = 0; + return lowerBound; +} + Message RefValue( const model::DatabaseId& database_id, const model::DocumentKey& document_key) { diff --git a/Firestore/core/src/model/value_util.h b/Firestore/core/src/model/value_util.h index 91e26a21ebb..708b71ccd16 100644 --- a/Firestore/core/src/model/value_util.h +++ b/Firestore/core/src/model/value_util.h @@ -23,6 +23,7 @@ #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/core/src/nanopb/message.h" +#include "Firestore/core/src/nanopb/nanopb_util.h" #include "absl/types/optional.h" namespace firebase { @@ -37,6 +38,25 @@ namespace model { class DocumentKey; class DatabaseId; +/** The smallest reference value. */ +extern pb_bytes_array_s* kMinimumReferenceValue; + +/** The field type of a special object type. */ +extern const char* kRawTypeValueFieldKey; +extern pb_bytes_array_s* kTypeValueFieldKey; + +/** The field value of a maximum proto value. */ +extern const char* kRawMaxValueFieldValue; +extern pb_bytes_array_s* kMaxValueFieldValue; + +/** The type of a VectorValue proto. */ +extern const char* kRawVectorTypeFieldValue; +extern pb_bytes_array_s* kVectorTypeFieldValue; + +/** The value key of a VectorValue proto. */ +extern const char* kRawVectorValueFieldKey; +extern pb_bytes_array_s* kVectorValueFieldKey; + /** * The order of types in Firestore. This order is based on the backend's * ordering, but modified to support server timestamps. @@ -52,8 +72,9 @@ enum class TypeOrder { kReference = 7, kGeoPoint = 8, kArray = 9, - kMap = 10, - kMaxValue = 11 + kVector = 10, + kMap = 11, + kMaxValue = 12 }; /** Returns the backend's type order of the given Value type. */ @@ -94,7 +115,7 @@ std::string CanonicalId(const google_firestore_v1_Value& value); * The returned value might point to heap allocated memory that is owned by * this function. To take ownership of this memory, call `DeepClone`. */ -google_firestore_v1_Value GetLowerBound(pb_size_t value_tag); +google_firestore_v1_Value GetLowerBound(const google_firestore_v1_Value& value); /** * Returns the largest value for the given value type (exclusive). @@ -102,7 +123,7 @@ google_firestore_v1_Value GetLowerBound(pb_size_t value_tag); * The returned value might point to heap allocated memory that is owned by * this function. To take ownership of this memory, call `DeepClone`. */ -google_firestore_v1_Value GetUpperBound(pb_size_t value_tag); +google_firestore_v1_Value GetUpperBound(const google_firestore_v1_Value& value); /** * Generates the canonical ID for the provided array value (as used in Target @@ -155,6 +176,22 @@ google_firestore_v1_Value MaxValue(); */ bool IsMaxValue(const google_firestore_v1_Value& value); +/** + * Returns `true` if `value` represents a VectorValue.. + */ +bool IsVectorValue(const google_firestore_v1_Value& value); + +/** + * Returns the index of the specified key (`kRawTypeValueFieldKey`) in the + * map (`mapValue`). `kTypeValueFieldKey` is an alternative representation + * of the key specified in `kRawTypeValueFieldKey`. + * If the key is not found, then `absl::nullopt` is returned. + */ +absl::optional IndexOfKey( + const google_firestore_v1_MapValue& mapValue, + const char* kRawTypeValueFieldKey, + pb_bytes_array_s* kTypeValueFieldKey); + /** * Returns `NaN` in its Protobuf representation. * @@ -166,6 +203,26 @@ google_firestore_v1_Value NaNValue(); /** Returns `true` if `value` is `NaN` in its Protobuf representation. */ bool IsNaNValue(const google_firestore_v1_Value& value); +google_firestore_v1_Value MinBoolean(); + +google_firestore_v1_Value MinNumber(); + +google_firestore_v1_Value MinTimestamp(); + +google_firestore_v1_Value MinString(); + +google_firestore_v1_Value MinBytes(); + +google_firestore_v1_Value MinReference(); + +google_firestore_v1_Value MinGeoPoint(); + +google_firestore_v1_Value MinArray(); + +google_firestore_v1_Value MinVector(); + +google_firestore_v1_Value MinMap(); + /** * Returns a Protobuf reference value representing the given location. * diff --git a/Firestore/core/test/unit/local/leveldb_index_manager_test.cc b/Firestore/core/test/unit/local/leveldb_index_manager_test.cc index 290218d254e..3cbc7667dd8 100644 --- a/Firestore/core/test/unit/local/leveldb_index_manager_test.cc +++ b/Firestore/core/test/unit/local/leveldb_index_manager_test.cc @@ -49,6 +49,7 @@ using testutil::Map; using testutil::OrderBy; using testutil::OrFilters; using testutil::Query; +using testutil::VectorType; using testutil::Version; std::unique_ptr PersistenceFactory() { @@ -929,6 +930,52 @@ TEST_F(LevelDbIndexManagerTest, IndexEntriesAreUpdatedWithDeletedDoc) { }); } +TEST_F(LevelDbIndexManagerTest, IndexVectorValueFields) { + persistence_->Run("TestIndexVectorValueFields", [&]() { + index_manager_->Start(); + index_manager_->AddFieldIndex( + MakeFieldIndex("coll", "embedding", model::Segment::kAscending)); + + AddDoc("coll/arr1", Map("embedding", Array(1.0, 2.0, 3.0))); + AddDoc("coll/map2", Map("embedding", Map())); + AddDoc("coll/doc3", Map("embedding", VectorType(4.0, 5.0, 6.0))); + AddDoc("coll/doc4", Map("embedding", VectorType(5.0))); + + auto query = Query("coll").AddingOrderBy(OrderBy("embedding")); + { + SCOPED_TRACE("no filter"); + VerifyResults(query, + {"coll/arr1", "coll/doc4", "coll/doc3", "coll/map2"}); + } + + query = + Query("coll") + .AddingOrderBy(OrderBy("embedding")) + .AddingFilter(Filter("embedding", "==", VectorType(4.0, 5.0, 6.0))); + { + SCOPED_TRACE("vector<4.0, 5.0, 6.0>"); + VerifyResults(query, {"coll/doc3"}); + } + + query = + Query("coll") + .AddingOrderBy(OrderBy("embedding")) + .AddingFilter(Filter("embedding", ">", VectorType(4.0, 5.0, 6.0))); + { + SCOPED_TRACE("> vector<4.0, 5.0, 6.0>"); + VerifyResults(query, {}); + } + + query = Query("coll") + .AddingOrderBy(OrderBy("embedding")) + .AddingFilter(Filter("embedding", ">", VectorType(4.0))); + { + SCOPED_TRACE("> vector<4.0>"); + VerifyResults(query, {"coll/doc4", "coll/doc3"}); + } + }); +} + TEST_F(LevelDbIndexManagerTest, AdvancedQueries) { // This test compares local query results with those received from the Java // Server SDK. diff --git a/Firestore/core/test/unit/model/value_util_test.cc b/Firestore/core/test/unit/model/value_util_test.cc index d4db43dfe20..c6d2479929c 100644 --- a/Firestore/core/test/unit/model/value_util_test.cc +++ b/Firestore/core/test/unit/model/value_util_test.cc @@ -99,6 +99,9 @@ class ValueUtilTest : public ::testing::Test { ComparisonResult expected_result) { for (pb_size_t i = 0; i < left->values_count; ++i) { for (pb_size_t j = 0; j < right->values_count; ++j) { + if (expected_result != Compare(left->values[i], right->values[j])) { + std::cout << "here" << std::endl; + } EXPECT_EQ(expected_result, Compare(left->values[i], right->values[j])) << "Order check failed for '" << CanonicalId(left->values[i]) << "' and '" << CanonicalId(right->values[j]) << "' (expected " @@ -243,6 +246,8 @@ TEST_F(ValueUtilTest, Equality) { Add(equals_group, Array("foo", "bar"), Array("foo", "bar")); Add(equals_group, Array("foo", "bar", "baz")); Add(equals_group, Array("foo")); + Add(equals_group, Map("__type__", "__vector__", "value", Array()), + DeepClone(MinVector())); Add(equals_group, Map("bar", 1, "foo", 2), Map("bar", 1, "foo", 2)); Add(equals_group, Map("bar", 2, "foo", 1)); Add(equals_group, Map("bar", 1)); @@ -271,8 +276,7 @@ TEST_F(ValueUtilTest, StrictOrdering) { Add(comparison_groups, true); // numbers - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_integer_value_tag))); + Add(comparison_groups, DeepClone(MinNumber())); Add(comparison_groups, -1e20); Add(comparison_groups, std::numeric_limits::min()); Add(comparison_groups, -0.1); @@ -285,8 +289,7 @@ TEST_F(ValueUtilTest, StrictOrdering) { Add(comparison_groups, 1e20); // dates - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_timestamp_value_tag))); + Add(comparison_groups, DeepClone(MinTimestamp())); Add(comparison_groups, kTimestamp1); Add(comparison_groups, kTimestamp2); @@ -316,8 +319,7 @@ TEST_F(ValueUtilTest, StrictOrdering) { Add(comparison_groups, BlobValue(255)); // resource names - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_reference_value_tag))); + Add(comparison_groups, DeepClone(MinReference())); Add(comparison_groups, RefValue(DbId("p1/d1"), Key("c1/doc1"))); Add(comparison_groups, RefValue(DbId("p1/d1"), Key("c1/doc2"))); Add(comparison_groups, RefValue(DbId("p1/d1"), Key("c10/doc1"))); @@ -340,23 +342,28 @@ TEST_F(ValueUtilTest, StrictOrdering) { Add(comparison_groups, GeoPoint(90, 180)); // arrays - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_array_value_tag))); + Add(comparison_groups, DeepClone(MinArray())); Add(comparison_groups, Array("bar")); Add(comparison_groups, Array("foo", 1)); Add(comparison_groups, Array("foo", 2)); Add(comparison_groups, Array("foo", "0")); - // objects + // vectors + Add(comparison_groups, DeepClone(MinVector())); + Add(comparison_groups, Map("__type__", "__vector__", "value", Array(100))); + Add(comparison_groups, + Map("__type__", "__vector__", "value", Array(1.0, 2.0, 3.0))); Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_map_value_tag))); + Map("__type__", "__vector__", "value", Array(1.0, 3.0, 2.0))); + + // objects + Add(comparison_groups, DeepClone(MinMap())); Add(comparison_groups, Map("bar", 0)); Add(comparison_groups, Map("bar", 0, "foo", 1)); Add(comparison_groups, Map("foo", 1)); Add(comparison_groups, Map("foo", 2)); Add(comparison_groups, Map("foo", "0")); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_map_value_tag))); + Add(comparison_groups, DeepClone(MaxValue())); for (size_t i = 0; i < comparison_groups.size(); ++i) { for (size_t j = i; j < comparison_groups.size(); ++j) { @@ -377,25 +384,19 @@ TEST_F(ValueUtilTest, RelaxedOrdering) { std::vector> comparison_groups; // null first - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_null_value_tag))); + Add(comparison_groups, DeepClone(NullValue())); Add(comparison_groups, nullptr); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_null_value_tag))); + Add(comparison_groups, DeepClone(MinBoolean())); // booleans - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_boolean_value_tag))); + Add(comparison_groups, DeepClone(MinBoolean())); Add(comparison_groups, false); Add(comparison_groups, true); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_boolean_value_tag))); + Add(comparison_groups, DeepClone(MinNumber())); // numbers - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_integer_value_tag))); - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_double_value_tag))); + Add(comparison_groups, DeepClone(MinNumber())); + Add(comparison_groups, DeepClone(MinNumber())); Add(comparison_groups, -1e20); Add(comparison_groups, std::numeric_limits::min()); Add(comparison_groups, -0.1); @@ -406,14 +407,11 @@ TEST_F(ValueUtilTest, RelaxedOrdering) { Add(comparison_groups, 1.0, 1L); Add(comparison_groups, std::numeric_limits::max()); Add(comparison_groups, 1e20); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_integer_value_tag))); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_double_value_tag))); + Add(comparison_groups, DeepClone(MinTimestamp())); + Add(comparison_groups, DeepClone(MinTimestamp())); // dates - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_timestamp_value_tag))); + Add(comparison_groups, DeepClone(MinTimestamp())); Add(comparison_groups, kTimestamp1); Add(comparison_groups, kTimestamp2); @@ -421,12 +419,10 @@ TEST_F(ValueUtilTest, RelaxedOrdering) { // NOTE: server timestamps can't be parsed with . Add(comparison_groups, EncodeServerTimestamp(kTimestamp1, absl::nullopt)); Add(comparison_groups, EncodeServerTimestamp(kTimestamp2, absl::nullopt)); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_timestamp_value_tag))); + Add(comparison_groups, DeepClone(MinString())); // strings - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_string_value_tag))); + Add(comparison_groups, DeepClone(MinString())); Add(comparison_groups, ""); Add(comparison_groups, "\001\ud7ff\ue000\uffff"); Add(comparison_groups, "(╯°□°)╯︵ ┻━┻"); @@ -438,35 +434,29 @@ TEST_F(ValueUtilTest, RelaxedOrdering) { Add(comparison_groups, "æ"); // latin small letter e with acute accent + latin small letter a Add(comparison_groups, "\u00e9a"); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_string_value_tag))); + Add(comparison_groups, DeepClone(MinBytes())); // blobs - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_bytes_value_tag))); + Add(comparison_groups, DeepClone(MinBytes())); Add(comparison_groups, BlobValue()); Add(comparison_groups, BlobValue(0)); Add(comparison_groups, BlobValue(0, 1, 2, 3, 4)); Add(comparison_groups, BlobValue(0, 1, 2, 4, 3)); Add(comparison_groups, BlobValue(255)); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_bytes_value_tag))); + Add(comparison_groups, DeepClone(MinReference())); // resource names - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_reference_value_tag))); + Add(comparison_groups, DeepClone(MinReference())); Add(comparison_groups, RefValue(DbId("p1/d1"), Key("c1/doc1"))); Add(comparison_groups, RefValue(DbId("p1/d1"), Key("c1/doc2"))); Add(comparison_groups, RefValue(DbId("p1/d1"), Key("c10/doc1"))); Add(comparison_groups, RefValue(DbId("p1/d1"), Key("c2/doc1"))); Add(comparison_groups, RefValue(DbId("p1/d2"), Key("c1/doc1"))); Add(comparison_groups, RefValue(DbId("p2/d1"), Key("c1/doc1"))); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_reference_value_tag))); + Add(comparison_groups, DeepClone(MinGeoPoint())); // geo points - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_geo_point_value_tag))); + Add(comparison_groups, DeepClone(MinGeoPoint())); Add(comparison_groups, GeoPoint(-90, -180)); Add(comparison_groups, GeoPoint(-90, 0)); Add(comparison_groups, GeoPoint(-90, 180)); @@ -479,29 +469,32 @@ TEST_F(ValueUtilTest, RelaxedOrdering) { Add(comparison_groups, GeoPoint(90, -180)); Add(comparison_groups, GeoPoint(90, 0)); Add(comparison_groups, GeoPoint(90, 180)); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_geo_point_value_tag))); + Add(comparison_groups, DeepClone(MinArray())); // arrays - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_array_value_tag))); + Add(comparison_groups, DeepClone(MinArray())); Add(comparison_groups, Array("bar")); Add(comparison_groups, Array("foo", 1)); Add(comparison_groups, Array("foo", 2)); Add(comparison_groups, Array("foo", "0")); + Add(comparison_groups, DeepClone(MinVector())); + + // vectors + Add(comparison_groups, DeepClone(MinVector())); + Add(comparison_groups, Map("__type__", "__vector__", "value", Array(100))); + Add(comparison_groups, + Map("__type__", "__vector__", "value", Array(1.0, 2.0, 3.0))); Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_array_value_tag))); + Map("__type__", "__vector__", "value", Array(1.0, 3.0, 2.0))); // objects - Add(comparison_groups, - DeepClone(GetLowerBound(google_firestore_v1_Value_map_value_tag))); + Add(comparison_groups, DeepClone(MinMap())); Add(comparison_groups, Map("bar", 0)); Add(comparison_groups, Map("bar", 0, "foo", 1)); Add(comparison_groups, Map("foo", 1)); Add(comparison_groups, Map("foo", 2)); Add(comparison_groups, Map("foo", "0")); - Add(comparison_groups, - DeepClone(GetUpperBound(google_firestore_v1_Value_map_value_tag))); + Add(comparison_groups, DeepClone(MaxValue())); for (size_t i = 0; i < comparison_groups.size(); ++i) { for (size_t j = i; j < comparison_groups.size(); ++j) { @@ -526,6 +519,9 @@ TEST_F(ValueUtilTest, CanonicalId) { VerifyCanonicalId(Map("a", 1, "b", 2, "c", "3"), "{a:1,b:2,c:3}"); VerifyCanonicalId(Map("a", Array("b", Map("c", GeoPoint(30, 60)))), "{a:[b,{c:geo(30.0,60.0)}]}"); + VerifyCanonicalId( + Map("__type__", "__vector__", "value", Array(1.0, 1.0, -2.0, 3.14)), + "{__type__:__vector__,value:[1.0,1.0,-2.0,3.1]}"); } TEST_F(ValueUtilTest, DeepClone) { diff --git a/Firestore/core/test/unit/remote/serializer_test.cc b/Firestore/core/test/unit/remote/serializer_test.cc index bea30617594..0de665273ee 100644 --- a/Firestore/core/test/unit/remote/serializer_test.cc +++ b/Firestore/core/test/unit/remote/serializer_test.cc @@ -821,6 +821,24 @@ TEST_F(SerializerTest, EncodesNestedObjects) { ExpectRoundTrip(model, proto, TypeOrder::kMap); } +TEST_F(SerializerTest, EncodesVectorValue) { + Message model = + Map("__type__", "__vector__", "value", Array(1.0, 2.0, 3.0)); + + v1::Value array_proto; + *array_proto.mutable_array_value()->add_values() = ValueProto(1.0); + *array_proto.mutable_array_value()->add_values() = ValueProto(2.0); + *array_proto.mutable_array_value()->add_values() = ValueProto(3.0); + + v1::Value proto; + google::protobuf::Map* fields = + proto.mutable_map_value()->mutable_fields(); + (*fields)["__type__"] = ValueProto("__vector__"); + (*fields)["value"] = array_proto; + + ExpectRoundTrip(model, proto, TypeOrder::kVector); +} + TEST_F(SerializerTest, EncodesFieldValuesWithRepeatedEntries) { // Technically, serialized Value protos can contain multiple values. (The last // one "wins".) However, well-behaved proto emitters (such as libprotobuf) diff --git a/Firestore/core/test/unit/testutil/testutil.h b/Firestore/core/test/unit/testutil/testutil.h index 50845c21d6c..234ef3d5d12 100644 --- a/Firestore/core/test/unit/testutil/testutil.h +++ b/Firestore/core/test/unit/testutil/testutil.h @@ -288,6 +288,12 @@ nanopb::Message Map(Args... key_value_pairs) { return details::MakeMap(std::move(key_value_pairs)...); } +template +nanopb::Message VectorType(Args&&... values) { + return Map("__type__", "__vector__", "value", + details::MakeArray(std::move(values)...)); +} + model::DocumentKey Key(absl::string_view path); model::FieldPath Field(absl::string_view field); diff --git a/scripts/run_firestore_emulator.sh b/scripts/run_firestore_emulator.sh index dac3f81ad7c..7401009c44d 100755 --- a/scripts/run_firestore_emulator.sh +++ b/scripts/run_firestore_emulator.sh @@ -25,7 +25,7 @@ if [[ ! -z "${JAVA_HOME_11_X64:-}" ]]; then export JAVA_HOME=$JAVA_HOME_11_X64 fi -VERSION='1.18.2' +VERSION='1.19.7' FILENAME="cloud-firestore-emulator-v${VERSION}.jar" URL="https://storage.googleapis.com/firebase-preview-drop/emulator/${FILENAME}"