Skip to content

Commit cc23f17

Browse files
authored
feat(firestore): add support for VectorValue (#16476)
* feat(firestore): add support for VectorValue * android * fix iOS * android * fix format * add to example app * android * JS * with correct JS version * format * more tests * fix tests * change to test
1 parent 71e1f21 commit cc23f17

File tree

16 files changed

+291
-4
lines changed

16 files changed

+291
-4
lines changed

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestoreMessageCodec.java

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.firebase.firestore.Query;
2424
import com.google.firebase.firestore.QuerySnapshot;
2525
import com.google.firebase.firestore.SnapshotMetadata;
26+
import com.google.firebase.firestore.VectorValue;
2627
import io.flutter.plugin.common.StandardMessageCodec;
2728
import java.io.ByteArrayOutputStream;
2829
import java.nio.ByteBuffer;
@@ -55,6 +56,7 @@ class FlutterFirebaseFirestoreMessageCodec extends StandardMessageCodec {
5556
private static final byte DATA_TYPE_FIRESTORE_INSTANCE = (byte) 196;
5657
private static final byte DATA_TYPE_FIRESTORE_QUERY = (byte) 197;
5758
private static final byte DATA_TYPE_FIRESTORE_SETTINGS = (byte) 198;
59+
private static final byte DATA_TYPE_VECTOR_VALUE = (byte) 199;
5860

5961
@Override
6062
protected void writeValue(ByteArrayOutputStream stream, Object value) {
@@ -70,6 +72,9 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) {
7072
writeAlignment(stream, 8);
7173
writeDouble(stream, ((GeoPoint) value).getLatitude());
7274
writeDouble(stream, ((GeoPoint) value).getLongitude());
75+
} else if (value instanceof VectorValue) {
76+
stream.write(DATA_TYPE_VECTOR_VALUE);
77+
writeValue(stream, ((VectorValue) value).toArray());
7378
} else if (value instanceof DocumentReference) {
7479
stream.write(DATA_TYPE_DOCUMENT_REFERENCE);
7580
FirebaseFirestore firestore = ((DocumentReference) value).getFirestore();
@@ -238,6 +243,13 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) {
238243
case DATA_TYPE_GEO_POINT:
239244
readAlignment(buffer, 8);
240245
return new GeoPoint(buffer.getDouble(), buffer.getDouble());
246+
case DATA_TYPE_VECTOR_VALUE:
247+
final ArrayList<Double> arrayList = (ArrayList<Double>) readValue(buffer);
248+
double[] doubleArray = new double[arrayList.size()];
249+
for (int i = 0; i < arrayList.size(); i++) {
250+
doubleArray[i] = Objects.requireNonNull(arrayList.get(i), "Null value at index " + i);
251+
}
252+
return FieldValue.vector(doubleArray);
241253
case DATA_TYPE_DOCUMENT_REFERENCE:
242254
FirebaseFirestore firestore = (FirebaseFirestore) readValue(buffer);
243255
final String path = (String) readValue(buffer);

packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart

+6
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ void runDocumentReferenceTests() {
408408
'null': null,
409409
'timestamp': Timestamp.now(),
410410
'geopoint': const GeoPoint(1, 2),
411+
'vectorValue': const VectorValue([1, 2, 3]),
411412
'reference': firestore.doc('foo/bar'),
412413
'nan': double.nan,
413414
'infinity': double.infinity,
@@ -444,6 +445,11 @@ void runDocumentReferenceTests() {
444445
expect(data['geopoint'], isA<GeoPoint>());
445446
expect((data['geopoint'] as GeoPoint).latitude, equals(1));
446447
expect((data['geopoint'] as GeoPoint).longitude, equals(2));
448+
expect(data['vectorValue'], isA<VectorValue>());
449+
expect(
450+
(data['vectorValue'] as VectorValue).toArray(),
451+
equals([1, 2, 3]),
452+
);
447453
expect(data['reference'], isA<DocumentReference>());
448454
expect((data['reference'] as DocumentReference).id, equals('bar'));
449455
expect(data['nan'].isNaN, equals(true));

packages/cloud_firestore/cloud_firestore/example/integration_test/e2e_test.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import 'settings_e2e.dart';
2222
import 'snapshot_metadata_e2e.dart';
2323
import 'timestamp_e2e.dart';
2424
import 'transaction_e2e.dart';
25-
import 'write_batch_e2e.dart';
25+
import 'vector_value_e2e.dart';
2626
import 'web_snapshot_listeners.dart';
27+
import 'write_batch_e2e.dart';
2728

2829
bool kUseFirestoreEmulator = true;
2930

@@ -52,6 +53,7 @@ void main() {
5253
runDocumentReferenceTests();
5354
runFieldValueTests();
5455
runGeoPointTests();
56+
runVectorValueTests();
5557
runQueryTests();
5658
runSnapshotMetadataTests();
5759
runTimestampTests();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright 2020, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:cloud_firestore/cloud_firestore.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
void runVectorValueTests() {
9+
group('$VectorValue', () {
10+
late FirebaseFirestore firestore;
11+
12+
setUpAll(() async {
13+
firestore = FirebaseFirestore.instance;
14+
});
15+
16+
Future<DocumentReference<Map<String, dynamic>>> initializeTest(
17+
String path,
18+
) async {
19+
String prefixedPath = 'flutter-tests/$path';
20+
await firestore.doc(prefixedPath).delete();
21+
return firestore.doc(prefixedPath);
22+
}
23+
24+
test('sets a $VectorValue & returns one', () async {
25+
DocumentReference<Map<String, dynamic>> doc =
26+
await initializeTest('vector-value');
27+
28+
await doc.set({
29+
'foo': const VectorValue([10.0, -10.0]),
30+
});
31+
32+
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
33+
34+
VectorValue vectorValue = snapshot.data()!['foo'];
35+
expect(vectorValue, isA<VectorValue>());
36+
expect(vectorValue.toArray(), equals([10.0, -10.0]));
37+
});
38+
39+
test('updates a $VectorValue & returns', () async {
40+
DocumentReference<Map<String, dynamic>> doc =
41+
await initializeTest('vector-value-update');
42+
43+
await doc.set({
44+
'foo': const VectorValue([10.0, -10.0]),
45+
});
46+
47+
await doc.update({
48+
'foo': const VectorValue([-10.0, 10.0]),
49+
});
50+
51+
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
52+
53+
VectorValue vectorValue = snapshot.data()!['foo'];
54+
expect(vectorValue, isA<VectorValue>());
55+
expect(vectorValue.toArray(), equals([-10.0, 10.0]));
56+
});
57+
58+
test('handles empty vector', () async {
59+
DocumentReference<Map<String, dynamic>> doc =
60+
await initializeTest('vector-value-empty');
61+
62+
try {
63+
await doc.set({
64+
'foo': const VectorValue([]),
65+
});
66+
fail('Should have thrown an exception');
67+
} catch (e) {
68+
expect(e, isA<FirebaseException>());
69+
expect(
70+
(e as FirebaseException).code.contains('invalid-argument'),
71+
isTrue,
72+
);
73+
}
74+
});
75+
76+
test('handles single dimension vector', () async {
77+
DocumentReference<Map<String, dynamic>> doc =
78+
await initializeTest('vector-value-single');
79+
80+
await doc.set({
81+
'foo': const VectorValue([42.0]),
82+
});
83+
84+
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
85+
86+
VectorValue vectorValue = snapshot.data()!['foo'];
87+
expect(vectorValue, isA<VectorValue>());
88+
expect(vectorValue.toArray(), equals([42.0]));
89+
});
90+
91+
test('handles maximum dimensions vector', () async {
92+
List<double> maxDimensions = List.filled(2048, 1);
93+
DocumentReference<Map<String, dynamic>> doc =
94+
await initializeTest('vector-value-max-dimensions');
95+
96+
await doc.set({
97+
'foo': VectorValue(maxDimensions),
98+
});
99+
100+
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
101+
102+
VectorValue vectorValue = snapshot.data()!['foo'];
103+
expect(vectorValue, isA<VectorValue>());
104+
expect(vectorValue.toArray(), equals(maxDimensions));
105+
});
106+
107+
test('handles maximum dimensions + 1 vector', () async {
108+
List<double> maxPlusOneDimensions = List.filled(2049, 1);
109+
DocumentReference<Map<String, dynamic>> doc =
110+
await initializeTest('vector-value-max-plus-one');
111+
112+
try {
113+
await doc.set({
114+
'foo': VectorValue(maxPlusOneDimensions),
115+
});
116+
117+
fail('Should have thrown an exception');
118+
} catch (e) {
119+
expect(e, isA<FirebaseException>());
120+
expect(
121+
(e as FirebaseException).code.contains('invalid-argument'),
122+
isTrue,
123+
);
124+
}
125+
});
126+
127+
test('handles very large values in vector', () async {
128+
DocumentReference<Map<String, dynamic>> doc =
129+
await initializeTest('vector-value-large-values');
130+
131+
await doc.set({
132+
'foo': const VectorValue([1e10, -1e10]),
133+
});
134+
135+
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
136+
137+
VectorValue vectorValue = snapshot.data()!['foo'];
138+
expect(vectorValue, isA<VectorValue>());
139+
expect(vectorValue.toArray(), equals([1e10, -1e10]));
140+
});
141+
142+
test('handles floats in vector', () async {
143+
DocumentReference<Map<String, dynamic>> doc =
144+
await initializeTest('vector-value-floats');
145+
146+
await doc.set({
147+
'foo': const VectorValue([3.14, 2.718]),
148+
});
149+
150+
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
151+
152+
VectorValue vectorValue = snapshot.data()!['foo'];
153+
expect(vectorValue, isA<VectorValue>());
154+
expect(vectorValue.toArray(), equals([3.14, 2.718]));
155+
});
156+
157+
test('handles negative values in vector', () async {
158+
DocumentReference<Map<String, dynamic>> doc =
159+
await initializeTest('vector-value-negative');
160+
161+
await doc.set({
162+
'foo': const VectorValue([-42.0, -100.0]),
163+
});
164+
165+
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
166+
167+
VectorValue vectorValue = snapshot.data()!['foo'];
168+
expect(vectorValue, isA<VectorValue>());
169+
expect(vectorValue.toArray(), equals([-42.0, -100.0]));
170+
});
171+
});
172+
}

packages/cloud_firestore/cloud_firestore/example/lib/main.dart

+13
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ class _FilmListState extends State<FilmList> {
232232
list.map((e) => e.taskState),
233233
);
234234
return;
235+
case 'vectorValue':
236+
const vectorValue = VectorValue([1.0, 2.0, 3.0]);
237+
final vectorValueDoc = await FirebaseFirestore.instance
238+
.collection('firestore-example-app')
239+
.add({'vectorValue': vectorValue});
240+
241+
final snapshot = await vectorValueDoc.get();
242+
print(snapshot.data());
243+
return;
235244
default:
236245
return;
237246
}
@@ -250,6 +259,10 @@ class _FilmListState extends State<FilmList> {
250259
value: 'load_bundle',
251260
child: Text('Load bundle'),
252261
),
262+
const PopupMenuItem(
263+
value: 'vectorValue',
264+
child: Text('Test Vector Value'),
265+
),
253266
];
254267
},
255268
),

packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTFirebaseFirestoreReader.m

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ - (id)readValueOfType:(UInt8)type {
3737
[self readBytes:&longitude length:8];
3838
return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude];
3939
}
40+
case FirestoreDataTypeVectorValue: {
41+
return [[FIRVectorValue alloc] initWithArray:[self readValue]];
42+
}
4043
case FirestoreDataTypeDocumentReference: {
4144
FIRFirestore *firestore = [self readValue];
4245
NSString *documentPath = [self readValue];

packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTFirebaseFirestoreWriter.m

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ - (void)writeValue:(id)value {
3232
[self writeAlignment:8];
3333
[self writeBytes:(UInt8 *)&latitude length:8];
3434
[self writeBytes:(UInt8 *)&longitude length:8];
35+
} else if ([value isKindOfClass:[FIRVectorValue class]]) {
36+
FIRVectorValue *vector = value;
37+
[self writeByte:FirestoreDataTypeVectorValue];
38+
[self writeValue:vector.array];
3539
} else if ([value isKindOfClass:[FIRDocumentReference class]]) {
3640
FIRDocumentReference *document = value;
3741
NSString *documentPath = [document path];

packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Private/FLTFirebaseFirestoreUtils.h

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ typedef NS_ENUM(UInt8, FirestoreDataType) {
3838
FirestoreDataTypeFirestoreInstance = 196,
3939
FirestoreDataTypeFirestoreQuery = 197,
4040
FirestoreDataTypeFirestoreSettings = 198,
41+
FirestoreDataTypeVectorValue = 199,
4142
};
4243

4344
@interface FLTFirebaseFirestoreReaderWriter : FlutterStandardReaderWriter

packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export 'package:cloud_firestore_platform_interface/cloud_firestore_platform_inte
2222
FieldPath,
2323
Blob,
2424
GeoPoint,
25+
VectorValue,
2526
Timestamp,
2627
Source,
2728
GetOptions,
@@ -57,11 +58,11 @@ part 'src/filters.dart';
5758
part 'src/firestore.dart';
5859
part 'src/load_bundle_task.dart';
5960
part 'src/load_bundle_task_snapshot.dart';
61+
part 'src/persistent_cache_index_manager.dart';
6062
part 'src/query.dart';
6163
part 'src/query_document_snapshot.dart';
6264
part 'src/query_snapshot.dart';
6365
part 'src/snapshot_metadata.dart';
6466
part 'src/transaction.dart';
6567
part 'src/utils/codec_utility.dart';
6668
part 'src/write_batch.dart';
67-
part 'src/persistent_cache_index_manager.dart';

packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ export 'src/platform_interface/platform_interface_firestore.dart';
2828
export 'src/platform_interface/platform_interface_index_definitions.dart';
2929
export 'src/platform_interface/platform_interface_load_bundle_task.dart';
3030
export 'src/platform_interface/platform_interface_load_bundle_task_snapshot.dart';
31+
export 'src/platform_interface/platform_interface_persistent_cache_index_manager.dart';
3132
export 'src/platform_interface/platform_interface_query.dart';
3233
export 'src/platform_interface/platform_interface_query_snapshot.dart';
3334
export 'src/platform_interface/platform_interface_transaction.dart';
3435
export 'src/platform_interface/platform_interface_write_batch.dart';
35-
export 'src/platform_interface/platform_interface_persistent_cache_index_manager.dart';
3636
export 'src/platform_interface/utils/load_bundle_task_state.dart';
3737
export 'src/set_options.dart';
3838
export 'src/settings.dart';
3939
export 'src/snapshot_metadata.dart';
4040
export 'src/timestamp.dart';
41+
export 'src/vector_value.dart';
4142

4243
/// Helper method exposed to determine whether a given [collectionPath] points to
4344
/// a valid Firestore collection.

packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/utils/firestore_message_codec.dart

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
// TODO(Lyokone): remove once we bump Flutter SDK min version to 3.3
77
// ignore: unnecessary_import
8-
import 'dart:typed_data';
8+
import 'dart:core';
99

1010
import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart';
1111
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_field_value.dart';
@@ -43,6 +43,7 @@ class FirestoreMessageCodec extends StandardMessageCodec {
4343
static const int _kFirestoreInstance = 196;
4444
static const int _kFirestoreQuery = 197;
4545
static const int _kFirestoreSettings = 198;
46+
static const int _kVectorValue = 199;
4647

4748
static const Map<FieldValueType, int> _kFieldValueCodes =
4849
<FieldValueType, int>{
@@ -124,6 +125,9 @@ class FirestoreMessageCodec extends StandardMessageCodec {
124125
buffer.putUint8(_kInfinity);
125126
} else if (value == double.negativeInfinity) {
126127
buffer.putUint8(_kNegativeInfinity);
128+
} else if (value is VectorValue) {
129+
buffer.putUint8(_kVectorValue);
130+
writeValue(buffer, value.toArray());
127131
} else {
128132
super.writeValue(buffer, value);
129133
}
@@ -148,6 +152,10 @@ class FirestoreMessageCodec extends StandardMessageCodec {
148152
FirebaseFirestorePlatform.instanceFor(
149153
app: app, databaseId: databaseId);
150154
return firestore.doc(path);
155+
case _kVectorValue:
156+
final List<Object?> vector = (readValue(buffer)!) as List<Object?>;
157+
final List<double> doubles = vector.map((e) => e! as double).toList();
158+
return VectorValue(doubles);
151159
case _kBlob:
152160
final int length = readSize(buffer);
153161
final List<int> bytes = buffer.getUint8List(length);

0 commit comments

Comments
 (0)