Skip to content

Commit

Permalink
feat(firestore): count() feature for counting documents without ret…
Browse files Browse the repository at this point in the history
…rieving documents. (#9699)
  • Loading branch information
russellwheatley committed Oct 18, 2022
1 parent 1829ee7 commit ac0bf73
Show file tree
Hide file tree
Showing 23 changed files with 492 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
--maxwidth 100
--wrapparameters afterfirst
--disable sortedImports,unusedArguments,wrapMultilineStatementBraces
--exclude Pods,**/MainFlutterWindow.swift,**/AppDelegate.swift,**/.symlinks/**
--exclude Pods,**/MainFlutterWindow.swift,**/AppDelegate.swift,**/.symlinks/**
--swiftversion 5.7
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.firestore.AggregateQuery;
import com.google.firebase.firestore.AggregateQuerySnapshot;
import com.google.firebase.firestore.AggregateSource;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FieldPath;
Expand Down Expand Up @@ -482,6 +485,32 @@ private Task<Void> waitForPendingWrites(Map<String, Object> arguments) {
return taskCompletionSource.getTask();
}

private Task<Map<String, Object>> aggregateQuery(Map<String, Object> arguments) {
TaskCompletionSource<Map<String, Object>> taskCompletionSource = new TaskCompletionSource<>();

cachedThreadPool.execute(
() -> {
try {
Query query = (Query) Objects.requireNonNull(arguments.get("query"));
// NOTE: There is only "server" as the source at the moment. So this
// is unused for the time being. Using "AggregateSource.SERVER".
// String source = (String) Objects.requireNonNull(arguments.get("source"));

AggregateQuery aggregateQuery = query.count();
AggregateQuerySnapshot aggregateQuerySnapshot =
Tasks.await(aggregateQuery.get(AggregateSource.SERVER));
Map<String, Object> result = new HashMap<>();
result.put("count", aggregateQuerySnapshot.getCount());
taskCompletionSource.setResult(result);

} catch (Exception e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

@Override
public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result result) {
Task<?> methodCallTask;
Expand Down Expand Up @@ -560,6 +589,9 @@ public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result re
case "Firestore#waitForPendingWrites":
methodCallTask = waitForPendingWrites(call.arguments());
break;
case "AggregateQuery#count":
methodCallTask = aggregateQuery(call.arguments());
break;
default:
result.notImplemented();
return;
Expand Down
3 changes: 3 additions & 0 deletions packages/cloud_firestore/cloud_firestore/example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ end

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
end
flutter_additional_ios_build_settings(target)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -1807,6 +1807,27 @@ void runQueryTests() {
},
timeout: const Timeout.factor(3),
);

test(
'count()',
() async {
final collection = await initializeTest('count');

await Future.wait([
collection.add({'foo': 'bar'}),
collection.add({'bar': 'baz'})
]);

AggregateQuery query = collection.count();

AggregateQuerySnapshot snapshot = await query.get();

expect(
snapshot.count,
2,
);
},
);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter
withMethodCallResult:methodCallResult];
} else if ([@"LoadBundle#snapshots" isEqualToString:call.method]) {
[self setupLoadBundleListener:call.arguments withMethodCallResult:methodCallResult];
} else if ([@"AggregateQuery#count" isEqualToString:call.method]) {
[self aggregateQuery:call.arguments withMethodCallResult:methodCallResult];
} else {
methodCallResult.success(FlutterMethodNotImplemented);
}
Expand Down Expand Up @@ -535,6 +537,29 @@ - (void)batchCommit:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallRes
}];
}

- (void)aggregateQuery:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
FIRQuery *query = arguments[@"query"];

// NOTE: There is only "server" as the source at the moment. So this
// is unused for the time being. Using "FIRAggregateSourceServer".
// NSString *source = arguments[@"source"];

FIRAggregateQuery *aggregateQuery = [query count];

[aggregateQuery aggregationWithSource:FIRAggregateSourceServer
completion:^(FIRAggregateQuerySnapshot *_Nullable snapshot,
NSError *_Nullable error) {
if (error != nil) {
result.error(nil, nil, nil, error);
} else {
NSMutableDictionary *response = [NSMutableDictionary dictionary];
response[@"count"] = snapshot.count;

result.success(response);
}
}];
}

- (NSString *)registerEventChannelWithPrefix:(NSString *)prefix
streamHandler:(NSObject<FlutterStreamHandler> *)handler {
return [self registerEventChannelWithPrefix:prefix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:meta/meta.dart';

export 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'
show
AggregateSource,
ListEquality,
FieldPath,
Blob,
Expand Down Expand Up @@ -46,3 +47,5 @@ part 'src/snapshot_metadata.dart';
part 'src/transaction.dart';
part 'src/utils/codec_utility.dart';
part 'src/write_batch.dart';
part 'src/aggregate_query.dart';
part 'src/aggregate_query_snapshot.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2022, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of cloud_firestore;

/// [AggregateQuery] represents the data at a particular location for retrieving metadata
/// without retrieving the actual documents.
class AggregateQuery {
AggregateQuery._(this._delegate, this.query) {
AggregateQueryPlatform.verifyExtends(_delegate);
}

/// [Query] represents the query over the data at a particular location used by the [AggregateQuery] to
/// retrieve the metadata.
final Query query;

final AggregateQueryPlatform _delegate;

/// Returns an [AggregateQuerySnapshot] with the count of the documents that match the query.
Future<AggregateQuerySnapshot> get({
AggregateSource source = AggregateSource.server,
}) async {
return AggregateQuerySnapshot._(await _delegate.get(source: source), query);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2022, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of cloud_firestore;

/// [AggregateQuerySnapshot] represents a response to an [AggregateQuery] request.
class AggregateQuerySnapshot {
AggregateQuerySnapshot._(this._delegate, this.query) {
AggregateQuerySnapshotPlatform.verifyExtends(_delegate);
}
final AggregateQuerySnapshotPlatform _delegate;

/// [Query] represents the query over the data at a particular location used by the [AggregateQuery] to
/// retrieve the metadata.
final Query query;

/// Returns the count of the documents that match the query.
int get count => _delegate.count;
}
16 changes: 16 additions & 0 deletions packages/cloud_firestore/cloud_firestore/lib/src/query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ abstract class Query<T extends Object?> {
required FromFirestore<R> fromFirestore,
required ToFirestore<R> toFirestore,
});

AggregateQuery count();
}

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

@override
int get hashCode => Object.hash(runtimeType, firestore, _delegate);

/// Represents an [AggregateQuery] over the data at a particular location for retrieving metadata
/// without retrieving the actual documents.
@override
AggregateQuery count() {
return AggregateQuery._(_delegate.count(), this);
}
}

class _WithConverterQuery<T extends Object?> implements Query<T> {
Expand Down Expand Up @@ -970,4 +979,11 @@ class _WithConverterQuery<T extends Object?> implements Query<T> {
@override
int get hashCode =>
Object.hash(runtimeType, _fromFirestore, _toFirestore, _originalQuery);

/// Represents an [AggregateQuery] over the data at a particular location for retrieving metadata
/// without retrieving the actual documents.
@override
AggregateQuery count() {
return _originalQuery.count();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_firestore.dart';
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_query.dart';
import 'package:cloud_firestore_platform_interface/src/method_channel/utils/firestore_message_codec.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart';

import './mock.dart';

int kCount = 4;

void main() {
setupCloudFirestoreMocks();
MethodChannelFirebaseFirestore.channel = const MethodChannel(
'plugins.flutter.io/firebase_firestore',
StandardMethodCodec(AggregateQueryMessageCodec()),
);

MethodChannelFirebaseFirestore.channel.setMockMethodCallHandler((call) async {
if (call.method == 'AggregateQuery#count') {
return {
'count': kCount,
};
}

return null;
});

FirebaseFirestore? firestore;

group('$AggregateQuery', () {
setUpAll(() async {
await Firebase.initializeApp();
firestore = FirebaseFirestore.instance;
});

test('returns the correct `AggregateQuerySnapshot` with correct `count`',
() async {
Query query = firestore!.collection('flutter-tests');
AggregateQuery aggregateQuery = query.count();

expect(query, aggregateQuery.query);
AggregateQuerySnapshot snapshot = await aggregateQuery.get();

expect(snapshot.count, equals(kCount));
});
});
}

class AggregateQueryMessageCodec extends FirestoreMessageCodec {
/// Constructor.
const AggregateQueryMessageCodec();
static const int _kFirestoreInstance = 144;
static const int _kFirestoreQuery = 145;
static const int _kFirestoreSettings = 146;

@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
// The following cases are only used by unit tests, and not by actual application
// code paths.
case _kFirestoreInstance:
String appName = readValue(buffer)! as String;
readValue(buffer);
final FirebaseApp app = Firebase.app(appName);
return MethodChannelFirebaseFirestore(app: app);
case _kFirestoreQuery:
Map<dynamic, dynamic> values =
readValue(buffer)! as Map<dynamic, dynamic>;
final FirebaseApp app = Firebase.app();
return MethodChannelQuery(
MethodChannelFirebaseFirestore(app: app),
values['path'],
);
case _kFirestoreSettings:
readValue(buffer);
return const Settings();
default:
return super.readValueOfType(type, buffer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export 'src/platform_interface/platform_interface_transaction.dart';
export 'src/platform_interface/platform_interface_write_batch.dart';
export 'src/platform_interface/platform_interface_load_bundle_task.dart';
export 'src/platform_interface/platform_interface_load_bundle_task_snapshot.dart';
export 'src/platform_interface/platform_interface_aggregate_query.dart';
export 'src/platform_interface/platform_interface_aggregate_query_snapshot.dart';
export 'src/aggregate_source.dart';
export 'src/snapshot_metadata.dart';
export 'src/source.dart';
export 'src/load_bundle_task_state.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2022, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// [AggregateSource] represents the source of data for an [AggregateQuery].
enum AggregateSource {
/// Indicates that the data should be retrieved from the server.
server,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2022, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:cloud_firestore_platform_interface/src/method_channel/utils/source.dart';

import 'method_channel_firestore.dart';
import '../../cloud_firestore_platform_interface.dart';

/// An implementation of [AggregateQueryPlatform] for the [MethodChannel]
class MethodChannelAggregateQuery extends AggregateQueryPlatform {
MethodChannelAggregateQuery(QueryPlatform query) : super(query);

@override
Future<AggregateQuerySnapshotPlatform> get({
required AggregateSource source,
}) async {
final Map<String, dynamic>? data = await MethodChannelFirebaseFirestore
.channel
.invokeMapMethod<String, dynamic>(
'AggregateQuery#count',
<String, dynamic>{
'query': query,
'firestore': query.firestore,
'source': getAggregateSourceString(source),
},
);

return AggregateQuerySnapshotPlatform(
count: data!['count'] as int,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:cloud_firestore_platform_interface/src/internal/pointer.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';

import 'method_channel_aggregate_query.dart';
import 'method_channel_firestore.dart';
import 'method_channel_query_snapshot.dart';
import 'utils/source.dart';
Expand Down Expand Up @@ -212,6 +213,13 @@ class MethodChannelQuery extends QueryPlatform {
});
}

@override
AggregateQueryPlatform count() {
return MethodChannelAggregateQuery(
this,
);
}

@override
bool operator ==(Object other) {
return runtimeType == other.runtimeType &&
Expand Down

0 comments on commit ac0bf73

Please sign in to comment.