mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
use namespace aliases instead. This makes the Objective-C code much more like what it's going to end up being like when translated to C++: headers will include classes declared in a namespace relative to firebase::firestore so everything else will be named relative to firebase::firestore. Meanwhile the using statements in the headers were causing us to miss adding using statements in source files which was moving us away from where we wanted to be.
656 lines
29 KiB
Plaintext
656 lines
29 KiB
Plaintext
/*
|
|
* Copyright 2017 Google
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import "FIRQuery.h"
|
|
|
|
#include <memory>
|
|
#include <utility>
|
|
|
|
#import "FIRDocumentReference.h"
|
|
#import "FIRFirestoreErrors.h"
|
|
#import "Firestore/Source/API/FIRDocumentReference+Internal.h"
|
|
#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h"
|
|
#import "Firestore/Source/API/FIRFieldPath+Internal.h"
|
|
#import "Firestore/Source/API/FIRFieldValue+Internal.h"
|
|
#import "Firestore/Source/API/FIRFirestore+Internal.h"
|
|
#import "Firestore/Source/API/FIRFirestoreSource+Internal.h"
|
|
#import "Firestore/Source/API/FIRListenerRegistration+Internal.h"
|
|
#import "Firestore/Source/API/FIRQuery+Internal.h"
|
|
#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h"
|
|
#import "Firestore/Source/API/FIRQuery_Init.h"
|
|
#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h"
|
|
#import "Firestore/Source/API/FSTUserDataConverter.h"
|
|
#import "Firestore/Source/Core/FSTEventManager.h"
|
|
#import "Firestore/Source/Core/FSTFirestoreClient.h"
|
|
#import "Firestore/Source/Core/FSTQuery.h"
|
|
#import "Firestore/Source/Model/FSTDocument.h"
|
|
#import "Firestore/Source/Model/FSTFieldValue.h"
|
|
|
|
#include "Firestore/core/src/firebase/firestore/api/input_validation.h"
|
|
#include "Firestore/core/src/firebase/firestore/core/filter.h"
|
|
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
|
|
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
|
|
#include "Firestore/core/src/firebase/firestore/model/field_value.h"
|
|
#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
|
|
#include "Firestore/core/src/firebase/firestore/util/error_apple.h"
|
|
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
|
|
#include "Firestore/core/src/firebase/firestore/util/statusor.h"
|
|
#include "Firestore/core/src/firebase/firestore/util/string_apple.h"
|
|
|
|
namespace util = firebase::firestore::util;
|
|
using firebase::firestore::api::Firestore;
|
|
using firebase::firestore::api::ListenerRegistration;
|
|
using firebase::firestore::api::SnapshotMetadata;
|
|
using firebase::firestore::api::Source;
|
|
using firebase::firestore::api::ThrowInvalidArgument;
|
|
using firebase::firestore::core::AsyncEventListener;
|
|
using firebase::firestore::core::EventListener;
|
|
using firebase::firestore::core::Filter;
|
|
using firebase::firestore::core::ListenOptions;
|
|
using firebase::firestore::core::QueryListener;
|
|
using firebase::firestore::core::ViewSnapshot;
|
|
using firebase::firestore::model::DocumentKey;
|
|
using firebase::firestore::model::FieldPath;
|
|
using firebase::firestore::model::FieldValue;
|
|
using firebase::firestore::model::ResourcePath;
|
|
using firebase::firestore::util::MakeNSError;
|
|
using firebase::firestore::util::StatusOr;
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
@interface FIRQuery ()
|
|
@property(nonatomic, strong, readonly) FSTQuery *query;
|
|
@end
|
|
|
|
@implementation FIRQuery (Internal)
|
|
+ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore {
|
|
return [[FIRQuery alloc] initWithQuery:query firestore:firestore];
|
|
}
|
|
@end
|
|
|
|
@implementation FIRQuery
|
|
|
|
#pragma mark - Constructor Methods
|
|
|
|
- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore {
|
|
if (self = [super init]) {
|
|
_query = query;
|
|
_firestore = firestore;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - NSObject Methods
|
|
|
|
- (BOOL)isEqual:(nullable id)other {
|
|
if (other == self) return YES;
|
|
if (![[other class] isEqual:[self class]]) return NO;
|
|
|
|
return [self isEqualToQuery:other];
|
|
}
|
|
|
|
- (BOOL)isEqualToQuery:(nullable FIRQuery *)query {
|
|
if (self == query) return YES;
|
|
if (query == nil) return NO;
|
|
|
|
return [self.firestore isEqual:query.firestore] && [self.query isEqual:query.query];
|
|
}
|
|
|
|
- (NSUInteger)hash {
|
|
NSUInteger hash = [self.firestore hash];
|
|
hash = hash * 31u + [self.query hash];
|
|
return hash;
|
|
}
|
|
|
|
#pragma mark - Public Methods
|
|
|
|
- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot,
|
|
NSError *_Nullable error))completion {
|
|
[self getDocumentsWithSource:FIRFirestoreSourceDefault completion:completion];
|
|
}
|
|
|
|
- (void)getDocumentsWithSource:(FIRFirestoreSource)publicSource
|
|
completion:(void (^)(FIRQuerySnapshot *_Nullable snapshot,
|
|
NSError *_Nullable error))completion {
|
|
Source source = api::MakeSource(publicSource);
|
|
if (source == Source::Cache) {
|
|
[self.firestore.wrapped->client() getDocumentsFromLocalCache:self completion:completion];
|
|
return;
|
|
}
|
|
|
|
ListenOptions listenOptions(
|
|
/*include_query_metadata_changes=*/true,
|
|
/*include_document_metadata_changes=*/true,
|
|
/*wait_for_sync_when_online=*/true);
|
|
|
|
dispatch_semaphore_t registered = dispatch_semaphore_create(0);
|
|
__block id<FIRListenerRegistration> listenerRegistration;
|
|
FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) {
|
|
if (error) {
|
|
completion(nil, error);
|
|
return;
|
|
}
|
|
|
|
// Remove query first before passing event to user to avoid user actions affecting the
|
|
// now stale query.
|
|
dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER);
|
|
[listenerRegistration remove];
|
|
|
|
if (snapshot.metadata.fromCache && source == Source::Server) {
|
|
completion(nil, [NSError errorWithDomain:FIRFirestoreErrorDomain
|
|
code:FIRFirestoreErrorCodeUnavailable
|
|
userInfo:@{
|
|
NSLocalizedDescriptionKey :
|
|
@"Failed to get documents from server. (However, these "
|
|
@"documents may exist in the local cache. Run again "
|
|
@"without setting source to FirestoreSourceServer to "
|
|
@"retrieve the cached documents.)"
|
|
}]);
|
|
} else {
|
|
completion(snapshot, nil);
|
|
}
|
|
};
|
|
|
|
listenerRegistration = [self addSnapshotListenerInternalWithOptions:listenOptions
|
|
listener:listener];
|
|
dispatch_semaphore_signal(registered);
|
|
}
|
|
|
|
- (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener {
|
|
return [self addSnapshotListenerWithIncludeMetadataChanges:NO listener:listener];
|
|
}
|
|
|
|
- (id<FIRListenerRegistration>)
|
|
addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges
|
|
listener:(FIRQuerySnapshotBlock)listener {
|
|
auto options = ListenOptions::FromIncludeMetadataChanges(includeMetadataChanges);
|
|
return [self addSnapshotListenerInternalWithOptions:options listener:listener];
|
|
}
|
|
|
|
- (id<FIRListenerRegistration>)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions
|
|
listener:
|
|
(FIRQuerySnapshotBlock)listener {
|
|
Firestore *firestore = self.firestore.wrapped;
|
|
FSTQuery *query = self.query;
|
|
|
|
// Convert from ViewSnapshots to QuerySnapshots.
|
|
auto view_listener = EventListener<ViewSnapshot>::Create(
|
|
[listener, firestore, query](StatusOr<ViewSnapshot> maybe_snapshot) {
|
|
if (!maybe_snapshot.status().ok()) {
|
|
listener(nil, MakeNSError(maybe_snapshot.status()));
|
|
return;
|
|
}
|
|
|
|
ViewSnapshot snapshot = std::move(maybe_snapshot).ValueOrDie();
|
|
SnapshotMetadata metadata(snapshot.has_pending_writes(), snapshot.from_cache());
|
|
|
|
listener([[FIRQuerySnapshot alloc] initWithFirestore:firestore
|
|
originalQuery:query
|
|
snapshot:std::move(snapshot)
|
|
metadata:std::move(metadata)],
|
|
nil);
|
|
});
|
|
|
|
// Call the view_listener on the user Executor.
|
|
auto async_listener = AsyncEventListener<ViewSnapshot>::Create(firestore->client().userExecutor,
|
|
std::move(view_listener));
|
|
|
|
std::shared_ptr<QueryListener> query_listener =
|
|
[firestore->client() listenToQuery:query options:internalOptions listener:async_listener];
|
|
|
|
return [[FSTListenerRegistration alloc]
|
|
initWithRegistration:ListenerRegistration(firestore->client(), std::move(async_listener),
|
|
std::move(query_listener))];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::Equal field:field value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::Equal path:path.internalValue value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::LessThan field:field value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::LessThan
|
|
path:path.internalValue
|
|
value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::LessThanOrEqual field:field value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::LessThanOrEqual
|
|
path:path.internalValue
|
|
value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::GreaterThan field:field value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::GreaterThan
|
|
path:path.internalValue
|
|
value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereField:(NSString *)field arrayContains:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::ArrayContains field:field value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path arrayContains:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::ArrayContains
|
|
path:path.internalValue
|
|
value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::GreaterThanOrEqual
|
|
field:field
|
|
value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value {
|
|
return [self queryWithFilterOperator:Filter::Operator::GreaterThanOrEqual
|
|
path:path.internalValue
|
|
value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate {
|
|
NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate;
|
|
if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) {
|
|
ThrowInvalidArgument("Invalid query. Predicate cannot have an aggregate modifier.");
|
|
}
|
|
NSString *path;
|
|
id value = nil;
|
|
if ([comparison.leftExpression expressionType] == NSKeyPathExpressionType &&
|
|
[comparison.rightExpression expressionType] == NSConstantValueExpressionType) {
|
|
path = comparison.leftExpression.keyPath;
|
|
value = comparison.rightExpression.constantValue;
|
|
switch (comparison.predicateOperatorType) {
|
|
case NSEqualToPredicateOperatorType:
|
|
return [self queryWhereField:path isEqualTo:value];
|
|
case NSLessThanPredicateOperatorType:
|
|
return [self queryWhereField:path isLessThan:value];
|
|
case NSLessThanOrEqualToPredicateOperatorType:
|
|
return [self queryWhereField:path isLessThanOrEqualTo:value];
|
|
case NSGreaterThanPredicateOperatorType:
|
|
return [self queryWhereField:path isGreaterThan:value];
|
|
case NSGreaterThanOrEqualToPredicateOperatorType:
|
|
return [self queryWhereField:path isGreaterThanOrEqualTo:value];
|
|
default:; // Fallback below to throw assertion.
|
|
}
|
|
} else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType &&
|
|
[comparison.rightExpression expressionType] == NSKeyPathExpressionType) {
|
|
path = comparison.rightExpression.keyPath;
|
|
value = comparison.leftExpression.constantValue;
|
|
switch (comparison.predicateOperatorType) {
|
|
case NSEqualToPredicateOperatorType:
|
|
return [self queryWhereField:path isEqualTo:value];
|
|
case NSLessThanPredicateOperatorType:
|
|
return [self queryWhereField:path isGreaterThan:value];
|
|
case NSLessThanOrEqualToPredicateOperatorType:
|
|
return [self queryWhereField:path isGreaterThanOrEqualTo:value];
|
|
case NSGreaterThanPredicateOperatorType:
|
|
return [self queryWhereField:path isLessThan:value];
|
|
case NSGreaterThanOrEqualToPredicateOperatorType:
|
|
return [self queryWhereField:path isLessThanOrEqualTo:value];
|
|
default:; // Fallback below to throw assertion.
|
|
}
|
|
} else {
|
|
ThrowInvalidArgument(
|
|
"Invalid query. Predicate comparisons must include a key path and a constant.");
|
|
}
|
|
// Fallback cases of unsupported comparison operator.
|
|
switch (comparison.predicateOperatorType) {
|
|
case NSCustomSelectorPredicateOperatorType:
|
|
ThrowInvalidArgument("Invalid query. Custom predicate filters are not supported.");
|
|
break;
|
|
default:
|
|
ThrowInvalidArgument("Invalid query. Operator type %s is not supported.",
|
|
comparison.predicateOperatorType);
|
|
}
|
|
}
|
|
|
|
- (FIRQuery *)queryFilteredUsingCompoundPredicate:(NSPredicate *)predicate {
|
|
NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate;
|
|
if (compound.compoundPredicateType != NSAndPredicateType || compound.subpredicates.count == 0) {
|
|
ThrowInvalidArgument("Invalid query. Only compound queries using AND are supported.");
|
|
}
|
|
FIRQuery *query = self;
|
|
for (NSPredicate *pred in compound.subpredicates) {
|
|
query = [query queryFilteredUsingPredicate:pred];
|
|
}
|
|
return query;
|
|
}
|
|
|
|
- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate {
|
|
if ([predicate isKindOfClass:[NSComparisonPredicate class]]) {
|
|
return [self queryFilteredUsingComparisonPredicate:predicate];
|
|
} else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) {
|
|
return [self queryFilteredUsingCompoundPredicate:predicate];
|
|
} else if ([predicate isKindOfClass:[[NSPredicate
|
|
predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) {
|
|
return true;
|
|
}] class]]) {
|
|
ThrowInvalidArgument("Invalid query. Block-based predicates are not supported. Please use "
|
|
"predicateWithFormat to create predicates instead.");
|
|
} else {
|
|
ThrowInvalidArgument("Invalid query. Expect comparison or compound of comparison predicate. "
|
|
"Please use predicateWithFormat to create predicates.");
|
|
}
|
|
}
|
|
|
|
- (FIRQuery *)queryOrderedByField:(NSString *)field {
|
|
return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field]
|
|
descending:NO];
|
|
}
|
|
|
|
- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath {
|
|
return [self queryOrderedByFieldPath:fieldPath descending:NO];
|
|
}
|
|
|
|
- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending {
|
|
return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field]
|
|
descending:descending];
|
|
}
|
|
|
|
- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending {
|
|
[self validateNewOrderByPath:fieldPath.internalValue];
|
|
if (self.query.startAt) {
|
|
ThrowInvalidArgument(
|
|
"Invalid query. You must not specify a starting point before specifying the order by.");
|
|
}
|
|
if (self.query.endAt) {
|
|
ThrowInvalidArgument(
|
|
"Invalid query. You must not specify an ending point before specifying the order by.");
|
|
}
|
|
FSTSortOrder *sortOrder = [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue
|
|
ascending:!descending];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryLimitedTo:(NSInteger)limit {
|
|
if (limit <= 0) {
|
|
ThrowInvalidArgument("Invalid Query. Query limit (%s) is invalid. Limit must be positive.",
|
|
limit);
|
|
}
|
|
return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot {
|
|
FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues {
|
|
FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot {
|
|
FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues {
|
|
FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot {
|
|
FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues {
|
|
FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot {
|
|
FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues {
|
|
FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO];
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
#pragma mark - Private Methods
|
|
|
|
/** Private helper for all of the queryWhereField: methods. */
|
|
- (FIRQuery *)queryWithFilterOperator:(Filter::Operator)filterOperator
|
|
field:(NSString *)field
|
|
value:(id)value {
|
|
return [self queryWithFilterOperator:filterOperator
|
|
path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue
|
|
value:value];
|
|
}
|
|
|
|
- (FIRQuery *)queryWithFilterOperator:(Filter::Operator)filterOperator
|
|
path:(const FieldPath &)fieldPath
|
|
value:(id)value {
|
|
FSTFieldValue *fieldValue;
|
|
if (fieldPath.IsKeyFieldPath()) {
|
|
if (filterOperator == Filter::Operator::ArrayContains) {
|
|
ThrowInvalidArgument("Invalid query. You can't perform arrayContains queries on document ID "
|
|
"since document IDs are not arrays.");
|
|
}
|
|
if ([value isKindOfClass:[NSString class]]) {
|
|
NSString *documentKey = (NSString *)value;
|
|
if (documentKey.length == 0) {
|
|
ThrowInvalidArgument("Invalid query. When querying by document ID you must provide a valid "
|
|
"document ID, but it was an empty string.");
|
|
}
|
|
if (![self.query isCollectionGroupQuery] && [documentKey containsString:@"/"]) {
|
|
ThrowInvalidArgument("Invalid query. When querying a collection by document ID you must "
|
|
"provide a plain document ID, but '%s' contains a '/' character.",
|
|
documentKey);
|
|
}
|
|
ResourcePath path =
|
|
self.query.path.Append(ResourcePath::FromString([documentKey UTF8String]));
|
|
if (!DocumentKey::IsDocumentKey(path)) {
|
|
ThrowInvalidArgument("Invalid query. When querying a collection group by document ID, the "
|
|
"value provided must result in a valid document path, but '%s' is not "
|
|
"because it has an odd number of segments.",
|
|
path.CanonicalString());
|
|
}
|
|
fieldValue =
|
|
[FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:DocumentKey{path}]
|
|
databaseID:self.firestore.databaseID];
|
|
} else if ([value isKindOfClass:[FIRDocumentReference class]]) {
|
|
FIRDocumentReference *ref = (FIRDocumentReference *)value;
|
|
fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:ref.key]
|
|
databaseID:self.firestore.databaseID];
|
|
} else {
|
|
ThrowInvalidArgument("Invalid query. When querying by document ID you must provide a valid "
|
|
"string or DocumentReference, but it was of type: %s",
|
|
NSStringFromClass([value class]));
|
|
}
|
|
} else {
|
|
fieldValue = [self.firestore.dataConverter parsedQueryValue:value];
|
|
}
|
|
|
|
FSTFilter *filter = [FSTFilter filterWithField:fieldPath
|
|
filterOperator:filterOperator
|
|
value:fieldValue];
|
|
|
|
if ([filter isKindOfClass:[FSTRelationFilter class]]) {
|
|
[self validateNewRelationFilter:(FSTRelationFilter *)filter];
|
|
}
|
|
|
|
return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter]
|
|
firestore:self.firestore];
|
|
}
|
|
|
|
- (void)validateNewRelationFilter:(FSTRelationFilter *)filter {
|
|
if ([filter isInequality]) {
|
|
const FieldPath *existingField = [self.query inequalityFilterField];
|
|
if (existingField && *existingField != filter.field) {
|
|
ThrowInvalidArgument(
|
|
"Invalid Query. All where filters with an inequality "
|
|
"(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same "
|
|
"field. But you have inequality filters on '%s' and '%s'",
|
|
existingField->CanonicalString(), filter.field.CanonicalString());
|
|
}
|
|
|
|
const FieldPath *firstOrderByField = [self.query firstSortOrderField];
|
|
if (firstOrderByField) {
|
|
[self validateOrderByField:*firstOrderByField matchesInequalityField:filter.field];
|
|
}
|
|
} else if (filter.filterOperator == Filter::Operator::ArrayContains) {
|
|
if ([self.query hasArrayContainsFilter]) {
|
|
ThrowInvalidArgument("Invalid Query. Queries only support a single arrayContains filter.");
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)validateNewOrderByPath:(const FieldPath &)fieldPath {
|
|
if (![self.query firstSortOrderField]) {
|
|
// This is the first order by. It must match any inequality.
|
|
const FieldPath *inequalityField = [self.query inequalityFilterField];
|
|
if (inequalityField) {
|
|
[self validateOrderByField:fieldPath matchesInequalityField:*inequalityField];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)validateOrderByField:(const FieldPath &)orderByField
|
|
matchesInequalityField:(const FieldPath &)inequalityField {
|
|
if (orderByField != inequalityField) {
|
|
ThrowInvalidArgument("Invalid query. You have a where filter with an inequality "
|
|
"(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field "
|
|
"'%s' and so you must also use '%s' as your first queryOrderedBy field, "
|
|
"but your first queryOrderedBy is currently on field '%s' instead.",
|
|
inequalityField.CanonicalString(), inequalityField.CanonicalString(),
|
|
orderByField.CanonicalString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a FSTBound from a query given the document.
|
|
*
|
|
* Note that the FSTBound will always include the key of the document and the position will be
|
|
* unambiguous.
|
|
*
|
|
* Will throw if the document does not contain all fields of the order by of
|
|
* the query or if any of the fields in the order by are an uncommitted server
|
|
* timestamp.
|
|
*/
|
|
- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore {
|
|
if (![snapshot exists]) {
|
|
ThrowInvalidArgument("Invalid query. You are trying to start or end a query using a document "
|
|
"that doesn't exist.");
|
|
}
|
|
FSTDocument *document = snapshot.internalDocument;
|
|
NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array];
|
|
|
|
// Because people expect to continue/end a query at the exact document provided, we need to
|
|
// use the implicit sort order rather than the explicit sort order, because it's guaranteed to
|
|
// contain the document key. That way the position becomes unambiguous and the query
|
|
// continues/ends exactly at the provided document. Without the key (by using the explicit sort
|
|
// orders), multiple documents could match the position, yielding duplicate results.
|
|
for (FSTSortOrder *sortOrder in self.query.sortOrders) {
|
|
if (sortOrder.field == FieldPath::KeyFieldPath()) {
|
|
[components addObject:[FSTReferenceValue
|
|
referenceValue:[FSTDocumentKey keyWithDocumentKey:document.key]
|
|
databaseID:self.firestore.databaseID]];
|
|
} else {
|
|
FSTFieldValue *value = [document fieldForPath:sortOrder.field];
|
|
|
|
if (value.type == FieldValue::Type::ServerTimestamp) {
|
|
ThrowInvalidArgument(
|
|
"Invalid query. You are trying to start or end a query using a document for which the "
|
|
"field '%s' is an uncommitted server timestamp. (Since the value of this field is "
|
|
"unknown, you cannot start/end a query with it.)",
|
|
sortOrder.field.CanonicalString());
|
|
} else if (value != nil) {
|
|
[components addObject:value];
|
|
} else {
|
|
ThrowInvalidArgument(
|
|
"Invalid query. You are trying to start or end a query using a document for which the "
|
|
"field '%s' (used as the order by) does not exist.",
|
|
sortOrder.field.CanonicalString());
|
|
}
|
|
}
|
|
}
|
|
return [FSTBound boundWithPosition:components isBefore:isBefore];
|
|
}
|
|
|
|
/** Converts a list of field values to an FSTBound. */
|
|
- (FSTBound *)boundFromFieldValues:(NSArray<id> *)fieldValues isBefore:(BOOL)isBefore {
|
|
// Use explicit sort order because it has to match the query the user made
|
|
NSArray<FSTSortOrder *> *explicitSortOrders = self.query.explicitSortOrders;
|
|
if (fieldValues.count > explicitSortOrders.count) {
|
|
ThrowInvalidArgument("Invalid query. You are trying to start or end a query using more values "
|
|
"than were specified in the order by.");
|
|
}
|
|
|
|
NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array];
|
|
[fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) {
|
|
FSTSortOrder *sortOrder = explicitSortOrders[idx];
|
|
if (sortOrder.field == FieldPath::KeyFieldPath()) {
|
|
if (![rawValue isKindOfClass:[NSString class]]) {
|
|
ThrowInvalidArgument("Invalid query. Expected a string for the document ID.");
|
|
}
|
|
NSString *documentID = (NSString *)rawValue;
|
|
if (![self.query isCollectionGroupQuery] && [documentID containsString:@"/"]) {
|
|
ThrowInvalidArgument("Invalid query. When querying a collection and ordering by document "
|
|
"ID, you must pass a plain document ID, but '%s' contains a slash.",
|
|
documentID);
|
|
}
|
|
ResourcePath path = self.query.path.Append(ResourcePath::FromString([documentID UTF8String]));
|
|
if (!DocumentKey::IsDocumentKey(path)) {
|
|
ThrowInvalidArgument("Invalid query. When querying a collection group and ordering by "
|
|
"document ID, you must pass a value that results in a valid document "
|
|
"path, but '%s' is not because it contains an odd number of segments.",
|
|
path.CanonicalString());
|
|
}
|
|
DocumentKey key{path};
|
|
[components
|
|
addObject:[FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:key]
|
|
databaseID:self.firestore.databaseID]];
|
|
} else {
|
|
FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue];
|
|
[components addObject:fieldValue];
|
|
}
|
|
}];
|
|
|
|
return [FSTBound boundWithPosition:components isBefore:isBefore];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|