aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Firestore/Example/Firestore.xcodeproj/project.pbxproj6
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm274
-rw-r--r--Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc211
-rw-r--r--Firestore/core/src/firebase/firestore/local/leveldb_transaction.h204
4 files changed, 695 insertions, 0 deletions
diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
index 3b1c328..f80bd8c 100644
--- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj
+++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
@@ -24,6 +24,7 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
+ 132E3E53179DE287D875F3F2 /* FSTLevelDBTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */; };
3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; };
5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5436F32320008FAD006E51E3 /* string_printf_test.cc */; };
5467FB01203E5717009C9584 /* FIRFirestoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */; };
@@ -229,6 +230,7 @@
/* Begin PBXFileReference section */
04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.release.xcconfig"; sourceTree = "<group>"; };
12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
+ 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBTransactionTests.mm; sourceTree = "<group>"; };
245812330F6A31632BB4B623 /* Pods_Firestore_Example_Firestore_SwiftTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_Firestore_SwiftTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = "<group>"; };
@@ -541,6 +543,8 @@
6003F58C195388D20070C39A /* Frameworks */,
6003F58B195388D20070C39A /* Products */,
A47A1BF74A48BCAEAFBCBF1E /* Pods */,
+ 132E3B6B902D5CBD779D901A,
+ 132E37F450D23EF90B2352D9,
);
sourceTree = "<group>";
};
@@ -744,6 +748,7 @@
5492E0902021552B00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm */,
5492E09B2021552C00B64F25 /* FSTWriteGroupTests.mm */,
5492E0932021552B00B64F25 /* StringViewTests.mm */,
+ 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */,
);
path = Local;
sourceTree = "<group>";
@@ -1540,6 +1545,7 @@
5492E057202154AB00B64F25 /* FIRSnapshotMetadataTests.mm in Sources */,
54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */,
5492E0BE2021555100B64F25 /* FSTMutationTests.mm in Sources */,
+ 132E3E53179DE287D875F3F2 /* FSTLevelDBTransactionTests.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm
new file mode 100644
index 0000000..704f8c6
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h"
+
+#import <XCTest/XCTest.h>
+#include <absl/strings/string_view.h>
+#include <leveldb/db.h>
+#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h"
+#import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using leveldb::DB;
+using leveldb::Options;
+using leveldb::ReadOptions;
+using leveldb::WriteOptions;
+using leveldb::Status;
+using firebase::firestore::local::LevelDbTransaction;
+
+@interface FSTLevelDBTransactionTests : XCTestCase
+@end
+
+@implementation FSTLevelDBTransactionTests {
+ std::shared_ptr<DB> _db;
+}
+
+- (void)setUp {
+ Options options;
+ options.error_if_exists = true;
+ options.create_if_missing = true;
+
+ NSString *dir = [FSTPersistenceTestHelpers levelDBDir];
+ DB *db;
+ Status status = DB::Open(options, [dir UTF8String], &db);
+ XCTAssert(status.ok(), @"Failed to create db: %s", status.ToString().c_str());
+ _db.reset(db);
+}
+
+- (void)tearDown {
+ _db.reset();
+}
+
+- (void)testCreateTransaction {
+ LevelDbTransaction transaction(_db.get());
+ std::string key = "key1";
+
+ transaction.Put(key, "value");
+ std::unique_ptr<LevelDbTransaction::Iterator> iter(transaction.NewIterator());
+ iter->Seek(key);
+ XCTAssertEqual(key, iter->key());
+ iter->Next();
+ XCTAssertFalse(iter->Valid());
+}
+
+- (void)testCanReadCommittedAndMutations {
+ const std::string committed_key1 = "c_key1";
+ const std::string committed_value1 = "c_value1";
+ const WriteOptions &writeOptions = LevelDbTransaction::DefaultWriteOptions();
+ // add two things committed, mutate one, add another mutation
+ // verify you can get the original committed, the mutation, and the addition
+ Status status = _db->Put(writeOptions, committed_key1, committed_value1);
+ XCTAssertTrue(status.ok());
+
+ const std::string committed_key2 = "c_key2";
+ const std::string committed_value2 = "c_value2";
+ status = _db->Put(writeOptions, committed_key2, committed_value2);
+ XCTAssertTrue(status.ok());
+
+ LevelDbTransaction transaction(_db.get());
+ const std::string mutation_key1 = "m_key1";
+ const std::string mutation_value1 = "m_value1";
+ transaction.Put(mutation_key1, mutation_value1);
+
+ const std::string mutation_key2 = committed_key2;
+ const std::string mutation_value2 = "m_value2";
+ transaction.Put(mutation_key2, mutation_value2);
+
+ std::string value;
+ status = transaction.Get(committed_key1, &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual(value, committed_value1);
+
+ status = transaction.Get(mutation_key1, &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual(value, mutation_value1);
+
+ status = transaction.Get(committed_key2, &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual(value, mutation_value2);
+}
+
+- (void)testDeleteCommitted {
+ // add something committed, delete it, verify you can't read it
+ for (int i = 0; i < 3; ++i) {
+ Status status = _db->Put(LevelDbTransaction::DefaultWriteOptions(), "key_" + std::to_string(i),
+ "value_" + std::to_string(i));
+ XCTAssertTrue(status.ok());
+ }
+ LevelDbTransaction transaction(_db.get());
+ transaction.Put("key_1", "new_value");
+ std::string value;
+ Status status = transaction.Get("key_1", &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual(value, "new_value");
+
+ transaction.Delete("key_1");
+ status = transaction.Get("key_1", &value);
+ XCTAssertTrue(status.IsNotFound());
+
+ LevelDbTransaction::Iterator iter(&transaction);
+ iter.Seek("");
+ XCTAssertEqual(iter.key(), "key_0");
+ iter.Next();
+ XCTAssertEqual(iter.key(), "key_2");
+ iter.Next();
+ XCTAssertFalse(iter.Valid());
+}
+
+- (void)testMutateDeleted {
+ // delete something, then mutate it, then read it.
+ // Also include an actual deletion
+ for (int i = 0; i < 4; ++i) {
+ Status status = _db->Put(LevelDbTransaction::DefaultWriteOptions(), "key_" + std::to_string(i),
+ "value_" + std::to_string(i));
+ XCTAssertTrue(status.ok());
+ }
+ std::string value;
+ LevelDbTransaction transaction(_db.get());
+ transaction.Delete("key_1");
+ Status status = transaction.Get("key_1", &value);
+ XCTAssertTrue(status.IsNotFound());
+
+ transaction.Put("key_1", "new_value");
+ status = transaction.Get("key_1", &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual(value, "new_value");
+
+ transaction.Delete("key_3");
+
+ LevelDbTransaction::Iterator iter(&transaction);
+ iter.Seek("");
+ XCTAssertEqual(iter.key(), "key_0");
+ iter.Next();
+ XCTAssertEqual(iter.key(), "key_1");
+ XCTAssertEqual(iter.value(), "new_value");
+ iter.Next();
+ XCTAssertEqual(iter.key(), "key_2");
+ iter.Next();
+ XCTAssertFalse(iter.Valid());
+
+ // Commit, then check underlying db.
+ transaction.Commit();
+
+ const ReadOptions &readOptions = LevelDbTransaction::DefaultReadOptions();
+ status = _db->Get(readOptions, "key_0", &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual("value_0", value);
+
+ status = _db->Get(readOptions, "key_1", &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual("new_value", value);
+
+ status = _db->Get(readOptions, "key_2", &value);
+ XCTAssertTrue(status.ok());
+ XCTAssertEqual("value_2", value);
+
+ status = _db->Get(readOptions, "key_3", &value);
+ XCTAssertTrue(status.IsNotFound());
+}
+
+- (void)testProtobufSupport {
+ LevelDbTransaction transaction(_db.get());
+
+ FSTPBTarget *target = [FSTPBTarget message];
+ target.targetId = 1;
+ target.lastListenSequenceNumber = 2;
+
+ std::string key("theKey");
+ transaction.Put(key, target);
+
+ std::string value;
+ Status status = transaction.Get("theKey", &value);
+ NSData *result =
+ [[NSData alloc] initWithBytesNoCopy:(void *)value.data() length:value.size() freeWhenDone:NO];
+ NSError *error;
+ FSTPBTarget *parsed = [FSTPBTarget parseFromData:result error:&error];
+ XCTAssertNil(error);
+ XCTAssertTrue([target isEqual:parsed]);
+}
+
+- (void)testCanIterateAndDelete {
+ LevelDbTransaction transaction(_db.get());
+
+ for (int i = 0; i < 4; ++i) {
+ transaction.Put("key_" + std::to_string(i), "value_" + std::to_string(i));
+ }
+
+ std::unique_ptr<LevelDbTransaction::Iterator> it(transaction.NewIterator());
+ it->Seek("key_0");
+ for (int i = 0; i < 4; ++i) {
+ XCTAssertTrue(it->Valid());
+ const absl::string_view &key = it->key();
+ std::string expected = "key_" + std::to_string(i);
+ XCTAssertEqual(expected, key);
+ transaction.Delete(key);
+ it->Next();
+ }
+}
+
+- (void)testCanIterateFromDeletionToCommitted {
+ // Write keys key_0 and key_1
+ for (int i = 0; i < 2; ++i) {
+ Status status = _db->Put(LevelDbTransaction::DefaultWriteOptions(), "key_" + std::to_string(i),
+ "value_" + std::to_string(i));
+ XCTAssertTrue(status.ok());
+ }
+
+ // Create a transaction, iterate, deleting key_0. Verify we still iterate key_1.
+ LevelDbTransaction transaction(_db.get());
+ std::unique_ptr<LevelDbTransaction::Iterator> it(transaction.NewIterator());
+ it->Seek("key_0");
+ XCTAssertTrue(it->Valid());
+ XCTAssertEqual("key_0", it->key());
+ transaction.Delete("key_0");
+ it->Next();
+ XCTAssertTrue(it->Valid());
+ XCTAssertEqual("key_1", it->key());
+ it->Next();
+ XCTAssertFalse(it->Valid());
+}
+
+- (void)testDeletingAheadOfAnIterator {
+ // Write keys
+ for (int i = 0; i < 4; ++i) {
+ Status status = _db->Put(LevelDbTransaction::DefaultWriteOptions(), "key_" + std::to_string(i),
+ "value_" + std::to_string(i));
+ XCTAssertTrue(status.ok());
+ }
+
+ // Create a transaction, iterate to key_1, delete key_2. Verify we still iterate key_3.
+ LevelDbTransaction transaction(_db.get());
+ std::unique_ptr<LevelDbTransaction::Iterator> it(transaction.NewIterator());
+ it->Seek("key_0");
+ XCTAssertTrue(it->Valid());
+ XCTAssertEqual("key_0", it->key());
+ it->Next();
+ XCTAssertTrue(it->Valid());
+ XCTAssertEqual("key_1", it->key());
+ transaction.Delete("key_2");
+ it->Next();
+ XCTAssertTrue(it->Valid());
+ XCTAssertEqual("key_3", it->key());
+ XCTAssertTrue(it->Valid());
+ it->Next();
+ XCTAssertFalse(it->Valid());
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc
new file mode 100644
index 0000000..af72716
--- /dev/null
+++ b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h"
+
+#include <leveldb/write_batch.h>
+
+#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h"
+
+using leveldb::DB;
+using leveldb::ReadOptions;
+using leveldb::Slice;
+using leveldb::Status;
+using leveldb::WriteBatch;
+using leveldb::WriteOptions;
+
+namespace firebase {
+namespace firestore {
+namespace local {
+
+LevelDbTransaction::Iterator::Iterator(LevelDbTransaction* txn)
+ : db_iter_(txn->db_->NewIterator(txn->read_options_)),
+ last_version_(txn->version_),
+ txn_(txn),
+ mutations_iter_(txn->mutations_.begin()),
+ current_(),
+ is_mutation_(false),
+ // Iterator doesn't really point to anything yet, so is
+ // invalid
+ is_valid_(false) {
+}
+
+void LevelDbTransaction::Iterator::UpdateCurrent() {
+ bool mutation_is_valid = mutations_iter_ != txn_->mutations_.end();
+ is_valid_ = mutation_is_valid || db_iter_->Valid();
+
+ if (is_valid_) {
+ if (!mutation_is_valid) {
+ is_mutation_ = false;
+ } else if (!db_iter_->Valid()) {
+ is_mutation_ = true;
+ } else {
+ // Both iterators are valid. If the leveldb key is equal to or greater
+ // than the current mutation key, we are looking at a mutation next. It's
+ // either sooner in the iteration or directly shadowing the underlying
+ // committed value in leveldb.
+ is_mutation_ = db_iter_->key().compare(mutations_iter_->first) >= 0;
+ }
+ if (is_mutation_) {
+ current_ = *mutations_iter_;
+ } else {
+ current_ = {db_iter_->key().ToString(), db_iter_->value().ToString()};
+ }
+ }
+}
+
+void LevelDbTransaction::Iterator::Seek(const std::string& key) {
+ db_iter_->Seek(key);
+ for (; db_iter_->Valid() && IsDeleted(db_iter_->key()); db_iter_->Next()) {
+ }
+ mutations_iter_ = txn_->mutations_.lower_bound(key);
+ UpdateCurrent();
+ last_version_ = txn_->version_;
+}
+
+absl::string_view LevelDbTransaction::Iterator::key() {
+ FIREBASE_ASSERT_MESSAGE(Valid(), "key() called on invalid iterator");
+ return current_.first;
+}
+
+absl::string_view LevelDbTransaction::Iterator::value() {
+ FIREBASE_ASSERT_MESSAGE(Valid(), "value() called on invalid iterator");
+ return current_.second;
+}
+
+bool LevelDbTransaction::Iterator::IsDeleted(leveldb::Slice slice) {
+ return txn_->deletions_.find(slice.ToString()) != txn_->deletions_.end();
+}
+
+bool LevelDbTransaction::Iterator::SyncToTransaction() {
+ if (last_version_ < txn_->version_) {
+ // Intentionally copying here since Seek() may update current_. We need the
+ // copy to do the comparison below.
+ const std::string current_key = current_.first;
+ Seek(current_key);
+ // If we advanced, we don't need to advance again.
+ return is_valid_ && current_.first > current_key;
+ } else {
+ return false;
+ }
+}
+
+void LevelDbTransaction::Iterator::AdvanceLDB() {
+ do {
+ db_iter_->Next();
+ } while (db_iter_->Valid() && IsDeleted(db_iter_->key()));
+}
+
+void LevelDbTransaction::Iterator::Next() {
+ FIREBASE_ASSERT_MESSAGE(Valid(), "Next() called on invalid iterator");
+ bool advanced = SyncToTransaction();
+ if (!advanced) {
+ if (is_mutation_) {
+ // A mutation might be shadowing leveldb. If so, advance both.
+ if (db_iter_->Valid() && db_iter_->key() == mutations_iter_->first) {
+ AdvanceLDB();
+ }
+ ++mutations_iter_;
+ } else {
+ AdvanceLDB();
+ }
+ UpdateCurrent();
+ }
+}
+
+bool LevelDbTransaction::Iterator::Valid() {
+ return is_valid_;
+}
+
+LevelDbTransaction::LevelDbTransaction(DB* db,
+ const ReadOptions& read_options,
+ const WriteOptions& write_options)
+ : db_(db),
+ mutations_(),
+ deletions_(),
+ read_options_(read_options),
+ write_options_(write_options),
+ version_(0) {
+}
+
+const ReadOptions& LevelDbTransaction::DefaultReadOptions() {
+ static ReadOptions options = ([]() {
+ ReadOptions read_options;
+ read_options.verify_checksums = true;
+ return read_options;
+ })();
+ return options;
+}
+
+const WriteOptions& LevelDbTransaction::DefaultWriteOptions() {
+ static WriteOptions options;
+ return options;
+}
+
+void LevelDbTransaction::Put(const absl::string_view& key,
+ const absl::string_view& value) {
+ std::string key_string(key);
+ std::string value_string(value);
+ mutations_[key_string] = value_string;
+ deletions_.erase(key_string);
+ version_++;
+}
+
+LevelDbTransaction::Iterator* LevelDbTransaction::NewIterator() {
+ return new LevelDbTransaction::Iterator(this);
+}
+
+Status LevelDbTransaction::Get(const absl::string_view& key,
+ std::string* value) {
+ std::string key_string(key);
+ if (deletions_.find(key_string) != deletions_.end()) {
+ return Status::NotFound(key_string + " is not present in the transaction");
+ } else {
+ Mutations::iterator iter(mutations_.find(key_string));
+ if (iter != mutations_.end()) {
+ *value = iter->second;
+ return Status::OK();
+ } else {
+ return db_->Get(read_options_, key_string, value);
+ }
+ }
+}
+
+void LevelDbTransaction::Delete(const absl::string_view& key) {
+ std::string to_delete(key);
+ deletions_.insert(to_delete);
+ mutations_.erase(to_delete);
+ version_++;
+}
+
+void LevelDbTransaction::Commit() {
+ WriteBatch batch;
+ for (auto it = deletions_.begin(); it != deletions_.end(); it++) {
+ batch.Delete(*it);
+ }
+
+ for (auto it = mutations_.begin(); it != mutations_.end(); it++) {
+ batch.Put(it->first, it->second);
+ }
+
+ Status status = db_->Write(write_options_, &batch);
+ FIREBASE_ASSERT_MESSAGE(status.ok(), "Failed to commit transaction: %s",
+ status.ToString().c_str());
+}
+
+} // namespace local
+} // namespace firestore
+} // namespace firebase
diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_transaction.h b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.h
new file mode 100644
index 0000000..015a6cb
--- /dev/null
+++ b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.h
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_TRANSACTION_H_
+#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_TRANSACTION_H_
+
+#include <absl/strings/string_view.h>
+#include <leveldb/db.h>
+
+#include <stdint.h>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+
+#if __OBJC__
+#import <Protobuf/GPBProtocolBuffers.h>
+#endif
+
+namespace firebase {
+namespace firestore {
+namespace local {
+
+/**
+ * LevelDBTransaction tracks pending changes to entries in leveldb, including
+ * deletions. It also provides an Iterator to traverse a merged view of pending
+ * changes and committed values.
+ */
+class LevelDbTransaction {
+ using Deletions = std::set<std::string>;
+ using Mutations = std::map<std::string, std::string>;
+
+ public:
+ /**
+ * Iterator iterates over a merged view of pending changes from the
+ * transaction and any unchanged values in the underlying leveldb instance.
+ */
+ class Iterator {
+ public:
+ explicit Iterator(LevelDbTransaction* txn);
+
+ /**
+ * Returns true if this iterator points to an entry
+ */
+ bool Valid();
+
+ /**
+ * Seeks this iterator to the first key equal to or greater than the given
+ * key
+ */
+ void Seek(const std::string& key);
+
+ /**
+ * Advances the iterator to the next entry
+ */
+ void Next();
+
+ /**
+ * Returns the key of the current entry
+ */
+ absl::string_view key();
+
+ /**
+ * Returns the value of the current entry
+ */
+ absl::string_view value();
+
+ private:
+ /**
+ * Advances to the next non-deleted key in leveldb.
+ */
+ void AdvanceLDB();
+
+ /**
+ * Returns true if the given slice matches a key present in the deletions_
+ * set.
+ */
+ bool IsDeleted(leveldb::Slice slice);
+
+ /**
+ * Syncs with the underlying transaction. If the transaction has been
+ * updated, the mutation iterator may need to be reset. Returns true if this
+ * resulted in moving to a new underlying entry (i.e. the entry represented
+ * by current_ was deleted).
+ */
+ bool SyncToTransaction();
+
+ /**
+ * Given the current state of the internal iterators, set is_valid_,
+ * is_mutation_, and current_.
+ */
+ void UpdateCurrent();
+
+ std::unique_ptr<leveldb::Iterator> db_iter_;
+
+ // The last observed version of the underlying transaction
+ int32_t last_version_;
+ // The underlying transaction.
+ LevelDbTransaction* txn_;
+ Mutations::iterator mutations_iter_;
+ // We save the current key and value so that once an iterator is Valid(), it
+ // remains so at least until the next call to Seek() or Next(), even if the
+ // underlying data is deleted.
+ std::pair<std::string, std::string> current_;
+ // True if current_ represents an entry in the mutations_ map, rather than
+ // committed data.
+ bool is_mutation_;
+ // True if the iterator pointed to a valid entry the last time Next() or
+ // Seek() was called.
+ bool is_valid_;
+ };
+
+ explicit LevelDbTransaction(
+ leveldb::DB* db,
+ const leveldb::ReadOptions& read_options = DefaultReadOptions(),
+ const leveldb::WriteOptions& write_options = DefaultWriteOptions());
+
+ LevelDbTransaction(const LevelDbTransaction& other) = delete;
+
+ LevelDbTransaction& operator=(const LevelDbTransaction& other) = delete;
+
+ /**
+ * Returns a default set of ReadOptions
+ */
+ static const leveldb::ReadOptions& DefaultReadOptions();
+
+ /**
+ * Returns a default set of WriteOptions
+ */
+ static const leveldb::WriteOptions& DefaultWriteOptions();
+
+ /**
+ * Remove the database entry (if any) for "key". It is not an error if "key"
+ * did not exist in the database.
+ */
+ void Delete(const absl::string_view& key);
+
+#if __OBJC__
+ /**
+ * Schedules the row identified by `key` to be set to the given protocol
+ * buffer message when this transaction commits.
+ */
+ void Put(const absl::string_view& key, GPBMessage* message) {
+ NSData* data = [message data];
+ std::string key_string(key);
+ mutations_[key_string] = std::string((const char*)data.bytes, data.length);
+ version_++;
+ }
+#endif
+
+ /**
+ * Schedules the row identified by `key` to be set to `value` when this
+ * transaction commits.
+ */
+ void Put(const absl::string_view& key, const absl::string_view& value);
+
+ /**
+ * Sets the contents of `value` to the latest known value for the given key,
+ * including any pending mutations and `Status::OK` is returned. If the key
+ * doesn't exist in leveldb, or it is scheduled for deletion in this
+ * transaction, `Status::NotFound` is returned.
+ */
+ leveldb::Status Get(const absl::string_view& key, std::string* value);
+
+ /**
+ * Returns a new Iterator over the pending changes in this transaction, merged
+ * with the existing values already in leveldb.
+ */
+ Iterator* NewIterator();
+
+ /**
+ * Commits the transaction. All pending changes are written. The transaction
+ * should not be used after calling this method.
+ */
+ void Commit();
+
+ private:
+ leveldb::DB* db_;
+ Mutations mutations_;
+ Deletions deletions_;
+ leveldb::ReadOptions read_options_;
+ leveldb::WriteOptions write_options_;
+ int32_t version_;
+};
+
+} // namespace local
+} // namespace firestore
+} // namespace firebase
+
+#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_TRANSACTION_H_