Skip to content

Commit b1766c2

Browse files
authored
Implement startAfter and endBefore for RTDB queries (#7209)
1 parent 634e5c3 commit b1766c2

File tree

12 files changed

+1329
-22
lines changed

12 files changed

+1329
-22
lines changed

FirebaseDatabase/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# v7.5.0
2+
- [added] Implmement `queryStartingAfterValue` and `queryEndingBeforeValue` for FirebaseDatabase query pagination.
23
- [added] Added `DatabaseQuery#getData` which returns data from the server when cache is stale (#7110).
34

45
# v7.2.0

FirebaseDatabase/Sources/Api/FIRDatabaseQuery.m

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#import "FirebaseDatabase/Sources/FValueIndex.h"
2929
#import "FirebaseDatabase/Sources/Snapshot/FLeafNode.h"
3030
#import "FirebaseDatabase/Sources/Snapshot/FSnapshotUtilities.h"
31+
#import "FirebaseDatabase/Sources/Utilities/FNextPushId.h"
3132
#import "FirebaseDatabase/Sources/Utilities/FValidation.h"
3233

3334
@implementation FIRDatabaseQuery
@@ -91,7 +92,8 @@ - (FQuerySpec *)querySpec {
9192
- (void)validateQueryEndpointsForParams:(FQueryParams *)params {
9293
if ([params.index isEqual:[FKeyIndex keyIndex]]) {
9394
if ([params hasStart]) {
94-
if (params.indexStartKey != [FUtilities minName]) {
95+
if (params.indexStartKey != [FUtilities minName] &&
96+
params.indexStartKey != [FUtilities maxName]) {
9597
[NSException raise:INVALID_QUERY_PARAM_ERROR
9698
format:@"Can't use queryStartingAtValue:childKey: "
9799
@"or queryEqualTo:andChildKey: in "
@@ -106,7 +108,8 @@ - (void)validateQueryEndpointsForParams:(FQueryParams *)params {
106108
}
107109
}
108110
if ([params hasEnd]) {
109-
if (params.indexEndKey != [FUtilities maxName]) {
111+
if (params.indexEndKey != [FUtilities maxName] &&
112+
params.indexEndKey != [FUtilities minName]) {
110113
[NSException raise:INVALID_QUERY_PARAM_ERROR
111114
format:@"Can't use queryEndingAtValue:childKey: or "
112115
@"queryEqualToValue:childKey: in "
@@ -183,9 +186,44 @@ - (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue
183186
@"queryOrderedByKey:"
184187
userInfo:nil];
185188
}
189+
NSString *methodName = @"queryStartingAtValue:childKey:";
190+
if (childKey != nil) {
191+
[FValidation validateFrom:methodName validKey:childKey];
192+
}
186193
return [self queryStartingAtInternal:startValue
187194
childKey:childKey
188-
from:@"queryStartingAtValue:childKey:"
195+
from:methodName
196+
priorityMethod:NO];
197+
}
198+
199+
- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue {
200+
return [self queryStartingAfterValue:startAfterValue childKey:nil];
201+
}
202+
203+
- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue
204+
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];
216+
} else {
217+
childKey = [FNextPushId successor:childKey];
218+
NSLog(@"successor of child key %@", childKey);
219+
}
220+
NSString *methodName = @"queryStartingAfterValue:childKey:";
221+
if (childKey != nil && ![childKey isEqual:[FUtilities maxName]]) {
222+
[FValidation validateFrom:methodName validKey:childKey];
223+
}
224+
return [self queryStartingAtInternal:startAfterValue
225+
childKey:childKey
226+
from:methodName
189227
priorityMethod:NO];
190228
}
191229

@@ -194,9 +232,6 @@ - (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
194232
from:(NSString *)methodName
195233
priorityMethod:(BOOL)priorityMethod {
196234
[self validateIndexValueType:startValue fromMethod:methodName];
197-
if (childKey != nil) {
198-
[FValidation validateFrom:methodName validKey:childKey];
199-
}
200235
if ([self.queryParams hasStart]) {
201236
[NSException raise:INVALID_QUERY_PARAM_ERROR
202237
format:@"Can't call %@ after queryStartingAtValue or "
@@ -232,10 +267,44 @@ - (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue
232267
@"queryOrderedByKey:"
233268
userInfo:nil];
234269
}
270+
NSString *methodName = @"queryEndingAtValue:childKey:";
271+
if (childKey != nil) {
272+
[FValidation validateFrom:methodName validKey:childKey];
273+
}
274+
return [self queryEndingAtInternal:endValue
275+
childKey:childKey
276+
from:methodName
277+
priorityMethod:NO];
278+
}
235279

280+
- (FIRDatabaseQuery *)queryEndingBeforeValue:(id)endValue {
281+
return [self queryEndingBeforeValue:endValue childKey:nil];
282+
}
283+
284+
- (FIRDatabaseQuery *)queryEndingBeforeValue:(id)endValue
285+
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];
298+
} else {
299+
childKey = [FNextPushId predecessor:childKey];
300+
}
301+
NSString *methodName = @"queryEndingBeforeValue:childKey:";
302+
if (childKey != nil && ![childKey isEqual:[FUtilities minName]]) {
303+
[FValidation validateFrom:methodName validKey:childKey];
304+
}
236305
return [self queryEndingAtInternal:endValue
237306
childKey:childKey
238-
from:@"queryEndingAtValue:childKey:"
307+
from:methodName
239308
priorityMethod:NO];
240309
}
241310

@@ -244,9 +313,6 @@ - (FIRDatabaseQuery *)queryEndingAtInternal:(id)endValue
244313
from:(NSString *)methodName
245314
priorityMethod:(BOOL)priorityMethod {
246315
[self validateIndexValueType:endValue fromMethod:methodName];
247-
if (childKey != nil) {
248-
[FValidation validateFrom:methodName validKey:childKey];
249-
}
250316
if ([self.queryParams hasEnd]) {
251317
[NSException raise:INVALID_QUERY_PARAM_ERROR
252318
format:@"Can't call %@ after queryEndingAtValue or "

FirebaseDatabase/Sources/FIRDatabaseReference.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,15 @@ - (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue
472472
return [super queryStartingAtValue:startValue childKey:childKey];
473473
}
474474

475+
- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue {
476+
return [super queryStartingAfterValue:startAfterValue];
477+
}
478+
479+
- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue
480+
childKey:(NSString *)childKey {
481+
return [super queryStartingAfterValue:startAfterValue childKey:childKey];
482+
}
483+
475484
- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
476485
return [super queryEndingAtValue:endValue];
477486
}

FirebaseDatabase/Sources/Public/FirebaseDatabase/FIRDatabaseQuery.h

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,36 @@ NS_SWIFT_NAME(DatabaseQuery)
327327
- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue
328328
childKey:(nullable NSString *)childKey;
329329

330+
/**
331+
* queryStartingAfterValue: is used to generate a reference to a
332+
* limited view of the data at this location. The FIRDatabaseQuery instance
333+
* returned by queryStartingAfterValue: will respond to events at nodes
334+
* with a value greater than startAfterValue.
335+
*
336+
* @param startAfterValue The lower bound, exclusive, for the value of data
337+
* visible to the returned FIRDatabaseQuery
338+
* @return A FIRDatabaseQuery instance, limited to data with value greater
339+
* startAfterValue
340+
*/
341+
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue;
342+
343+
/**
344+
* queryStartingAfterValue:childKey: is used to generate a reference to a
345+
* limited view of the data at this location. The FIRDatabaseQuery instance
346+
* returned by queryStartingAfterValue:childKey will respond to events at nodes
347+
* with a value greater than startAfterValue, or equal to startAfterValue and
348+
* with a key greater than childKey. This is most useful when implementing
349+
* pagination in a case where multiple nodes can match the startAfterValue.
350+
*
351+
* @param startAfterValue The lower bound, inclusive, for the value of data
352+
* visible to the returned FIRDatabaseQuery
353+
* @param childKey The lower bound, exclusive, for the key of nodes with value
354+
* equal to startAfterValue
355+
* @return A FIRDatabaseQuery instance, limited to data with value greater than
356+
* startAfterValue, or equal to startAfterValue with a key greater than childKey
357+
*/
358+
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue
359+
childKey:(nullable NSString *)childKey;
330360
/**
331361
* queryEndingAtValue: is used to generate a reference to a limited view of the
332362
* data at this location. The FIRDatabaseQuery instance returned by
@@ -358,6 +388,35 @@ NS_SWIFT_NAME(DatabaseQuery)
358388
- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue
359389
childKey:(nullable NSString *)childKey;
360390

391+
/**
392+
* queryEndingBeforeValue: is used to generate a reference to a limited view of
393+
* the data at this location. The FIRDatabaseQuery instance returned by
394+
* queryEndingBeforeValue: will respond to events at nodes with a value less
395+
* than endValue.
396+
*
397+
* @param endValue The upper bound, exclusive, for the value of data visible to
398+
* the returned FIRDatabaseQuery
399+
* @return A FIRDatabaseQuery instance, limited to data with value less than
400+
* endValue
401+
*/
402+
- (FIRDatabaseQuery *)queryEndingBeforeValue:(nullable id)endValue;
403+
404+
/**
405+
* queryEndingBeforeValue:childKey: is used to generate a reference to a limited
406+
* view of the data at this location. The FIRDatabaseQuery instance returned by
407+
* queryEndingBeforeValue:childKey will respond to events at nodes with a value
408+
* less than endValue, or equal to endValue and with a key less than childKey.
409+
*
410+
* @param endValue The upper bound, inclusive, for the value of data visible to
411+
* the returned FIRDatabaseQuery
412+
* @param childKey The upper bound, exclusive, for the key of nodes with value
413+
* equal to endValue
414+
* @return A FIRDatabaseQuery instance, limited to data with value less than or
415+
* equal to endValue
416+
*/
417+
- (FIRDatabaseQuery *)queryEndingBeforeValue:(nullable id)endValue
418+
childKey:(nullable NSString *)childKey;
419+
361420
/**
362421
* queryEqualToValue: is used to generate a reference to a limited view of the
363422
* data at this location. The FIRDatabaseQuery instance returned by

FirebaseDatabase/Sources/Public/FirebaseDatabase/FIRDatabaseReference.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,38 @@ priority is meant to be preserved, you should use setValue:andPriority: instead.
483483
- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue
484484
childKey:(nullable NSString *)childKey;
485485

486+
/**
487+
* queryStartingAfterValue: is used to generate a reference to a limited view of
488+
* the data at this location. The FIRDatabaseQuery instance returned by
489+
* queryStartingAfterValue: will respond to events at nodes with a value greater
490+
* than startAfterValue.
491+
*
492+
* @param startAfterValue The lower bound, exclusive, for the value of data
493+
* visible to the returned FIRDatabaseQuery
494+
* @return A FIRDatabaseQuery instance, limited to data with value greater than
495+
* startAfterValue
496+
*/
497+
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue;
498+
499+
/**
500+
* queryStartingAfterValue:childKey: is used to generate a reference to a
501+
* limited view of the data at this location. The FIRDatabaseQuery instance
502+
* returned by queryStartingAfterValue:childKey will respond to events at nodes
503+
* with a value greater than startAfterValue, or equal to startAfterValue and
504+
* with a key greater than childKey. This is most useful when implementing
505+
* pagination in a case where multiple nodes can match the startAfterValue.
506+
*
507+
* @param startAfterValue The lower bound, inclusive, for the value of data
508+
* visible to the returned FIRDatabaseQuery
509+
* @param childKey The lower bound, exclusive, for the key of nodes with value
510+
* equal to startAfterValue
511+
* @return A FIRDatabaseQuery instance, limited to data with value greater than
512+
* or equal to startAfterValue, or equal to startAfterValue and with a key
513+
* greater than childKey.
514+
*/
515+
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue
516+
childKey:(nullable NSString *)childKey;
517+
486518
/**
487519
* queryEndingAtValue: is used to generate a reference to a limited view of the
488520
* data at this location. The FIRDatabaseQuery instance returned by

FirebaseDatabase/Sources/Utilities/FNextPushId.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@
2020

2121
+ (NSString *)get:(NSTimeInterval)now;
2222

23+
+ (NSString *)successor:(NSString *)key;
24+
25+
+ (NSString *)predecessor:(NSString *)key;
26+
2327
@end

FirebaseDatabase/Sources/Utilities/FNextPushId.m

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
static NSString *const PUSH_CHARS =
2121
@"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
2222

23+
static NSString *const MIN_PUSH_CHAR = @"-";
24+
25+
static NSString *const MAX_PUSH_CHAR = @"z";
26+
27+
static NSInteger const MAX_KEY_LEN = 786;
28+
2329
@implementation FNextPushId
2430

2531
+ (NSString *)get:(NSTimeInterval)currentTime {
@@ -59,4 +65,80 @@ + (NSString *)get:(NSTimeInterval)currentTime {
5965
return [NSString stringWithString:id];
6066
}
6167

68+
+ (NSString *)successor:(NSString *_Nonnull)key {
69+
NSInteger keyAsInt;
70+
if ([FUtilities tryParseString:key asInt:&keyAsInt]) {
71+
if (keyAsInt == [FUtilities int32max]) {
72+
return MIN_PUSH_CHAR;
73+
}
74+
return [NSString stringWithFormat:@"%ld", (long)keyAsInt + 1];
75+
}
76+
NSMutableString *next = [NSMutableString stringWithString:key];
77+
if ([next length] < MAX_KEY_LEN) {
78+
[next insertString:MIN_PUSH_CHAR atIndex:[key length]];
79+
return next;
80+
}
81+
82+
long i = [next length] - 1;
83+
while (i >= 0) {
84+
if ([next characterAtIndex:i] != [MAX_PUSH_CHAR characterAtIndex:0]) {
85+
break;
86+
}
87+
--i;
88+
}
89+
90+
// `nextAfter` was called on the largest possible key, so return the
91+
// maxName, which sorts larger than all keys.
92+
if (i == -1) {
93+
return [FUtilities maxName];
94+
}
95+
96+
NSString *source =
97+
[NSString stringWithFormat:@"%C", [next characterAtIndex:i]];
98+
NSInteger sourceIndex = [PUSH_CHARS rangeOfString:source].location;
99+
NSString *sourcePlusOne = [NSString
100+
stringWithFormat:@"%C", [PUSH_CHARS characterAtIndex:sourceIndex + 1]];
101+
102+
[next replaceCharactersInRange:NSMakeRange(i, i + 1)
103+
withString:sourcePlusOne];
104+
return [next substringWithRange:NSMakeRange(0, i + 1)];
105+
}
106+
107+
// `key` is assumed to be non-empty.
108+
+ (NSString *)predecessor:(NSString *_Nonnull)key {
109+
NSInteger keyAsInt;
110+
if ([FUtilities tryParseString:key asInt:&keyAsInt]) {
111+
if (keyAsInt == [FUtilities int32min]) {
112+
return [FUtilities minName];
113+
}
114+
return [NSString stringWithFormat:@"%ld", (long)keyAsInt - 1];
115+
}
116+
NSMutableString *next = [NSMutableString stringWithString:key];
117+
if ([next characterAtIndex:(next.length - 1)] ==
118+
[MIN_PUSH_CHAR characterAtIndex:0]) {
119+
if ([next length] == 1) {
120+
return
121+
[NSString stringWithFormat:@"%ld", (long)[FUtilities int32max]];
122+
}
123+
// If the last character is the smallest possible character, then the
124+
// next smallest string is the prefix of `key` without it.
125+
[next replaceCharactersInRange:NSMakeRange([next length] - 1, 1)
126+
withString:@""];
127+
return next;
128+
}
129+
// Replace the last character with its immedate predecessor, and fill the
130+
// suffix of the key with MAX_PUSH_CHAR. This is the lexicographically
131+
// largest possible key smaller than `key`.
132+
unichar curr = [next characterAtIndex:next.length - 1];
133+
NSRange dstRange = NSMakeRange([next length] - 1, 1);
134+
NSRange srcRange =
135+
[PUSH_CHARS rangeOfString:[NSString stringWithFormat:@"%C", curr]];
136+
srcRange.location -= 1;
137+
[next replaceCharactersInRange:dstRange
138+
withString:[PUSH_CHARS substringWithRange:srcRange]];
139+
return [next stringByPaddingToLength:MAX_KEY_LEN
140+
withString:MAX_PUSH_CHAR
141+
startingAtIndex:0];
142+
};
143+
62144
@end

FirebaseDatabase/Sources/Utilities/FUtilities.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
+ (NSString *)getJavascriptType:(id)obj;
2828
+ (NSError *)errorForStatus:(NSString *)status andReason:(NSString *)reason;
2929
+ (NSNumber *)intForString:(NSString *)string;
30+
+ (NSInteger)int32min;
31+
+ (NSInteger)int32max;
3032
+ (NSString *)ieee754StringForNumber:(NSNumber *)val;
33+
+ (BOOL)tryParseString:(NSString *)string asInt:(NSInteger *)integer;
3134
+ (void)setLoggingEnabled:(BOOL)enabled;
3235
+ (BOOL)getLoggingEnabled;
3336

@@ -76,6 +79,9 @@ FOUNDATION_EXPORT NSString *const kFPersistenceLogTag;
7679
} \
7780
} while (0)
7881

82+
#define INTEGER_32_MIN (-2147483648)
83+
#define INTEGER_32_MAX 2147483647
84+
7985
extern FIRLoggerService kFIRLoggerDatabase;
8086
BOOL FFIsLoggingEnabled(FLogLevel logLevel);
8187
void firebaseUncaughtExceptionHandler(NSException *exception);

0 commit comments

Comments
 (0)