From 9060078609232082a989c7906e8d53b6c3b490b4 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Thu, 15 Mar 2018 16:52:13 -0700 Subject: Implements transactions for leveldb (#881) * Start work on leveldb transactions * Style * Working API. Not plumbed in yet * Move files into correct place * Wrangling file locations and associations * Tests pass * Add some comments * style * Fix copyright * Rewrite iterator internals to handle deletion-while-iterating. Also add tests for same * Switch to strings instead of slices * Style * More style fixes * Response to feedback before updating docs * Style * Add comment * Initialize version_ * Satisfy the linter * Fix style * snake_case * More snakes * LevelDBTransaction -> LevelDbTransaction --- .../Example/Firestore.xcodeproj/project.pbxproj | 6 + .../Tests/Local/FSTLevelDBTransactionTests.mm | 274 +++++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm (limited to 'Firestore/Example') 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 = ""; }; 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; + 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBTransactionTests.mm; sourceTree = ""; }; 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 = ""; }; @@ -541,6 +543,8 @@ 6003F58C195388D20070C39A /* Frameworks */, 6003F58B195388D20070C39A /* Products */, A47A1BF74A48BCAEAFBCBF1E /* Pods */, + 132E3B6B902D5CBD779D901A, + 132E37F450D23EF90B2352D9, ); sourceTree = ""; }; @@ -744,6 +748,7 @@ 5492E0902021552B00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm */, 5492E09B2021552C00B64F25 /* FSTWriteGroupTests.mm */, 5492E0932021552B00B64F25 /* StringViewTests.mm */, + 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */, ); path = Local; sourceTree = ""; @@ -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 +#include +#include +#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; +} + +- (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 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 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 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 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 -- cgit v1.2.3