2017-10-03 08:55:22 -07:00
|
|
|
|
/*
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2017-11-16 20:52:01 -08:00
|
|
|
|
#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h"
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-04-09 11:54:39 -07:00
|
|
|
|
#include <memory>
|
2017-10-03 08:55:22 -07:00
|
|
|
|
#include <set>
|
|
|
|
|
|
#include <string>
|
2018-08-06 08:11:06 -07:00
|
|
|
|
#include <utility>
|
2018-08-06 11:40:27 -07:00
|
|
|
|
#include <vector>
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2017-11-16 20:52:01 -08:00
|
|
|
|
#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h"
|
|
|
|
|
|
#import "Firestore/Source/Core/FSTQuery.h"
|
|
|
|
|
|
#import "Firestore/Source/Local/FSTLevelDB.h"
|
|
|
|
|
|
#import "Firestore/Source/Local/FSTLocalSerializer.h"
|
|
|
|
|
|
#import "Firestore/Source/Model/FSTMutation.h"
|
|
|
|
|
|
#import "Firestore/Source/Model/FSTMutationBatch.h"
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-02-15 17:23:08 -05:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/auth/user.h"
|
2018-08-06 11:40:27 -07:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h"
|
2018-03-26 11:57:34 -07:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h"
|
2018-08-06 11:40:27 -07:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/local/leveldb_util.h"
|
2018-03-14 08:57:31 -04:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
|
2018-05-24 11:00:23 -07:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
|
2018-02-15 17:23:08 -05:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/util/string_apple.h"
|
2018-01-25 13:03:07 -05:00
|
|
|
|
#include "Firestore/core/src/firebase/firestore/util/string_util.h"
|
2018-04-09 11:54:39 -07:00
|
|
|
|
#include "absl/strings/match.h"
|
|
|
|
|
|
#include "leveldb/db.h"
|
|
|
|
|
|
#include "leveldb/write_batch.h"
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
2018-02-06 13:51:11 -05:00
|
|
|
|
namespace util = firebase::firestore::util;
|
2018-02-15 17:23:08 -05:00
|
|
|
|
using firebase::firestore::auth::User;
|
2018-08-06 11:40:27 -07:00
|
|
|
|
using firebase::firestore::local::DescribeKey;
|
2018-08-08 12:34:03 -07:00
|
|
|
|
using firebase::firestore::local::LevelDbDocumentMutationKey;
|
|
|
|
|
|
using firebase::firestore::local::LevelDbMutationKey;
|
|
|
|
|
|
using firebase::firestore::local::LevelDbMutationQueueKey;
|
|
|
|
|
|
using firebase::firestore::local::LevelDbTransaction;
|
|
|
|
|
|
using firebase::firestore::local::MakeStringView;
|
2018-03-23 17:52:01 -04:00
|
|
|
|
using firebase::firestore::model::DocumentKey;
|
2018-07-09 10:03:27 -07:00
|
|
|
|
using firebase::firestore::model::DocumentKeySet;
|
2018-03-14 08:57:31 -04:00
|
|
|
|
using firebase::firestore::model::ResourcePath;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
using leveldb::DB;
|
|
|
|
|
|
using leveldb::Iterator;
|
|
|
|
|
|
using leveldb::ReadOptions;
|
|
|
|
|
|
using leveldb::Slice;
|
|
|
|
|
|
using leveldb::Status;
|
|
|
|
|
|
using leveldb::WriteBatch;
|
|
|
|
|
|
using leveldb::WriteOptions;
|
|
|
|
|
|
|
|
|
|
|
|
@interface FSTLevelDBMutationQueue ()
|
|
|
|
|
|
|
2018-08-06 08:11:06 -07:00
|
|
|
|
- (instancetype)initWithUserID:(std::string)userID
|
2018-03-26 11:57:34 -07:00
|
|
|
|
db:(FSTLevelDB *)db
|
2017-10-03 08:55:22 -07:00
|
|
|
|
serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Next value to use when assigning sequential IDs to each mutation batch.
|
|
|
|
|
|
*
|
|
|
|
|
|
* NOTE: There can only be one FSTLevelDBMutationQueue for a given db at a time, hence it is safe
|
|
|
|
|
|
* to track nextBatchID as an instance-level property. Should we ever relax this constraint we'll
|
|
|
|
|
|
* need to revisit this.
|
|
|
|
|
|
*/
|
|
|
|
|
|
@property(nonatomic, assign) FSTBatchID nextBatchID;
|
|
|
|
|
|
|
|
|
|
|
|
/** A write-through cache copy of the metadata describing the current queue. */
|
|
|
|
|
|
@property(nonatomic, strong, nullable) FSTPBMutationQueue *metadata;
|
|
|
|
|
|
|
|
|
|
|
|
@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer;
|
|
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation FSTLevelDBMutationQueue {
|
2018-03-26 11:57:34 -07:00
|
|
|
|
FSTLevelDB *_db;
|
2018-08-06 08:11:06 -07:00
|
|
|
|
|
|
|
|
|
|
/** The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB keys. */
|
|
|
|
|
|
std::string _userID;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-02-15 17:23:08 -05:00
|
|
|
|
+ (instancetype)mutationQueueWithUser:(const User &)user
|
2018-03-26 11:57:34 -07:00
|
|
|
|
db:(FSTLevelDB *)db
|
2017-10-03 08:55:22 -07:00
|
|
|
|
serializer:(FSTLocalSerializer *)serializer {
|
2018-08-06 08:11:06 -07:00
|
|
|
|
std::string userID = user.is_authenticated() ? user.uid() : "";
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-08-06 08:11:06 -07:00
|
|
|
|
return [[FSTLevelDBMutationQueue alloc] initWithUserID:std::move(userID)
|
|
|
|
|
|
db:db
|
|
|
|
|
|
serializer:serializer];
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-06 08:11:06 -07:00
|
|
|
|
- (instancetype)initWithUserID:(std::string)userID
|
2018-03-26 11:57:34 -07:00
|
|
|
|
db:(FSTLevelDB *)db
|
2017-10-03 08:55:22 -07:00
|
|
|
|
serializer:(FSTLocalSerializer *)serializer {
|
|
|
|
|
|
if (self = [super init]) {
|
2018-08-06 08:11:06 -07:00
|
|
|
|
_userID = std::move(userID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
_db = db;
|
|
|
|
|
|
_serializer = serializer;
|
|
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-03-29 08:55:10 -07:00
|
|
|
|
- (void)start {
|
2018-03-26 11:57:34 -07:00
|
|
|
|
FSTBatchID nextBatchID = [FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr];
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
// On restart, nextBatchId may end up lower than lastAcknowledgedBatchId since it's computed from
|
|
|
|
|
|
// the queue contents, and there may be no mutations in the queue. In this case, we need to reset
|
|
|
|
|
|
// lastAcknowledgedBatchId (which is safe since the queue must be empty).
|
|
|
|
|
|
std::string key = [self keyForCurrentMutationQueue];
|
|
|
|
|
|
FSTPBMutationQueue *metadata = [self metadataForKey:key];
|
|
|
|
|
|
if (!metadata) {
|
|
|
|
|
|
metadata = [FSTPBMutationQueue message];
|
|
|
|
|
|
|
|
|
|
|
|
// proto3's default value for lastAcknowledgedBatchId is zero, but that would consider the first
|
|
|
|
|
|
// entry in the queue to be acknowledged without that acknowledgement actually happening.
|
|
|
|
|
|
metadata.lastAcknowledgedBatchId = kFSTBatchIDUnknown;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
FSTBatchID lastAcked = metadata.lastAcknowledgedBatchId;
|
|
|
|
|
|
if (lastAcked >= nextBatchID) {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_ASSERT([self isEmpty], "Reset nextBatchID is only possible when the queue is empty");
|
2017-10-03 08:55:22 -07:00
|
|
|
|
lastAcked = kFSTBatchIDUnknown;
|
|
|
|
|
|
|
|
|
|
|
|
metadata.lastAcknowledgedBatchId = lastAcked;
|
2018-03-29 08:55:10 -07:00
|
|
|
|
_db.currentTransaction->Put([self keyForCurrentMutationQueue], metadata);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.nextBatchID = nextBatchID;
|
|
|
|
|
|
self.metadata = metadata;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-06-13 08:12:09 -07:00
|
|
|
|
+ (FSTBatchID)loadNextBatchIDFromDB:(DB *)db {
|
2018-03-26 11:57:34 -07:00
|
|
|
|
// TODO(gsoltis): implement Prev() and SeekToLast() on LevelDbTransaction::Iterator, then port
|
|
|
|
|
|
// this to a transaction.
|
|
|
|
|
|
std::unique_ptr<Iterator> it(db->NewIterator(LevelDbTransaction::DefaultReadOptions()));
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
auto tableKey = LevelDbMutationKey::KeyPrefix();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
LevelDbMutationKey rowKey;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
FSTBatchID maxBatchID = kFSTBatchIDUnknown;
|
|
|
|
|
|
|
|
|
|
|
|
BOOL moreUserIDs = NO;
|
|
|
|
|
|
std::string nextUserID;
|
|
|
|
|
|
|
|
|
|
|
|
it->Seek(tableKey);
|
2018-08-08 12:34:03 -07:00
|
|
|
|
if (it->Valid() && rowKey.Decode(MakeStringView(it->key()))) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
moreUserIDs = YES;
|
2018-08-08 12:34:03 -07:00
|
|
|
|
nextUserID = rowKey.user_id();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// This loop assumes that nextUserId contains the next username at the start of the iteration.
|
|
|
|
|
|
while (moreUserIDs) {
|
|
|
|
|
|
// Compute the first key after the last mutation for nextUserID.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
auto userEnd = LevelDbMutationKey::KeyPrefix(nextUserID);
|
2018-02-06 13:51:11 -05:00
|
|
|
|
userEnd = util::PrefixSuccessor(userEnd);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
// Seek to that key with the intent of finding the boundary between nextUserID's mutations
|
|
|
|
|
|
// and the one after that (if any).
|
|
|
|
|
|
it->Seek(userEnd);
|
|
|
|
|
|
|
|
|
|
|
|
// At this point there are three possible cases to handle differently. Each case must prepare
|
|
|
|
|
|
// the next iteration (by assigning to nextUserID or setting moreUserIDs = NO) and seek the
|
|
|
|
|
|
// iterator to the last row in the current user's mutation sequence.
|
|
|
|
|
|
if (!it->Valid()) {
|
|
|
|
|
|
// The iterator is past the last row altogether (there are no additional userIDs and now
|
|
|
|
|
|
// rows in any table after mutations). The last row will have the highest batchID.
|
|
|
|
|
|
moreUserIDs = NO;
|
|
|
|
|
|
it->SeekToLast();
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
} else if (rowKey.Decode(MakeStringView(it->key()))) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
// The iterator is valid and the key decoded successfully so the next user was just decoded.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
nextUserID = rowKey.user_id();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
it->Prev();
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// The iterator is past the end of the mutations table but there are other rows.
|
|
|
|
|
|
moreUserIDs = NO;
|
|
|
|
|
|
it->Prev();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// In all the cases above there was at least one row for the current user and each case has
|
|
|
|
|
|
// set things up such that iterator points to it.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
if (!rowKey.Decode(MakeStringView(it->key()))) {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_FAIL("There should have been a key previous to %s", userEnd);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
if (rowKey.batch_id() > maxBatchID) {
|
|
|
|
|
|
maxBatchID = rowKey.batch_id();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return maxBatchID + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (BOOL)isEmpty {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string userKey = LevelDbMutationKey::KeyPrefix(_userID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto it = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
it->Seek(userKey);
|
|
|
|
|
|
|
|
|
|
|
|
BOOL empty = YES;
|
2018-03-26 11:57:34 -07:00
|
|
|
|
if (it->Valid() && absl::StartsWith(it->key(), userKey)) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
empty = NO;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (FSTBatchID)highestAcknowledgedBatchID {
|
|
|
|
|
|
return self.metadata.lastAcknowledgedBatchId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-03-29 08:55:10 -07:00
|
|
|
|
- (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
FSTBatchID batchID = batch.batchID;
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_ASSERT(batchID > self.highestAcknowledgedBatchID,
|
|
|
|
|
|
"Mutation batchIDs must be acknowledged in order");
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
FSTPBMutationQueue *metadata = self.metadata;
|
|
|
|
|
|
metadata.lastAcknowledgedBatchId = batchID;
|
|
|
|
|
|
metadata.lastStreamToken = streamToken;
|
|
|
|
|
|
|
2018-03-29 08:55:10 -07:00
|
|
|
|
_db.currentTransaction->Put([self keyForCurrentMutationQueue], metadata);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (nullable NSData *)lastStreamToken {
|
|
|
|
|
|
return self.metadata.lastStreamToken;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-03-29 08:55:10 -07:00
|
|
|
|
- (void)setLastStreamToken:(nullable NSData *)streamToken {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
FSTPBMutationQueue *metadata = self.metadata;
|
|
|
|
|
|
metadata.lastStreamToken = streamToken;
|
|
|
|
|
|
|
2018-03-29 08:55:10 -07:00
|
|
|
|
_db.currentTransaction->Put([self keyForCurrentMutationQueue], metadata);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (std::string)keyForCurrentMutationQueue {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
return LevelDbMutationQueueKey::Key(_userID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (nullable FSTPBMutationQueue *)metadataForKey:(const std::string &)key {
|
|
|
|
|
|
std::string value;
|
2018-03-26 11:57:34 -07:00
|
|
|
|
Status status = _db.currentTransaction->Get(key, &value);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
if (status.ok()) {
|
|
|
|
|
|
return [self parsedMetadata:value];
|
|
|
|
|
|
} else if (status.IsNotFound()) {
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
} else {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_FAIL("metadataForKey: failed loading key %s with status: %s", key, status.ToString());
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-02-20 17:33:57 -05:00
|
|
|
|
- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime
|
2018-03-29 08:55:10 -07:00
|
|
|
|
mutations:(NSArray<FSTMutation *> *)mutations {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
FSTBatchID batchID = self.nextBatchID;
|
|
|
|
|
|
self.nextBatchID += 1;
|
|
|
|
|
|
|
|
|
|
|
|
FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID
|
|
|
|
|
|
localWriteTime:localWriteTime
|
|
|
|
|
|
mutations:mutations];
|
|
|
|
|
|
std::string key = [self mutationKeyForBatch:batch];
|
2018-03-29 08:55:10 -07:00
|
|
|
|
_db.currentTransaction->Put(key, [self.serializer encodedMutationBatch:batch]);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
// Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the
|
|
|
|
|
|
// future if we wanted to store some other kind of value here, we can parse these empty values as
|
|
|
|
|
|
// with some other protocol buffer (and the parser will see all default values).
|
|
|
|
|
|
std::string emptyBuffer;
|
|
|
|
|
|
|
|
|
|
|
|
for (FSTMutation *mutation in mutations) {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
key = LevelDbDocumentMutationKey::Key(_userID, mutation.key, batchID);
|
2018-03-29 08:55:10 -07:00
|
|
|
|
_db.currentTransaction->Put(key, emptyBuffer);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return batch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID {
|
|
|
|
|
|
std::string key = [self mutationKeyForBatchID:batchID];
|
|
|
|
|
|
|
|
|
|
|
|
std::string value;
|
2018-03-26 11:57:34 -07:00
|
|
|
|
Status status = _db.currentTransaction->Get(key, &value);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
if (!status.ok()) {
|
|
|
|
|
|
if (status.IsNotFound()) {
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
2018-08-06 08:11:06 -07:00
|
|
|
|
HARD_FAIL("Lookup mutation batch (%s, %s) failed with status: %s", _userID, batchID,
|
2018-05-24 11:00:23 -07:00
|
|
|
|
status.ToString());
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [self decodedMutationBatch:value];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID {
|
2018-03-12 11:53:26 -07:00
|
|
|
|
// All batches with batchID <= self.metadata.lastAcknowledgedBatchId have been acknowledged so
|
|
|
|
|
|
// the first unacknowledged batch after batchID will have a batchID larger than both of these
|
|
|
|
|
|
// values.
|
|
|
|
|
|
FSTBatchID nextBatchID = MAX(batchID, self.metadata.lastAcknowledgedBatchId) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
std::string key = [self mutationKeyForBatchID:nextBatchID];
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto it = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
it->Seek(key);
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
LevelDbMutationKey rowKey;
|
|
|
|
|
|
if (!it->Valid() || !rowKey.Decode(it->key())) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
// Past the last row in the DB or out of the mutations table
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
if (rowKey.user_id() != _userID) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
// Jumped past the last mutation for this user
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
HARD_ASSERT(rowKey.batch_id() >= nextBatchID, "Should have found mutation after %s", nextBatchID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
return [self decodedMutationBatch:it->value()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSArray<FSTMutationBatch *> *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string userKey = LevelDbMutationKey::KeyPrefix(_userID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto it = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
it->Seek(userKey);
|
|
|
|
|
|
|
|
|
|
|
|
NSMutableArray *result = [NSMutableArray array];
|
2018-08-08 12:34:03 -07:00
|
|
|
|
LevelDbMutationKey rowKey;
|
|
|
|
|
|
for (; it->Valid() && rowKey.Decode(it->key()); it->Next()) {
|
|
|
|
|
|
if (rowKey.user_id() != _userID) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
// End of this user's mutations
|
|
|
|
|
|
break;
|
2018-08-08 12:34:03 -07:00
|
|
|
|
} else if (rowKey.batch_id() > batchID) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
// This mutation is past what we're looking for
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[result addObject:[self decodedMutationBatch:it->value()]];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey:
|
2018-03-23 17:52:01 -04:00
|
|
|
|
(const DocumentKey &)documentKey {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
// Scan the document-mutation index starting with a prefix starting with the given documentKey.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, documentKey.path());
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto indexIterator = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
indexIterator->Seek(indexPrefix);
|
|
|
|
|
|
|
|
|
|
|
|
// Simultaneously scan the mutation queue. This works because each (key, batchID) pair is unique
|
|
|
|
|
|
// and ordered, so when scanning a table prefixed by exactly key, all the batchIDs encountered
|
|
|
|
|
|
// will be unique and in order.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string mutationsPrefix = LevelDbMutationKey::KeyPrefix(_userID);
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto mutationIterator = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
NSMutableArray *result = [NSMutableArray array];
|
2018-08-08 12:34:03 -07:00
|
|
|
|
LevelDbDocumentMutationKey rowKey;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
for (; indexIterator->Valid(); indexIterator->Next()) {
|
2018-07-09 10:03:27 -07:00
|
|
|
|
// Only consider rows matching exactly the specific key of interest. Index rows have this
|
|
|
|
|
|
// form (with markers in brackets):
|
|
|
|
|
|
//
|
|
|
|
|
|
// <User>user <Path>collection <Path>doc <BatchId>2 <Terminator>
|
|
|
|
|
|
// <User>user <Path>collection <Path>doc <BatchId>3 <Terminator>
|
|
|
|
|
|
// <User>user <Path>collection <Path>doc <Path>sub <Path>doc <BatchId>3 <Terminator>
|
|
|
|
|
|
//
|
|
|
|
|
|
// Note that Path markers sort after BatchId markers so this means that when searching for
|
|
|
|
|
|
// collection/doc, all the entries for it will be contiguous in the table, allowing a break
|
|
|
|
|
|
// after any mismatch.
|
2018-03-26 11:57:34 -07:00
|
|
|
|
if (!absl::StartsWith(indexIterator->key(), indexPrefix) ||
|
2018-08-08 12:34:03 -07:00
|
|
|
|
!rowKey.Decode(indexIterator->key()) || rowKey.document_key() != documentKey) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Each row is a unique combination of key and batchID, so this foreign key reference can
|
|
|
|
|
|
// only occur once.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string mutationKey = LevelDbMutationKey::Key(_userID, rowKey.batch_id());
|
2017-10-03 08:55:22 -07:00
|
|
|
|
mutationIterator->Seek(mutationKey);
|
|
|
|
|
|
if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_FAIL(
|
|
|
|
|
|
"Dangling document-mutation reference found: "
|
|
|
|
|
|
"%s points to %s; seeking there found %s",
|
2018-08-06 11:40:27 -07:00
|
|
|
|
DescribeKey(indexIterator), DescribeKey(mutationKey), DescribeKey(mutationIterator));
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[result addObject:[self decodedMutationBatch:mutationIterator->value()]];
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-07-09 10:03:27 -07:00
|
|
|
|
- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKeys:
|
|
|
|
|
|
(const DocumentKeySet &)documentKeys {
|
|
|
|
|
|
// Take a pass through the document keys and collect the set of unique mutation batchIDs that
|
|
|
|
|
|
// affect them all. Some batches can affect more than one key.
|
|
|
|
|
|
std::set<FSTBatchID> batchIDs;
|
|
|
|
|
|
|
|
|
|
|
|
auto indexIterator = _db.currentTransaction->NewIterator();
|
2018-08-08 12:34:03 -07:00
|
|
|
|
LevelDbDocumentMutationKey rowKey;
|
2018-07-09 10:03:27 -07:00
|
|
|
|
for (const DocumentKey &documentKey : documentKeys) {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, documentKey.path());
|
2018-07-09 10:03:27 -07:00
|
|
|
|
for (indexIterator->Seek(indexPrefix); indexIterator->Valid(); indexIterator->Next()) {
|
|
|
|
|
|
// Only consider rows matching exactly the specific key of interest. Index rows have this
|
|
|
|
|
|
// form (with markers in brackets):
|
|
|
|
|
|
//
|
|
|
|
|
|
// <User>user <Path>collection <Path>doc <BatchId>2 <Terminator>
|
|
|
|
|
|
// <User>user <Path>collection <Path>doc <BatchId>3 <Terminator>
|
|
|
|
|
|
// <User>user <Path>collection <Path>doc <Path>sub <Path>doc <BatchId>3 <Terminator>
|
|
|
|
|
|
//
|
|
|
|
|
|
// Note that Path markers sort after BatchId markers so this means that when searching for
|
|
|
|
|
|
// collection/doc, all the entries for it will be contiguous in the table, allowing a break
|
|
|
|
|
|
// after any mismatch.
|
|
|
|
|
|
if (!absl::StartsWith(indexIterator->key(), indexPrefix) ||
|
2018-08-08 12:34:03 -07:00
|
|
|
|
!rowKey.Decode(indexIterator->key()) || rowKey.document_key() != documentKey) {
|
2018-07-09 10:03:27 -07:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
batchIDs.insert(rowKey.batch_id());
|
2018-07-09 10:03:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [self allMutationBatchesWithBatchIDs:batchIDs];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-10-03 08:55:22 -07:00
|
|
|
|
- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_ASSERT(![query isDocumentQuery], "Document queries shouldn't go down this path");
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-03-14 08:57:31 -04:00
|
|
|
|
const ResourcePath &queryPath = query.path;
|
|
|
|
|
|
size_t immediateChildrenPathLength = queryPath.size() + 1;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
// TODO(mcg): Actually implement a single-collection query
|
|
|
|
|
|
//
|
|
|
|
|
|
// This is actually executing an ancestor query, traversing the whole subtree below the
|
|
|
|
|
|
// collection which can be horrifically inefficient for some structures. The right way to
|
|
|
|
|
|
// solve this is to implement the full value index, but that's not in the cards in the near
|
|
|
|
|
|
// future so this is the best we can do for the moment.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Since we don't yet index the actual properties in the mutations, our current approach is to
|
|
|
|
|
|
// just return all mutation batches that affect documents in the collection being queried.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the document-mutation
|
|
|
|
|
|
// index for more than a single document so the associated batchIDs will be neither necessarily
|
|
|
|
|
|
// unique nor in order. This means an efficient simultaneous scan isn't possible.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, queryPath);
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto indexIterator = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
indexIterator->Seek(indexPrefix);
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
LevelDbDocumentMutationKey rowKey;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
// Collect up unique batchIDs encountered during a scan of the index. Use a set<FSTBatchID> to
|
|
|
|
|
|
// accumulate batch IDs so they can be traversed in order in a scan of the main table.
|
|
|
|
|
|
//
|
|
|
|
|
|
// This method is faster than performing lookups of the keys with _db->Get and keeping a hash of
|
|
|
|
|
|
// batchIDs that have already been looked up. The performance difference is minor for small
|
|
|
|
|
|
// numbers of keys but > 30% faster for larger numbers of keys.
|
2018-07-09 10:03:27 -07:00
|
|
|
|
std::set<FSTBatchID> uniqueBatchIDs;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
for (; indexIterator->Valid(); indexIterator->Next()) {
|
2018-03-26 11:57:34 -07:00
|
|
|
|
if (!absl::StartsWith(indexIterator->key(), indexPrefix) ||
|
2018-08-08 12:34:03 -07:00
|
|
|
|
!rowKey.Decode(indexIterator->key())) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Rows with document keys more than one segment longer than the query path can't be matches.
|
|
|
|
|
|
// For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx.
|
|
|
|
|
|
// TODO(mcg): we'll need a different scanner when we implement ancestor queries.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
if (rowKey.document_key().path().size() != immediateChildrenPathLength) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-08 12:34:03 -07:00
|
|
|
|
uniqueBatchIDs.insert(rowKey.batch_id());
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-07-09 10:03:27 -07:00
|
|
|
|
return [self allMutationBatchesWithBatchIDs:uniqueBatchIDs];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Constructs an array of matching batches, sorted by batchID to ensure that multiple mutations
|
|
|
|
|
|
* affecting the same document key are applied in order.
|
|
|
|
|
|
*/
|
|
|
|
|
|
- (NSArray<FSTMutationBatch *> *)allMutationBatchesWithBatchIDs:
|
|
|
|
|
|
(const std::set<FSTBatchID> &)batchIDs {
|
|
|
|
|
|
NSMutableArray *result = [NSMutableArray array];
|
|
|
|
|
|
|
2017-10-03 08:55:22 -07:00
|
|
|
|
// Given an ordered set of unique batchIDs perform a skipping scan over the main table to find
|
|
|
|
|
|
// the mutation batches.
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto mutationIterator = _db.currentTransaction->NewIterator();
|
2018-07-09 10:03:27 -07:00
|
|
|
|
for (FSTBatchID batchID : batchIDs) {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string mutationKey = LevelDbMutationKey::Key(_userID, batchID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
mutationIterator->Seek(mutationKey);
|
|
|
|
|
|
if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_FAIL(
|
|
|
|
|
|
"Dangling document-mutation reference found: "
|
|
|
|
|
|
"Missing batch %s; seeking there found %s",
|
2018-08-06 11:40:27 -07:00
|
|
|
|
DescribeKey(mutationKey), DescribeKey(mutationIterator));
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[result addObject:[self decodedMutationBatch:mutationIterator->value()]];
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSArray<FSTMutationBatch *> *)allMutationBatches {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string userKey = LevelDbMutationKey::KeyPrefix(_userID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto it = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
it->Seek(userKey);
|
|
|
|
|
|
|
|
|
|
|
|
NSMutableArray *result = [NSMutableArray array];
|
2018-03-26 11:57:34 -07:00
|
|
|
|
for (; it->Valid() && absl::StartsWith(it->key(), userKey); it->Next()) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
[result addObject:[self decodedMutationBatch:it->value()]];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-03-29 08:55:10 -07:00
|
|
|
|
- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches {
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto checkIterator = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
for (FSTMutationBatch *batch in batches) {
|
|
|
|
|
|
FSTBatchID batchID = batch.batchID;
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string key = LevelDbMutationKey::Key(_userID, batchID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
// As a sanity check, verify that the mutation batch exists before deleting it.
|
|
|
|
|
|
checkIterator->Seek(key);
|
2018-08-06 11:40:27 -07:00
|
|
|
|
HARD_ASSERT(checkIterator->Valid(), "Mutation batch %s did not exist", DescribeKey(key));
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_ASSERT(key == checkIterator->key(), "Mutation batch %s not found; found %s",
|
2018-08-06 11:40:27 -07:00
|
|
|
|
DescribeKey(key), DescribeKey(checkIterator));
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
2018-03-29 08:55:10 -07:00
|
|
|
|
_db.currentTransaction->Delete(key);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
for (FSTMutation *mutation in batch.mutations) {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
key = LevelDbDocumentMutationKey::Key(_userID, mutation.key, batchID);
|
2018-03-29 08:55:10 -07:00
|
|
|
|
_db.currentTransaction->Delete(key);
|
2018-05-08 10:49:54 -07:00
|
|
|
|
[_db.referenceDelegate removeMutationReference:mutation.key];
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)performConsistencyCheck {
|
|
|
|
|
|
if (![self isEmpty]) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Verify that there are no entries in the document-mutation index if the queue is empty.
|
2018-08-08 12:34:03 -07:00
|
|
|
|
std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID);
|
2018-03-26 11:57:34 -07:00
|
|
|
|
auto indexIterator = _db.currentTransaction->NewIterator();
|
2017-10-03 08:55:22 -07:00
|
|
|
|
indexIterator->Seek(indexPrefix);
|
|
|
|
|
|
|
2018-08-06 11:40:27 -07:00
|
|
|
|
std::vector<std::string> danglingMutationReferences;
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
for (; indexIterator->Valid(); indexIterator->Next()) {
|
|
|
|
|
|
// Only consider rows matching this index prefix for the current user.
|
2018-03-26 11:57:34 -07:00
|
|
|
|
if (!absl::StartsWith(indexIterator->key(), indexPrefix)) {
|
2017-10-03 08:55:22 -07:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-06 11:40:27 -07:00
|
|
|
|
danglingMutationReferences.push_back(DescribeKey(indexIterator));
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-06 11:40:27 -07:00
|
|
|
|
HARD_ASSERT(danglingMutationReferences.empty(),
|
2018-05-24 11:00:23 -07:00
|
|
|
|
"Document leak -- detected dangling mutation references when queue "
|
|
|
|
|
|
"is empty. Dangling keys: %s",
|
2018-08-06 11:40:27 -07:00
|
|
|
|
util::ToString(danglingMutationReferences));
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (std::string)mutationKeyForBatch:(FSTMutationBatch *)batch {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
return LevelDbMutationKey::Key(_userID, batch.batchID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (std::string)mutationKeyForBatchID:(FSTBatchID)batchID {
|
2018-08-08 12:34:03 -07:00
|
|
|
|
return LevelDbMutationKey::Key(_userID, batchID);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Parses the MutationQueue metadata from the given LevelDB row contents. */
|
|
|
|
|
|
- (FSTPBMutationQueue *)parsedMetadata:(Slice)slice {
|
|
|
|
|
|
NSData *data =
|
|
|
|
|
|
[[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO];
|
|
|
|
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
|
|
FSTPBMutationQueue *proto = [FSTPBMutationQueue parseFromData:data error:&error];
|
|
|
|
|
|
if (!proto) {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_FAIL("FSTPBMutationQueue failed to parse: %s", error);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return proto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-03-26 11:57:34 -07:00
|
|
|
|
- (FSTMutationBatch *)decodedMutationBatch:(absl::string_view)encoded {
|
|
|
|
|
|
NSData *data = [[NSData alloc] initWithBytesNoCopy:(void *)encoded.data()
|
|
|
|
|
|
length:encoded.size()
|
|
|
|
|
|
freeWhenDone:NO];
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
|
|
FSTPBWriteBatch *proto = [FSTPBWriteBatch parseFromData:data error:&error];
|
|
|
|
|
|
if (!proto) {
|
2018-05-24 11:00:23 -07:00
|
|
|
|
HARD_FAIL("FSTPBMutationBatch failed to parse: %s", error);
|
2017-10-03 08:55:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [self.serializer decodedMutationBatch:proto];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|