Skip to content

Commit 6db2e0d

Browse files
authored
Fix startAfter/endBefore for orderByKeys queries (#7403)
1 parent 8f529ec commit 6db2e0d

File tree

10 files changed

+369
-70
lines changed

10 files changed

+369
-70
lines changed

FirebaseDatabase/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- [fixed] Fix variable length array diagnostics warning (#7460).
33
# v7.5.1
44
- [changed] Optimize `FIRDatabaseQuery#getDataWithCompletionBlock` when in-memory active listener cache exists (#7312).
5+
- [fixed] Fixed an issue with `FIRDatabaseQuery#{queryStartingAfterValue,queryEndingBeforeValue} when used in `queryOrderedByKey` queries (#7403).
56

67
# v7.5.0
78
- [added] Implmement `queryStartingAfterValue` and `queryEndingBeforeValue` for FirebaseDatabase query pagination.

FirebaseDatabase/Sources/Api/FIRDatabaseQuery.m

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -95,32 +95,35 @@ - (void)validateQueryEndpointsForParams:(FQueryParams *)params {
9595
if (params.indexStartKey != [FUtilities minName] &&
9696
params.indexStartKey != [FUtilities maxName]) {
9797
[NSException raise:INVALID_QUERY_PARAM_ERROR
98-
format:@"Can't use queryStartingAtValue:childKey: "
98+
format:@"Can't use queryStartingAtValue:childKey:, "
99+
@"queryStartingAfterValue:childKey:, "
99100
@"or queryEqualTo:andChildKey: in "
100101
@"combination with queryOrderedByKey"];
101102
}
102103
if (![params.indexStartValue.val isKindOfClass:[NSString class]]) {
103-
[NSException
104-
raise:INVALID_QUERY_PARAM_ERROR
105-
format:
106-
@"Can't use queryStartingAtValue: with other types "
107-
@"than string in combination with queryOrderedByKey"];
104+
[NSException raise:INVALID_QUERY_PARAM_ERROR
105+
format:@"Can't use queryStartingAtValue: or "
106+
@"queryStartingAfterValue: "
107+
@"with non-string types when used with "
108+
@"queryOrderedByKey"];
108109
}
109110
}
110111
if ([params hasEnd]) {
111112
if (params.indexEndKey != [FUtilities maxName] &&
112113
params.indexEndKey != [FUtilities minName]) {
113114
[NSException raise:INVALID_QUERY_PARAM_ERROR
114115
format:@"Can't use queryEndingAtValue:childKey: or "
116+
@"queryEndingBeforeValue:childKey: "
115117
@"queryEqualToValue:childKey: in "
116118
@"combination with queryOrderedByKey"];
117119
}
118120
if (![params.indexEndValue.val isKindOfClass:[NSString class]]) {
119121
[NSException
120122
raise:INVALID_QUERY_PARAM_ERROR
121-
format:
122-
@"Can't use queryEndingAtValue: with other types than "
123-
@"string in combination with queryOrderedByKey"];
123+
format:@"Can't use queryEndingAtValue: or "
124+
@"queryEndingBeforeValue: "
125+
@"with other types than string in combination with "
126+
@"queryOrderedByKey"];
124127
}
125128
}
126129
} else if ([params.index isEqual:[FPriorityIndex priorityIndex]]) {
@@ -131,7 +134,8 @@ - (void)validateQueryEndpointsForParams:(FQueryParams *)params {
131134
[NSException
132135
raise:INVALID_QUERY_PARAM_ERROR
133136
format:@"When using queryOrderedByPriority, values provided to "
134-
@"queryStartingAtValue:, queryEndingAtValue:, or "
137+
@"queryStartingAtValue:, queryStartingAfterValue:, "
138+
@"queryEndingAtValue:, queryEndingBeforeValue:, or "
135139
@"queryEqualToValue: must be valid priorities."];
136140
}
137141
}
@@ -142,13 +146,14 @@ - (void)validateEqualToCall {
142146
[NSException
143147
raise:INVALID_QUERY_PARAM_ERROR
144148
format:
145-
@"Cannot combine queryEqualToValue: and queryStartingAtValue:"];
149+
@"Cannot combine queryEqualToValue: and queryStartingAtValue: "
150+
@"or queryStartingAfterValue:"];
146151
}
147152
if ([self.queryParams hasEnd]) {
148153
[NSException
149154
raise:INVALID_QUERY_PARAM_ERROR
150-
format:
151-
@"Cannot combine queryEqualToValue: and queryEndingAtValue:"];
155+
format:@"Cannot combine queryEqualToValue: and queryEndingAtValue: "
156+
@"or queryEndingBeforeValue:"];
152157
}
153158
}
154159

@@ -202,20 +207,25 @@ - (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue {
202207

203208
- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue
204209
childKey:(NSString *)childKey {
205-
if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]] &&
206-
childKey != nil) {
207-
@throw [[NSException alloc]
208-
initWithName:INVALID_QUERY_PARAM_ERROR
209-
reason:@"You must use queryStartingAfterValue: instead of "
210-
@"queryStartingAfterValue:childKey: when using "
211-
@"queryOrderedByKey:"
212-
userInfo:nil];
213-
}
214-
if (childKey == nil) {
215-
childKey = [FUtilities maxName];
210+
if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
211+
if (childKey != nil) {
212+
@throw [[NSException alloc]
213+
initWithName:INVALID_QUERY_PARAM_ERROR
214+
reason:
215+
@"You must use queryStartingAfterValue: instead of "
216+
@"queryStartingAfterValue:childKey: when using "
217+
@"queryOrderedByKey:"
218+
userInfo:nil];
219+
}
220+
if ([startAfterValue isKindOfClass:[NSString class]]) {
221+
startAfterValue = [FNextPushId successor:startAfterValue];
222+
}
216223
} else {
217-
childKey = [FNextPushId successor:childKey];
218-
NSLog(@"successor of child key %@", childKey);
224+
if (childKey == nil) {
225+
childKey = [FUtilities maxName];
226+
} else {
227+
childKey = [FNextPushId successor:childKey];
228+
}
219229
}
220230
NSString *methodName = @"queryStartingAfterValue:childKey:";
221231
if (childKey != nil && ![childKey isEqual:[FUtilities maxName]]) {
@@ -234,7 +244,8 @@ - (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
234244
[self validateIndexValueType:startValue fromMethod:methodName];
235245
if ([self.queryParams hasStart]) {
236246
[NSException raise:INVALID_QUERY_PARAM_ERROR
237-
format:@"Can't call %@ after queryStartingAtValue or "
247+
format:@"Can't call %@ after queryStartingAtValue, "
248+
@"queryStartingAfterValue, or "
238249
@"queryEqualToValue was previously called",
239250
methodName];
240251
}
@@ -283,20 +294,24 @@ - (FIRDatabaseQuery *)queryEndingBeforeValue:(id)endValue {
283294

284295
- (FIRDatabaseQuery *)queryEndingBeforeValue:(id)endValue
285296
childKey:(NSString *)childKey {
286-
if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]] &&
287-
childKey != nil) {
288-
@throw [[NSException alloc]
289-
initWithName:INVALID_QUERY_PARAM_ERROR
290-
reason:@"You must use queryEndingBeforeValue: instead of "
291-
@"queryEndingBeforeValue:childKey: when using "
292-
@"queryOrderedByKey:"
293-
userInfo:nil];
294-
}
295-
296-
if (childKey == nil) {
297-
childKey = [FUtilities minName];
297+
if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
298+
if (childKey != nil) {
299+
@throw [[NSException alloc]
300+
initWithName:INVALID_QUERY_PARAM_ERROR
301+
reason:@"You must use queryEndingBeforeValue: instead of "
302+
@"queryEndingBeforeValue:childKey: when using "
303+
@"queryOrderedByKey:"
304+
userInfo:nil];
305+
}
306+
if ([endValue isKindOfClass:[NSString class]]) {
307+
endValue = [FNextPushId predecessor:endValue];
308+
}
298309
} else {
299-
childKey = [FNextPushId predecessor:childKey];
310+
if (childKey == nil) {
311+
childKey = [FUtilities minName];
312+
} else {
313+
childKey = [FNextPushId predecessor:childKey];
314+
}
300315
}
301316
NSString *methodName = @"queryEndingBeforeValue:childKey:";
302317
if (childKey != nil && ![childKey isEqual:[FUtilities minName]]) {
@@ -361,12 +376,12 @@ - (FIRDatabaseQuery *)queryEqualToInternal:(id)value
361376
[FValidation validateFrom:methodName validKey:childKey];
362377
}
363378
if ([self.queryParams hasEnd] || [self.queryParams hasStart]) {
364-
[NSException
365-
raise:INVALID_QUERY_PARAM_ERROR
366-
format:
367-
@"Can't call %@ after queryStartingAtValue, queryEndingAtValue "
368-
@"or queryEqualToValue was previously called",
369-
methodName];
379+
[NSException raise:INVALID_QUERY_PARAM_ERROR
380+
format:@"Can't call %@ after queryStartingAtValue, "
381+
@"queryStartingAfterValue, queryEndingAtValue, "
382+
@"queryEndingBeforeValue or queryEqualToValue "
383+
@"was previously called",
384+
methodName];
370385
}
371386
id<FNode> node = [FSnapshotUtilities nodeFrom:value];
372387
FQueryParams *params = [[self.queryParams startAt:node

FirebaseDatabase/Sources/Core/FRepo.m

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -518,10 +518,8 @@ - (void)getData:(FIRDatabaseQuery *)query
518518
(void (^_Nonnull)(NSError *__nullable error,
519519
FIRDataSnapshot *__nullable snapshot))block {
520520
FQuerySpec *querySpec = [query querySpec];
521-
id<FNode> node =
522-
[self.serverSyncTree calcCompleteEventCacheAtPath:querySpec.path
523-
excludeWriteIds:@[]];
524-
if (![node isEmpty]) {
521+
id<FNode> node = [self.serverSyncTree getServerValue:[query querySpec]];
522+
if (node != nil) {
525523
block(nil, [[FIRDataSnapshot alloc]
526524
initWithRef:query.ref
527525
indexedNode:[FIndexedNode

FirebaseDatabase/Sources/Core/FSyncPoint.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
writesCache:(FWriteTreeRef *)writesCache
4242
serverCache:(id<FNode>)optCompleteServerCache;
4343

44+
- (FView *)getView:(FQuerySpec *)query
45+
writesCache:(FWriteTreeRef *)writesCache
46+
serverCache:(FCacheNode *)serverCache;
47+
4448
/**
4549
* Returns array of FEvent
4650
*/
@@ -61,6 +65,7 @@
6165
*/
6266
- (NSArray *)queryViews;
6367
- (id<FNode>)completeServerCacheAtPath:(FPath *)path;
68+
- (id<FNode>)completeEventCacheAtPath:(FPath *)path;
6469
- (FView *)viewForQuery:(FQuerySpec *)query;
6570
- (BOOL)viewExistsForQuery:(FQuerySpec *)query;
6671
- (BOOL)hasCompleteView;

FirebaseDatabase/Sources/Core/FSyncPoint.m

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,13 @@ - (NSArray *)applyOperation:(id<FOperation>)operation
128128
}
129129
}
130130

131-
/**
132-
* Add an event callback for the specified query
133-
* Returns Array of FEvent events to raise.
134-
*/
135-
- (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
136-
forNonExistingViewForQuery:(FQuerySpec *)query
137-
writesCache:(FWriteTreeRef *)writesCache
138-
serverCache:(FCacheNode *)serverCache {
139-
NSAssert(self.views[query.params] == nil, @"Found view for query: %@",
140-
query.params);
141-
// TODO: make writesCache take flag for complete server node
131+
- (FView *)getView:(FQuerySpec *)query
132+
writesCache:(FWriteTreeRef *)writesCache
133+
serverCache:(FCacheNode *)serverCache {
134+
FView *view = self.views[query.params];
135+
if (view != nil) {
136+
return view;
137+
}
142138
id<FNode> eventCache = [writesCache
143139
calculateCompleteEventCacheWithCompleteServerCache:
144140
serverCache.isFullyInitialized ? serverCache.node : nil];
@@ -147,8 +143,9 @@ - (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
147143
eventCacheComplete = YES;
148144
} else {
149145
eventCache = [writesCache
150-
calculateCompleteEventChildrenWithCompleteServerChildren:serverCache
151-
.node];
146+
calculateCompleteEventChildrenWithCompleteServerChildren:
147+
serverCache.node != nil ? serverCache.node
148+
: [FEmptyNode emptyNode]];
152149
eventCacheComplete = NO;
153150
}
154151

@@ -161,8 +158,24 @@ - (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
161158
FViewCache *viewCache =
162159
[[FViewCache alloc] initWithEventCache:eventCacheNode
163160
serverCache:serverCache];
164-
FView *view = [[FView alloc] initWithQuery:query
165-
initialViewCache:viewCache];
161+
return [[FView alloc] initWithQuery:query initialViewCache:viewCache];
162+
}
163+
164+
/**
165+
* Add an event callback for the specified query
166+
* Returns an array of events to raise.
167+
*/
168+
- (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
169+
forNonExistingViewForQuery:(FQuerySpec *)query
170+
writesCache:(FWriteTreeRef *)writesCache
171+
serverCache:(FCacheNode *)serverCache {
172+
NSAssert(self.views[query.params] == nil, @"Found view for query: %@",
173+
query.params);
174+
// TODO: make writesCache take flag for complete server node
175+
FView *view = [self getView:query
176+
writesCache:writesCache
177+
serverCache:serverCache];
178+
166179
// If this is a non-default query we need to tell persistence our current
167180
// view of the data
168181
if (!query.loadsAllData) {
@@ -273,6 +286,16 @@ - (NSArray *)queryViews {
273286
return serverCache;
274287
}
275288

289+
- (id<FNode>)completeEventCacheAtPath:(FPath *)path {
290+
__block id<FNode> eventCache = nil;
291+
[self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key,
292+
FView *view, BOOL *stop) {
293+
eventCache = [view completeEventCacheFor:path];
294+
*stop = (eventCache != nil);
295+
}];
296+
return eventCache;
297+
}
298+
276299
- (FView *)viewForQuery:(FQuerySpec *)query {
277300
return [self.views objectForKey:query.params];
278301
}

FirebaseDatabase/Sources/Core/FSyncTree.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
- (NSArray *)removeAllWrites;
7979

8080
- (FIndexedNode *)persistenceServerCache:(FQuerySpec *)querySpec;
81+
- (id<FNode>)getServerValue:(FQuerySpec *)query;
8182
- (id<FNode>)calcCompleteEventCacheAtPath:(FPath *)path
8283
excludeWriteIds:(NSArray *)writeIdsToExclude;
8384

FirebaseDatabase/Sources/Core/FSyncTree.m

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,47 @@ - (FIndexedNode *)persistenceServerCache:(FQuerySpec *)querySpec {
734734
return cacheNode.node;
735735
}
736736

737+
- (id<FNode>)getServerValue:(FQuerySpec *)query {
738+
__block id<FNode> serverCacheNode = nil;
739+
__block FSyncPoint *targetSyncPoint = nil;
740+
[self.syncPointTree
741+
forEachOnPath:query.path
742+
whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) {
743+
FPath *relativePath = [FPath relativePathFrom:pathToSyncPoint
744+
to:query.path];
745+
serverCacheNode =
746+
[syncPoint completeEventCacheAtPath:relativePath];
747+
targetSyncPoint = syncPoint;
748+
return serverCacheNode == nil;
749+
}];
750+
751+
if (targetSyncPoint == nil) {
752+
targetSyncPoint = [[FSyncPoint alloc]
753+
initWithPersistenceManager:self.persistenceManager];
754+
self.syncPointTree = [self.syncPointTree setValue:targetSyncPoint
755+
atPath:[query path]];
756+
} else {
757+
serverCacheNode =
758+
serverCacheNode != nil
759+
? serverCacheNode
760+
: [targetSyncPoint completeServerCacheAtPath:[FPath empty]];
761+
}
762+
763+
FIndexedNode *indexed = [FIndexedNode
764+
indexedNodeWithNode:serverCacheNode != nil ? serverCacheNode
765+
: [FEmptyNode emptyNode]
766+
index:query.index];
767+
FCacheNode *serverCache =
768+
[[FCacheNode alloc] initWithIndexedNode:indexed
769+
isFullyInitialized:serverCacheNode != nil
770+
isFiltered:NO];
771+
FView *view = [targetSyncPoint
772+
getView:query
773+
writesCache:[_pendingWriteTree childWritesForPath:[query path]]
774+
serverCache:serverCache];
775+
return [view completeEventCache];
776+
}
777+
737778
/**
738779
* Returns a complete cache, if we have one, of the data at a particular path.
739780
* The location must have a listener above it, but as this is only used by

FirebaseDatabase/Sources/Core/View/FView.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141

4242
- (id<FNode>)eventCache;
4343
- (id<FNode>)serverCache;
44+
- (id<FNode>)completeEventCache;
4445
- (id<FNode>)completeServerCacheFor:(FPath *)path;
46+
- (id<FNode>)completeEventCacheFor:(FPath *)path;
4547
- (BOOL)isEmpty;
4648

4749
- (void)addEventRegistration:(id<FEventRegistration>)eventRegistration;

FirebaseDatabase/Sources/Core/View/FView.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ - (id)initWithQuery:(FQuerySpec *)query
130130
return self.viewCache.cachedEventSnap.node;
131131
}
132132

133+
- (id<FNode>)completeEventCache {
134+
return self.viewCache.completeEventSnap;
135+
}
136+
133137
- (id<FNode>)completeServerCacheFor:(FPath *)path {
134138
id<FNode> cache = self.viewCache.completeServerSnap;
135139
if (cache) {
@@ -145,6 +149,21 @@ - (id)initWithQuery:(FQuerySpec *)query
145149
return nil;
146150
}
147151

152+
- (id<FNode>)completeEventCacheFor:(FPath *)path {
153+
id<FNode> cache = self.viewCache.completeEventSnap;
154+
if (cache) {
155+
// If this isn't a "loadsAllData" view, then cache isn't actually a
156+
// complete cache and we need to see if it contains the child we're
157+
// interested in.
158+
if ([self.query loadsAllData] ||
159+
(!path.isEmpty &&
160+
![cache getImmediateChild:path.getFront].isEmpty)) {
161+
return [cache getChild:path];
162+
}
163+
}
164+
return nil;
165+
}
166+
148167
- (BOOL)isEmpty {
149168
return self.eventRegistrations.count == 0;
150169
}

0 commit comments

Comments
 (0)