Browse Source

Firestore VectorValue type (#13404)

Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com>
Mark Duckworth 1 year ago
parent
commit
c3999178d4
28 changed files with 1241 additions and 148 deletions
  1. 15 0
      FirebaseFirestoreInternal/FirebaseFirestore/FIRVectorValue.h
  2. 3 0
      Firestore/CHANGELOG.md
  3. 25 3
      Firestore/Example/Firestore.xcodeproj/project.pbxproj
  4. 11 15
      Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests_iOS.xcscheme
  5. 1 0
      Firestore/Example/Tests/API/FIRFieldValueTests.mm
  6. 48 0
      Firestore/Example/Tests/API/FIRVectorValueTests.mm
  7. 5 0
      Firestore/Source/API/FIRFieldValue.mm
  8. 48 0
      Firestore/Source/API/FIRVectorValue.m
  9. 40 1
      Firestore/Source/API/FSTUserDataReader.mm
  10. 15 0
      Firestore/Source/API/FSTUserDataWriter.mm
  11. 9 0
      Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h
  12. 41 0
      Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h
  13. 2 1
      Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift
  14. 61 0
      Firestore/Swift/Source/Codable/VectorValue+Codable.swift
  15. 43 0
      Firestore/Swift/Source/SwiftAPI/FieldValue+Swift.swift
  16. 37 0
      Firestore/Swift/Source/SwiftAPI/VectorValue+Swift.swift
  17. 28 1
      Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift
  18. 80 0
      Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift
  19. 268 0
      Firestore/Swift/Tests/Integration/VectorIntegrationTests.swift
  20. 2 4
      Firestore/core/src/core/target.cc
  21. 30 0
      Firestore/core/src/index/firestore_index_value_writer.cc
  22. 245 63
      Firestore/core/src/model/value_util.cc
  23. 61 4
      Firestore/core/src/model/value_util.h
  24. 47 0
      Firestore/core/test/unit/local/leveldb_index_manager_test.cc
  25. 51 55
      Firestore/core/test/unit/model/value_util_test.cc
  26. 18 0
      Firestore/core/test/unit/remote/serializer_test.cc
  27. 6 0
      Firestore/core/test/unit/testutil/testutil.h
  28. 1 1
      scripts/run_firestore_emulator.sh

+ 15 - 0
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 <FirebaseFirestoreInternal/FIRVectorValue.h>

+ 3 - 0
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

+ 25 - 3
Firestore/Example/Firestore.xcodeproj/project.pbxproj

@@ -1524,6 +1524,12 @@
 		EE6DBFB0874A50578CE97A7F /* leveldb_remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0840319686A223CC4AD3FAB1 /* leveldb_remote_document_cache_test.cc */; };
 		EECC1EC64CA963A8376FA55C /* persistence_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9113B6F513D0473AEABBAF1F /* persistence_testing.cc */; };
 		EF3518F84255BAF3EBD317F6 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; };
+		EF3A65482C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */; };
+		EF3A65492C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */; };
+		EF3A654A2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */; };
+		EF3A654B2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */; };
+		EF3A654C2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */; };
+		EF3A654D2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */; };
 		EF409F2A8AE28D177CCF635D /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B96CC29E9946508F022859C /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json */; };
 		EF43FF491B9282E0330E4CA2 /* remote_event_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 584AE2C37A55B408541A6FF3 /* remote_event_test.cc */; };
 		EF4FB3034994E6386F3C78FF /* leveldb_overlay_migration_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D8A6D52723B1BABE1B7B8D8F /* leveldb_overlay_migration_manager_test.cc */; };
@@ -1536,6 +1542,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 +1696,7 @@
 		132E32997D781B896672D30A /* reference_set_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = reference_set_test.cc; sourceTree = "<group>"; };
 		166CE73C03AB4366AAC5201C /* leveldb_index_manager_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_index_manager_test.cc; sourceTree = "<group>"; };
 		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 = "<group>"; };
-		1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = thread_safe_memoizer_test.cc; sourceTree = "<group>"; };
+		1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = thread_safe_memoizer_test.cc; sourceTree = "<group>"; };
 		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 = "<group>"; };
 		1B9F95EC29FAD3F100EEC075 /* FIRAggregateQueryUnitTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateQueryUnitTests.mm; sourceTree = "<group>"; };
 		1C01D8CE367C56BB2624E299 /* index.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = index.pb.h; path = admin/index.pb.h; sourceTree = "<group>"; };
@@ -1744,7 +1753,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 = "<group>"; };
 		4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_benchmark.mm; sourceTree = "<group>"; };
 		4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnapshotListenerSourceTests.swift; sourceTree = "<group>"; };
-		4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; path = listen_source_spec_test.json; sourceTree = "<group>"; };
+		4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; path = listen_source_spec_test.json; sourceTree = "<group>"; };
 		4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bundle_builder.cc; sourceTree = "<group>"; };
 		526D755F65AC676234F57125 /* target_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_test.cc; sourceTree = "<group>"; };
 		52756B7624904C36FBB56000 /* fake_target_metadata_provider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = fake_target_metadata_provider.h; sourceTree = "<group>"; };
@@ -1898,7 +1907,7 @@
 		62E54B832A9E910A003347C8 /* IndexingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexingTests.swift; sourceTree = "<group>"; };
 		63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_index_matcher_test.cc; sourceTree = "<group>"; };
 		64AA92CFA356A2360F3C5646 /* filesystem_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = filesystem_testing.h; sourceTree = "<group>"; };
-		65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; path = FIRCompositeIndexQueryTests.mm; sourceTree = "<group>"; };
+		65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRCompositeIndexQueryTests.mm; sourceTree = "<group>"; };
 		67786C62C76A740AEDBD8CD3 /* FSTTestingHooks.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = FSTTestingHooks.h; sourceTree = "<group>"; };
 		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 = "<group>"; };
 		6A7A30A2DB3367E08939E789 /* bloom_filter.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bloom_filter.pb.h; sourceTree = "<group>"; };
@@ -2080,9 +2089,11 @@
 		E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = "<group>"; };
 		E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_target_cache_test.cc; sourceTree = "<group>"; };
 		ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRVectorValueTests.mm; sourceTree = "<group>"; };
 		EF6C285029E462A200A7D4F1 /* FIRAggregateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateTests.mm; sourceTree = "<group>"; };
 		EF6C286C29E6D22200A7D4F1 /* AggregationIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregationIntegrationTests.swift; sourceTree = "<group>"; };
 		EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_migrations_test.cc; sourceTree = "<group>"; };
+		EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorIntegrationTests.swift; sourceTree = "<group>"; };
 		F02F734F272C3C70D1307076 /* filter_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = filter_test.cc; sourceTree = "<group>"; };
 		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 = "<group>"; };
 		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 = "<group>"; };
@@ -2226,6 +2237,7 @@
 				62E54B832A9E910A003347C8 /* IndexingTests.swift */,
 				621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */,
 				4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */,
+				EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */,
 			);
 			path = Integration;
 			sourceTree = "<group>";
@@ -2918,6 +2930,7 @@
 				FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */,
 				5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */,
 				CF39ECA1293D21A0A2AB2626 /* FIRTransactionOptionsTests.mm */,
+				EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */,
 				5492E047202154AA00B64F25 /* FSTAPIHelpers.h */,
 				5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */,
 				8D9892F204959C50613F16C8 /* FSTUserDataReaderTests.mm */,
@@ -4111,6 +4124,7 @@
 				D39F0216BF1EA8CD54C76CF8 /* FIRQueryUnitTests.mm in Sources */,
 				2EAD77559EC654E6CA4D3E21 /* FIRSnapshotMetadataTests.mm in Sources */,
 				16FF9073CA381CA43CA9BF29 /* FIRTransactionOptionsTests.mm in Sources */,
+				EF3A654A2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
 				9D71628E38D9F64C965DF29E /* FSTAPIHelpers.mm in Sources */,
 				F4F00BF4E87D7F0F0F8831DB /* FSTEventAccumulator.mm in Sources */,
 				4F55A97F725D86E5CC6BE2DC /* FSTExceptionCatcher.m in Sources */,
@@ -4328,6 +4342,7 @@
 				518BF03D57FBAD7C632D18F8 /* FIRQueryUnitTests.mm in Sources */,
 				ED420D8F49DA5C41EEF93913 /* FIRSnapshotMetadataTests.mm in Sources */,
 				DBFE8B2E803C1D0DECB71FF6 /* FIRTransactionOptionsTests.mm in Sources */,
+				EF3A654C2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
 				6E4854B19B120C6F0F8192CC /* FSTAPIHelpers.mm in Sources */,
 				73E42D984FB36173A2BDA57C /* FSTEventAccumulator.mm in Sources */,
 				21E588CF29C72813D8A7A0A1 /* FSTExceptionCatcher.m in Sources */,
@@ -4560,6 +4575,7 @@
 				58B84B550725D9812729C7F7 /* FIRTransactionOptionsTests.mm in Sources */,
 				75D124966E727829A5F99249 /* FIRTypeTests.mm in Sources */,
 				12DB753599571E24DCED0C2C /* FIRValidationTests.mm in Sources */,
+				EF3A654D2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
 				BC0C98A9201E8F98B9A176A9 /* FIRWriteBatchTests.mm in Sources */,
 				D550446303227FB1B381133C /* FSTAPIHelpers.mm in Sources */,
 				A4ECA8335000CBDF94586C94 /* FSTDatastoreTests.mm in Sources */,
@@ -4581,6 +4597,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 */,
@@ -4800,6 +4817,7 @@
 				339D4DD13E1518BA79FF12EA /* FIRTransactionOptionsTests.mm in Sources */,
 				5F05A801B1EA44BC1264E55A /* FIRTypeTests.mm in Sources */,
 				8403D519C916C72B9C7F2FA1 /* FIRValidationTests.mm in Sources */,
+				EF3A654B2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
 				8705C4856498F66E471A0997 /* FIRWriteBatchTests.mm in Sources */,
 				881E55152AB34465412F8542 /* FSTAPIHelpers.mm in Sources */,
 				4A64A339BCA77B9F875D1D8B /* FSTDatastoreTests.mm in Sources */,
@@ -4821,6 +4839,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 */,
@@ -5035,6 +5054,7 @@
 				CB2C731116D6C9464220626F /* FIRQueryUnitTests.mm in Sources */,
 				5492E057202154AB00B64F25 /* FIRSnapshotMetadataTests.mm in Sources */,
 				85A33A9CE33207C2333DDD32 /* FIRTransactionOptionsTests.mm in Sources */,
+				EF3A65482C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
 				5492E058202154AB00B64F25 /* FSTAPIHelpers.mm in Sources */,
 				5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */,
 				38C37F0CE0AB18F1AAE6E67C /* FSTExceptionCatcher.m in Sources */,
@@ -5286,6 +5306,7 @@
 				913C2DB6951A2ED24778686C /* FIRTransactionOptionsTests.mm in Sources */,
 				5492E07A202154D600B64F25 /* FIRTypeTests.mm in Sources */,
 				5492E076202154D600B64F25 /* FIRValidationTests.mm in Sources */,
+				EF3A65492C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
 				5492E078202154D600B64F25 /* FIRWriteBatchTests.mm in Sources */,
 				D9EF7FC0E3F8646B272B427E /* FSTAPIHelpers.mm in Sources */,
 				5492E082202154EC00B64F25 /* FSTDatastoreTests.mm in Sources */,
@@ -5307,6 +5328,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 */,

+ 11 - 15
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">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "6003F5AD195388D20070C39A"
+            BuildableName = "Firestore_Tests_iOS.xctest"
+            BlueprintName = "Firestore_Tests_iOS"
+            ReferencedContainer = "container:Firestore.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
       <Testables>
          <TestableReference
             skipped = "NO">
@@ -40,17 +49,6 @@
             </BuildableReference>
          </TestableReference>
       </Testables>
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "6003F5AD195388D20070C39A"
-            BuildableName = "Firestore_Tests_iOS.xctest"
-            BlueprintName = "Firestore_Tests_iOS"
-            ReferencedContainer = "container:Firestore.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -71,8 +69,6 @@
             ReferencedContainer = "container:Firestore.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 0
Firestore/Example/Tests/API/FIRFieldValueTests.mm

@@ -15,6 +15,7 @@
  */
 
 #import <FirebaseFirestore/FIRFieldValue.h>
+#import <FirebaseFirestore/FIRVectorValue.h>
 
 #import <XCTest/XCTest.h>
 

+ 48 - 0
Firestore/Example/Tests/API/FIRVectorValueTests.mm

@@ -0,0 +1,48 @@
+/*
+ * 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 <FirebaseFirestore/FIRFieldValue.h>
+#import <FirebaseFirestore/FIRVectorValue.h>
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVectorValueTests : XCTestCase
+@end
+
+@implementation FIRVectorValueTests
+
+- (void)testCreateAndReadVectorValue {
+  FIRVectorValue *vector = [FIRFieldValue vectorWithArray:@[
+    @DBL_MIN, @0.0, [NSNumber numberWithLong:((long)pow(2, 53)) + 1], @DBL_MAX, @DBL_EPSILON,
+    @INT64_MAX
+  ]];
+  NSArray<NSNumber *> *outArray = vector.array;
+
+  XCTAssertEqualObjects([outArray objectAtIndex:0], @DBL_MIN);
+  XCTAssertEqualObjects([outArray objectAtIndex:1], @0.0);
+  // Assert that if the vector is created with large long values,
+  // then the data will be truncated as a double.
+  XCTAssertEqual([outArray objectAtIndex:2].longValue, pow(2, 53));
+  XCTAssertEqualObjects([outArray objectAtIndex:3], @DBL_MAX);
+  XCTAssertEqualObjects([outArray objectAtIndex:4], @DBL_EPSILON);
+  XCTAssertEqualObjects([outArray objectAtIndex:5], @INT64_MAX);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 5 - 0
Firestore/Source/API/FIRFieldValue.mm

@@ -15,6 +15,7 @@
  */
 
 #import "Firestore/Source/API/FIRFieldValue+Internal.h"
+#import "Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -176,6 +177,10 @@ NS_ASSUME_NONNULL_BEGIN
   return [[FSTNumericIncrementFieldValue alloc] initWithOperand:@(l)];
 }
 
++ (nonnull FIRVectorValue *)vectorWithArray:(nonnull NSArray<NSNumber *> *)array {
+  return [[FIRVectorValue alloc] initWithArray:array];
+}
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 48 - 0
Firestore/Source/API/FIRVectorValue.m

@@ -0,0 +1,48 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#include "Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRVectorValue
+
+- (instancetype)initWithArray:(NSArray<NSNumber *> *)array {
+  if (self = [super init]) {
+    _array = [array valueForKey:@"doubleValue"];
+  }
+  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

+ 40 - 1
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 @@ NS_ASSUME_NONNULL_BEGIN
   return std::move(result);
 }
 
+- (Message<google_firestore_v1_Value>)parseVectorValue:(FIRVectorValue *)vectorValue
+                                               context:(ParseContext &&)context {
+  __block Message<google_firestore_v1_Value> 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<google_firestore_v1_MapValue_FieldsEntry>(2);
+
+  result->map_value.fields[0].key = nanopb::CopyBytesArray(model::kTypeValueFieldKey);
+  result->map_value.fields[0].value = *[self encodeStringValue:MakeString(@"__vector__")].release();
+
+  NSArray<NSNumber *> *vectorArray = vectorValue.array;
+
+  __block Message<google_firestore_v1_Value> 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<google_firestore_v1_Value>(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<google_firestore_v1_Value>)parseArray:(NSArray<id> *)array
                                          context:(ParseContext &&)context {
   __block Message<google_firestore_v1_Value> result;
@@ -529,7 +566,9 @@ NS_ASSUME_NONNULL_BEGIN
           _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());

+ 15 - 0
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 @@ NS_ASSUME_NONNULL_BEGIN
     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 @@ NS_ASSUME_NONNULL_BEGIN
   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<id> *)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) {

+ 9 - 0
Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h

@@ -17,6 +17,7 @@
 #import <Foundation/Foundation.h>
 
 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<NSNumber *> *)array NS_REFINED_FOR_SWIFT;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 41 - 0
Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h

@@ -0,0 +1,41 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents a vector type in Firestore documents.
+ */
+NS_SWIFT_NAME(VectorValue)
+@interface FIRVectorValue : NSObject
+
+/** Returns a copy of the raw number array that represents the vector. */
+@property(atomic, readonly) NSArray<NSNumber *> *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<NSNumber *> *)array NS_REFINED_FOR_SWIFT;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 2 - 1
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
   }
 }

+ 61 - 0
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 {}

+ 43 - 0
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)
+  }
+}

+ 37 - 0
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) }
+  }
+}

+ 28 - 1
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

+ 80 - 0
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()
+  }
 }

+ 268 - 0
Firestore/Swift/Tests/Integration/VectorIntegrationTests.swift

@@ -0,0 +1,268 @@
+/*
+ * 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 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<QuerySnapshot, Error>() { 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<T: Decodable>: 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<Model>.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])
+  }
+}

+ 2 - 4
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:

+ 30 - 0
Firestore/core/src/index/firestore_index_value_writer.cc

@@ -21,6 +21,7 @@
 #include <string>
 
 #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<pb_size_t> 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<int>::max());
         break;
+      } else if (model::IsVectorValue(index_value)) {
+        WriteIndexVector(index_value.map_value, encoder);
+        break;
       }
       WriteIndexMap(index_value.map_value, encoder);
       WriteTruncationMarker(encoder);

+ 245 - 63
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<pb_size_t> leftIndex =
+      IndexOfKey(left.map_value, kRawVectorValueFieldKey, kVectorValueFieldKey);
+  absl::optional<pb_size_t> 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<int64_t>::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<pb_size_t> 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<pb_size_t> 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<pb_size_t> 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<int64_t>::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<google_firestore_v1_MapValue_FieldsEntry>(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<google_firestore_v1_Value> RefValue(
     const model::DatabaseId& database_id,
     const model::DocumentKey& document_key) {

+ 61 - 4
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<pb_size_t> 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.
  *

+ 47 - 0
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<Persistence> 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.

+ 51 - 55
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<int64_t>::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<Message<google_firestore_v1_ArrayValue>> 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<int64_t>::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<int64_t>::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) {

+ 18 - 0
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<google_firestore_v1_Value> 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<std::string, v1::Value>* 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)

+ 6 - 0
Firestore/core/test/unit/testutil/testutil.h

@@ -288,6 +288,12 @@ nanopb::Message<google_firestore_v1_Value> Map(Args... key_value_pairs) {
   return details::MakeMap(std::move(key_value_pairs)...);
 }
 
+template <typename... Args>
+nanopb::Message<google_firestore_v1_Value> 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);

+ 1 - 1
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}"