[current results] Change GetResults API method to take a single filter parameter

Change-Id: I844b048ddb6367f035cf16c88ec22068e121b418
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/154040
Commit-Queue: William Hesse <whesse@google.com>
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/current_results/bin/client.dart b/current_results/bin/client.dart
index 9b183a9..6fc7975 100644
--- a/current_results/bin/client.dart
+++ b/current_results/bin/client.dart
@@ -37,18 +37,23 @@
 
 class QueryCommand extends gRpcCommand {
   QueryCommand() {
-    argParser.addMultiOption('name',
-        abbr: 'n', help: 'test name or prefix to fetch results for');
-    argParser.addMultiOption('configuration',
-        abbr: 'c', help: 'configuration to fetch results for');
+    argParser.addOption('filter',
+        abbr: 'f',
+        help: 'test names, test name prefixes, and configurations, '
+            'comma-separated, to fetch results for');
+    argParser.addOption('limit',
+        abbr: 'l', help: 'number of results to return');
   }
   String get name => 'getResults';
   String get description => 'Send a GetResults gRPC request to the server';
 
   Future<void> runWithChannel(ClientChannel channel) async {
     final request = GetResultsRequest();
-    request.names.addAll(argResults['name']);
-    request.configurations.addAll(argResults['configuration']);
+    request.filter = argResults['filter'] ?? '';
+    if (argResults['limit'] != null) {
+      request.pageSize = int.parse(argResults['limit']);
+    }
+
     final result = await QueryClient(channel).getResults(request);
     print(result.toProto3Json());
   }
diff --git a/current_results/lib/protos/query.proto b/current_results/lib/protos/query.proto
index 05186f4..c7d4eeb 100644
--- a/current_results/lib/protos/query.proto
+++ b/current_results/lib/protos/query.proto
@@ -26,23 +26,22 @@
 }
 
 message GetResultsRequest {
-  // If present, return only results with test names starting with one of
-  // these prefixes.  If absent, return all tests.
-  repeated string names = 1;
-
-  // If present, return only results with configurations on this list.
-  // If absent, return results for all configurations.
-  repeated string configurations = 2;
+  // The filter contains test names and test name prefixes, and configuration
+  // names, separated by commas. If there are test names and/or test name
+  // prefixes, only results for tests matching them are returned.
+  // If there are configuration names, only results on those configurations
+  // are returned. If absent, return all results.
+  string filter = 1;
 
   // The maximum number of results to return.
   // The service may return fewer than this value.
   // If unspecified, will be 100,000.
   // The maximum value is 100,000.
-  int32 page_size = 3;
+  int32 page_size = 2;
 
   // The page token received from a previous call to GetResults.
   // All arguments except page_size must be identical to the previous call.
-  string page_token = 4;
+  string page_token = 3;
 }
 
 message GetResultsResponse {
diff --git a/current_results/lib/src/api_impl.dart b/current_results/lib/src/api_impl.dart
index 00d03b9..99241e7 100644
--- a/current_results/lib/src/api_impl.dart
+++ b/current_results/lib/src/api_impl.dart
@@ -21,7 +21,7 @@
   @override
   Future<GetResultsResponse> getResults(
           ServiceCall call, GetResultsRequest query) =>
-      Future.value(current.query(query));
+      Future.value(current.results(query));
 
   @override
   Future<ListTestsResponse> listTests(
diff --git a/current_results/lib/src/generated/query.pb.dart b/current_results/lib/src/generated/query.pb.dart
index 907e4f4..9b33fe0 100644
--- a/current_results/lib/src/generated/query.pb.dart
+++ b/current_results/lib/src/generated/query.pb.dart
@@ -10,96 +10,76 @@
 import 'package:protobuf/protobuf.dart' as $pb;
 
 class GetResultsRequest extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsRequest',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
-    ..pPS(1, 'names')
-    ..pPS(2, 'configurations')
-    ..a<$core.int>(3, 'pageSize', $pb.PbFieldType.O3)
-    ..aOS(4, 'pageToken')
-    ..hasRequiredFields = false;
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsRequest', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
+    ..aOS(1, 'filter')
+    ..a<$core.int>(2, 'pageSize', $pb.PbFieldType.O3)
+    ..aOS(3, 'pageToken')
+    ..hasRequiredFields = false
+  ;
 
   GetResultsRequest._() : super();
   factory GetResultsRequest() => create();
-  factory GetResultsRequest.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory GetResultsRequest.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
+  factory GetResultsRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory GetResultsRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   GetResultsRequest clone() => GetResultsRequest()..mergeFromMessage(this);
-  GetResultsRequest copyWith(void Function(GetResultsRequest) updates) =>
-      super.copyWith((message) => updates(message as GetResultsRequest));
+  GetResultsRequest copyWith(void Function(GetResultsRequest) updates) => super.copyWith((message) => updates(message as GetResultsRequest));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static GetResultsRequest create() => GetResultsRequest._();
   GetResultsRequest createEmptyInstance() => create();
-  static $pb.PbList<GetResultsRequest> createRepeated() =>
-      $pb.PbList<GetResultsRequest>();
+  static $pb.PbList<GetResultsRequest> createRepeated() => $pb.PbList<GetResultsRequest>();
   @$core.pragma('dart2js:noInline')
-  static GetResultsRequest getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<GetResultsRequest>(create);
+  static GetResultsRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GetResultsRequest>(create);
   static GetResultsRequest _defaultInstance;
 
   @$pb.TagNumber(1)
-  $core.List<$core.String> get names => $_getList(0);
+  $core.String get filter => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set filter($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasFilter() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearFilter() => clearField(1);
 
   @$pb.TagNumber(2)
-  $core.List<$core.String> get configurations => $_getList(1);
+  $core.int get pageSize => $_getIZ(1);
+  @$pb.TagNumber(2)
+  set pageSize($core.int v) { $_setSignedInt32(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasPageSize() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearPageSize() => clearField(2);
 
   @$pb.TagNumber(3)
-  $core.int get pageSize => $_getIZ(2);
+  $core.String get pageToken => $_getSZ(2);
   @$pb.TagNumber(3)
-  set pageSize($core.int v) {
-    $_setSignedInt32(2, v);
-  }
-
+  set pageToken($core.String v) { $_setString(2, v); }
   @$pb.TagNumber(3)
-  $core.bool hasPageSize() => $_has(2);
+  $core.bool hasPageToken() => $_has(2);
   @$pb.TagNumber(3)
-  void clearPageSize() => clearField(3);
-
-  @$pb.TagNumber(4)
-  $core.String get pageToken => $_getSZ(3);
-  @$pb.TagNumber(4)
-  set pageToken($core.String v) {
-    $_setString(3, v);
-  }
-
-  @$pb.TagNumber(4)
-  $core.bool hasPageToken() => $_has(3);
-  @$pb.TagNumber(4)
-  void clearPageToken() => clearField(4);
+  void clearPageToken() => clearField(3);
 }
 
 class GetResultsResponse extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsResponse',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsResponse', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
     ..pc<Result>(1, 'results', $pb.PbFieldType.PM, subBuilder: Result.create)
     ..aOS(2, 'nextPageToken')
-    ..hasRequiredFields = false;
+    ..hasRequiredFields = false
+  ;
 
   GetResultsResponse._() : super();
   factory GetResultsResponse() => create();
-  factory GetResultsResponse.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory GetResultsResponse.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
+  factory GetResultsResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory GetResultsResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   GetResultsResponse clone() => GetResultsResponse()..mergeFromMessage(this);
-  GetResultsResponse copyWith(void Function(GetResultsResponse) updates) =>
-      super.copyWith((message) => updates(message as GetResultsResponse));
+  GetResultsResponse copyWith(void Function(GetResultsResponse) updates) => super.copyWith((message) => updates(message as GetResultsResponse));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static GetResultsResponse create() => GetResultsResponse._();
   GetResultsResponse createEmptyInstance() => create();
-  static $pb.PbList<GetResultsResponse> createRepeated() =>
-      $pb.PbList<GetResultsResponse>();
+  static $pb.PbList<GetResultsResponse> createRepeated() => $pb.PbList<GetResultsResponse>();
   @$core.pragma('dart2js:noInline')
-  static GetResultsResponse getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<GetResultsResponse>(create);
+  static GetResultsResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GetResultsResponse>(create);
   static GetResultsResponse _defaultInstance;
 
   @$pb.TagNumber(1)
@@ -108,10 +88,7 @@
   @$pb.TagNumber(2)
   $core.String get nextPageToken => $_getSZ(1);
   @$pb.TagNumber(2)
-  set nextPageToken($core.String v) {
-    $_setString(1, v);
-  }
-
+  set nextPageToken($core.String v) { $_setString(1, v); }
   @$pb.TagNumber(2)
   $core.bool hasNextPageToken() => $_has(1);
   @$pb.TagNumber(2)
@@ -119,45 +96,35 @@
 }
 
 class Result extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('Result',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('Result', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
     ..aOS(1, 'name')
     ..aOS(2, 'configuration')
     ..aOS(3, 'result')
     ..aOS(4, 'expected')
     ..aOB(5, 'flaky')
     ..a<$core.int>(6, 'timeMs', $pb.PbFieldType.O3)
-    ..hasRequiredFields = false;
+    ..hasRequiredFields = false
+  ;
 
   Result._() : super();
   factory Result() => create();
-  factory Result.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory Result.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
+  factory Result.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory Result.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   Result clone() => Result()..mergeFromMessage(this);
-  Result copyWith(void Function(Result) updates) =>
-      super.copyWith((message) => updates(message as Result));
+  Result copyWith(void Function(Result) updates) => super.copyWith((message) => updates(message as Result));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static Result create() => Result._();
   Result createEmptyInstance() => create();
   static $pb.PbList<Result> createRepeated() => $pb.PbList<Result>();
   @$core.pragma('dart2js:noInline')
-  static Result getDefault() =>
-      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Result>(create);
+  static Result getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Result>(create);
   static Result _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get name => $_getSZ(0);
   @$pb.TagNumber(1)
-  set name($core.String v) {
-    $_setString(0, v);
-  }
-
+  set name($core.String v) { $_setString(0, v); }
   @$pb.TagNumber(1)
   $core.bool hasName() => $_has(0);
   @$pb.TagNumber(1)
@@ -166,10 +133,7 @@
   @$pb.TagNumber(2)
   $core.String get configuration => $_getSZ(1);
   @$pb.TagNumber(2)
-  set configuration($core.String v) {
-    $_setString(1, v);
-  }
-
+  set configuration($core.String v) { $_setString(1, v); }
   @$pb.TagNumber(2)
   $core.bool hasConfiguration() => $_has(1);
   @$pb.TagNumber(2)
@@ -178,10 +142,7 @@
   @$pb.TagNumber(3)
   $core.String get result => $_getSZ(2);
   @$pb.TagNumber(3)
-  set result($core.String v) {
-    $_setString(2, v);
-  }
-
+  set result($core.String v) { $_setString(2, v); }
   @$pb.TagNumber(3)
   $core.bool hasResult() => $_has(2);
   @$pb.TagNumber(3)
@@ -190,10 +151,7 @@
   @$pb.TagNumber(4)
   $core.String get expected => $_getSZ(3);
   @$pb.TagNumber(4)
-  set expected($core.String v) {
-    $_setString(3, v);
-  }
-
+  set expected($core.String v) { $_setString(3, v); }
   @$pb.TagNumber(4)
   $core.bool hasExpected() => $_has(3);
   @$pb.TagNumber(4)
@@ -202,10 +160,7 @@
   @$pb.TagNumber(5)
   $core.bool get flaky => $_getBF(4);
   @$pb.TagNumber(5)
-  set flaky($core.bool v) {
-    $_setBool(4, v);
-  }
-
+  set flaky($core.bool v) { $_setBool(4, v); }
   @$pb.TagNumber(5)
   $core.bool hasFlaky() => $_has(4);
   @$pb.TagNumber(5)
@@ -214,10 +169,7 @@
   @$pb.TagNumber(6)
   $core.int get timeMs => $_getIZ(5);
   @$pb.TagNumber(6)
-  set timeMs($core.int v) {
-    $_setSignedInt32(5, v);
-  }
-
+  set timeMs($core.int v) { $_setSignedInt32(5, v); }
   @$pb.TagNumber(6)
   $core.bool hasTimeMs() => $_has(5);
   @$pb.TagNumber(6)
@@ -225,42 +177,31 @@
 }
 
 class ListTestsRequest extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsRequest',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsRequest', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
     ..aOS(1, 'prefix')
     ..a<$core.int>(2, 'limit', $pb.PbFieldType.O3)
-    ..hasRequiredFields = false;
+    ..hasRequiredFields = false
+  ;
 
   ListTestsRequest._() : super();
   factory ListTestsRequest() => create();
-  factory ListTestsRequest.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory ListTestsRequest.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
+  factory ListTestsRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory ListTestsRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   ListTestsRequest clone() => ListTestsRequest()..mergeFromMessage(this);
-  ListTestsRequest copyWith(void Function(ListTestsRequest) updates) =>
-      super.copyWith((message) => updates(message as ListTestsRequest));
+  ListTestsRequest copyWith(void Function(ListTestsRequest) updates) => super.copyWith((message) => updates(message as ListTestsRequest));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListTestsRequest create() => ListTestsRequest._();
   ListTestsRequest createEmptyInstance() => create();
-  static $pb.PbList<ListTestsRequest> createRepeated() =>
-      $pb.PbList<ListTestsRequest>();
+  static $pb.PbList<ListTestsRequest> createRepeated() => $pb.PbList<ListTestsRequest>();
   @$core.pragma('dart2js:noInline')
-  static ListTestsRequest getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<ListTestsRequest>(create);
+  static ListTestsRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListTestsRequest>(create);
   static ListTestsRequest _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get prefix => $_getSZ(0);
   @$pb.TagNumber(1)
-  set prefix($core.String v) {
-    $_setString(0, v);
-  }
-
+  set prefix($core.String v) { $_setString(0, v); }
   @$pb.TagNumber(1)
   $core.bool hasPrefix() => $_has(0);
   @$pb.TagNumber(1)
@@ -269,10 +210,7 @@
   @$pb.TagNumber(2)
   $core.int get limit => $_getIZ(1);
   @$pb.TagNumber(2)
-  set limit($core.int v) {
-    $_setSignedInt32(1, v);
-  }
-
+  set limit($core.int v) { $_setSignedInt32(1, v); }
   @$pb.TagNumber(2)
   $core.bool hasLimit() => $_has(1);
   @$pb.TagNumber(2)
@@ -280,32 +218,24 @@
 }
 
 class ListTestsResponse extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsResponse',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsResponse', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
     ..pPS(1, 'names')
-    ..hasRequiredFields = false;
+    ..hasRequiredFields = false
+  ;
 
   ListTestsResponse._() : super();
   factory ListTestsResponse() => create();
-  factory ListTestsResponse.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory ListTestsResponse.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
+  factory ListTestsResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory ListTestsResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   ListTestsResponse clone() => ListTestsResponse()..mergeFromMessage(this);
-  ListTestsResponse copyWith(void Function(ListTestsResponse) updates) =>
-      super.copyWith((message) => updates(message as ListTestsResponse));
+  ListTestsResponse copyWith(void Function(ListTestsResponse) updates) => super.copyWith((message) => updates(message as ListTestsResponse));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListTestsResponse create() => ListTestsResponse._();
   ListTestsResponse createEmptyInstance() => create();
-  static $pb.PbList<ListTestsResponse> createRepeated() =>
-      $pb.PbList<ListTestsResponse>();
+  static $pb.PbList<ListTestsResponse> createRepeated() => $pb.PbList<ListTestsResponse>();
   @$core.pragma('dart2js:noInline')
-  static ListTestsResponse getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<ListTestsResponse>(create);
+  static ListTestsResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListTestsResponse>(create);
   static ListTestsResponse _defaultInstance;
 
   @$pb.TagNumber(1)
@@ -313,44 +243,30 @@
 }
 
 class ListConfigurationsRequest extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListConfigurationsRequest',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListConfigurationsRequest', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
     ..aOS(1, 'prefix')
-    ..hasRequiredFields = false;
+    ..hasRequiredFields = false
+  ;
 
   ListConfigurationsRequest._() : super();
   factory ListConfigurationsRequest() => create();
-  factory ListConfigurationsRequest.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory ListConfigurationsRequest.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
-  ListConfigurationsRequest clone() =>
-      ListConfigurationsRequest()..mergeFromMessage(this);
-  ListConfigurationsRequest copyWith(
-          void Function(ListConfigurationsRequest) updates) =>
-      super
-          .copyWith((message) => updates(message as ListConfigurationsRequest));
+  factory ListConfigurationsRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory ListConfigurationsRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  ListConfigurationsRequest clone() => ListConfigurationsRequest()..mergeFromMessage(this);
+  ListConfigurationsRequest copyWith(void Function(ListConfigurationsRequest) updates) => super.copyWith((message) => updates(message as ListConfigurationsRequest));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListConfigurationsRequest create() => ListConfigurationsRequest._();
   ListConfigurationsRequest createEmptyInstance() => create();
-  static $pb.PbList<ListConfigurationsRequest> createRepeated() =>
-      $pb.PbList<ListConfigurationsRequest>();
+  static $pb.PbList<ListConfigurationsRequest> createRepeated() => $pb.PbList<ListConfigurationsRequest>();
   @$core.pragma('dart2js:noInline')
-  static ListConfigurationsRequest getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<ListConfigurationsRequest>(create);
+  static ListConfigurationsRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListConfigurationsRequest>(create);
   static ListConfigurationsRequest _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get prefix => $_getSZ(0);
   @$pb.TagNumber(1)
-  set prefix($core.String v) {
-    $_setString(0, v);
-  }
-
+  set prefix($core.String v) { $_setString(0, v); }
   @$pb.TagNumber(1)
   $core.bool hasPrefix() => $_has(0);
   @$pb.TagNumber(1)
@@ -358,36 +274,24 @@
 }
 
 class ListConfigurationsResponse extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
-      'ListConfigurationsResponse',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListConfigurationsResponse', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
     ..pPS(1, 'configurations')
-    ..hasRequiredFields = false;
+    ..hasRequiredFields = false
+  ;
 
   ListConfigurationsResponse._() : super();
   factory ListConfigurationsResponse() => create();
-  factory ListConfigurationsResponse.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory ListConfigurationsResponse.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
-  ListConfigurationsResponse clone() =>
-      ListConfigurationsResponse()..mergeFromMessage(this);
-  ListConfigurationsResponse copyWith(
-          void Function(ListConfigurationsResponse) updates) =>
-      super.copyWith(
-          (message) => updates(message as ListConfigurationsResponse));
+  factory ListConfigurationsResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory ListConfigurationsResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  ListConfigurationsResponse clone() => ListConfigurationsResponse()..mergeFromMessage(this);
+  ListConfigurationsResponse copyWith(void Function(ListConfigurationsResponse) updates) => super.copyWith((message) => updates(message as ListConfigurationsResponse));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListConfigurationsResponse create() => ListConfigurationsResponse._();
   ListConfigurationsResponse createEmptyInstance() => create();
-  static $pb.PbList<ListConfigurationsResponse> createRepeated() =>
-      $pb.PbList<ListConfigurationsResponse>();
+  static $pb.PbList<ListConfigurationsResponse> createRepeated() => $pb.PbList<ListConfigurationsResponse>();
   @$core.pragma('dart2js:noInline')
-  static ListConfigurationsResponse getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<ListConfigurationsResponse>(create);
+  static ListConfigurationsResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListConfigurationsResponse>(create);
   static ListConfigurationsResponse _defaultInstance;
 
   @$pb.TagNumber(1)
@@ -395,33 +299,24 @@
 }
 
 class FetchResponse extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('FetchResponse',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
-    ..pc<ConfigurationUpdate>(1, 'updates', $pb.PbFieldType.PM,
-        subBuilder: ConfigurationUpdate.create)
-    ..hasRequiredFields = false;
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('FetchResponse', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
+    ..pc<ConfigurationUpdate>(1, 'updates', $pb.PbFieldType.PM, subBuilder: ConfigurationUpdate.create)
+    ..hasRequiredFields = false
+  ;
 
   FetchResponse._() : super();
   factory FetchResponse() => create();
-  factory FetchResponse.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory FetchResponse.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
+  factory FetchResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory FetchResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   FetchResponse clone() => FetchResponse()..mergeFromMessage(this);
-  FetchResponse copyWith(void Function(FetchResponse) updates) =>
-      super.copyWith((message) => updates(message as FetchResponse));
+  FetchResponse copyWith(void Function(FetchResponse) updates) => super.copyWith((message) => updates(message as FetchResponse));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static FetchResponse create() => FetchResponse._();
   FetchResponse createEmptyInstance() => create();
-  static $pb.PbList<FetchResponse> createRepeated() =>
-      $pb.PbList<FetchResponse>();
+  static $pb.PbList<FetchResponse> createRepeated() => $pb.PbList<FetchResponse>();
   @$core.pragma('dart2js:noInline')
-  static FetchResponse getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<FetchResponse>(create);
+  static FetchResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<FetchResponse>(create);
   static FetchResponse _defaultInstance;
 
   @$pb.TagNumber(1)
@@ -429,43 +324,33 @@
 }
 
 class ConfigurationUpdate extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ConfigurationUpdate',
-      package: const $pb.PackageName('current_results'),
-      createEmptyInstance: create)
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ConfigurationUpdate', package: const $pb.PackageName('current_results'), createEmptyInstance: create)
     ..aOS(1, 'configuration')
-    ..hasRequiredFields = false;
+    ..hasRequiredFields = false
+  ;
 
   ConfigurationUpdate._() : super();
   factory ConfigurationUpdate() => create();
-  factory ConfigurationUpdate.fromBuffer($core.List<$core.int> i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromBuffer(i, r);
-  factory ConfigurationUpdate.fromJson($core.String i,
-          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
-      create()..mergeFromJson(i, r);
+  factory ConfigurationUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory ConfigurationUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   ConfigurationUpdate clone() => ConfigurationUpdate()..mergeFromMessage(this);
-  ConfigurationUpdate copyWith(void Function(ConfigurationUpdate) updates) =>
-      super.copyWith((message) => updates(message as ConfigurationUpdate));
+  ConfigurationUpdate copyWith(void Function(ConfigurationUpdate) updates) => super.copyWith((message) => updates(message as ConfigurationUpdate));
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ConfigurationUpdate create() => ConfigurationUpdate._();
   ConfigurationUpdate createEmptyInstance() => create();
-  static $pb.PbList<ConfigurationUpdate> createRepeated() =>
-      $pb.PbList<ConfigurationUpdate>();
+  static $pb.PbList<ConfigurationUpdate> createRepeated() => $pb.PbList<ConfigurationUpdate>();
   @$core.pragma('dart2js:noInline')
-  static ConfigurationUpdate getDefault() => _defaultInstance ??=
-      $pb.GeneratedMessage.$_defaultFor<ConfigurationUpdate>(create);
+  static ConfigurationUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ConfigurationUpdate>(create);
   static ConfigurationUpdate _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get configuration => $_getSZ(0);
   @$pb.TagNumber(1)
-  set configuration($core.String v) {
-    $_setString(0, v);
-  }
-
+  set configuration($core.String v) { $_setString(0, v); }
   @$pb.TagNumber(1)
   $core.bool hasConfiguration() => $_has(0);
   @$pb.TagNumber(1)
   void clearConfiguration() => clearField(1);
 }
+
diff --git a/current_results/lib/src/generated/query.pbenum.dart b/current_results/lib/src/generated/query.pbenum.dart
index 277e345..f48105f 100644
--- a/current_results/lib/src/generated/query.pbenum.dart
+++ b/current_results/lib/src/generated/query.pbenum.dart
@@ -4,3 +4,4 @@
 //
 // @dart = 2.3
 // ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
+
diff --git a/current_results/lib/src/generated/query.pbjson.dart b/current_results/lib/src/generated/query.pbjson.dart
index c41129e..163e17b 100644
--- a/current_results/lib/src/generated/query.pbjson.dart
+++ b/current_results/lib/src/generated/query.pbjson.dart
@@ -8,37 +8,17 @@
 const GetResultsRequest$json = const {
   '1': 'GetResultsRequest',
   '2': const [
-    const {'1': 'names', '3': 1, '4': 3, '5': 9, '10': 'names'},
-    const {
-      '1': 'configurations',
-      '3': 2,
-      '4': 3,
-      '5': 9,
-      '10': 'configurations'
-    },
-    const {'1': 'page_size', '3': 3, '4': 1, '5': 5, '10': 'pageSize'},
-    const {'1': 'page_token', '3': 4, '4': 1, '5': 9, '10': 'pageToken'},
+    const {'1': 'filter', '3': 1, '4': 1, '5': 9, '10': 'filter'},
+    const {'1': 'page_size', '3': 2, '4': 1, '5': 5, '10': 'pageSize'},
+    const {'1': 'page_token', '3': 3, '4': 1, '5': 9, '10': 'pageToken'},
   ],
 };
 
 const GetResultsResponse$json = const {
   '1': 'GetResultsResponse',
   '2': const [
-    const {
-      '1': 'results',
-      '3': 1,
-      '4': 3,
-      '5': 11,
-      '6': '.current_results.Result',
-      '10': 'results'
-    },
-    const {
-      '1': 'next_page_token',
-      '3': 2,
-      '4': 1,
-      '5': 9,
-      '10': 'nextPageToken'
-    },
+    const {'1': 'results', '3': 1, '4': 3, '5': 11, '6': '.current_results.Result', '10': 'results'},
+    const {'1': 'next_page_token', '3': 2, '4': 1, '5': 9, '10': 'nextPageToken'},
   ],
 };
 
@@ -79,27 +59,14 @@
 const ListConfigurationsResponse$json = const {
   '1': 'ListConfigurationsResponse',
   '2': const [
-    const {
-      '1': 'configurations',
-      '3': 1,
-      '4': 3,
-      '5': 9,
-      '10': 'configurations'
-    },
+    const {'1': 'configurations', '3': 1, '4': 3, '5': 9, '10': 'configurations'},
   ],
 };
 
 const FetchResponse$json = const {
   '1': 'FetchResponse',
   '2': const [
-    const {
-      '1': 'updates',
-      '3': 1,
-      '4': 3,
-      '5': 11,
-      '6': '.current_results.ConfigurationUpdate',
-      '10': 'updates'
-    },
+    const {'1': 'updates', '3': 1, '4': 3, '5': 11, '6': '.current_results.ConfigurationUpdate', '10': 'updates'},
   ],
 };
 
@@ -109,3 +76,4 @@
     const {'1': 'configuration', '3': 1, '4': 1, '5': 9, '10': 'configuration'},
   ],
 };
+
diff --git a/current_results/lib/src/slice.dart b/current_results/lib/src/slice.dart
index c910630..cca8382 100644
--- a/current_results/lib/src/slice.dart
+++ b/current_results/lib/src/slice.dart
@@ -6,7 +6,6 @@
 import 'dart:math';
 
 import 'package:collection/collection.dart';
-import 'package:grpc/grpc.dart';
 
 import 'package:current_results/src/result.dart';
 import 'package:current_results/src/generated/query.pb.dart' as query_api;
@@ -90,23 +89,28 @@
     return result;
   }
 
-  query_api.GetResultsResponse query(query_api.GetResultsRequest query) {
+  query_api.GetResultsResponse results(query_api.GetResultsRequest query) {
     final response = query_api.GetResultsResponse();
-    final configurations =
-        query.configurations.isEmpty ? _stored.keys : query.configurations;
-    if (query.names.isEmpty) {
-      for (final configuration in configurations) {
-        response.results.addAll(_stored[configuration].map(Result.toApi));
-      }
+    final limit = min(100000, query.pageSize == 0 ? 100000 : query.pageSize);
+    final filterTerms =
+        query.filter.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty);
+    var configurations = filterTerms.where(_stored.containsKey);
+    if (configurations.isEmpty) {
+      configurations = _stored.keys;
+    }
+    final tests = filterTerms.where((t) => !_stored.containsKey(t)).toList();
+    if (tests.isEmpty) {
+      response.results.addAll(configurations
+          .expand((configuration) => _stored[configuration])
+          .map(Result.toApi)
+          .take(limit));
+
       return response;
     }
     for (final configuration in configurations) {
       final sorted = _stored[configuration];
-      if (sorted == null) {
-        throw GrpcError.notFound(
-            "Results for configuration $configuration not found");
-      }
-      for (final testNamePrefix in query.names) {
+      for (final testNamePrefix in tests) {
+        if (response.results.length >= limit) break;
         final prefixResult =
             Result(testNamePrefix, null, null, null, null, null, null);
         final start =
@@ -119,8 +123,10 @@
             end = lowerBound<Result>(sorted, prefixResult,
                 compare: compareNamesPrefixMatchesBelow);
           }
-          response.results
-              .addAll(sorted.getRange(start, end).map(Result.toApi));
+          response.results.addAll(sorted
+              .getRange(start, end)
+              .map(Result.toApi)
+              .take(limit - response.results.length));
         }
       }
     }