Skip to content

Implement startAfter for RTDB queries #7209

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 18 commits into from
Jan 14, 2021
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
1 change: 1 addition & 0 deletions FirebaseDatabase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# v7.5.0
- [added] Implmement `queryStartingAfterValue` and `queryEndingBeforeValue` for FirebaseDatabase query pagination.
- [added] Added `DatabaseQuery#getData` which returns data from the server when cache is stale (#7110).

# v7.2.0
Expand Down
86 changes: 76 additions & 10 deletions FirebaseDatabase/Sources/Api/FIRDatabaseQuery.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#import "FirebaseDatabase/Sources/FValueIndex.h"
#import "FirebaseDatabase/Sources/Snapshot/FLeafNode.h"
#import "FirebaseDatabase/Sources/Snapshot/FSnapshotUtilities.h"
#import "FirebaseDatabase/Sources/Utilities/FNextPushId.h"
#import "FirebaseDatabase/Sources/Utilities/FValidation.h"

@implementation FIRDatabaseQuery
Expand Down Expand Up @@ -91,7 +92,8 @@ - (FQuerySpec *)querySpec {
- (void)validateQueryEndpointsForParams:(FQueryParams *)params {
if ([params.index isEqual:[FKeyIndex keyIndex]]) {
if ([params hasStart]) {
if (params.indexStartKey != [FUtilities minName]) {
if (params.indexStartKey != [FUtilities minName] &&
params.indexStartKey != [FUtilities maxName]) {
[NSException raise:INVALID_QUERY_PARAM_ERROR
format:@"Can't use queryStartingAtValue:childKey: "
@"or queryEqualTo:andChildKey: in "
Expand All @@ -106,7 +108,8 @@ - (void)validateQueryEndpointsForParams:(FQueryParams *)params {
}
}
if ([params hasEnd]) {
if (params.indexEndKey != [FUtilities maxName]) {
if (params.indexEndKey != [FUtilities maxName] &&
params.indexEndKey != [FUtilities minName]) {
[NSException raise:INVALID_QUERY_PARAM_ERROR
format:@"Can't use queryEndingAtValue:childKey: or "
@"queryEqualToValue:childKey: in "
Expand Down Expand Up @@ -183,9 +186,44 @@ - (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue
@"queryOrderedByKey:"
userInfo:nil];
}
NSString *methodName = @"queryStartingAtValue:childKey:";
if (childKey != nil) {
[FValidation validateFrom:methodName validKey:childKey];
}
return [self queryStartingAtInternal:startValue
childKey:childKey
from:@"queryStartingAtValue:childKey:"
from:methodName
priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue {
return [self queryStartingAfterValue:startAfterValue childKey:nil];
}

- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue
childKey:(NSString *)childKey {
if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]] &&
childKey != nil) {
@throw [[NSException alloc]
initWithName:INVALID_QUERY_PARAM_ERROR
reason:@"You must use queryStartingAfterValue: instead of "
@"queryStartingAfterValue:childKey: when using "
@"queryOrderedByKey:"
userInfo:nil];
}
if (childKey == nil) {
childKey = [FUtilities maxName];
} else {
childKey = [FNextPushId successor:childKey];
NSLog(@"successor of child key %@", childKey);
}
NSString *methodName = @"queryStartingAfterValue:childKey:";
if (childKey != nil && ![childKey isEqual:[FUtilities maxName]]) {
[FValidation validateFrom:methodName validKey:childKey];
}
return [self queryStartingAtInternal:startAfterValue
childKey:childKey
from:methodName
priorityMethod:NO];
}

Expand All @@ -194,9 +232,6 @@ - (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
from:(NSString *)methodName
priorityMethod:(BOOL)priorityMethod {
[self validateIndexValueType:startValue fromMethod:methodName];
if (childKey != nil) {
[FValidation validateFrom:methodName validKey:childKey];
}
if ([self.queryParams hasStart]) {
[NSException raise:INVALID_QUERY_PARAM_ERROR
format:@"Can't call %@ after queryStartingAtValue or "
Expand Down Expand Up @@ -232,10 +267,44 @@ - (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue
@"queryOrderedByKey:"
userInfo:nil];
}
NSString *methodName = @"queryEndingAtValue:childKey:";
if (childKey != nil) {
[FValidation validateFrom:methodName validKey:childKey];
}
return [self queryEndingAtInternal:endValue
childKey:childKey
from:methodName
priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryEndingBeforeValue:(id)endValue {
return [self queryEndingBeforeValue:endValue childKey:nil];
}

- (FIRDatabaseQuery *)queryEndingBeforeValue:(id)endValue
childKey:(NSString *)childKey {
if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]] &&
childKey != nil) {
@throw [[NSException alloc]
initWithName:INVALID_QUERY_PARAM_ERROR
reason:@"You must use queryEndingBeforeValue: instead of "
@"queryEndingBeforeValue:childKey: when using "
@"queryOrderedByKey:"
userInfo:nil];
}

if (childKey == nil) {
childKey = [FUtilities minName];
} else {
childKey = [FNextPushId predecessor:childKey];
}
NSString *methodName = @"queryEndingBeforeValue:childKey:";
if (childKey != nil && ![childKey isEqual:[FUtilities minName]]) {
[FValidation validateFrom:methodName validKey:childKey];
}
return [self queryEndingAtInternal:endValue
childKey:childKey
from:@"queryEndingAtValue:childKey:"
from:methodName
priorityMethod:NO];
}

Expand All @@ -244,9 +313,6 @@ - (FIRDatabaseQuery *)queryEndingAtInternal:(id)endValue
from:(NSString *)methodName
priorityMethod:(BOOL)priorityMethod {
[self validateIndexValueType:endValue fromMethod:methodName];
if (childKey != nil) {
[FValidation validateFrom:methodName validKey:childKey];
}
if ([self.queryParams hasEnd]) {
[NSException raise:INVALID_QUERY_PARAM_ERROR
format:@"Can't call %@ after queryEndingAtValue or "
Expand Down
9 changes: 9 additions & 0 deletions FirebaseDatabase/Sources/FIRDatabaseReference.m
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,15 @@ - (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue
return [super queryStartingAtValue:startValue childKey:childKey];
}

- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue {
return [super queryStartingAfterValue:startAfterValue];
}

- (FIRDatabaseQuery *)queryStartingAfterValue:(id)startAfterValue
childKey:(NSString *)childKey {
return [super queryStartingAfterValue:startAfterValue childKey:childKey];
}

- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
return [super queryEndingAtValue:endValue];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,36 @@ NS_SWIFT_NAME(DatabaseQuery)
- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue
childKey:(nullable NSString *)childKey;

/**
* queryStartingAfterValue: is used to generate a reference to a
* limited view of the data at this location. The FIRDatabaseQuery instance
* returned by queryStartingAfterValue: will respond to events at nodes
* with a value greater than startAfterValue.
*
* @param startAfterValue The lower bound, exclusive, for the value of data
* visible to the returned FIRDatabaseQuery
* @return A FIRDatabaseQuery instance, limited to data with value greater
* startAfterValue
*/
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue;

/**
* queryStartingAfterValue:childKey: is used to generate a reference to a
* limited view of the data at this location. The FIRDatabaseQuery instance
* returned by queryStartingAfterValue:childKey will respond to events at nodes
* with a value greater than startAfterValue, or equal to startAfterValue and
* with a key greater than childKey. This is most useful when implementing
* pagination in a case where multiple nodes can match the startAfterValue.
*
* @param startAfterValue The lower bound, inclusive, for the value of data
* visible to the returned FIRDatabaseQuery
* @param childKey The lower bound, exclusive, for the key of nodes with value
* equal to startAfterValue
* @return A FIRDatabaseQuery instance, limited to data with value greater than
* startAfterValue, or equal to startAfterValue with a key greater than childKey
*/
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue
childKey:(nullable NSString *)childKey;
/**
* queryEndingAtValue: is used to generate a reference to a limited view of the
* data at this location. The FIRDatabaseQuery instance returned by
Expand Down Expand Up @@ -358,6 +388,35 @@ NS_SWIFT_NAME(DatabaseQuery)
- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue
childKey:(nullable NSString *)childKey;

/**
* queryEndingBeforeValue: is used to generate a reference to a limited view of
* the data at this location. The FIRDatabaseQuery instance returned by
* queryEndingBeforeValue: will respond to events at nodes with a value less
* than endValue.
*
* @param endValue The upper bound, exclusive, for the value of data visible to
* the returned FIRDatabaseQuery
* @return A FIRDatabaseQuery instance, limited to data with value less than
* endValue
*/
- (FIRDatabaseQuery *)queryEndingBeforeValue:(nullable id)endValue;

/**
* queryEndingBeforeValue:childKey: is used to generate a reference to a limited
* view of the data at this location. The FIRDatabaseQuery instance returned by
* queryEndingBeforeValue:childKey will respond to events at nodes with a value
* less than endValue, or equal to endValue and with a key less than childKey.
*
* @param endValue The upper bound, inclusive, for the value of data visible to
* the returned FIRDatabaseQuery
* @param childKey The upper bound, exclusive, for the key of nodes with value
* equal to endValue
* @return A FIRDatabaseQuery instance, limited to data with value less than or
* equal to endValue
*/
- (FIRDatabaseQuery *)queryEndingBeforeValue:(nullable id)endValue
childKey:(nullable NSString *)childKey;

/**
* queryEqualToValue: is used to generate a reference to a limited view of the
* data at this location. The FIRDatabaseQuery instance returned by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,38 @@ priority is meant to be preserved, you should use setValue:andPriority: instead.
- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue
childKey:(nullable NSString *)childKey;

/**
* queryStartingAfterValue: is used to generate a reference to a limited view of
* the data at this location. The FIRDatabaseQuery instance returned by
* queryStartingAfterValue: will respond to events at nodes with a value greater
* than startAfterValue.
*
* @param startAfterValue The lower bound, exclusive, for the value of data
* visible to the returned FIRDatabaseQuery
* @return A FIRDatabaseQuery instance, limited to data with value greater than
* startAfterValue
*/
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue;

/**
* queryStartingAfterValue:childKey: is used to generate a reference to a
* limited view of the data at this location. The FIRDatabaseQuery instance
* returned by queryStartingAfterValue:childKey will respond to events at nodes
* with a value greater than startAfterValue, or equal to startAfterValue and
* with a key greater than childKey. This is most useful when implementing
* pagination in a case where multiple nodes can match the startAfterValue.
*
* @param startAfterValue The lower bound, inclusive, for the value of data
* visible to the returned FIRDatabaseQuery
* @param childKey The lower bound, exclusive, for the key of nodes with value
* equal to startAfterValue
* @return A FIRDatabaseQuery instance, limited to data with value greater than
* or equal to startAfterValue, or equal to startAfterValue and with a key
* greater than childKey.
*/
- (FIRDatabaseQuery *)queryStartingAfterValue:(nullable id)startAfterValue
childKey:(nullable NSString *)childKey;

/**
* queryEndingAtValue: is used to generate a reference to a limited view of the
* data at this location. The FIRDatabaseQuery instance returned by
Expand Down
4 changes: 4 additions & 0 deletions FirebaseDatabase/Sources/Utilities/FNextPushId.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@

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

+ (NSString *)successor:(NSString *)key;

+ (NSString *)predecessor:(NSString *)key;

@end
82 changes: 82 additions & 0 deletions FirebaseDatabase/Sources/Utilities/FNextPushId.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
static NSString *const PUSH_CHARS =
@"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";

static NSString *const MIN_PUSH_CHAR = @"-";

static NSString *const MAX_PUSH_CHAR = @"z";

static NSInteger const MAX_KEY_LEN = 786;

@implementation FNextPushId

+ (NSString *)get:(NSTimeInterval)currentTime {
Expand Down Expand Up @@ -59,4 +65,80 @@ + (NSString *)get:(NSTimeInterval)currentTime {
return [NSString stringWithString:id];
}

+ (NSString *)successor:(NSString *_Nonnull)key {
NSInteger keyAsInt;
if ([FUtilities tryParseString:key asInt:&keyAsInt]) {
if (keyAsInt == [FUtilities int32max]) {
return MIN_PUSH_CHAR;
}
return [NSString stringWithFormat:@"%ld", (long)keyAsInt + 1];
}
NSMutableString *next = [NSMutableString stringWithString:key];
if ([next length] < MAX_KEY_LEN) {
[next insertString:MIN_PUSH_CHAR atIndex:[key length]];
return next;
}

long i = [next length] - 1;
while (i >= 0) {
if ([next characterAtIndex:i] != [MAX_PUSH_CHAR characterAtIndex:0]) {
break;
}
--i;
}

// `nextAfter` was called on the largest possible key, so return the
// maxName, which sorts larger than all keys.
if (i == -1) {
return [FUtilities maxName];
}

NSString *source =
[NSString stringWithFormat:@"%C", [next characterAtIndex:i]];
NSInteger sourceIndex = [PUSH_CHARS rangeOfString:source].location;
NSString *sourcePlusOne = [NSString
stringWithFormat:@"%C", [PUSH_CHARS characterAtIndex:sourceIndex + 1]];

[next replaceCharactersInRange:NSMakeRange(i, i + 1)
withString:sourcePlusOne];
return [next substringWithRange:NSMakeRange(0, i + 1)];
}

// `key` is assumed to be non-empty.
+ (NSString *)predecessor:(NSString *_Nonnull)key {
NSInteger keyAsInt;
if ([FUtilities tryParseString:key asInt:&keyAsInt]) {
if (keyAsInt == [FUtilities int32min]) {
return [FUtilities minName];
}
return [NSString stringWithFormat:@"%ld", (long)keyAsInt - 1];
}
NSMutableString *next = [NSMutableString stringWithString:key];
if ([next characterAtIndex:(next.length - 1)] ==
[MIN_PUSH_CHAR characterAtIndex:0]) {
if ([next length] == 1) {
return
[NSString stringWithFormat:@"%ld", (long)[FUtilities int32max]];
}
// If the last character is the smallest possible character, then the
// next smallest string is the prefix of `key` without it.
[next replaceCharactersInRange:NSMakeRange([next length] - 1, 1)
withString:@""];
return next;
}
// Replace the last character with its immedate predecessor, and fill the
// suffix of the key with MAX_PUSH_CHAR. This is the lexicographically
// largest possible key smaller than `key`.
unichar curr = [next characterAtIndex:next.length - 1];
NSRange dstRange = NSMakeRange([next length] - 1, 1);
NSRange srcRange =
[PUSH_CHARS rangeOfString:[NSString stringWithFormat:@"%C", curr]];
srcRange.location -= 1;
[next replaceCharactersInRange:dstRange
withString:[PUSH_CHARS substringWithRange:srcRange]];
return [next stringByPaddingToLength:MAX_KEY_LEN
withString:MAX_PUSH_CHAR
startingAtIndex:0];
};

@end
6 changes: 6 additions & 0 deletions FirebaseDatabase/Sources/Utilities/FUtilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
+ (NSString *)getJavascriptType:(id)obj;
+ (NSError *)errorForStatus:(NSString *)status andReason:(NSString *)reason;
+ (NSNumber *)intForString:(NSString *)string;
+ (NSInteger)int32min;
+ (NSInteger)int32max;
+ (NSString *)ieee754StringForNumber:(NSNumber *)val;
+ (BOOL)tryParseString:(NSString *)string asInt:(NSInteger *)integer;
+ (void)setLoggingEnabled:(BOOL)enabled;
+ (BOOL)getLoggingEnabled;

Expand Down Expand Up @@ -76,6 +79,9 @@ FOUNDATION_EXPORT NSString *const kFPersistenceLogTag;
} \
} while (0)

#define INTEGER_32_MIN (-2147483648)
#define INTEGER_32_MAX 2147483647

extern FIRLoggerService kFIRLoggerDatabase;
BOOL FFIsLoggingEnabled(FLogLevel logLevel);
void firebaseUncaughtExceptionHandler(NSException *exception);
Expand Down
Loading