Skip to content

Add IN and ARRAY_CONTAINS_ANY (not publicly exposed) #3492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 77 additions & 5 deletions Firestore/Example/Tests/Integration/API/FIRQueryTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#import <XCTest/XCTest.h>

#import "Firestore/Source/API/FIRQuery+Internal.h"

#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h"
#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"

Expand Down Expand Up @@ -302,7 +304,7 @@ - (void)testCanHaveMultipleMutationsWhileOffline {
]));
}

- (void)testArrayContainsQueries {
- (void)testQueriesCanUseArrayContainsFilters {
NSDictionary *testDocs = @{
@"a" : @{@"array" : @[ @42 ]},
@"b" : @{@"array" : @[ @"a", @42, @"c" ]},
Expand All @@ -314,15 +316,85 @@ - (void)testArrayContainsQueries {
// Search for 42
FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"array"
arrayContains:@42]];
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
@{@"array" : @[ @42 ]}, @{@"array" : @[ @"a", @42, @"c" ]},
@{@"array" : @[ @42 ], @"array2" : @[ @"bingo" ]}
]));
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot),
(@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"d"] ]));

// NOTE: The backend doesn't currently support null, NaN, objects, or arrays, so there isn't much
// of anything else interesting to test.
}

- (void)testQueriesCanUseInFilters {
// TODO(b/138855186): Re-enable in prod once feature lands in backend.
if (![FSTIntegrationTestCase isRunningAgainstEmulator]) return;

NSDictionary *testDocs = @{
@"a" : @{@"zip" : @98101},
@"b" : @{@"zip" : @91102},
@"c" : @{@"zip" : @98103},
@"d" : @{@"zip" : @[ @98101 ]},
@"e" : @{@"zip" : @[ @"98101", @{@"zip" : @98101} ]},
@"f" : @{@"zip" : @{@"code" : @500}}
};
FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];

// Search for zips matching [98101, 98103].
FIRQuerySnapshot *snapshot =
[self readDocumentSetForRef:[collection queryWhereField:@"zip" in:@[ @98101, @98103 ]]];
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ testDocs[@"a"], testDocs[@"c"] ]));

// With objects
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip"
in:@[ @{@"code" : @500} ]]];
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ testDocs[@"f"] ]));
}

- (void)testQueriesCanUseInFiltersWithDocIds {
// TODO(b/138855186): Re-enable in prod once feature lands in backend.
if (![FSTIntegrationTestCase isRunningAgainstEmulator]) return;

NSDictionary *testDocs = @{
@"aa" : @{@"key" : @"aa"},
@"ab" : @{@"key" : @"ab"},
@"ba" : @{@"key" : @"ba"},
@"bb" : @{@"key" : @"bb"},
};
FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];

FIRQuerySnapshot *snapshot =
[self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
in:@[ @"aa", @"ab" ]]];
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ testDocs[@"aa"], testDocs[@"ab"] ]));
}

- (void)testQueriesCanUseArrayContainsAnyFilters {
// TODO(b/138855186): Re-enable in prod once feature lands in backend.
if (![FSTIntegrationTestCase isRunningAgainstEmulator]) return;

NSDictionary *testDocs = @{
@"a" : @{@"array" : @[ @42 ]},
@"b" : @{@"array" : @[ @"a", @42, @"c" ]},
@"c" : @{@"array" : @[ @41.999, @"42", @{@"a" : @[ @42 ]} ]},
@"d" : @{@"array" : @[ @42 ], @"array2" : @[ @"bingo" ]},
@"e" : @{@"array" : @[ @43 ]},
@"f" : @{@"array" : @[ @{@"a" : @42} ]},
@"g" : @{@"array" : @42},
};
FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];

// Search for zips matching [42, 43].
FIRQuerySnapshot *snapshot = [self
readDocumentSetForRef:[collection queryWhereField:@"array" arrayContainsAny:@[ @42, @43 ]]];
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot),
(@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"d"], testDocs[@"e"] ]));

// With objects.
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"array"
arrayContainsAny:@[ @{@"a" : @42} ]]];
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
testDocs[@"f"],
]));
}

- (void)testCollectionGroupQueries {
// Use .document() to get a random collection group name to use but ensure it starts with 'b'
// for predictable ordering.
Expand Down
142 changes: 139 additions & 3 deletions Firestore/Example/Tests/Integration/API/FIRValidationTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <limits>

#import "Firestore/Source/API/FIRFieldValue+Internal.h"
#import "Firestore/Source/API/FIRQuery+Internal.h"

#import "Firestore/Example/Tests/Util/FSTHelpers.h"
#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
Expand Down Expand Up @@ -399,6 +400,10 @@ - (void)testNonEqualityQueriesOnNullOrNaNFail {
@"Invalid Query. Null supports only equality comparisons.");
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContains:[NSNull null]],
@"Invalid Query. Null supports only equality comparisons.");
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContainsAny:[NSNull null]],
@"Invalid Query. A non-empty array is required for 'arrayContainsAny' filters.");
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContainsAny:[NSNull null]],
@"Invalid Query. A non-empty array is required for 'arrayContainsAny' filters.");

FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)],
@"Invalid Query. NaN supports only equality comparisons.");
Expand Down Expand Up @@ -511,7 +516,7 @@ - (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder {
FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
}

- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences {
- (void)testQueriesFilteredByDocumentIdMustUseStringsOrDocumentReferences {
FIRCollectionReference *collection = [self collectionRef];
NSString *reason = @"Invalid query. When querying by document ID you must provide a valid "
"document ID, but it was an empty string.";
Expand Down Expand Up @@ -539,6 +544,37 @@ - (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences {
"are not arrays.";
FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] arrayContains:@1],
reason);

reason = @"Invalid query. You can't perform arrayContainsAny queries on document ID since "
@"document IDs "
"are not arrays.";
FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] arrayContainsAny:@1],
reason);
}

- (void)testQueriesUsingInAndDocumentIdMustHaveProperDocumentReferencesInArray {
FIRCollectionReference *collection = [self collectionRef];
NSString *reason = @"Invalid query. When querying by document ID you must provide a valid "
"document ID, but it was an empty string.";
FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] in:@[ @"" ]], reason);

reason = @"Invalid query. When querying a collection by document ID you must provide a "
"plain document ID, but 'foo/bar/baz' contains a '/' character.";
FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] in:@[ @"foo/bar/baz" ]],
reason);

reason = @"Invalid query. When querying by document ID you must provide a valid string or "
"DocumentReference, but it was of type: __NSArrayI";
NSArray *value = @[ @1, @2 ];
FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] in:value], reason);

reason = @"Invalid query. When querying a collection group by document ID, the value "
"provided must result in a valid document path, but 'foo' is not because it "
"has an odd number of segments.";
FSTAssertThrows(
[[self.db collectionGroupWithID:@"collection"] queryWhereFieldPath:[FIRFieldPath documentID]
in:@[ @"foo" ]],
reason);
}

- (void)testQueryInequalityFieldMustMatchFirstOrderByField {
Expand Down Expand Up @@ -569,6 +605,9 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
@"Inequality and equality on different fields works");
XCTAssertNoThrow([base queryWhereField:@"y" arrayContains:@"cat"],
@"Inequality and array_contains on different fields works");
XCTAssertNoThrow([base queryWhereField:@"y" arrayContainsAny:@[ @"cat" ]],
@"array-contains-any on different fields works");
XCTAssertNoThrow([base queryWhereField:@"y" in:@[ @"cat" ]], @"IN on different fields works");

XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works");
XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32],
Expand All @@ -585,11 +624,108 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
@"array_contains different than orderBy works.");
}

- (void)testQueryMustNotHaveMultipleArrayContainsFilters {
- (void)testQueriesWithMultipleArrayFiltersFail {
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
FSTAssertThrows([[coll queryWhereField:@"foo" arrayContains:@1] queryWhereField:@"foo"
arrayContains:@2],
@"Invalid Query. Queries only support a single arrayContains filter.");
@"Invalid Query. You cannot use more than one 'arrayContains' filter.");

FSTAssertThrows(
[[coll queryWhereField:@"foo" arrayContains:@1] queryWhereField:@"foo"
arrayContainsAny:@[ @2 ]],
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'arrayContains' filters.");

FSTAssertThrows(
[[coll queryWhereField:@"foo" arrayContainsAny:@[ @1 ]] queryWhereField:@"foo"
arrayContains:@2],
@"Invalid Query. You cannot use 'arrayContains' filters with 'arrayContainsAny' filters.");
}

- (void)testQueriesWithMultipleDisjunctiveFiltersFail {
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
FSTAssertThrows([[coll queryWhereField:@"foo" in:@[ @1 ]] queryWhereField:@"foo" in:@[ @2 ]],
@"Invalid Query. You cannot use more than one 'in' filter.");

FSTAssertThrows([[coll queryWhereField:@"foo" arrayContainsAny:@[ @1 ]] queryWhereField:@"foo"
arrayContainsAny:@[ @2 ]],
@"Invalid Query. You cannot use more than one 'arrayContainsAny' filter.");

FSTAssertThrows([[coll queryWhereField:@"foo" arrayContainsAny:@[ @1 ]] queryWhereField:@"foo"
in:@[ @2 ]],
@"Invalid Query. You cannot use 'in' filters with 'arrayContainsAny' filters.");

FSTAssertThrows([[coll queryWhereField:@"foo" in:@[ @1 ]] queryWhereField:@"foo"
arrayContainsAny:@[ @2 ]],
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");

// This is redundant with the above tests, but makes sure our validation doesn't get confused.
FSTAssertThrows([[[coll queryWhereField:@"foo"
in:@[ @1 ]] queryWhereField:@"foo"
arrayContains:@2] queryWhereField:@"foo"
arrayContainsAny:@[ @2 ]],
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");

FSTAssertThrows([[[coll queryWhereField:@"foo"
arrayContains:@1] queryWhereField:@"foo"
in:@[ @2 ]] queryWhereField:@"foo"
arrayContainsAny:@[ @2 ]],
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");
}

- (void)testQueriesCanUseInWithArrayContain {
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
XCTAssertNoThrow([[coll queryWhereField:@"foo" arrayContains:@1] queryWhereField:@"foo"
in:@[ @2 ]],
@"arrayContains with IN works.");

XCTAssertNoThrow([[coll queryWhereField:@"foo" in:@[ @1 ]] queryWhereField:@"foo"
arrayContains:@2],
@"IN with arrayContains works.");

FSTAssertThrows([[[coll queryWhereField:@"foo"
in:@[ @1 ]] queryWhereField:@"foo"
arrayContains:@2] queryWhereField:@"foo"
arrayContains:@3],
@"Invalid Query. You cannot use more than one 'arrayContains' filter.");

FSTAssertThrows([[[coll queryWhereField:@"foo"
arrayContains:@1] queryWhereField:@"foo"
in:@[ @2 ]] queryWhereField:@"foo"
in:@[ @3 ]],
@"Invalid Query. You cannot use more than one 'in' filter.");
}

- (void)testQueriesInAndArrayContainsAnyArrayRules {
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];

FSTAssertThrows([coll queryWhereField:@"foo" in:@[]],
@"Invalid Query. A non-empty array is required for 'in' filters.");

FSTAssertThrows([coll queryWhereField:@"foo" arrayContainsAny:@[]],
@"Invalid Query. A non-empty array is required for 'arrayContainsAny' filters.");

// The 10 element max includes duplicates.
NSArray *values = @[ @1, @2, @3, @4, @5, @6, @7, @8, @9, @9, @9 ];
FSTAssertThrows(
[coll queryWhereField:@"foo" in:values],
@"Invalid Query. 'in' filters support a maximum of 10 elements in the value array.");
FSTAssertThrows([coll queryWhereField:@"foo" arrayContainsAny:values],
@"Invalid Query. 'arrayContainsAny' filters support a maximum of 10 elements"
" in the value array.");

NSArray *withNullValues = @[ @1, [NSNull null] ];
FSTAssertThrows([coll queryWhereField:@"foo" in:withNullValues],
@"Invalid Query. 'in' filters cannot contain 'null' in the value array.");
FSTAssertThrows(
[coll queryWhereField:@"foo" arrayContainsAny:withNullValues],
@"Invalid Query. 'arrayContainsAny' filters cannot contain 'null' in the value array.");

NSArray *withNaNValues = @[ @2, @(NAN) ];
FSTAssertThrows([coll queryWhereField:@"foo" in:withNaNValues],
@"Invalid Query. 'in' filters cannot contain 'NaN' in the value array.");
FSTAssertThrows(
[coll queryWhereField:@"foo" arrayContainsAny:withNaNValues],
@"Invalid Query. 'arrayContainsAny' filters cannot contain 'NaN' in the value array.");
}

#pragma mark - GeoPoint Validation
Expand Down
30 changes: 30 additions & 0 deletions Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,36 @@ - (void)testEncodesArrayContainsFilter {
XCTAssertEqual(*input, *roundTripped);
}

- (void)testEncodesArrayContainsAnyFilter {
auto input = Filter("item.tags", "array-contains-any", Array("food"));
GCFSStructuredQuery_Filter *actual = [self.serializer encodedUnaryOrFieldFilter:*input];

GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter;
prop.field.fieldPath = @"item.tags";
prop.op = GCFSStructuredQuery_FieldFilter_Operator_ArrayContainsAny;
[prop.value.arrayValue.valuesArray addObject:[self.serializer encodedString:"food"]];
XCTAssertEqualObjects(actual, expected);

auto roundTripped = [self.serializer decodedFieldFilter:prop];
XCTAssertEqual(*input, *roundTripped);
}

- (void)testEncodesInFilter {
auto input = Filter("item.tags", "in", Array("food"));
GCFSStructuredQuery_Filter *actual = [self.serializer encodedUnaryOrFieldFilter:*input];

GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter;
prop.field.fieldPath = @"item.tags";
prop.op = GCFSStructuredQuery_FieldFilter_Operator_In;
[prop.value.arrayValue.valuesArray addObject:[self.serializer encodedString:"food"]];
XCTAssertEqualObjects(actual, expected);

auto roundTripped = [self.serializer decodedFieldFilter:prop];
XCTAssertEqual(*input, *roundTripped);
}

- (void)testEncodesKeyFieldFilter {
auto input = Filter("__name__", "==", Ref("p/d", "coll/doc"));
GCFSStructuredQuery_Filter *actual = [self.serializer encodedUnaryOrFieldFilter:*input];
Expand Down
2 changes: 2 additions & 0 deletions Firestore/Protos/cpp/google/firestore/v1/query.pb.cc
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,8 @@ const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::GREATER_
const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::GREATER_THAN_OR_EQUAL;
const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::EQUAL;
const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::ARRAY_CONTAINS;
const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::IN;
const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::ARRAY_CONTAINS_ANY;
const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::Operator_MIN;
const StructuredQuery_FieldFilter_Operator StructuredQuery_FieldFilter::Operator_MAX;
const int StructuredQuery_FieldFilter::Operator_ARRAYSIZE;
Expand Down
6 changes: 6 additions & 0 deletions Firestore/Protos/cpp/google/firestore/v1/query.pb.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ enum StructuredQuery_FieldFilter_Operator {
StructuredQuery_FieldFilter_Operator_GREATER_THAN_OR_EQUAL = 4,
StructuredQuery_FieldFilter_Operator_EQUAL = 5,
StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS = 7,
StructuredQuery_FieldFilter_Operator_IN = 8,
StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS_ANY = 9,
StructuredQuery_FieldFilter_Operator_StructuredQuery_FieldFilter_Operator_INT_MIN_SENTINEL_DO_NOT_USE_ = ::google::protobuf::kint32min,
StructuredQuery_FieldFilter_Operator_StructuredQuery_FieldFilter_Operator_INT_MAX_SENTINEL_DO_NOT_USE_ = ::google::protobuf::kint32max
};
Expand Down Expand Up @@ -714,6 +716,10 @@ class StructuredQuery_FieldFilter : public ::google::protobuf::Message /* @@prot
StructuredQuery_FieldFilter_Operator_EQUAL;
static const Operator ARRAY_CONTAINS =
StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS;
static const Operator IN =
StructuredQuery_FieldFilter_Operator_IN;
static const Operator ARRAY_CONTAINS_ANY =
StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS_ANY;
static inline bool Operator_IsValid(int value) {
return StructuredQuery_FieldFilter_Operator_IsValid(value);
}
Expand Down
4 changes: 3 additions & 1 deletion Firestore/Protos/nanopb/google/firestore/v1/query.nanopb.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ typedef enum _google_firestore_v1_StructuredQuery_FieldFilter_Operator {
google_firestore_v1_StructuredQuery_FieldFilter_Operator_GREATER_THAN = 3,
google_firestore_v1_StructuredQuery_FieldFilter_Operator_GREATER_THAN_OR_EQUAL = 4,
google_firestore_v1_StructuredQuery_FieldFilter_Operator_EQUAL = 5,
google_firestore_v1_StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS = 7
google_firestore_v1_StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS = 7,
google_firestore_v1_StructuredQuery_FieldFilter_Operator_IN = 8,
google_firestore_v1_StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS_ANY = 9
} google_firestore_v1_StructuredQuery_FieldFilter_Operator;
#define _google_firestore_v1_StructuredQuery_FieldFilter_Operator_MIN google_firestore_v1_StructuredQuery_FieldFilter_Operator_OPERATOR_UNSPECIFIED
#define _google_firestore_v1_StructuredQuery_FieldFilter_Operator_MAX google_firestore_v1_StructuredQuery_FieldFilter_Operator_ARRAY_CONTAINS
Expand Down
Loading