aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/Core/View/Filter
diff options
context:
space:
mode:
Diffstat (limited to 'Firebase/Database/Core/View/Filter')
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m80
-rw-r--r--Firebase/Database/Core/View/Filter/FCompleteChildSource.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.h27
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.m147
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.h26
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.m243
-rw-r--r--Firebase/Database/Core/View/Filter/FNodeFilter.h71
8 files changed, 650 insertions, 0 deletions
diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h
new file mode 100644
index 0000000..59b0a85
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h
@@ -0,0 +1,28 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FChange;
+
+
+@interface FChildChangeAccumulator : NSObject
+
+- (id) init;
+- (void) trackChildChange:(FChange *)change;
+- (NSArray *) changes;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m
new file mode 100644
index 0000000..e43fd7c
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m
@@ -0,0 +1,80 @@
+/*
+ * 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 "FChildChangeAccumulator.h"
+#import "FChange.h"
+#import "FIndex.h"
+
+@interface FChildChangeAccumulator ()
+@property (nonatomic, strong) NSMutableDictionary *changeMap;
+@end
+
+@implementation FChildChangeAccumulator
+
+- (id) init {
+ self = [super init];
+ if (self) {
+ self.changeMap = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void) trackChildChange:(FChange *)change {
+ FIRDataEventType type = change.type;
+ NSString *childKey = change.childKey;
+ NSAssert(type == FIRDataEventTypeChildAdded || type == FIRDataEventTypeChildChanged || type == FIRDataEventTypeChildRemoved, @"Only child changes supported for tracking.");
+ NSAssert(![change.childKey isEqualToString:@".priority"], @"Changes not tracked on priority");
+ if (self.changeMap[childKey] != nil) {
+ FChange *oldChange = [self.changeMap objectForKey:childKey];
+ FIRDataEventType oldType = oldChange.type;
+ if (type == FIRDataEventTypeChildAdded && oldType == FIRDataEventTypeChildRemoved) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:change.indexedNode
+ childKey:childKey
+ oldIndexedNode:oldChange.indexedNode];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildAdded) {
+ [self.changeMap removeObjectForKey:childKey];
+ } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildChanged) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:oldChange.oldIndexedNode
+ childKey:childKey];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildAdded) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:change.indexedNode
+ childKey:childKey];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildChanged) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:change.indexedNode
+ childKey:childKey
+ oldIndexedNode:oldChange.oldIndexedNode];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else {
+ NSString *reason = [NSString stringWithFormat:@"Illegal combination of changes: %@ occurred after %@", change, oldChange];
+ @throw [[NSException alloc] initWithName:@"FirebaseDatabaseInternalError" reason:reason userInfo:nil];
+ }
+ } else {
+ [self.changeMap setObject:change forKey:childKey];
+ }
+}
+
+- (NSArray *) changes {
+ return [self.changeMap allValues];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FCompleteChildSource.h b/Firebase/Database/Core/View/Filter/FCompleteChildSource.h
new file mode 100644
index 0000000..4e99045
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FCompleteChildSource.h
@@ -0,0 +1,28 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FNamedNode;
+@protocol FIndex;
+
+@protocol FCompleteChildSource<NSObject>
+
+- (id<FNode>) completeChild:(NSString *)childKey;
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.h b/Firebase/Database/Core/View/Filter/FIndexedFilter.h
new file mode 100644
index 0000000..5081a77
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@protocol FIndex;
+
+
+@interface FIndexedFilter : NSObject<FNodeFilter>
+
+- (id) initWithIndex:(id<FIndex>)theIndex;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.m b/Firebase/Database/Core/View/Filter/FIndexedFilter.m
new file mode 100644
index 0000000..44c411c
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.m
@@ -0,0 +1,147 @@
+/*
+ * 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 "FNode.h"
+#import "FIndexedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FIndex.h"
+#import "FChange.h"
+#import "FChildrenNode.h"
+#import "FKeyIndex.h"
+#import "FEmptyNode.h"
+#import "FIndexedNode.h"
+
+@interface FIndexedFilter ()
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@end
+
+@implementation FIndexedFilter
+- (id) initWithIndex:(id<FIndex>)theIndex {
+ self = [super init];
+ if (self) {
+ self.index = theIndex;
+ }
+ return self;
+}
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)indexedNode
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ NSAssert([indexedNode hasIndex:self.index], @"The index in FIndexedNode must match the index of the filter");
+ id<FNode> node = indexedNode.node;
+ id<FNode> oldChildSnap = [node getImmediateChild:childKey];
+
+ // Check if anything actually changed.
+ if ([[oldChildSnap getChild:affectedPath] isEqual:[newChildSnap getChild:affectedPath]]) {
+ // There's an edge case where a child can enter or leave the view because affectedPath was set to null.
+ // In this case, affectedPath will appear null in both the old and new snapshots. So we need
+ // to avoid treating these cases as "nothing changed."
+ if (oldChildSnap.isEmpty == newChildSnap.isEmpty) {
+ // Nothing changed.
+ #ifdef DEBUG
+ NSAssert([oldChildSnap isEqual:newChildSnap], @"Old and new snapshots should be equal.");
+ #endif
+
+ return indexedNode;
+ }
+ }
+ if (optChangeAccumulator) {
+ if (newChildSnap.isEmpty) {
+ if ([node hasChild:childKey]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ } else {
+ NSAssert(node.isLeafNode, @"A child remove without an old child only makes sense on a leaf node.");
+ }
+ } else if (oldChildSnap.isEmpty) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ } else {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }
+ if (node.isLeafNode && newChildSnap.isEmpty) {
+ return indexedNode;
+ } else {
+ return [indexedNode updateChild:childKey withNewChild:newChildSnap];
+ }
+}
+
+- (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (optChangeAccumulator) {
+ [oldSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if (![newSnap.node hasChild:childKey]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }];
+
+ [newSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if ([oldSnap.node hasChild:childKey]) {
+ id<FNode> oldChildSnap = [oldSnap.node getImmediateChild:childKey];
+ if (![oldChildSnap isEqual:childNode]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ } else {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }];
+ }
+ return newSnap;
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ if ([oldSnap.node isEmpty]) {
+ return oldSnap;
+ } else {
+ return [oldSnap updatePriority:priority];
+ }
+}
+
+- (BOOL) filtersNodes {
+ return NO;
+}
+
+- (id<FNodeFilter>) indexedFilter {
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.h b/Firebase/Database/Core/View/Filter/FLimitedFilter.h
new file mode 100644
index 0000000..1690980
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@class FQueryParams;
+
+
+@interface FLimitedFilter : NSObject<FNodeFilter>
+
+- (id) initWithQueryParams:(FQueryParams *)params;
+@end
diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.m b/Firebase/Database/Core/View/Filter/FLimitedFilter.m
new file mode 100644
index 0000000..8bc6e87
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.m
@@ -0,0 +1,243 @@
+/*
+ * 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 "FLimitedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FIndex.h"
+#import "FRangedFilter.h"
+#import "FQueryParams.h"
+#import "FQueryParams.h"
+#import "FNamedNode.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FCompleteChildSource.h"
+#import "FChange.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FLimitedFilter ()
+@property (nonatomic, strong) FRangedFilter *rangedFilter;
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@property (nonatomic) NSInteger limit;
+@property (nonatomic) BOOL reverse;
+
+@end
+
+@implementation FLimitedFilter
+- (id) initWithQueryParams:(FQueryParams *)params {
+ self = [super init];
+ if (self) {
+ self.rangedFilter = [[FRangedFilter alloc] initWithQueryParams:params];
+ self.index = params.index;
+ self.limit = params.limit;
+ self.reverse = !params.isViewFromLeft;
+ }
+ return self;
+}
+
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (![self.rangedFilter matchesKey:childKey andNode:newChildSnap]) {
+ newChildSnap = [FEmptyNode emptyNode];
+ }
+ if ([[oldSnap.node getImmediateChild:childKey] isEqual:newChildSnap]) {
+ // No change
+ return oldSnap;
+ } else if (oldSnap.node.numChildren < self.limit) {
+ return [[self.rangedFilter indexedFilter] updateChildIn:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ affectedPath:affectedPath
+ fromSource:source
+ accumulator:optChangeAccumulator];
+ } else {
+ return [self fullLimitUpdateNode:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ fromSource:source
+ accumulator:optChangeAccumulator];
+ }
+}
+
+- (FIndexedNode *)fullLimitUpdateNode:(FIndexedNode *)oldIndexed
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ NSAssert(oldIndexed.node.numChildren == self.limit, @"Should have number of children equal to limit.");
+
+ FNamedNode *windowBoundary = self.reverse ? oldIndexed.firstChild : oldIndexed.lastChild;
+
+ BOOL inRange = [self.rangedFilter matchesKey:childKey andNode:newChildSnap];
+ if ([oldIndexed.node hasChild:childKey]) {
+ // `childKey` was already in `oldSnap`. Figure out if it remains in the window or needs to be replaced.
+ id<FNode> oldChildSnap = [oldIndexed.node getImmediateChild:childKey];
+
+ // In case the `newChildSnap` falls outside the window, get the `nextChild` that might replace it.
+ FNamedNode *nextChild = [source childByIndex:self.index afterChild:windowBoundary isReverse:(BOOL)self.reverse];
+ if (nextChild != nil && ([nextChild.name isEqualToString:childKey] ||
+ [oldIndexed.node hasChild:nextChild.name])) {
+ // There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't
+ // been applied to the limited filter yet. Ignore this next child which will be updated later in
+ // the limited filter...
+ nextChild = [source childByIndex:self.index afterChild:nextChild isReverse:self.reverse];
+ }
+
+
+
+ // Figure out if `newChildSnap` is in range and ordered before `nextChild`
+ BOOL remainsInWindow = inRange && !newChildSnap.isEmpty;
+ remainsInWindow = remainsInWindow && (!nextChild || [self.index compareKey:nextChild.name
+ andNode:nextChild.node
+ toOtherKey:childKey
+ andNode:newChildSnap
+ reverse:self.reverse] >= NSOrderedSame);
+ if (remainsInWindow) {
+ // `newChildSnap` is ordered before `nextChild`, so it's a child changed event
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ return [oldIndexed updateChild:childKey withNewChild:newChildSnap];
+ } else {
+ // `newChildSnap` is ordered after `nextChild`, so it's a child removed event
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ FIndexedNode *newIndexed = [oldIndexed updateChild:childKey withNewChild:[FEmptyNode emptyNode]];
+
+ // We need to check if the `nextChild` is actually in range before adding it
+ BOOL nextChildInRange = (nextChild != nil) && [self.rangedFilter matchesKey:nextChild.name
+ andNode:nextChild.node];
+ if (nextChildInRange) {
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:nextChild.node]
+ childKey:nextChild.name];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ return [newIndexed updateChild:nextChild.name withNewChild:nextChild.node];
+ } else {
+ return newIndexed;
+ }
+ }
+ } else if (newChildSnap.isEmpty) {
+ // We're deleting a node, but it was not in the window, so ignore it.
+ return oldIndexed;
+ } else if (inRange) {
+ // `newChildSnap` is in range, but was ordered after `windowBoundary`. If this has changed, we bump out the
+ // `windowBoundary` and add the `newChildSnap`
+ if ([self.index compareKey:windowBoundary.name
+ andNode:windowBoundary.node
+ toOtherKey:childKey
+ andNode:newChildSnap
+ reverse:self.reverse] >= NSOrderedSame) {
+ if (optChangeAccumulator != nil) {
+ FChange *removedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:windowBoundary.node]
+ childKey:windowBoundary.name];
+ FChange *addedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:removedChange];
+ [optChangeAccumulator trackChildChange:addedChange];
+ }
+ return [[oldIndexed updateChild:childKey withNewChild:newChildSnap] updateChild:windowBoundary.name
+ withNewChild:[FEmptyNode emptyNode]];
+ } else {
+ return oldIndexed;
+ }
+ } else {
+ // `newChildSnap` was not in range and remains not in range, so ignore it.
+ return oldIndexed;
+ }
+}
+
+- (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ __block FIndexedNode *filtered;
+ if (newSnap.node.isLeafNode || newSnap.node.isEmpty) {
+ // Make sure we have a children node with the correct index, not a leaf node
+ filtered = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:self.index];
+ } else {
+ filtered = newSnap;
+ // Don't support priorities on queries.
+ filtered = [filtered updatePriority:[FEmptyNode emptyNode]];
+ FNamedNode *startPost = nil;
+ FNamedNode *endPost = nil;
+ if (self.reverse) {
+ startPost = self.rangedFilter.endPost;
+ endPost = self.rangedFilter.startPost;
+ } else {
+ startPost = self.rangedFilter.startPost;
+ endPost = self.rangedFilter.endPost;
+ }
+ __block BOOL foundStartPost = NO;
+ __block NSUInteger count = 0;
+ [newSnap enumerateChildrenReverse:self.reverse usingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if (!foundStartPost && [self.index compareKey:startPost.name
+ andNode:startPost.node
+ toOtherKey:childKey
+ andNode:childNode
+ reverse:self.reverse] <= NSOrderedSame) {
+ // Start adding
+ foundStartPost = YES;
+ }
+ BOOL inRange = foundStartPost && count < self.limit;
+ inRange = inRange && [self.index compareKey:childKey
+ andNode:childNode
+ toOtherKey:endPost.name
+ andNode:endPost.node
+ reverse:self.reverse] <= NSOrderedSame;
+ if (inRange) {
+ count++;
+ } else {
+ filtered = [filtered updateChild:childKey withNewChild:[FEmptyNode emptyNode]];
+ }
+ }];
+ }
+ return [self.indexedFilter updateFullNode:oldSnap withNewNode:filtered accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ // Don't support priorities on queries.
+ return oldSnap;
+}
+
+- (BOOL) filtersNodes {
+ return YES;
+}
+
+- (id<FNodeFilter>) indexedFilter {
+ return self.rangedFilter.indexedFilter;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FNodeFilter.h b/Firebase/Database/Core/View/Filter/FNodeFilter.h
new file mode 100644
index 0000000..f29a85a
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FNodeFilter.h
@@ -0,0 +1,71 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FIndexedNode;
+@protocol FCompleteChildSource;
+@class FChildChangeAccumulator;
+@protocol FIndex;
+@class FPath;
+
+/**
+* FNodeFilter is used to update nodes and complete children of nodes while applying queries on the fly and keeping
+* track of any child changes. This class does not track value changes as value changes depend on more than just the
+* node itself. Different kind of queries require different kind of implementations of this interface.
+*/
+@protocol FNodeFilter<NSObject>
+
+/**
+* Update a single complete child in the snap. If the child equals the old child in the snap, this is a no-op.
+* The method expects an indexed snap.
+*/
+- (FIndexedNode *) updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator;
+
+/**
+* Update a node in full and output any resulting change from this complete update.
+*/
+- (FIndexedNode *) updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator;
+
+/**
+* Update the priority of the root node
+*/
+- (FIndexedNode *) updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap;
+
+/**
+* Returns true if children might be filtered due to query critiera
+*/
+- (BOOL) filtersNodes;
+
+/**
+* Returns the index filter that this filter uses to get a NodeFilter that doesn't filter any children.
+*/
+@property (nonatomic, strong, readonly) id<FNodeFilter> indexedFilter;
+
+/**
+* Returns the index that this filter uses
+*/
+@property (nonatomic, strong, readonly) id<FIndex> index;
+
+@end