aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Source/Core/FSTQuery.mm
diff options
context:
space:
mode:
Diffstat (limited to 'Firestore/Source/Core/FSTQuery.mm')
-rw-r--r--Firestore/Source/Core/FSTQuery.mm771
1 files changed, 771 insertions, 0 deletions
diff --git a/Firestore/Source/Core/FSTQuery.mm b/Firestore/Source/Core/FSTQuery.mm
new file mode 100644
index 0000000..8c98687
--- /dev/null
+++ b/Firestore/Source/Core/FSTQuery.mm
@@ -0,0 +1,771 @@
+/*
+ * 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 "Firestore/Source/Core/FSTQuery.h"
+
+#import "Firestore/Source/API/FIRFirestore+Internal.h"
+#import "Firestore/Source/Model/FSTDocument.h"
+#import "Firestore/Source/Model/FSTDocumentKey.h"
+#import "Firestore/Source/Model/FSTFieldValue.h"
+#import "Firestore/Source/Model/FSTPath.h"
+#import "Firestore/Source/Util/FSTAssert.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTRelationFilterOperator functions
+
+/**
+ * Returns the reverse order (i.e. Ascending => Descending) etc.
+ */
+static constexpr NSComparisonResult ReverseOrder(NSComparisonResult result) {
+ return static_cast<NSComparisonResult>(-static_cast<NSInteger>(result));
+}
+
+NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOperator) {
+ switch (filterOperator) {
+ case FSTRelationFilterOperatorLessThan:
+ return @"<";
+ case FSTRelationFilterOperatorLessThanOrEqual:
+ return @"<=";
+ case FSTRelationFilterOperatorEqual:
+ return @"==";
+ case FSTRelationFilterOperatorGreaterThanOrEqual:
+ return @">=";
+ case FSTRelationFilterOperatorGreaterThan:
+ return @">";
+ default:
+ FSTCFail(@"Unknown FSTRelationFilterOperator %lu", (unsigned long)filterOperator);
+ }
+}
+
+#pragma mark - FSTRelationFilter
+
+@interface FSTRelationFilter ()
+
+/**
+ * Initializes the receiver relation filter.
+ *
+ * @param field A path to a field in the document to filter on. The LHS of the expression.
+ * @param filterOperator The binary operator to apply.
+ * @param value A constant value to compare @a field to. The RHS of the expression.
+ */
+- (instancetype)initWithField:(FSTFieldPath *)field
+ filterOperator:(FSTRelationFilterOperator)filterOperator
+ value:(FSTFieldValue *)value NS_DESIGNATED_INITIALIZER;
+
+/** Returns YES if @a document matches the receiver's constraint. */
+- (BOOL)matchesDocument:(FSTDocument *)document;
+
+/**
+ * A canonical string identifying the filter. Two different instances of equivalent filters will
+ * return the same canonicalID.
+ */
+- (NSString *)canonicalID;
+
+@end
+
+@implementation FSTRelationFilter
+
+#pragma mark - Constructor methods
+
++ (instancetype)filterWithField:(FSTFieldPath *)field
+ filterOperator:(FSTRelationFilterOperator)filterOperator
+ value:(FSTFieldValue *)value {
+ return [[FSTRelationFilter alloc] initWithField:field filterOperator:filterOperator value:value];
+}
+
+- (instancetype)initWithField:(FSTFieldPath *)field
+ filterOperator:(FSTRelationFilterOperator)filterOperator
+ value:(FSTFieldValue *)value {
+ self = [super init];
+ if (self) {
+ _field = field;
+ _filterOperator = filterOperator;
+ _value = value;
+ }
+ return self;
+}
+
+#pragma mark - Public Methods
+
+- (BOOL)isInequality {
+ return self.filterOperator != FSTRelationFilterOperatorEqual;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %@ %@", [self.field canonicalString],
+ FSTStringFromQueryRelationOperator(self.filterOperator),
+ self.value];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTRelationFilter class]]) {
+ return NO;
+ }
+ return [self isEqualToFilter:(FSTRelationFilter *)other];
+}
+
+#pragma mark - Private methods
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ if ([self.field isKeyFieldPath]) {
+ FSTAssert([self.value isKindOfClass:[FSTReferenceValue class]],
+ @"Comparing on key, but filter value not a FSTReferenceValue.");
+ FSTReferenceValue *refValue = (FSTReferenceValue *)self.value;
+ NSComparisonResult comparison = FSTDocumentKeyComparator(document.key, refValue.value);
+ return [self matchesComparison:comparison];
+ } else {
+ return [self matchesValue:[document fieldForPath:self.field]];
+ }
+}
+
+- (NSString *)canonicalID {
+ // TODO(b/37283291): This should be collision robust and avoid relying on |description| methods.
+ return [NSString stringWithFormat:@"%@%@%@", [self.field canonicalString],
+ FSTStringFromQueryRelationOperator(self.filterOperator),
+ [self.value value]];
+}
+
+- (BOOL)isEqualToFilter:(FSTRelationFilter *)other {
+ if (self.filterOperator != other.filterOperator) {
+ return NO;
+ }
+ if (![self.field isEqual:other.field]) {
+ return NO;
+ }
+ if (![self.value isEqual:other.value]) {
+ return NO;
+ }
+ return YES;
+}
+
+/** Returns YES if receiver is true with the given value as its LHS. */
+- (BOOL)matchesValue:(FSTFieldValue *)other {
+ // Only compare types with matching backend order (such as double and int).
+ return self.value.typeOrder == other.typeOrder &&
+ [self matchesComparison:[other compare:self.value]];
+}
+
+- (BOOL)matchesComparison:(NSComparisonResult)comparison {
+ switch (self.filterOperator) {
+ case FSTRelationFilterOperatorLessThan:
+ return comparison == NSOrderedAscending;
+ case FSTRelationFilterOperatorLessThanOrEqual:
+ return comparison == NSOrderedAscending || comparison == NSOrderedSame;
+ case FSTRelationFilterOperatorEqual:
+ return comparison == NSOrderedSame;
+ case FSTRelationFilterOperatorGreaterThanOrEqual:
+ return comparison == NSOrderedDescending || comparison == NSOrderedSame;
+ case FSTRelationFilterOperatorGreaterThan:
+ return comparison == NSOrderedDescending;
+ default:
+ FSTFail(@"Unknown operator: %ld", (long)self.filterOperator);
+ }
+}
+
+@end
+
+#pragma mark - FSTNullFilter
+
+@interface FSTNullFilter ()
+@property(nonatomic, strong, readonly) FSTFieldPath *field;
+@end
+
+@implementation FSTNullFilter
+- (instancetype)initWithField:(FSTFieldPath *)field {
+ if (self = [super init]) {
+ _field = field;
+ }
+ return self;
+}
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ FSTFieldValue *fieldValue = [document fieldForPath:self.field];
+ return fieldValue != nil && [fieldValue isEqual:[FSTNullValue nullValue]];
+}
+
+- (NSString *)canonicalID {
+ return [NSString stringWithFormat:@"%@ IS NULL", [self.field canonicalString]];
+}
+
+- (NSString *)description {
+ return [self canonicalID];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (![[other class] isEqual:[self class]]) return NO;
+
+ return [self.field isEqual:((FSTNullFilter *)other).field];
+}
+
+- (NSUInteger)hash {
+ return [self.field hash];
+}
+
+@end
+
+#pragma mark - FSTNanFilter
+
+@interface FSTNanFilter ()
+@property(nonatomic, strong, readonly) FSTFieldPath *field;
+@end
+
+@implementation FSTNanFilter
+
+- (instancetype)initWithField:(FSTFieldPath *)field {
+ if (self = [super init]) {
+ _field = field;
+ }
+ return self;
+}
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ FSTFieldValue *fieldValue = [document fieldForPath:self.field];
+ return fieldValue != nil && [fieldValue isEqual:[FSTDoubleValue nanValue]];
+}
+
+- (NSString *)canonicalID {
+ return [NSString stringWithFormat:@"%@ IS NaN", [self.field canonicalString]];
+}
+
+- (NSString *)description {
+ return [self canonicalID];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (![[other class] isEqual:[self class]]) return NO;
+
+ return [self.field isEqual:((FSTNanFilter *)other).field];
+}
+
+- (NSUInteger)hash {
+ return [self.field hash];
+}
+@end
+
+#pragma mark - FSTSortOrder
+
+@interface FSTSortOrder ()
+
+/** Creates a new sort order with the given field and direction. */
+- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending;
+
+- (NSString *)canonicalID;
+
+@end
+
+@implementation FSTSortOrder
+
+#pragma mark - Constructor methods
+
++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending {
+ return [[FSTSortOrder alloc] initWithFieldPath:fieldPath ascending:ascending];
+}
+
+- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending {
+ self = [super init];
+ if (self) {
+ _field = fieldPath;
+ _ascending = ascending;
+ }
+ return self;
+}
+
+#pragma mark - Public methods
+
+- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2 {
+ NSComparisonResult result;
+ if ([self.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ result = FSTDocumentKeyComparator(document1.key, document2.key);
+ } else {
+ FSTFieldValue *value1 = [document1 fieldForPath:self.field];
+ FSTFieldValue *value2 = [document2 fieldForPath:self.field];
+ FSTAssert(value1 != nil && value2 != nil,
+ @"Trying to compare documents on fields that don't exist.");
+ result = [value1 compare:value2];
+ }
+ if (!self.isAscending) {
+ result = ReverseOrder(result);
+ }
+ return result;
+}
+
+- (NSString *)canonicalID {
+ return [NSString
+ stringWithFormat:@"%@%@", self.field.canonicalString, self.isAscending ? @"asc" : @"desc"];
+}
+
+- (BOOL)isEqualToSortOrder:(FSTSortOrder *)other {
+ return [self.field isEqual:other.field] && self.isAscending == other.isAscending;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTSortOrder: path:%@ dir:%@>", self.field,
+ self.ascending ? @"asc" : @"desc"];
+}
+
+- (BOOL)isEqual:(NSObject *)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTSortOrder class]]) {
+ return NO;
+ }
+ return [self isEqualToSortOrder:(FSTSortOrder *)other];
+}
+
+- (NSUInteger)hash {
+ return [self.canonicalID hash];
+}
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+ return self;
+}
+
+@end
+
+#pragma mark - FSTBound
+
+@implementation FSTBound
+
+- (instancetype)initWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore {
+ if (self = [super init]) {
+ _position = position;
+ _before = isBefore;
+ }
+ return self;
+}
+
++ (instancetype)boundWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore {
+ return [[FSTBound alloc] initWithPosition:position isBefore:isBefore];
+}
+
+- (NSString *)canonicalString {
+ // TODO(b/29183165): Make this collision robust.
+ NSMutableString *string = [NSMutableString string];
+ if (self.isBefore) {
+ [string appendString:@"b:"];
+ } else {
+ [string appendString:@"a:"];
+ }
+ for (FSTFieldValue *component in self.position) {
+ [string appendFormat:@"%@", component];
+ }
+ return string;
+}
+
+- (BOOL)sortsBeforeDocument:(FSTDocument *)document
+ usingSortOrder:(NSArray<FSTSortOrder *> *)sortOrder {
+ FSTAssert(self.position.count <= sortOrder.count,
+ @"FSTIndexPosition has more components than provided sort order.");
+ __block NSComparisonResult result = NSOrderedSame;
+ [self.position enumerateObjectsUsingBlock:^(FSTFieldValue *fieldValue, NSUInteger idx,
+ BOOL *stop) {
+ FSTSortOrder *sortOrderComponent = sortOrder[idx];
+ NSComparisonResult comparison;
+ if ([sortOrderComponent.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ FSTAssert([fieldValue isKindOfClass:[FSTReferenceValue class]],
+ @"FSTBound has a non-key value where the key path is being used %@", fieldValue);
+ FSTReferenceValue *refValue = (FSTReferenceValue *)fieldValue;
+ comparison = [refValue.value compare:document.key];
+ } else {
+ FSTFieldValue *docValue = [document fieldForPath:sortOrderComponent.field];
+ FSTAssert(docValue != nil, @"Field should exist since document matched the orderBy already.");
+ comparison = [fieldValue compare:docValue];
+ }
+
+ if (!sortOrderComponent.isAscending) {
+ comparison = ReverseOrder(comparison);
+ }
+
+ if (comparison != 0) {
+ result = comparison;
+ *stop = YES;
+ }
+ }];
+
+ return self.isBefore ? result <= NSOrderedSame : result < NSOrderedSame;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTBound: position:%@ before:%@>", self.position,
+ self.isBefore ? @"YES" : @"NO"];
+}
+
+- (BOOL)isEqual:(NSObject *)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTBound class]]) {
+ return NO;
+ }
+
+ FSTBound *otherBound = (FSTBound *)other;
+
+ return [self.position isEqualToArray:otherBound.position] && self.isBefore == otherBound.isBefore;
+}
+
+- (NSUInteger)hash {
+ return 31 * self.position.hash + (self.isBefore ? 0 : 1);
+}
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+ return self;
+}
+
+@end
+
+#pragma mark - FSTQuery
+
+@interface FSTQuery () {
+ // Cached value of the canonicalID property.
+ NSString *_canonicalID;
+}
+
+/**
+ * Initializes the receiver with the given query constraints.
+ *
+ * @param path The base path of the query.
+ * @param filters Filters specify which documents to include in the results.
+ * @param sortOrders The fields and directions to sort the results.
+ * @param limit If not NSNotFound, only this many results will be returned.
+ */
+- (instancetype)initWithPath:(FSTResourcePath *)path
+ filterBy:(NSArray<id<FSTFilter>> *)filters
+ orderBy:(NSArray<FSTSortOrder *> *)sortOrders
+ limit:(NSInteger)limit
+ startAt:(nullable FSTBound *)startAtBound
+ endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER;
+
+/** A list of fields given to sort by. This does not include the implicit key sort at the end. */
+@property(nonatomic, strong, readonly) NSArray<FSTSortOrder *> *explicitSortOrders;
+
+/** The memoized list of sort orders */
+@property(nonatomic, nullable, strong, readwrite) NSArray<FSTSortOrder *> *memoizedSortOrders;
+
+@end
+
+@implementation FSTQuery
+
+#pragma mark - Constructors
+
++ (instancetype)queryWithPath:(FSTResourcePath *)path {
+ return [[FSTQuery alloc] initWithPath:path
+ filterBy:@[]
+ orderBy:@[]
+ limit:NSNotFound
+ startAt:nil
+ endAt:nil];
+}
+
+- (instancetype)initWithPath:(FSTResourcePath *)path
+ filterBy:(NSArray<id<FSTFilter>> *)filters
+ orderBy:(NSArray<FSTSortOrder *> *)sortOrders
+ limit:(NSInteger)limit
+ startAt:(nullable FSTBound *)startAtBound
+ endAt:(nullable FSTBound *)endAtBound {
+ if (self = [super init]) {
+ _path = path;
+ _filters = filters;
+ _explicitSortOrders = sortOrders;
+ _limit = limit;
+ _startAt = startAtBound;
+ _endAt = endAtBound;
+ }
+ return self;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTQuery: canonicalID:%@>", self.canonicalID];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+ if (![object isKindOfClass:[FSTQuery class]]) {
+ return NO;
+ }
+ return [self isEqualToQuery:(FSTQuery *)object];
+}
+
+- (NSUInteger)hash {
+ return [self.canonicalID hash];
+}
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+ return self;
+}
+
+#pragma mark - Public methods
+
+- (NSArray *)sortOrders {
+ if (self.memoizedSortOrders == nil) {
+ FSTFieldPath *_Nullable inequalityField = [self inequalityFilterField];
+ FSTFieldPath *_Nullable firstSortOrderField = [self firstSortOrderField];
+ if (inequalityField && !firstSortOrderField) {
+ // In order to implicitly add key ordering, we must also add the inequality filter field for
+ // it to be a valid query. Note that the default inequality field and key ordering is
+ // ascending.
+ if ([inequalityField isKeyFieldPath]) {
+ self.memoizedSortOrders =
+ @[ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] ];
+ } else {
+ self.memoizedSortOrders = @[
+ [FSTSortOrder sortOrderWithFieldPath:inequalityField ascending:YES],
+ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES]
+ ];
+ }
+ } else {
+ FSTAssert(!inequalityField || [inequalityField isEqual:firstSortOrderField],
+ @"First orderBy %@ should match inequality field %@.", firstSortOrderField,
+ inequalityField);
+
+ __block BOOL foundKeyOrder = NO;
+
+ NSMutableArray *result = [NSMutableArray array];
+ for (FSTSortOrder *sortOrder in self.explicitSortOrders) {
+ [result addObject:sortOrder];
+ if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ foundKeyOrder = YES;
+ }
+ }
+
+ if (!foundKeyOrder) {
+ // The direction of the implicit key ordering always matches the direction of the last
+ // explicit sort order
+ BOOL lastIsAscending =
+ self.explicitSortOrders.count > 0 ? self.explicitSortOrders.lastObject.ascending : YES;
+ [result addObject:[FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath]
+ ascending:lastIsAscending]];
+ }
+
+ self.memoizedSortOrders = result;
+ }
+ }
+ return self.memoizedSortOrders;
+}
+
+- (instancetype)queryByAddingFilter:(id<FSTFilter>)filter {
+ FSTAssert(![FSTDocumentKey isDocumentKey:self.path], @"No filtering allowed for document query");
+
+ FSTFieldPath *_Nullable newInequalityField = nil;
+ if ([filter isKindOfClass:[FSTRelationFilter class]] &&
+ [((FSTRelationFilter *)filter)isInequality]) {
+ newInequalityField = filter.field;
+ }
+ FSTFieldPath *_Nullable queryInequalityField = [self inequalityFilterField];
+ FSTAssert(!queryInequalityField || !newInequalityField ||
+ [queryInequalityField isEqual:newInequalityField],
+ @"Query must only have one inequality field.");
+
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:[self.filters arrayByAddingObject:filter]
+ orderBy:self.explicitSortOrders
+ limit:self.limit
+ startAt:self.startAt
+ endAt:self.endAt];
+}
+
+- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder {
+ FSTAssert(![FSTDocumentKey isDocumentKey:self.path],
+ @"No ordering is allowed for a document query.");
+
+ // TODO(klimt): Validate that the same key isn't added twice.
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:[self.explicitSortOrders arrayByAddingObject:sortOrder]
+ limit:self.limit
+ startAt:self.startAt
+ endAt:self.endAt];
+}
+
+- (instancetype)queryBySettingLimit:(NSInteger)limit {
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:self.explicitSortOrders
+ limit:limit
+ startAt:self.startAt
+ endAt:self.endAt];
+}
+
+- (instancetype)queryByAddingStartAt:(FSTBound *)bound {
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:self.explicitSortOrders
+ limit:self.limit
+ startAt:bound
+ endAt:self.endAt];
+}
+
+- (instancetype)queryByAddingEndAt:(FSTBound *)bound {
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:self.explicitSortOrders
+ limit:self.limit
+ startAt:self.startAt
+ endAt:bound];
+}
+
+- (BOOL)isDocumentQuery {
+ return [FSTDocumentKey isDocumentKey:self.path] && self.filters.count == 0;
+}
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ return [self pathMatchesDocument:document] && [self orderByMatchesDocument:document] &&
+ [self filtersMatchDocument:document] && [self boundsMatchDocument:document];
+}
+
+- (NSComparator)comparator {
+ return ^NSComparisonResult(id document1, id document2) {
+ BOOL didCompareOnKeyField = NO;
+ for (FSTSortOrder *orderBy in self.sortOrders) {
+ NSComparisonResult comp = [orderBy compareDocument:document1 toDocument:document2];
+ if (comp != NSOrderedSame) {
+ return comp;
+ }
+ didCompareOnKeyField =
+ didCompareOnKeyField || [orderBy.field isEqual:[FSTFieldPath keyFieldPath]];
+ }
+ FSTAssert(didCompareOnKeyField, @"sortOrder of query did not include key ordering");
+ return NSOrderedSame;
+ };
+}
+
+- (FSTFieldPath *_Nullable)inequalityFilterField {
+ for (id<FSTFilter> filter in self.filters) {
+ if ([filter isKindOfClass:[FSTRelationFilter class]] &&
+ ((FSTRelationFilter *)filter).filterOperator != FSTRelationFilterOperatorEqual) {
+ return filter.field;
+ }
+ }
+ return nil;
+}
+
+- (FSTFieldPath *_Nullable)firstSortOrderField {
+ return self.explicitSortOrders.firstObject.field;
+}
+
+#pragma mark - Private properties
+
+- (NSString *)canonicalID {
+ if (_canonicalID) {
+ return _canonicalID;
+ }
+
+ NSMutableString *canonicalID = [[self.path canonicalString] mutableCopy];
+
+ // Add filters.
+ [canonicalID appendString:@"|f:"];
+ for (id<FSTFilter> predicate in self.filters) {
+ [canonicalID appendFormat:@"%@", [predicate canonicalID]];
+ }
+
+ // Add order by.
+ [canonicalID appendString:@"|ob:"];
+ for (FSTSortOrder *orderBy in self.sortOrders) {
+ [canonicalID appendString:orderBy.canonicalID];
+ }
+
+ // Add limit.
+ if (self.limit != NSNotFound) {
+ [canonicalID appendFormat:@"|l:%ld", (long)self.limit];
+ }
+
+ if (self.startAt) {
+ [canonicalID appendFormat:@"|lb:%@", self.startAt.canonicalString];
+ }
+
+ if (self.endAt) {
+ [canonicalID appendFormat:@"|ub:%@", self.endAt.canonicalString];
+ }
+
+ _canonicalID = canonicalID;
+ return canonicalID;
+}
+
+#pragma mark - Private methods
+
+- (BOOL)isEqualToQuery:(FSTQuery *)other {
+ return [self.path isEqual:other.path] && self.limit == other.limit &&
+ [self.filters isEqual:other.filters] && [self.sortOrders isEqual:other.sortOrders] &&
+ (self.startAt == other.startAt || [self.startAt isEqual:other.startAt]) &&
+ (self.endAt == other.endAt || [self.endAt isEqual:other.endAt]);
+}
+
+/* Returns YES if the document matches the path for the receiver. */
+- (BOOL)pathMatchesDocument:(FSTDocument *)document {
+ FSTResourcePath *documentPath = document.key.path;
+ if ([FSTDocumentKey isDocumentKey:self.path]) {
+ // Exact match for document queries.
+ return [self.path isEqual:documentPath];
+ } else {
+ // Shallow ancestor queries by default.
+ return [self.path isPrefixOfPath:documentPath] && self.path.length == documentPath.length - 1;
+ }
+}
+
+/**
+ * A document must have a value for every ordering clause in order to show up in the results.
+ */
+- (BOOL)orderByMatchesDocument:(FSTDocument *)document {
+ for (FSTSortOrder *orderBy in self.explicitSortOrders) {
+ FSTFieldPath *fieldPath = orderBy.field;
+ // order by key always matches
+ if (![fieldPath isEqual:[FSTFieldPath keyFieldPath]] &&
+ [document fieldForPath:fieldPath] == nil) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+/** Returns YES if the document matches all of the filters in the receiver. */
+- (BOOL)filtersMatchDocument:(FSTDocument *)document {
+ for (id<FSTFilter> filter in self.filters) {
+ if (![filter matchesDocument:document]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (BOOL)boundsMatchDocument:(FSTDocument *)document {
+ if (self.startAt && ![self.startAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) {
+ return NO;
+ }
+ if (self.endAt && [self.endAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) {
+ return NO;
+ }
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END