Skip to content

Commit ac0bf73

Browse files
feat(firestore): count() feature for counting documents without retrieving documents. (#9699)
1 parent 1829ee7 commit ac0bf73

23 files changed

+492
-3
lines changed

.swiftformat

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
--maxwidth 100
33
--wrapparameters afterfirst
44
--disable sortedImports,unusedArguments,wrapMultilineStatementBraces
5-
--exclude Pods,**/MainFlutterWindow.swift,**/AppDelegate.swift,**/.symlinks/**
5+
--exclude Pods,**/MainFlutterWindow.swift,**/AppDelegate.swift,**/.symlinks/**
6+
--swiftversion 5.7

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

+32
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import com.google.android.gms.tasks.TaskCompletionSource;
1111
import com.google.android.gms.tasks.Tasks;
1212
import com.google.firebase.FirebaseApp;
13+
import com.google.firebase.firestore.AggregateQuery;
14+
import com.google.firebase.firestore.AggregateQuerySnapshot;
15+
import com.google.firebase.firestore.AggregateSource;
1316
import com.google.firebase.firestore.DocumentReference;
1417
import com.google.firebase.firestore.DocumentSnapshot;
1518
import com.google.firebase.firestore.FieldPath;
@@ -482,6 +485,32 @@ private Task<Void> waitForPendingWrites(Map<String, Object> arguments) {
482485
return taskCompletionSource.getTask();
483486
}
484487

488+
private Task<Map<String, Object>> aggregateQuery(Map<String, Object> arguments) {
489+
TaskCompletionSource<Map<String, Object>> taskCompletionSource = new TaskCompletionSource<>();
490+
491+
cachedThreadPool.execute(
492+
() -> {
493+
try {
494+
Query query = (Query) Objects.requireNonNull(arguments.get("query"));
495+
// NOTE: There is only "server" as the source at the moment. So this
496+
// is unused for the time being. Using "AggregateSource.SERVER".
497+
// String source = (String) Objects.requireNonNull(arguments.get("source"));
498+
499+
AggregateQuery aggregateQuery = query.count();
500+
AggregateQuerySnapshot aggregateQuerySnapshot =
501+
Tasks.await(aggregateQuery.get(AggregateSource.SERVER));
502+
Map<String, Object> result = new HashMap<>();
503+
result.put("count", aggregateQuerySnapshot.getCount());
504+
taskCompletionSource.setResult(result);
505+
506+
} catch (Exception e) {
507+
taskCompletionSource.setException(e);
508+
}
509+
});
510+
511+
return taskCompletionSource.getTask();
512+
}
513+
485514
@Override
486515
public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result result) {
487516
Task<?> methodCallTask;
@@ -560,6 +589,9 @@ public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result re
560589
case "Firestore#waitForPendingWrites":
561590
methodCallTask = waitForPendingWrites(call.arguments());
562591
break;
592+
case "AggregateQuery#count":
593+
methodCallTask = aggregateQuery(call.arguments());
594+
break;
563595
default:
564596
result.notImplemented();
565597
return;

packages/cloud_firestore/cloud_firestore/example/ios/Podfile

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ end
5656

5757
post_install do |installer|
5858
installer.pods_project.targets.each do |target|
59+
target.build_configurations.each do |config|
60+
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
61+
end
5962
flutter_additional_ios_build_settings(target)
6063
end
6164
end

packages/cloud_firestore/cloud_firestore/example/test_driver/query_e2e.dart

+21
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,27 @@ void runQueryTests() {
18071807
},
18081808
timeout: const Timeout.factor(3),
18091809
);
1810+
1811+
test(
1812+
'count()',
1813+
() async {
1814+
final collection = await initializeTest('count');
1815+
1816+
await Future.wait([
1817+
collection.add({'foo': 'bar'}),
1818+
collection.add({'bar': 'baz'})
1819+
]);
1820+
1821+
AggregateQuery query = collection.count();
1822+
1823+
AggregateQuerySnapshot snapshot = await query.get();
1824+
1825+
expect(
1826+
snapshot.count,
1827+
2,
1828+
);
1829+
},
1830+
);
18101831
});
18111832
});
18121833
}

packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestorePlugin.m

+25
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter
225225
withMethodCallResult:methodCallResult];
226226
} else if ([@"LoadBundle#snapshots" isEqualToString:call.method]) {
227227
[self setupLoadBundleListener:call.arguments withMethodCallResult:methodCallResult];
228+
} else if ([@"AggregateQuery#count" isEqualToString:call.method]) {
229+
[self aggregateQuery:call.arguments withMethodCallResult:methodCallResult];
228230
} else {
229231
methodCallResult.success(FlutterMethodNotImplemented);
230232
}
@@ -535,6 +537,29 @@ - (void)batchCommit:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallRes
535537
}];
536538
}
537539

540+
- (void)aggregateQuery:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
541+
FIRQuery *query = arguments[@"query"];
542+
543+
// NOTE: There is only "server" as the source at the moment. So this
544+
// is unused for the time being. Using "FIRAggregateSourceServer".
545+
// NSString *source = arguments[@"source"];
546+
547+
FIRAggregateQuery *aggregateQuery = [query count];
548+
549+
[aggregateQuery aggregationWithSource:FIRAggregateSourceServer
550+
completion:^(FIRAggregateQuerySnapshot *_Nullable snapshot,
551+
NSError *_Nullable error) {
552+
if (error != nil) {
553+
result.error(nil, nil, nil, error);
554+
} else {
555+
NSMutableDictionary *response = [NSMutableDictionary dictionary];
556+
response[@"count"] = snapshot.count;
557+
558+
result.success(response);
559+
}
560+
}];
561+
}
562+
538563
- (NSString *)registerEventChannelWithPrefix:(NSString *)prefix
539564
streamHandler:(NSObject<FlutterStreamHandler> *)handler {
540565
return [self registerEventChannelWithPrefix:prefix

packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'package:meta/meta.dart';
1717

1818
export 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'
1919
show
20+
AggregateSource,
2021
ListEquality,
2122
FieldPath,
2223
Blob,
@@ -46,3 +47,5 @@ part 'src/snapshot_metadata.dart';
4647
part 'src/transaction.dart';
4748
part 'src/utils/codec_utility.dart';
4849
part 'src/write_batch.dart';
50+
part 'src/aggregate_query.dart';
51+
part 'src/aggregate_query_snapshot.dart';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2022, 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+
part of cloud_firestore;
6+
7+
/// [AggregateQuery] represents the data at a particular location for retrieving metadata
8+
/// without retrieving the actual documents.
9+
class AggregateQuery {
10+
AggregateQuery._(this._delegate, this.query) {
11+
AggregateQueryPlatform.verifyExtends(_delegate);
12+
}
13+
14+
/// [Query] represents the query over the data at a particular location used by the [AggregateQuery] to
15+
/// retrieve the metadata.
16+
final Query query;
17+
18+
final AggregateQueryPlatform _delegate;
19+
20+
/// Returns an [AggregateQuerySnapshot] with the count of the documents that match the query.
21+
Future<AggregateQuerySnapshot> get({
22+
AggregateSource source = AggregateSource.server,
23+
}) async {
24+
return AggregateQuerySnapshot._(await _delegate.get(source: source), query);
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2022, 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+
part of cloud_firestore;
6+
7+
/// [AggregateQuerySnapshot] represents a response to an [AggregateQuery] request.
8+
class AggregateQuerySnapshot {
9+
AggregateQuerySnapshot._(this._delegate, this.query) {
10+
AggregateQuerySnapshotPlatform.verifyExtends(_delegate);
11+
}
12+
final AggregateQuerySnapshotPlatform _delegate;
13+
14+
/// [Query] represents the query over the data at a particular location used by the [AggregateQuery] to
15+
/// retrieve the metadata.
16+
final Query query;
17+
18+
/// Returns the count of the documents that match the query.
19+
int get count => _delegate.count;
20+
}

packages/cloud_firestore/cloud_firestore/lib/src/query.dart

+16
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ abstract class Query<T extends Object?> {
186186
required FromFirestore<R> fromFirestore,
187187
required ToFirestore<R> toFirestore,
188188
});
189+
190+
AggregateQuery count();
189191
}
190192

191193
/// Represents a [Query] over the data at a particular location.
@@ -808,6 +810,13 @@ class _JsonQuery implements Query<Map<String, dynamic>> {
808810

809811
@override
810812
int get hashCode => Object.hash(runtimeType, firestore, _delegate);
813+
814+
/// Represents an [AggregateQuery] over the data at a particular location for retrieving metadata
815+
/// without retrieving the actual documents.
816+
@override
817+
AggregateQuery count() {
818+
return AggregateQuery._(_delegate.count(), this);
819+
}
811820
}
812821

813822
class _WithConverterQuery<T extends Object?> implements Query<T> {
@@ -970,4 +979,11 @@ class _WithConverterQuery<T extends Object?> implements Query<T> {
970979
@override
971980
int get hashCode =>
972981
Object.hash(runtimeType, _fromFirestore, _toFirestore, _originalQuery);
982+
983+
/// Represents an [AggregateQuery] over the data at a particular location for retrieving metadata
984+
/// without retrieving the actual documents.
985+
@override
986+
AggregateQuery count() {
987+
return _originalQuery.count();
988+
}
973989
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2022 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:cloud_firestore/cloud_firestore.dart';
6+
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_firestore.dart';
7+
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_query.dart';
8+
import 'package:cloud_firestore_platform_interface/src/method_channel/utils/firestore_message_codec.dart';
9+
import 'package:firebase_core/firebase_core.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:flutter/services.dart';
12+
13+
import './mock.dart';
14+
15+
int kCount = 4;
16+
17+
void main() {
18+
setupCloudFirestoreMocks();
19+
MethodChannelFirebaseFirestore.channel = const MethodChannel(
20+
'plugins.flutter.io/firebase_firestore',
21+
StandardMethodCodec(AggregateQueryMessageCodec()),
22+
);
23+
24+
MethodChannelFirebaseFirestore.channel.setMockMethodCallHandler((call) async {
25+
if (call.method == 'AggregateQuery#count') {
26+
return {
27+
'count': kCount,
28+
};
29+
}
30+
31+
return null;
32+
});
33+
34+
FirebaseFirestore? firestore;
35+
36+
group('$AggregateQuery', () {
37+
setUpAll(() async {
38+
await Firebase.initializeApp();
39+
firestore = FirebaseFirestore.instance;
40+
});
41+
42+
test('returns the correct `AggregateQuerySnapshot` with correct `count`',
43+
() async {
44+
Query query = firestore!.collection('flutter-tests');
45+
AggregateQuery aggregateQuery = query.count();
46+
47+
expect(query, aggregateQuery.query);
48+
AggregateQuerySnapshot snapshot = await aggregateQuery.get();
49+
50+
expect(snapshot.count, equals(kCount));
51+
});
52+
});
53+
}
54+
55+
class AggregateQueryMessageCodec extends FirestoreMessageCodec {
56+
/// Constructor.
57+
const AggregateQueryMessageCodec();
58+
static const int _kFirestoreInstance = 144;
59+
static const int _kFirestoreQuery = 145;
60+
static const int _kFirestoreSettings = 146;
61+
62+
@override
63+
Object? readValueOfType(int type, ReadBuffer buffer) {
64+
switch (type) {
65+
// The following cases are only used by unit tests, and not by actual application
66+
// code paths.
67+
case _kFirestoreInstance:
68+
String appName = readValue(buffer)! as String;
69+
readValue(buffer);
70+
final FirebaseApp app = Firebase.app(appName);
71+
return MethodChannelFirebaseFirestore(app: app);
72+
case _kFirestoreQuery:
73+
Map<dynamic, dynamic> values =
74+
readValue(buffer)! as Map<dynamic, dynamic>;
75+
final FirebaseApp app = Firebase.app();
76+
return MethodChannelQuery(
77+
MethodChannelFirebaseFirestore(app: app),
78+
values['path'],
79+
);
80+
case _kFirestoreSettings:
81+
readValue(buffer);
82+
return const Settings();
83+
default:
84+
return super.readValueOfType(type, buffer);
85+
}
86+
}
87+
}

packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export 'src/platform_interface/platform_interface_transaction.dart';
2424
export 'src/platform_interface/platform_interface_write_batch.dart';
2525
export 'src/platform_interface/platform_interface_load_bundle_task.dart';
2626
export 'src/platform_interface/platform_interface_load_bundle_task_snapshot.dart';
27+
export 'src/platform_interface/platform_interface_aggregate_query.dart';
28+
export 'src/platform_interface/platform_interface_aggregate_query_snapshot.dart';
29+
export 'src/aggregate_source.dart';
2730
export 'src/snapshot_metadata.dart';
2831
export 'src/source.dart';
2932
export 'src/load_bundle_task_state.dart';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright 2022, 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+
/// [AggregateSource] represents the source of data for an [AggregateQuery].
6+
enum AggregateSource {
7+
/// Indicates that the data should be retrieved from the server.
8+
server,
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2022, 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_platform_interface/src/method_channel/utils/source.dart';
6+
7+
import 'method_channel_firestore.dart';
8+
import '../../cloud_firestore_platform_interface.dart';
9+
10+
/// An implementation of [AggregateQueryPlatform] for the [MethodChannel]
11+
class MethodChannelAggregateQuery extends AggregateQueryPlatform {
12+
MethodChannelAggregateQuery(QueryPlatform query) : super(query);
13+
14+
@override
15+
Future<AggregateQuerySnapshotPlatform> get({
16+
required AggregateSource source,
17+
}) async {
18+
final Map<String, dynamic>? data = await MethodChannelFirebaseFirestore
19+
.channel
20+
.invokeMapMethod<String, dynamic>(
21+
'AggregateQuery#count',
22+
<String, dynamic>{
23+
'query': query,
24+
'firestore': query.firestore,
25+
'source': getAggregateSourceString(source),
26+
},
27+
);
28+
29+
return AggregateQuerySnapshotPlatform(
30+
count: data!['count'] as int,
31+
);
32+
}
33+
}

packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_query.dart

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:cloud_firestore_platform_interface/src/internal/pointer.dart';
1111
import 'package:collection/collection.dart';
1212
import 'package:flutter/services.dart';
1313

14+
import 'method_channel_aggregate_query.dart';
1415
import 'method_channel_firestore.dart';
1516
import 'method_channel_query_snapshot.dart';
1617
import 'utils/source.dart';
@@ -212,6 +213,13 @@ class MethodChannelQuery extends QueryPlatform {
212213
});
213214
}
214215

216+
@override
217+
AggregateQueryPlatform count() {
218+
return MethodChannelAggregateQuery(
219+
this,
220+
);
221+
}
222+
215223
@override
216224
bool operator ==(Object other) {
217225
return runtimeType == other.runtimeType &&

0 commit comments

Comments
 (0)