diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm index 1e0c51b5038..6031a054be4 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -1829,4 +1829,63 @@ - (void)testUnlimitedCacheSize { XCTAssertEqualObjects(result.data, data); } +- (void)testGetValidPersistentCacheIndexManager { + [FIRApp configure]; + + FIRFirestore *db1 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB1"]; + FIRFirestoreSettings *settings1 = [db1 settings]; + [settings1 setCacheSettings:[[FIRPersistentCacheSettings alloc] init]]; + [db1 setSettings:settings1]; + + XCTAssertNotNil(db1.persistentCacheIndexManager); + + // Use persistent disk cache (default) + FIRFirestore *db2 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB2"]; + XCTAssertNotNil(db2.persistentCacheIndexManager); + + // Disable persistent disk cache + FIRFirestore *db3 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB3"]; + FIRFirestoreSettings *settings3 = [db3 settings]; + [settings3 setCacheSettings:[[FIRMemoryCacheSettings alloc] init]]; + [db3 setSettings:settings3]; + + XCTAssertNil(db3.persistentCacheIndexManager); + + // Disable persistent disk cache (deprecated) + FIRFirestore *db4 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB4"]; + FIRFirestoreSettings *settings4 = [db4 settings]; + settings4.persistenceEnabled = NO; + [db4 setSettings:settings4]; + XCTAssertNil(db4.persistentCacheIndexManager); +} + +- (void)testCanGetSameOrDifferentPersistentCacheIndexManager { + [FIRApp configure]; + // Use persistent disk cache (explicit) + FIRFirestore *db1 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB5"]; + FIRFirestoreSettings *settings1 = [db1 settings]; + [settings1 setCacheSettings:[[FIRPersistentCacheSettings alloc] init]]; + [db1 setSettings:settings1]; + XCTAssertEqual(db1.persistentCacheIndexManager, db1.persistentCacheIndexManager); + + // Use persistent disk cache (default) + FIRFirestore *db2 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB6"]; + XCTAssertEqual(db2.persistentCacheIndexManager, db2.persistentCacheIndexManager); + + XCTAssertNotEqual(db1.persistentCacheIndexManager, db2.persistentCacheIndexManager); + + FIRFirestore *db3 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB7"]; + FIRFirestoreSettings *settings3 = [db3 settings]; + [settings3 setCacheSettings:[[FIRPersistentCacheSettings alloc] init]]; + [db3 setSettings:settings3]; + XCTAssertNotEqual(db1.persistentCacheIndexManager, db3.persistentCacheIndexManager); + XCTAssertNotEqual(db2.persistentCacheIndexManager, db3.persistentCacheIndexManager); + + // Use persistent disk cache (default) + FIRFirestore *db4 = [FIRFirestore firestoreForDatabase:@"PersistentCacheIndexManagerDB8"]; + XCTAssertNotEqual(db1.persistentCacheIndexManager, db4.persistentCacheIndexManager); + XCTAssertNotEqual(db2.persistentCacheIndexManager, db4.persistentCacheIndexManager); + XCTAssertNotEqual(db3.persistentCacheIndexManager, db4.persistentCacheIndexManager); +} + @end diff --git a/Firestore/Example/Tests/Integration/API/FIRIndexingTests.mm b/Firestore/Example/Tests/Integration/API/FIRIndexingTests.mm index 1b46fc56581..108fda54256 100644 --- a/Firestore/Example/Tests/Integration/API/FIRIndexingTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRIndexingTests.mm @@ -18,6 +18,10 @@ #import +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRPersistentCacheIndexManager+Internal.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" @interface FIRIndexingTests : FSTIntegrationTestCase @@ -29,15 +33,15 @@ @implementation FIRIndexingTests - (void)setUp { [super setUp]; self.db = [self firestore]; - XCTestExpectation* exp = [self expectationWithDescription:@"clear persistence"]; - [self.db clearPersistenceWithCompletion:^(NSError*) { + XCTestExpectation *exp = [self expectationWithDescription:@"clear persistence"]; + [self.db clearPersistenceWithCompletion:^(NSError *) { [exp fulfill]; }]; [self awaitExpectation:exp]; } - (void)testCanConfigureIndexes { - NSString* json = @"{\n" + NSString *json = @"{\n" "\t\"indexes\": [{\n" "\t\t\t\"collectionGroup\": \"restaurants\",\n" "\t\t\t\"queryScope\": \"COLLECTION\",\n" @@ -64,14 +68,14 @@ - (void)testCanConfigureIndexes { "}"; [self.db setIndexConfigurationFromJSON:json - completion:^(NSError* error) { + completion:^(NSError *error) { XCTAssertNil(error); }]; } - (void)testBadJsonDoesNotCrashClient { [self.db setIndexConfigurationFromJSON:@"{," - completion:^(NSError* error) { + completion:^(NSError *error) { XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument); @@ -79,7 +83,7 @@ - (void)testBadJsonDoesNotCrashClient { } - (void)testBadIndexDoesNotCrashClient { - NSString* json = @"{\n" + NSString *json = @"{\n" "\t\"indexes\": [{\n" "\t\t\"collectionGroup\": \"restaurants\",\n" "\t\t\"queryScope\": \"COLLECTION\",\n" @@ -92,11 +96,114 @@ - (void)testBadIndexDoesNotCrashClient { "}"; [self.db setIndexConfigurationFromJSON:json - completion:^(NSError* error) { + completion:^(NSError *error) { XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument); }]; } +/** + * After Auto Index Creation is enabled, through public API there is no way to see the indexes + * sitting inside SDK. So this test only checks the API of auto index creation. + */ +- (void)testAutoIndexCreationSetSuccessfully { + // Use persistent disk cache (explict) + FIRFirestoreSettings *settings = [self.db settings]; + [settings setCacheSettings:[[FIRPersistentCacheSettings alloc] init]]; + [self.db setSettings:settings]; + + FIRCollectionReference *coll = [self collectionRef]; + NSDictionary *testDocs = @{ + @"a" : @{@"match" : @YES}, + @"b" : @{@"match" : @NO}, + @"c" : @{@"match" : @NO}, + }; + [self writeAllDocuments:testDocs toCollection:coll]; + + FIRQuery *query = [coll queryWhereField:@"match" isEqualTo:@YES]; + + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; + + XCTAssertNoThrow([self.db.persistentCacheIndexManager enableIndexAutoCreation]); + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; + + XCTAssertNoThrow([self.db.persistentCacheIndexManager disableIndexAutoCreation]); + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; + + XCTAssertNoThrow([self.db.persistentCacheIndexManager deleteAllIndexes]); + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; +} + +- (void)testAutoIndexCreationSetSuccessfullyUsingDefault { + // Use persistent disk cache (default) + FIRCollectionReference *coll = [self collectionRef]; + NSDictionary *testDocs = @{ + @"a" : @{@"match" : @YES}, + @"b" : @{@"match" : @NO}, + @"c" : @{@"match" : @NO}, + }; + [self writeAllDocuments:testDocs toCollection:coll]; + + FIRQuery *query = [coll queryWhereField:@"match" isEqualTo:@YES]; + + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; + + XCTAssertNoThrow([self.db.persistentCacheIndexManager enableIndexAutoCreation]); + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; + + XCTAssertNoThrow([self.db.persistentCacheIndexManager disableIndexAutoCreation]); + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; + + XCTAssertNoThrow([self.db.persistentCacheIndexManager deleteAllIndexes]); + [query getDocumentsWithSource:FIRFirestoreSourceCache + completion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(results.count, 1); + }]; +} + +- (void)testAutoIndexCreationAfterFailsTermination { + [self terminateFirestore:self.db]; + + FSTAssertThrows([self.db.persistentCacheIndexManager enableIndexAutoCreation], + @"The client has already been terminated."); + + FSTAssertThrows([self.db.persistentCacheIndexManager disableIndexAutoCreation], + @"The client has already been terminated."); + + FSTAssertThrows([self.db.persistentCacheIndexManager deleteAllIndexes], + @"The client has already been terminated."); +} + +// TODO(b/296100693) Add testing hooks to verify indexes are created as expected. + @end diff --git a/Firestore/Source/API/FIRFirestore+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h index b52c7cd01b6..d0216c7f3c1 100644 --- a/Firestore/Source/API/FIRFirestore+Internal.h +++ b/Firestore/Source/API/FIRFirestore+Internal.h @@ -25,6 +25,7 @@ @class FIRApp; @class FSTUserDataReader; +@class FIRPersistentCacheIndexManager; namespace firebase { namespace firestore { @@ -78,6 +79,9 @@ NS_ASSUME_NONNULL_BEGIN - (const std::shared_ptr &)workerQueue; +// TODO(csi): make this function public +@property(nonatomic, readonly) FIRPersistentCacheIndexManager *persistentCacheIndexManager; + @property(nonatomic, assign, readonly) std::shared_ptr wrapped; @property(nonatomic, assign, readonly) const model::DatabaseId &databaseID; diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm index 6366294e364..db3ee8d6934 100644 --- a/Firestore/Source/API/FIRFirestore.mm +++ b/Firestore/Source/API/FIRFirestore.mm @@ -21,6 +21,7 @@ #include #import "FIRFirestoreSettings+Internal.h" +#import "FIRPersistentCacheIndexManager+Internal.h" #import "FIRTransactionOptions+Internal.h" #import "FIRTransactionOptions.h" @@ -106,6 +107,7 @@ @implementation FIRFirestore { std::shared_ptr _firestore; FIRFirestoreSettings *_settings; __weak id _registry; + FIRPersistentCacheIndexManager *_indexManager; } + (void)initialize { @@ -533,6 +535,17 @@ @implementation FIRFirestore (Internal) return _firestore->worker_queue(); } +- (FIRPersistentCacheIndexManager *)persistentCacheIndexManager { + if (!_indexManager) { + auto index_manager = _firestore->persistent_cache_index_manager(); + if (index_manager) { + _indexManager = [[FIRPersistentCacheIndexManager alloc] + initWithPersistentCacheIndexManager:index_manager]; + } + } + return _indexManager; +} + - (const DatabaseId &)databaseID { return _firestore->database_id(); } diff --git a/Firestore/Source/API/FIRPersistentCacheIndexManager+Internal.h b/Firestore/Source/API/FIRPersistentCacheIndexManager+Internal.h new file mode 100644 index 00000000000..ab7a3014c3e --- /dev/null +++ b/Firestore/Source/API/FIRPersistentCacheIndexManager+Internal.h @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + +#include + +#include "Firestore/core/src/api/persistent_cache_index_manager.h" + +NS_ASSUME_NONNULL_BEGIN + +// TODO(sum/avg) move the contents of this category to +// ../Public/FirebaseFirestore/FIRPersistentCacheIndexManager.h +/** + * A PersistentCacheIndexManager which you can config persistent cache indexes used for + * local query execution. + */ +NS_SWIFT_NAME(PersistentCacheIndexManager) +@interface FIRPersistentCacheIndexManager : NSObject + +/** :nodoc: */ +- (instancetype)init + __attribute__((unavailable("FIRPersistentCacheIndexManager cannot be created directly."))); + +/** + * Enables SDK to create persistent cache indexes automatically for local query execution when SDK + * believes cache indexes can help improves performance. + * + * This feature is disabled by default. + */ +- (void)enableIndexAutoCreation NS_SWIFT_NAME(enableIndexAutoCreation()); + +/** + * Stops creating persistent cache indexes automatically for local query execution. The indexes + * which have been created by calling `enableIndexAutoCreation` still take effect. + */ +- (void)disableIndexAutoCreation NS_SWIFT_NAME(disableIndexAutoCreation()); + +/** + * Removes all persistent cache indexes. Please note this function will also deletes indexes + * generated by [[FIRFirestore firestore] setIndexConfigurationFromJSON] and [[FIRFirestore + * firestore] setIndexConfigurationFromStream], which are deprecated. + */ +- (void)deleteAllIndexes NS_SWIFT_NAME(deleteAllIndexes()); + +@end + +@interface FIRPersistentCacheIndexManager (/* Init */) + +- (instancetype)initWithPersistentCacheIndexManager: + (std::shared_ptr)indexManager + NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRPersistentCacheIndexManager.mm b/Firestore/Source/API/FIRPersistentCacheIndexManager.mm new file mode 100644 index 00000000000..42043c4ff50 --- /dev/null +++ b/Firestore/Source/API/FIRPersistentCacheIndexManager.mm @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Google LLC + * + * 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 "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRPersistentCacheIndexManager+Internal.h" + +using firebase::firestore::api::Firestore; +using firebase::firestore::api::PersistentCacheIndexManager; + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRPersistentCacheIndexManager { + /** The `Firestore` instance that created this index manager. */ + std::shared_ptr _indexManager; +} + +- (instancetype)initWithPersistentCacheIndexManager: + (std::shared_ptr)indexManager { + if (self = [super init]) { + _indexManager = indexManager; + } + return self; +} + +- (void)enableIndexAutoCreation { + _indexManager->EnableIndexAutoCreation(); +} + +- (void)disableIndexAutoCreation { + _indexManager->DisableIndexAutoCreation(); +} + +- (void)deleteAllIndexes { + _indexManager->DeleteAllFieldIndexes(); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/api/firestore.cc b/Firestore/core/src/api/firestore.cc index 59ac06c2959..70cb975cc71 100644 --- a/Firestore/core/src/api/firestore.cc +++ b/Firestore/core/src/api/firestore.cc @@ -22,6 +22,7 @@ #include "Firestore/core/src/api/collection_reference.h" #include "Firestore/core/src/api/document_reference.h" #include "Firestore/core/src/api/listener_registration.h" +#include "Firestore/core/src/api/persistent_cache_index_manager.h" #include "Firestore/core/src/api/settings.h" #include "Firestore/core/src/api/snapshots_in_sync_listener_registration.h" #include "Firestore/core/src/api/write_batch.h" @@ -317,6 +318,17 @@ void Firestore::SetIndexConfiguration(const std::string& config, return; } +std::shared_ptr +Firestore::persistent_cache_index_manager() { + EnsureClientConfigured(); + if (!persistent_cache_index_manager_ && settings_.persistence_enabled()) { + persistent_cache_index_manager_ = + std::make_shared(client_); + } + + return persistent_cache_index_manager_; +} + std::shared_ptr Firestore::LoadBundle( std::unique_ptr bundle_data) { EnsureClientConfigured(); diff --git a/Firestore/core/src/api/firestore.h b/Firestore/core/src/api/firestore.h index eca480f1608..9323b0a4e81 100644 --- a/Firestore/core/src/api/firestore.h +++ b/Firestore/core/src/api/firestore.h @@ -46,6 +46,8 @@ struct Empty; namespace api { +class PersistentCacheIndexManager; + extern const int kDefaultTransactionMaxAttempts; class Firestore : public std::enable_shared_from_this { @@ -88,6 +90,9 @@ class Firestore : public std::enable_shared_from_this { void set_user_executor(std::unique_ptr user_executor); + std::shared_ptr + persistent_cache_index_manager(); + CollectionReference GetCollection(const std::string& collection_path); DocumentReference GetDocument(const std::string& document_path); WriteBatch GetBatch(); @@ -131,6 +136,9 @@ class Firestore : public std::enable_shared_from_this { auth_credentials_provider_; std::string persistence_key_; + std::shared_ptr + persistent_cache_index_manager_; + std::shared_ptr user_executor_; std::shared_ptr worker_queue_; diff --git a/Firestore/core/src/api/persistent_cache_index_manager.cc b/Firestore/core/src/api/persistent_cache_index_manager.cc new file mode 100644 index 00000000000..ce3ce2c2177 --- /dev/null +++ b/Firestore/core/src/api/persistent_cache_index_manager.cc @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Google LLC + * + * 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/api/persistent_cache_index_manager.h" + +#include + +#include "Firestore/core/src/core/firestore_client.h" + +namespace firebase { +namespace firestore { +namespace api { + +PersistentCacheIndexManager::PersistentCacheIndexManager( + std::shared_ptr client) + : client_(std::move(client)) { +} + +void PersistentCacheIndexManager::EnableIndexAutoCreation() const { + client_->SetIndexAutoCreationEnabled(true); +} + +void PersistentCacheIndexManager::DisableIndexAutoCreation() const { + client_->SetIndexAutoCreationEnabled(false); +} + +void PersistentCacheIndexManager::DeleteAllFieldIndexes() const { + client_->DeleteAllFieldIndexes(); +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/api/persistent_cache_index_manager.h b/Firestore/core/src/api/persistent_cache_index_manager.h new file mode 100644 index 00000000000..51f980dca05 --- /dev/null +++ b/Firestore/core/src/api/persistent_cache_index_manager.h @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Google LLC + * + * 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_API_PERSISTENT_CACHE_INDEX_MANAGER_H_ +#define FIRESTORE_CORE_SRC_API_PERSISTENT_CACHE_INDEX_MANAGER_H_ + +#include + +namespace firebase { +namespace firestore { + +namespace core { +class FirestoreClient; +} // namespace core + +namespace api { + +/** + * A PersistentCacheIndexManager which you can config persistent cache indexes + * used for local query execution. + * + * To use, call Firestore::persistent_cache_index_manager() to get an instance. + */ +class PersistentCacheIndexManager { + public: + explicit PersistentCacheIndexManager( + std::shared_ptr client); + + /** + * Enables SDK to create persistent cache indexes automatically for local + * query execution when SDK believes cache indexes can help improves + * performance. + * + * This feature is disabled by default. + */ + void EnableIndexAutoCreation() const; + + /** + * Stops creating persistent cache indexes automatically for local query + * execution. The indexes which have been created by calling + * EnableIndexAutoCreation() still take effect. + */ + void DisableIndexAutoCreation() const; + + /** + * Removes all persistent cache indexes. Please note this function will also + * deletes indexes generated by Firestore.SetIndexConfiguration(...), which + * are deprecated. + */ + void DeleteAllFieldIndexes() const; + + private: + const std::shared_ptr client_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_PERSISTENT_CACHE_INDEX_MANAGER_H_ diff --git a/Firestore/core/src/core/firestore_client.cc b/Firestore/core/src/core/firestore_client.cc index 65851522eb7..6282cfb2ae0 100644 --- a/Firestore/core/src/core/firestore_client.cc +++ b/Firestore/core/src/core/firestore_client.cc @@ -405,7 +405,7 @@ void FirestoreClient::WaitForPendingWrites(StatusCallback callback) { }); } -void FirestoreClient::VerifyNotTerminated() { +void FirestoreClient::VerifyNotTerminated() const { if (is_terminated()) { ThrowIllegalState("The client has already been terminated."); } @@ -597,6 +597,18 @@ void FirestoreClient::ConfigureFieldIndexes( }); } +void FirestoreClient::SetIndexAutoCreationEnabled(bool is_enabled) const { + VerifyNotTerminated(); + worker_queue_->Enqueue([this, is_enabled] { + local_store_->SetIndexAutoCreationEnabled(is_enabled); + }); +} + +void FirestoreClient::DeleteAllFieldIndexes() { + VerifyNotTerminated(); + worker_queue_->Enqueue([this] { local_store_->DeleteAllFieldIndexes(); }); +} + void FirestoreClient::LoadBundle( std::unique_ptr bundle_data, std::shared_ptr result_task) { diff --git a/Firestore/core/src/core/firestore_client.h b/Firestore/core/src/core/firestore_client.h index 1cc0aa235d1..d752deff66a 100644 --- a/Firestore/core/src/core/firestore_client.h +++ b/Firestore/core/src/core/firestore_client.h @@ -187,6 +187,10 @@ class FirestoreClient : public std::enable_shared_from_this { void ConfigureFieldIndexes(std::vector parsed_indexes); + void SetIndexAutoCreationEnabled(bool is_enabled) const; + + void DeleteAllFieldIndexes(); + void LoadBundle(std::unique_ptr bundle_data, std::shared_ptr result_task); @@ -211,7 +215,7 @@ class FirestoreClient : public std::enable_shared_from_this { void Initialize(const credentials::User& user, const api::Settings& settings); - void VerifyNotTerminated(); + void VerifyNotTerminated() const; void TerminateInternal(); diff --git a/Firestore/core/src/local/index_backfiller.cc b/Firestore/core/src/local/index_backfiller.cc index d1511e828db..b0be8be8fdf 100644 --- a/Firestore/core/src/local/index_backfiller.cc +++ b/Firestore/core/src/local/index_backfiller.cc @@ -36,7 +36,7 @@ using model::IndexOffset; /** * The maximum number of documents to process each time Backfill() is called. */ -static const int kMaxDocumentsToProcess = 50; +static const size_t kMaxDocumentsToProcess = 50; } // namespace @@ -47,7 +47,7 @@ IndexBackfiller::IndexBackfiller() { int IndexBackfiller::WriteIndexEntries(const LocalStore* local_store) { IndexManager* index_manager = local_store->index_manager(); std::unordered_set processed_collection_groups; - int documents_remaining = max_documents_to_process_; + size_t documents_remaining = max_documents_to_process_; while (documents_remaining > 0) { const auto collection_group = index_manager->GetNextCollectionGroupToUpdate(); diff --git a/Firestore/core/src/local/index_backfiller.h b/Firestore/core/src/local/index_backfiller.h index 9656d28d462..5ebd3aa8014 100644 --- a/Firestore/core/src/local/index_backfiller.h +++ b/Firestore/core/src/local/index_backfiller.h @@ -47,6 +47,7 @@ class IndexBackfiller { private: friend class IndexBackfillerTest; + friend class LocalStoreTestBase; /** * Writes entries for the provided collection group. Returns the number of @@ -61,11 +62,11 @@ class IndexBackfiller { const LocalWriteResult& lookup_result) const; // For testing - void SetMaxDocumentsToProcess(int new_max) { + void SetMaxDocumentsToProcess(size_t new_max) { max_documents_to_process_ = new_max; } - int max_documents_to_process_; + size_t max_documents_to_process_; }; } // namespace local diff --git a/Firestore/core/src/local/index_manager.h b/Firestore/core/src/local/index_manager.h index 821b5e2bb44..c180de12735 100644 --- a/Firestore/core/src/local/index_manager.h +++ b/Firestore/core/src/local/index_manager.h @@ -112,6 +112,12 @@ class IndexManager { /** Returns all configured field indexes. */ virtual std::vector GetFieldIndexes() const = 0; + /** Removes all field indexes and deletes all index values. */ + virtual void DeleteAllFieldIndexes() = 0; + + /** Creates a full matched field index which serves the given target. */ + virtual void CreateTargetIndexes(const core::Target& target) = 0; + /** * Iterates over all field indexes that are used to serve the given target, * and returns the minimum offset of them all. Asserts that the target can be diff --git a/Firestore/core/src/local/leveldb_index_manager.cc b/Firestore/core/src/local/leveldb_index_manager.cc index 2cc763fab17..ca8412744d0 100644 --- a/Firestore/core/src/local/leveldb_index_manager.cc +++ b/Firestore/core/src/local/leveldb_index_manager.cc @@ -464,6 +464,27 @@ absl::optional LevelDbIndexManager::GetFieldIndex( return result; } +void LevelDbIndexManager::DeleteAllFieldIndexes() { + HARD_ASSERT(started_, "IndexManager not started"); + + db_->DeleteAllFieldIndexes(); + memoized_indexes_.clear(); + next_index_to_update_ = QueueForNextIndexToUpdate(); +} + +void LevelDbIndexManager::CreateTargetIndexes(const core::Target& target) { + HARD_ASSERT(started_, "IndexManager not started"); + + for (const auto& subTarget : GetSubTargets(target)) { + IndexManager::IndexType type = GetIndexType(subTarget); + if (type == IndexManager::IndexType::NONE || + type == IndexManager::IndexType::PARTIAL) { + TargetIndexMatcher targetIndexMatcher(subTarget); + AddFieldIndex(targetIndexMatcher.BuildTargetIndex()); + } + } +} + model::IndexOffset LevelDbIndexManager::GetMinOffset( const core::Target& target) { std::vector indexes; diff --git a/Firestore/core/src/local/leveldb_index_manager.h b/Firestore/core/src/local/leveldb_index_manager.h index eb8102c587d..0b9843707fc 100644 --- a/Firestore/core/src/local/leveldb_index_manager.h +++ b/Firestore/core/src/local/leveldb_index_manager.h @@ -69,6 +69,10 @@ class LevelDbIndexManager : public IndexManager { std::vector GetFieldIndexes() const override; + void DeleteAllFieldIndexes() override; + + void CreateTargetIndexes(const core::Target& target) override; + model::IndexOffset GetMinOffset(const core::Target& target) override; model::IndexOffset GetMinOffset( diff --git a/Firestore/core/src/local/leveldb_persistence.cc b/Firestore/core/src/local/leveldb_persistence.cc index 47ed77f0794..c5ce4c60c2d 100644 --- a/Firestore/core/src/local/leveldb_persistence.cc +++ b/Firestore/core/src/local/leveldb_persistence.cc @@ -307,6 +307,17 @@ void LevelDbPersistence::ReleaseOtherUserSpecificComponents( } } +void LevelDbPersistence::DeleteAllFieldIndexes() { + DeleteEverythingWithPrefix("Delete All Index Configuration", + LevelDbIndexConfigurationKey::KeyPrefix()); + + DeleteEverythingWithPrefix("Delete All Index States", + LevelDbIndexStateKey::KeyPrefix()); + + DeleteEverythingWithPrefix("Delete All Index Entries", + LevelDbIndexEntryKey::KeyPrefix()); +} + void LevelDbPersistence::RunInternal(absl::string_view label, std::function block) { HARD_ASSERT(transaction_ == nullptr, @@ -329,6 +340,29 @@ leveldb::ReadOptions StandardReadOptions() { return options; } +void LevelDbPersistence::DeleteEverythingWithPrefix(absl::string_view label, + const std::string& prefix) { + bool more_deletes = true; + + auto fun = [&]() { + more_deletes = false; + + auto it = transaction_->NewIterator(); + for (it->Seek(prefix); it->Valid() && absl::StartsWith(it->key(), prefix); + it->Next()) { + if (transaction_->changed_keys() >= kMaxOperationPerTransaction) { + more_deletes = true; + break; + } + transaction_->Delete(it->key()); + } + }; + + while (more_deletes) { + RunInternal(label, fun); + } +} + } // namespace local } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/local/leveldb_persistence.h b/Firestore/core/src/local/leveldb_persistence.h index ae1748b953f..1374d91a08e 100644 --- a/Firestore/core/src/local/leveldb_persistence.h +++ b/Firestore/core/src/local/leveldb_persistence.h @@ -108,12 +108,20 @@ class LevelDbPersistence : public Persistence { private: friend class LevelDbOverlayMigrationManagerTest; + friend class LevelDbLocalStoreTest; + friend class LevelDbIndexManager; + LevelDbPersistence(std::unique_ptr db, util::Path directory, std::set users, LocalSerializer serializer, const LruParams& lru_params); + /** + * The maximum number of operation per transaction. + */ + static const size_t kMaxOperationPerTransaction = 1000U; + /** * Ensures that the given directory exists. */ @@ -129,6 +137,15 @@ class LevelDbPersistence : public Persistence { LocalSerializer serializer, const LruParams& lru_params); + void DeleteAllFieldIndexes() override; + + /** + * Remove the database entry (if any) for all "key" starting with given + * prefix. It is a no-op if the key does not exist. + */ + void DeleteEverythingWithPrefix(absl::string_view label, + const std::string& prefix); + std::unique_ptr db_; util::Path directory_; diff --git a/Firestore/core/src/local/leveldb_remote_document_cache.cc b/Firestore/core/src/local/leveldb_remote_document_cache.cc index c29f59c9da4..15179a43c08 100644 --- a/Firestore/core/src/local/leveldb_remote_document_cache.cc +++ b/Firestore/core/src/local/leveldb_remote_document_cache.cc @@ -25,6 +25,7 @@ #include "Firestore/core/src/local/leveldb_key.h" #include "Firestore/core/src/local/leveldb_persistence.h" #include "Firestore/core/src/local/local_serializer.h" +#include "Firestore/core/src/local/query_context.h" #include "Firestore/core/src/model/document_key_set.h" #include "Firestore/core/src/model/model_fwd.h" #include "Firestore/core/src/model/mutable_document.h" @@ -227,6 +228,16 @@ MutableDocumentMap LevelDbRemoteDocumentCache::GetDocumentsMatchingQuery( const model::IndexOffset& offset, absl::optional limit, const model::OverlayByDocumentKeyMap& mutated_docs) const { + absl::optional context; + return GetDocumentsMatchingQuery(query, offset, context, limit, mutated_docs); +} + +MutableDocumentMap LevelDbRemoteDocumentCache::GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context, + absl::optional limit, + const model::OverlayByDocumentKeyMap& mutated_docs) const { // Use the query path as a prefix for testing if a document matches the query. // Execute an index-free query and filter by read time. This is safe since @@ -262,6 +273,12 @@ MutableDocumentMap LevelDbRemoteDocumentCache::GetDocumentsMatchingQuery( } } + if (context.has_value()) { + // The next step is going to check every document in remote_map, so it will + // go through total of remote_map.size() documents. + context.value().IncrementDocumentReadCount(remote_map.size()); + } + return LevelDbRemoteDocumentCache::GetAllExisting(std::move(remote_map), query, mutated_docs); } diff --git a/Firestore/core/src/local/leveldb_remote_document_cache.h b/Firestore/core/src/local/leveldb_remote_document_cache.h index fb7db7be845..e0b0d17446e 100644 --- a/Firestore/core/src/local/leveldb_remote_document_cache.h +++ b/Firestore/core/src/local/leveldb_remote_document_cache.h @@ -70,6 +70,12 @@ class LevelDbRemoteDocumentCache : public RemoteDocumentCache { const model::IndexOffset& offset, absl::optional limit = absl::nullopt, const model::OverlayByDocumentKeyMap& mutated_docs = {}) const override; + model::MutableDocumentMap GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context, + absl::optional limit = absl::nullopt, + const model::OverlayByDocumentKeyMap& mutated_docs = {}) const override; void SetIndexManager(IndexManager* manager) override; diff --git a/Firestore/core/src/local/leveldb_transaction.cc b/Firestore/core/src/local/leveldb_transaction.cc index d2df5df396d..1067d1e8327 100644 --- a/Firestore/core/src/local/leveldb_transaction.cc +++ b/Firestore/core/src/local/leveldb_transaction.cc @@ -22,6 +22,7 @@ #include "Firestore/core/src/util/hard_assert.h" #include "Firestore/core/src/util/log.h" #include "absl/memory/memory.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "leveldb/write_batch.h" diff --git a/Firestore/core/src/local/local_documents_view.cc b/Firestore/core/src/local/local_documents_view.cc index 51a10901735..b4e2cd78807 100644 --- a/Firestore/core/src/local/local_documents_view.cc +++ b/Firestore/core/src/local/local_documents_view.cc @@ -75,12 +75,20 @@ Document LocalDocumentsView::GetDocument( DocumentMap LocalDocumentsView::GetDocumentsMatchingQuery( const Query& query, const model::IndexOffset& offset) { + absl::optional null_context; + return GetDocumentsMatchingQuery(query, offset, null_context); +} + +DocumentMap LocalDocumentsView::GetDocumentsMatchingQuery( + const Query& query, + const model::IndexOffset& offset, + absl::optional& context) { if (query.IsDocumentQuery()) { return GetDocumentsMatchingDocumentQuery(query.path()); } else if (query.IsCollectionGroupQuery()) { - return GetDocumentsMatchingCollectionGroupQuery(query, offset); + return GetDocumentsMatchingCollectionGroupQuery(query, offset, context); } else { - return GetDocumentsMatchingCollectionQuery(query, offset); + return GetDocumentsMatchingCollectionQuery(query, offset, context); } } @@ -96,7 +104,9 @@ DocumentMap LocalDocumentsView::GetDocumentsMatchingDocumentQuery( } model::DocumentMap LocalDocumentsView::GetDocumentsMatchingCollectionGroupQuery( - const Query& query, const IndexOffset& offset) { + const Query& query, + const IndexOffset& offset, + absl::optional& context) { HARD_ASSERT( query.path().empty(), "Currently we only support collection group queries at the root."); @@ -112,7 +122,7 @@ model::DocumentMap LocalDocumentsView::GetDocumentsMatchingCollectionGroupQuery( Query collection_query = query.AsCollectionQueryAtPath(parent.Append(collection_id)); DocumentMap collection_results = - GetDocumentsMatchingCollectionQuery(collection_query, offset); + GetDocumentsMatchingCollectionQuery(collection_query, offset, context); for (const auto& kv : collection_results) { const DocumentKey& key = kv.first; results = results.insert(key, Document(kv.second)); @@ -153,13 +163,15 @@ LocalWriteResult LocalDocumentsView::GetNextDocuments( } DocumentMap LocalDocumentsView::GetDocumentsMatchingCollectionQuery( - const Query& query, const IndexOffset& offset) { + const Query& query, + const IndexOffset& offset, + absl::optional& context) { // Get locally mutated documents OverlayByDocumentKeyMap overlays = document_overlay_cache_->GetOverlays( query.path(), offset.largest_batch_id()); MutableDocumentMap remote_documents = remote_document_cache_->GetDocumentsMatchingQuery( - query, offset, absl::nullopt, overlays); + query, offset, context, absl::nullopt, overlays); // As documents might match the query because of their overlay we need to // include documents for all overlays in the initial document set. diff --git a/Firestore/core/src/local/local_documents_view.h b/Firestore/core/src/local/local_documents_view.h index 104504890ef..7fc12b378fa 100644 --- a/Firestore/core/src/local/local_documents_view.h +++ b/Firestore/core/src/local/local_documents_view.h @@ -25,6 +25,7 @@ #include "Firestore/core/src/local/document_overlay_cache.h" #include "Firestore/core/src/local/index_manager.h" #include "Firestore/core/src/local/mutation_queue.h" +#include "Firestore/core/src/local/query_context.h" #include "Firestore/core/src/local/remote_document_cache.h" #include "Firestore/core/src/model/document.h" #include "Firestore/core/src/model/model_fwd.h" @@ -39,10 +40,9 @@ class Query; } // namespace core namespace local { -class LocalWriteResult; -} // namespace local -namespace local { +class LocalWriteResult; +class QueryContext; /** * A readonly view of the local state of all documents we're tracking (i.e. we @@ -141,6 +141,20 @@ class LocalDocumentsView { virtual model::DocumentMap GetDocumentsMatchingQuery( const core::Query& query, const model::IndexOffset& offset); + /** + * Performs a query against the local view of all documents. + * + * @param query The query to match documents against. + * @param offset Read time and document key to start scanning by (exclusive). + * @param context A optional tracker to keep a record of important details + * during database local query execution. + */ + // Virtual for testing. + virtual model::DocumentMap GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context); + private: friend class QueryEngine; @@ -155,11 +169,15 @@ class LocalDocumentsView { const model::ResourcePath& doc_path); model::DocumentMap GetDocumentsMatchingCollectionGroupQuery( - const core::Query& query, const model::IndexOffset& offset); + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context); /** Queries the remote documents and overlays mutations. */ model::DocumentMap GetDocumentsMatchingCollectionQuery( - const core::Query& query, const model::IndexOffset& offset); + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context); RemoteDocumentCache* remote_document_cache() { return remote_document_cache_; diff --git a/Firestore/core/src/local/local_store.cc b/Firestore/core/src/local/local_store.cc index ba31598ee13..5a054a31a67 100644 --- a/Firestore/core/src/local/local_store.cc +++ b/Firestore/core/src/local/local_store.cc @@ -699,6 +699,18 @@ void LocalStore::ConfigureFieldIndexes( }); } +void LocalStore::SetIndexAutoCreationEnabled(bool is_enabled) const { + query_engine_->SetIndexAutoCreationEnabled(is_enabled); +} + +void LocalStore::DeleteAllFieldIndexes() const { + // This step is not wrapped in `persistence_->Run()`. + // The reason is `persistence_->Run()` always assume each operation is + // executed in one transaction, while `DeleteAllFieldIndexes()` might need + // multiple transactions to finish. + index_manager_->DeleteAllFieldIndexes(); +} + Target LocalStore::NewUmbrellaTarget(const std::string& bundle_id) { // It is OK that the path used for the query is not valid, because this will // not be read and queried. diff --git a/Firestore/core/src/local/local_store.h b/Firestore/core/src/local/local_store.h index cb19c13f1be..8f2a0872f52 100644 --- a/Firestore/core/src/local/local_store.h +++ b/Firestore/core/src/local/local_store.h @@ -288,6 +288,10 @@ class LocalStore : public bundle::BundleCallback { void ConfigureFieldIndexes(std::vector new_field_indexes); + void SetIndexAutoCreationEnabled(bool is_enabled) const; + + void DeleteAllFieldIndexes() const; + private: friend class IndexBackfiller; friend class IndexBackfillerTest; diff --git a/Firestore/core/src/local/memory_index_manager.cc b/Firestore/core/src/local/memory_index_manager.cc index 8ab678059e3..9789a918950 100644 --- a/Firestore/core/src/local/memory_index_manager.cc +++ b/Firestore/core/src/local/memory_index_manager.cc @@ -89,6 +89,12 @@ std::vector MemoryIndexManager::GetFieldIndexes() const { return {}; } +void MemoryIndexManager::DeleteAllFieldIndexes() { +} + +void MemoryIndexManager::CreateTargetIndexes(const core::Target&) { +} + model::IndexOffset MemoryIndexManager::GetMinOffset(const core::Target&) { return model::IndexOffset::None(); } diff --git a/Firestore/core/src/local/memory_index_manager.h b/Firestore/core/src/local/memory_index_manager.h index 093ed3910ba..35cf47d85f6 100644 --- a/Firestore/core/src/local/memory_index_manager.h +++ b/Firestore/core/src/local/memory_index_manager.h @@ -67,6 +67,10 @@ class MemoryIndexManager : public IndexManager { std::vector GetFieldIndexes() const override; + void DeleteAllFieldIndexes() override; + + void CreateTargetIndexes(const core::Target&) override; + model::IndexOffset GetMinOffset(const core::Target&) override; model::IndexOffset GetMinOffset(const std::string&) const override; diff --git a/Firestore/core/src/local/memory_persistence.cc b/Firestore/core/src/local/memory_persistence.cc index 08ad8e91771..009036e1d8b 100644 --- a/Firestore/core/src/local/memory_persistence.cc +++ b/Firestore/core/src/local/memory_persistence.cc @@ -138,6 +138,9 @@ ReferenceDelegate* MemoryPersistence::reference_delegate() { void MemoryPersistence::ReleaseOtherUserSpecificComponents(const std::string&) { } +void MemoryPersistence::DeleteAllFieldIndexes() { +} + void MemoryPersistence::RunInternal(absl::string_view label, std::function block) { TransactionGuard guard(reference_delegate_.get(), label); diff --git a/Firestore/core/src/local/memory_persistence.h b/Firestore/core/src/local/memory_persistence.h index c5dfa1746ea..674577bbac2 100644 --- a/Firestore/core/src/local/memory_persistence.h +++ b/Firestore/core/src/local/memory_persistence.h @@ -113,6 +113,8 @@ class MemoryPersistence : public Persistence { void set_reference_delegate(std::unique_ptr delegate); + void DeleteAllFieldIndexes() override; + MutationQueues mutation_queues_; /** diff --git a/Firestore/core/src/local/memory_remote_document_cache.cc b/Firestore/core/src/local/memory_remote_document_cache.cc index 2c6bf3801a1..70e69b0cc77 100644 --- a/Firestore/core/src/local/memory_remote_document_cache.cc +++ b/Firestore/core/src/local/memory_remote_document_cache.cc @@ -19,6 +19,7 @@ #include "Firestore/core/src/core/query.h" #include "Firestore/core/src/local/memory_lru_reference_delegate.h" #include "Firestore/core/src/local/memory_persistence.h" +#include "Firestore/core/src/local/query_context.h" #include "Firestore/core/src/local/sizer.h" #include "Firestore/core/src/model/document.h" #include "Firestore/core/src/model/overlay.h" @@ -87,6 +88,16 @@ MutableDocumentMap MemoryRemoteDocumentCache::GetAll(const std::string&, MutableDocumentMap MemoryRemoteDocumentCache::GetDocumentsMatchingQuery( const core::Query& query, const model::IndexOffset& offset, + absl::optional limit, + const model::OverlayByDocumentKeyMap& mutated_docs) const { + absl::optional context; + return GetDocumentsMatchingQuery(query, offset, context, limit, mutated_docs); +} + +MutableDocumentMap MemoryRemoteDocumentCache::GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional&, absl::optional, const model::OverlayByDocumentKeyMap& mutated_docs) const { MutableDocumentMap results; diff --git a/Firestore/core/src/local/memory_remote_document_cache.h b/Firestore/core/src/local/memory_remote_document_cache.h index e008b72a409..a637cbeceaf 100644 --- a/Firestore/core/src/local/memory_remote_document_cache.h +++ b/Firestore/core/src/local/memory_remote_document_cache.h @@ -37,6 +37,7 @@ namespace local { class MemoryLruReferenceDelegate; class MemoryPersistence; class Sizer; +class QueryContext; class MemoryRemoteDocumentCache : public RemoteDocumentCache { public: @@ -57,6 +58,12 @@ class MemoryRemoteDocumentCache : public RemoteDocumentCache { const model::IndexOffset& offset, absl::optional limit = absl::nullopt, const model::OverlayByDocumentKeyMap& mutated_docs = {}) const override; + model::MutableDocumentMap GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional&, + absl::optional limit = absl::nullopt, + const model::OverlayByDocumentKeyMap& mutated_docs = {}) const override; void SetIndexManager(IndexManager* manager) override; diff --git a/Firestore/core/src/local/persistence.h b/Firestore/core/src/local/persistence.h index 6639f7d3564..b1f7ebde74f 100644 --- a/Firestore/core/src/local/persistence.h +++ b/Firestore/core/src/local/persistence.h @@ -193,6 +193,16 @@ class Persistence { private: virtual void RunInternal(absl::string_view label, std::function block) = 0; + + /** + * Removes all persistent cache indexes. This feature is implemented in + * `Persistence` instead of `IndexManager` like other SDKs. The reason for + * that is the total operation of `DeleteAllIndexes` may exceed maximum + * operation per transaction. So the SDK needs more than one transaction to + * execute the task, while all functions in `IndexManager` can be carried out + * in one transaction. + */ + virtual void DeleteAllFieldIndexes() = 0; }; } // namespace local diff --git a/Firestore/core/src/local/query_context.h b/Firestore/core/src/local/query_context.h new file mode 100644 index 00000000000..a360e1cb54e --- /dev/null +++ b/Firestore/core/src/local/query_context.h @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Google LLC + * + * 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_LOCAL_QUERY_CONTEXT_H_ +#define FIRESTORE_CORE_SRC_LOCAL_QUERY_CONTEXT_H_ + +namespace firebase { +namespace firestore { +namespace local { + +/** A tracker to keep a record of important details during database local query + * execution. */ +class QueryContext { + public: + size_t GetDocumentReadCount() const { + return document_read_count_; + } + + void IncrementDocumentReadCount(size_t num) { + document_read_count_ += num; + } + + private: + /** Counts the number of documents passed through during local query + * execution. */ + size_t document_read_count_ = 0; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_LOCAL_QUERY_CONTEXT_H_ diff --git a/Firestore/core/src/local/query_engine.cc b/Firestore/core/src/local/query_engine.cc index 905900a6622..9d5aa38d3df 100644 --- a/Firestore/core/src/local/query_engine.cc +++ b/Firestore/core/src/local/query_engine.cc @@ -17,11 +17,11 @@ #include "Firestore/core/src/local/query_engine.h" #include -#include #include "Firestore/core/src/core/query.h" #include "Firestore/core/src/core/target.h" #include "Firestore/core/src/local/local_documents_view.h" +#include "Firestore/core/src/local/query_context.h" #include "Firestore/core/src/model/document.h" #include "Firestore/core/src/model/document_set.h" #include "Firestore/core/src/model/mutable_document.h" @@ -32,6 +32,20 @@ namespace firebase { namespace firestore { namespace local { +namespace { + +static const int kDefaultIndexAutoCreationMinCollectionSize = 100; + +/** + * This cost represents the evaluation result of (([index, docKey] + [docKey, + * docContent]) per document in the result set) / ([docKey, docContent] per + * documents in full collection scan) coming from experiment + * https://github.com/firebase/firebase-ios-sdk/pull/11716. + */ + +static const double KDefaultRelativeIndexReadCostPerDocument = 3.4; +} // namespace + using core::LimitType; using core::Query; using model::Document; @@ -44,6 +58,10 @@ using model::SnapshotVersion; void QueryEngine::Initialize(LocalDocumentsView* local_documents) { local_documents_view_ = local_documents; index_manager_ = local_documents->index_manager(); + index_auto_creation_min_collection_size_ = + kDefaultIndexAutoCreationMinCollectionSize; + relative_index_read_cost_per_document_ = + KDefaultRelativeIndexReadCostPerDocument; } const DocumentMap QueryEngine::GetDocumentsMatchingQuery( @@ -65,7 +83,44 @@ const DocumentMap QueryEngine::GetDocumentsMatchingQuery( return key_result.value(); } - return ExecuteFullCollectionScan(query); + absl::optional context = QueryContext(); + auto full_scan_result = ExecuteFullCollectionScan(query, context); + if (index_auto_creation_enabled_) { + CreateCacheIndexes(query, context.value(), full_scan_result.size()); + } + return full_scan_result; +} + +void QueryEngine::CreateCacheIndexes(const core::Query& query, + const QueryContext& context, + size_t result_size) const { + if (context.GetDocumentReadCount() < + index_auto_creation_min_collection_size_) { + LOG_DEBUG( + "SDK will not create cache indexes for query: %s, since it only " + "creates cache indexes for collection contains more than or equal to " + "%s documents.", + query.ToString(), index_auto_creation_min_collection_size_); + return; + } + + LOG_DEBUG( + "Query: %s, scans %s local documents and returns %s documents as " + "results.", + query.ToString(), context.GetDocumentReadCount(), result_size); + + if (context.GetDocumentReadCount() > + relative_index_read_cost_per_document_ * result_size) { + index_manager_->CreateTargetIndexes(query.ToTarget()); + LOG_DEBUG( + "The SDK decides to create cache indexes for query: %s, as using cache " + "indexes may help improve performance.", + query.ToString()); + } +} + +void QueryEngine::SetIndexAutoCreationEnabled(bool is_enabled) { + index_auto_creation_enabled_ = is_enabled; } absl::optional QueryEngine::PerformQueryUsingIndex( @@ -218,11 +273,11 @@ bool QueryEngine::NeedsRefill( } const DocumentMap QueryEngine::ExecuteFullCollectionScan( - const Query& query) const { + const Query& query, absl::optional& context) const { LOG_DEBUG("Using full collection scan to execute query: %s", query.ToString()); return local_documents_view_->GetDocumentsMatchingQuery( - query, model::IndexOffset::None()); + query, model::IndexOffset::None(), context); } const DocumentMap QueryEngine::AppendRemainingResults( diff --git a/Firestore/core/src/local/query_engine.h b/Firestore/core/src/local/query_engine.h index d159d29003c..7573bbcad8a 100644 --- a/Firestore/core/src/local/query_engine.h +++ b/Firestore/core/src/local/query_engine.h @@ -31,6 +31,7 @@ namespace local { class LocalDocumentsView; class IndexManager; +class QueryContext; /** * Firestore queries can be executed in three modes. The Query Engine determines @@ -78,7 +79,12 @@ class QueryEngine { const model::SnapshotVersion& last_limbo_free_snapshot_version, const model::DocumentKeySet& remote_keys) const; + void SetIndexAutoCreationEnabled(bool is_enabled); + private: + friend class IndexManagerTest; + friend class LocalStoreTestBase; + /** * Performs an indexed query that evaluates the query based on a collection's * persisted index values. Returns nullopt if an index is not available. @@ -118,7 +124,7 @@ class QueryEngine { const model::SnapshotVersion& limbo_free_snapshot_version) const; const model::DocumentMap ExecuteFullCollectionScan( - const core::Query& query) const; + const core::Query& query, absl::optional& context) const; /** * Combines the results from an indexed execution with the remaining documents @@ -129,9 +135,31 @@ class QueryEngine { const core::Query& query, const model::IndexOffset& offset) const; + void CreateCacheIndexes(const core::Query& query, + const QueryContext& context, + size_t result_size) const; + LocalDocumentsView* local_documents_view_ = nullptr; IndexManager* index_manager_ = nullptr; + + bool index_auto_creation_enabled_ = false; + + /** SDK only decides whether it should create index when collection size is + * larger than this. */ + size_t index_auto_creation_min_collection_size_; + + double relative_index_read_cost_per_document_; + + // For testing + void SetIndexAutoCreationMinCollectionSize(size_t new_min) { + index_auto_creation_min_collection_size_ = new_min; + } + + // For testing + void SetRelativeIndexReadCostPerDocument(double new_cost) { + relative_index_read_cost_per_document_ = new_cost; + } }; } // namespace local diff --git a/Firestore/core/src/local/remote_document_cache.h b/Firestore/core/src/local/remote_document_cache.h index 2b38cae0421..bfe84648c93 100644 --- a/Firestore/core/src/local/remote_document_cache.h +++ b/Firestore/core/src/local/remote_document_cache.h @@ -33,6 +33,7 @@ class Query; namespace local { class IndexManager; +class QueryContext; /** * Represents cached documents received from the remote backend. @@ -117,6 +118,32 @@ class RemoteDocumentCache { absl::optional limit = absl::nullopt, const model::OverlayByDocumentKeyMap& mutated_docs = {}) const = 0; + /** + * Executes a query against the cached Document entries + * + * Implementations may return extra documents if convenient. The results + * should be re-filtered by the consumer before presenting them to the user. + * + * Cached DeletedDocument entries have no bearing on query results. + * + * @param query The query to match documents against. + * @param offset The read time and document key to start scanning at + * (exclusive). + * @param context A optional tracker to keep a record of important details + * during database local query execution. + * @param limit The maximum number of results to return. + * If the limit is not defined, returns all matching documents. + * @param mutated_docs The documents with local mutations, they are read + * regardless if the remote version matches the given query. + * @return The set of matching documents. + */ + virtual model::MutableDocumentMap GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context, + absl::optional limit = absl::nullopt, + const model::OverlayByDocumentKeyMap& mutated_docs = {}) const = 0; + /** * Sets the index manager used by remote document cache. * diff --git a/Firestore/core/src/model/target_index_matcher.cc b/Firestore/core/src/model/target_index_matcher.cc index 62491c539ae..82e7a93c72e 100644 --- a/Firestore/core/src/model/target_index_matcher.cc +++ b/Firestore/core/src/model/target_index_matcher.cc @@ -16,7 +16,9 @@ #include "Firestore/core/src/model/target_index_matcher.h" +#include #include +#include #include "Firestore/core/src/util/hard_assert.h" @@ -116,6 +118,63 @@ bool TargetIndexMatcher::ServedByIndex(const model::FieldIndex& index) { return true; } +model::FieldIndex TargetIndexMatcher::BuildTargetIndex() { + // We want to make sure only one segment created for one field. For example, + // in case like a == 3 and a > 2, Index: {a ASCENDING} will only be created + // once. + // Since `FieldPath` doesn't have hash function, std::set is used instead of + // std::unordered_set + std::set unique_fields; + std::vector segments; + + for (const auto& filter : equality_filters_) { + if (filter.field().IsKeyFieldPath()) { + continue; + } + + bool is_array_operator = + filter.op() == FieldFilter::Operator::ArrayContains || + filter.op() == FieldFilter::Operator::ArrayContainsAny; + if (is_array_operator) { + segments.push_back(Segment(filter.field(), Segment::Kind::kContains)); + } else { + if (unique_fields.find(filter.field()) != unique_fields.end()) { + continue; + } + unique_fields.insert(filter.field()); + segments.push_back(Segment(filter.field(), Segment::Kind::kAscending)); + } + } + + // Note: We do not explicitly check `inequality_filter_` but rather rely on + // the target defining an appropriate `order_bys_` to ensure that the required + // index segment is added. The query engine would reject a query with an + // inequality filter that lacks the required order-by clause. + for (const auto& order_by : order_bys_) { + // Stop adding more segments if we see a order-by on key. Typically this is + // the default implicit order-by which is covered in the index_entry table + // as a separate column. If it is not the default order-by, the generated + // index will be missing some segments optimized for order-bys, which is + // probably fine. + if (order_by.field().IsKeyFieldPath()) { + continue; + } + + if (unique_fields.find(order_by.field()) != unique_fields.end()) { + continue; + } + unique_fields.insert(order_by.field()); + + segments.push_back(Segment( + order_by.field(), order_by.direction() == core::Direction::Ascending + ? Segment::Kind::kAscending + : Segment::Kind::kDescending)); + } + + return FieldIndex(FieldIndex::UnknownId(), collection_id_, + std::move(segments), FieldIndex::InitialState()); +} + bool TargetIndexMatcher::HasMatchingEqualityFilter(const Segment& segment) { for (const auto& filter : equality_filters_) { if (MatchesFilter(filter, segment)) { diff --git a/Firestore/core/src/model/target_index_matcher.h b/Firestore/core/src/model/target_index_matcher.h index 5f7c3f46a8a..669ba0c4f7d 100644 --- a/Firestore/core/src/model/target_index_matcher.h +++ b/Firestore/core/src/model/target_index_matcher.h @@ -77,6 +77,9 @@ class TargetIndexMatcher { */ bool ServedByIndex(const model::FieldIndex& index); + /** Returns a full matched field index for this target. */ + model::FieldIndex BuildTargetIndex(); + private: bool HasMatchingEqualityFilter(const model::Segment& segment); diff --git a/Firestore/core/test/unit/local/counting_query_engine.cc b/Firestore/core/test/unit/local/counting_query_engine.cc index 25aac47f567..3ad9e16614b 100644 --- a/Firestore/core/test/unit/local/counting_query_engine.cc +++ b/Firestore/core/test/unit/local/counting_query_engine.cc @@ -190,8 +190,18 @@ model::MutableDocumentMap WrappedRemoteDocumentCache::GetDocumentsMatchingQuery( const model::IndexOffset& offset, absl::optional limit, const model::OverlayByDocumentKeyMap& mutated_docs) const { - auto result = - subject_->GetDocumentsMatchingQuery(query, offset, limit, mutated_docs); + absl::optional context; + return GetDocumentsMatchingQuery(query, offset, context, limit, mutated_docs); +} + +model::MutableDocumentMap WrappedRemoteDocumentCache::GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context, + absl::optional limit, + const model::OverlayByDocumentKeyMap& mutated_docs) const { + auto result = subject_->GetDocumentsMatchingQuery(query, offset, context, + limit, mutated_docs); query_engine_->documents_read_by_query_ += result.size(); return result; } diff --git a/Firestore/core/test/unit/local/counting_query_engine.h b/Firestore/core/test/unit/local/counting_query_engine.h index 939d01fa59c..98853f4443b 100644 --- a/Firestore/core/test/unit/local/counting_query_engine.h +++ b/Firestore/core/test/unit/local/counting_query_engine.h @@ -202,6 +202,13 @@ class WrappedRemoteDocumentCache : public RemoteDocumentCache { absl::optional, const model::OverlayByDocumentKeyMap& mutated_docs) const override; + model::MutableDocumentMap GetDocumentsMatchingQuery( + const core::Query& query, + const model::IndexOffset& offset, + absl::optional& context, + absl::optional limit, + const model::OverlayByDocumentKeyMap& mutated_docs) const override; + void SetIndexManager(IndexManager* manager) override { index_manager_ = NOT_NULL(manager); } diff --git a/Firestore/core/test/unit/local/leveldb_index_manager_test.cc b/Firestore/core/test/unit/local/leveldb_index_manager_test.cc index 280ae2155b3..20445421c22 100644 --- a/Firestore/core/test/unit/local/leveldb_index_manager_test.cc +++ b/Firestore/core/test/unit/local/leveldb_index_manager_test.cc @@ -1552,6 +1552,60 @@ TEST_F(LevelDbIndexManagerTest, IndexTypeForOrQueries) { }); } +TEST_F(LevelDbIndexManagerTest, + CreateTargetIndexesCreatesFullIndexesForEachSubTarget) { + persistence_->Run( + "TestCreateTargetIndexesCreatesFullIndexesForEachSubTarget", [&]() { + index_manager_->Start(); + + auto query = Query("coll").AddingFilter( + OrFilters({Filter("a", "==", 1), Filter("b", "==", 2), + Filter("c", "==", 3)})); + + auto subQuery1 = Query("coll").AddingFilter(Filter("a", "==", 1)); + auto subQuery2 = Query("coll").AddingFilter(Filter("b", "==", 2)); + auto subQuery3 = Query("coll").AddingFilter(Filter("c", "==", 3)); + + ValidateIndexType(query, IndexManager::IndexType::NONE); + ValidateIndexType(subQuery1, IndexManager::IndexType::NONE); + ValidateIndexType(subQuery2, IndexManager::IndexType::NONE); + ValidateIndexType(subQuery3, IndexManager::IndexType::NONE); + + index_manager_->CreateTargetIndexes(query.ToTarget()); + + ValidateIndexType(query, IndexManager::IndexType::FULL); + ValidateIndexType(subQuery1, IndexManager::IndexType::FULL); + ValidateIndexType(subQuery2, IndexManager::IndexType::FULL); + ValidateIndexType(subQuery3, IndexManager::IndexType::FULL); + }); +} + +TEST_F(LevelDbIndexManagerTest, + CreateTargetIndexesUpgradesPartialIndexToFullIndex) { + persistence_->Run( + "TestCreateTargetIndexesUpgradesPartialIndexToFullIndex", [&]() { + index_manager_->Start(); + + auto query = Query("coll").AddingFilter( + AndFilters({Filter("a", "==", 1), Filter("b", "==", 2)})); + + auto subQuery1 = Query("coll").AddingFilter(Filter("a", "==", 1)); + auto subQuery2 = Query("coll").AddingFilter(Filter("b", "==", 2)); + + index_manager_->CreateTargetIndexes(subQuery1.ToTarget()); + + ValidateIndexType(query, IndexManager::IndexType::PARTIAL); + ValidateIndexType(subQuery1, IndexManager::IndexType::FULL); + ValidateIndexType(subQuery2, IndexManager::IndexType::NONE); + + index_manager_->CreateTargetIndexes(query.ToTarget()); + + ValidateIndexType(query, IndexManager::IndexType::FULL); + ValidateIndexType(subQuery1, IndexManager::IndexType::FULL); + ValidateIndexType(subQuery2, IndexManager::IndexType::NONE); + }); +} + } // namespace local } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/unit/local/leveldb_local_store_test.cc b/Firestore/core/test/unit/local/leveldb_local_store_test.cc index cd3dbace267..fb233f12fd3 100644 --- a/Firestore/core/test/unit/local/leveldb_local_store_test.cc +++ b/Firestore/core/test/unit/local/leveldb_local_store_test.cc @@ -36,6 +36,7 @@ using model::FieldIndex; using model::IndexState; using testutil::AddedRemoteEvent; +using testutil::Array; using testutil::DeletedDoc; using testutil::DeleteMutation; using testutil::Doc; @@ -45,6 +46,7 @@ using testutil::Key; using testutil::MakeFieldIndex; using testutil::Map; using testutil::OrderBy; +using testutil::OrFilters; using testutil::OverlayTypeMap; using testutil::SetMutation; using testutil::UpdateRemoteEvent; @@ -85,8 +87,13 @@ INSTANTIATE_TEST_SUITE_P(LevelDbLocalStoreTest, class LevelDbLocalStoreTest : public LocalStoreTestBase { public: - LevelDbLocalStoreTest() : LocalStoreTestBase(Factory()) { + LevelDbLocalStoreTest() + : LocalStoreTestBase(Factory()), + max_operation_per_transaction_( + LevelDbPersistence::kMaxOperationPerTransaction) { } + + const size_t max_operation_per_transaction_; }; TEST_F(LevelDbLocalStoreTest, AddsIndexes) { @@ -322,6 +329,441 @@ TEST_F(LevelDbLocalStoreTest, IndexesServerTimestamps) { FSTAssertQueryReturned("coll/a"); } +TEST_F(LevelDbLocalStoreTest, CanAutoCreateIndexes) { + core::Query query = + testutil::Query("coll").AddingFilter(Filter("matches", "==", true)); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/a", 10, Map("matches", true)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/b", 10, Map("matches", false)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/c", 10, Map("matches", false)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/d", 10, Map("matches", false)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/e", 10, Map("matches", true)), {target_id})); + + // First time query runs without indexes. + // Based on current heuristic, collection document counts (5) > 2 * resultSize + // (2). Full matched index should be created. + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + BackfillIndexes(); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/f", 20, Map("matches", true)), {target_id})); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 1); + FSTAssertQueryReturned("coll/a", "coll/e", "coll/f"); +} + +TEST_F(LevelDbLocalStoreTest, CanAutoCreateIndexesWorksWithOrQuery) { + core::Query query = testutil::Query("coll").AddingFilter( + OrFilters({Filter("a", "==", 3), Filter("b", "==", true)})); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/a", 10, Map("b", true)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/b", 10, Map("b", false)), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent(Doc("coll/c", 10, Map("a", 5, "b", false)), + {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/d", 10, Map("a", true)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/e", 10, Map("a", 3, "b", true)), {target_id})); + + // First time query runs without indexes. + // Based on current heuristic, collection document counts (5) > 2 * resultSize + // (2). Full matched index should be created. + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + BackfillIndexes(); + + ApplyRemoteEvent(AddedRemoteEvent(Doc("coll/f", 20, Map("a", 3, "b", false)), + {target_id})); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 1); + FSTAssertQueryReturned("coll/a", "coll/e", "coll/f"); +} + +TEST_F(LevelDbLocalStoreTest, DoesNotAutoCreateIndexesForSmallCollections) { + core::Query query = testutil::Query("coll") + .AddingFilter(Filter("foo", "==", 9)) + .AddingFilter(Filter("count", ">=", 3)); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/a", 10, Map("foo", 9, "count", 5)), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/b", 10, Map("foo", 8, "count", 1)), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/c", 10, Map("foo", 9, "count", 0)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/d", 10, Map("count", 4)), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/e", 10, Map("foo", 9, "count", 3)), {target_id})); + + // SDK will not create indexes since collection size is too small. + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + BackfillIndexes(); + + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/f", 20, Map("foo", 9, "count", 15)), {target_id})); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 3); + FSTAssertQueryReturned("coll/a", "coll/e", "coll/f"); +} + +TEST_F(LevelDbLocalStoreTest, + DoesNotAutoCreateIndexesWhenIndexLookUpIsExpensive) { + core::Query query = testutil::Query("coll").AddingFilter( + Filter("array", "array-contains-any", Array(0, 7))); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(5); + + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/a", 10, Map("array", Array(2, 7))), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/b", 10, Map("array", Array())), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/c", 10, Map("array", Array(3))), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/d", 10, Map("array", Array(2, 10, 20))), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/e", 10, Map("array", Array(2, 0, 8))), {target_id})); + + // First time query runs without indexes. + // Based on current heuristic, collection document counts (5) > 2 * resultSize + // (2). Full matched index should be created. + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + BackfillIndexes(); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/f", 20, Map("array", Array(0))), {target_id})); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 3); + FSTAssertQueryReturned("coll/a", "coll/e", "coll/f"); +} + +TEST_F(LevelDbLocalStoreTest, IndexAutoCreationWorksWhenBackfillerRunsHalfway) { + core::Query query = + testutil::Query("coll").AddingFilter(Filter("matches", "==", "foo")); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/a", 10, Map("matches", "foo")), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/b", 10, Map("matches", "")), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/c", 10, Map("matches", "bar")), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/d", 10, Map("matches", 7)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/e", 10, Map("matches", "foo")), {target_id})); + + // First time query is running without indexes. + // Based on current heuristic, collection document counts (5) > 2 * resultSize + // (2). Full matched index should be created. + ExecuteQuery(query); + // Only document a matches the result + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + SetBackfillerMaxDocumentsToProcess(2); + BackfillIndexes(); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/f", 20, Map("matches", "foo")), {target_id})); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 1, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e", "coll/f"); +} + +TEST_F(LevelDbLocalStoreTest, + IndexCreatedByIndexAutoCreationExistsAfterTurnOffAutoCreation) { + core::Query query = + testutil::Query("coll").AddingFilter(Filter("value", "not-in", Array(3))); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/a", 10, Map("value", 5)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/b", 10, Map("value", 3)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/c", 10, Map("value", 3)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/d", 10, Map("value", 3)), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/e", 10, Map("value", 2)), {target_id})); + + // First time query runs without indexes. + // Based on current heuristic, collection document counts (5) > 2 * resultSize + // (2). Full matched index should be created. + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + SetIndexAutoCreationEnabled(false); + + BackfillIndexes(); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/f", 20, Map("value", 7)), {target_id})); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 1); + FSTAssertQueryReturned("coll/a", "coll/e", "coll/f"); +} + +TEST_F(LevelDbLocalStoreTest, DisableIndexAutoCreationWorks) { + core::Query query1 = + testutil::Query("coll").AddingFilter(Filter("value", "in", Array(0, 1))); + int target_id1 = AllocateQuery(query1); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/a", 10, Map("value", 1)), {target_id1})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/b", 10, Map("value", 8)), {target_id1})); + ApplyRemoteEvent(AddedRemoteEvent(Doc("coll/c", 10, Map("value", "string")), + {target_id1})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/d", 10, Map("value", false)), {target_id1})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/e", 10, Map("value", 0)), {target_id1})); + + // First time query is running without indexes. + // Based on current heuristic, collection document counts (5) > 2 * resultSize + // (2). Full matched index should be created. + ExecuteQuery(query1); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + SetIndexAutoCreationEnabled(false); + + BackfillIndexes(); + + ExecuteQuery(query1); + FSTAssertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 0); + FSTAssertQueryReturned("coll/a", "coll/e"); + + core::Query query2 = testutil::Query("foo").AddingFilter( + Filter("value", "!=", std::numeric_limits::quiet_NaN())); + int target_id2 = AllocateQuery(query2); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("foo/a", 10, Map("value", 5)), {target_id2})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("foo/b", 10, Map("value", std::numeric_limits::quiet_NaN())), + {target_id2})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("foo/c", 10, Map("value", std::numeric_limits::quiet_NaN())), + {target_id2})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("foo/d", 10, Map("value", std::numeric_limits::quiet_NaN())), + {target_id2})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("foo/e", 10, Map("value", "string")), {target_id2})); + + ExecuteQuery(query2); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("foo/a", "foo/e"); + + BackfillIndexes(); + + // Run the query in second time, test index won't be created + ExecuteQuery(query2); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("foo/a", "foo/e"); +} + +TEST_F(LevelDbLocalStoreTest, DeleteAllIndexesWorksWithIndexAutoCreation) { + core::Query query = + testutil::Query("coll").AddingFilter(Filter("value", "==", "match")); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/a", 10, Map("value", "match")), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/b", 10, Map("value", std::numeric_limits::quiet_NaN())), + {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/c", 10, Map("value", nullptr)), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent(Doc("coll/d", 10, Map("value", "mismatch")), + {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/e", 10, Map("value", "match")), {target_id})); + + // First time query is running without indexes. + // Based on current heuristic, collection document counts (5) > 2 * resultSize + // (2). Full matched index should be created. + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + BackfillIndexes(); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 0); + FSTAssertQueryReturned("coll/a", "coll/e"); + + DeleteAllIndexes(); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + // Field index is created again. + BackfillIndexes(); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 0); + FSTAssertQueryReturned("coll/a", "coll/e"); +} + +TEST_F(LevelDbLocalStoreTest, DeleteAllIndexesWorksWithManualAddedIndexes) { + FieldIndex index = + MakeFieldIndex("coll", 0, FieldIndex::InitialState(), "matches", + model::Segment::Kind::kAscending); + ConfigureFieldIndexes({index}); + + core::Query query = + testutil::Query("coll").AddingFilter(Filter("matches", "==", true)); + int target_id = AllocateQuery(query); + + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/a", 10, Map("matches", true)), {target_id})); + + BackfillIndexes(); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 1, /* byCollection= */ 0); + FSTAssertQueryReturned("coll/a"); + + DeleteAllIndexes(); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 1); + FSTAssertQueryReturned("coll/a"); +} + +TEST_F(LevelDbLocalStoreTest, + DeleteAllIndexesWorksWhenMoreThanOneTransactionRequiredToCompleteTask) { + FieldIndex index = + MakeFieldIndex("coll", 0, FieldIndex::InitialState(), "matches", + model::Segment::Kind::kAscending); + ConfigureFieldIndexes({index}); + + core::Query query = + testutil::Query("coll").AddingFilter(Filter("matches", "==", true)); + int target_id = AllocateQuery(query); + + // requires at least 2 transactions + const size_t num_of_documents = max_operation_per_transaction_ * 1.5; + + for (size_t count = 1; count <= num_of_documents; count++) { + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/" + std::to_string(count), 10, Map("matches", true)), + {target_id})); + } + + SetBackfillerMaxDocumentsToProcess(num_of_documents); + BackfillIndexes(); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ num_of_documents, + /* byCollection= */ 0); + + DeleteAllIndexes(); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, + /* byCollection= */ num_of_documents); +} + +TEST_F(LevelDbLocalStoreTest, IndexAutoCreationWorksWithMutation) { + core::Query query = testutil::Query("coll").AddingFilter( + Filter("value", "array-contains-any", Array(8, 1, "string"))); + int target_id = AllocateQuery(query); + + SetIndexAutoCreationEnabled(true); + SetMinCollectionSizeToAutoCreateIndex(0); + SetRelativeIndexReadCostPerDocument(2); + + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/a", 10, Map("value", Array(8, 1, "string"))), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/b", 10, Map("value", Array())), {target_id})); + ApplyRemoteEvent( + AddedRemoteEvent(Doc("coll/c", 10, Map("value", Array(3))), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/d", 10, Map("value", Array(0, 5))), {target_id})); + ApplyRemoteEvent(AddedRemoteEvent( + Doc("coll/e", 10, Map("value", Array("string"))), {target_id})); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + FSTAssertQueryReturned("coll/a", "coll/e"); + + WriteMutation(DeleteMutation("coll/e")); + + BackfillIndexes(); + + WriteMutation(SetMutation("coll/f", Map("value", Array(1)))); + + ExecuteQuery(query); + FSTAssertRemoteDocumentsRead(/* byKey= */ 1, /* byCollection= */ 0); + FSTAssertOverlaysRead(/* byKey= */ 1, /* byCollection= */ 1); + FSTAssertQueryReturned("coll/a", "coll/f"); +} + } // namespace local } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/unit/local/local_store_test.cc b/Firestore/core/test/unit/local/local_store_test.cc index b22553e7d4a..9864c681608 100644 --- a/Firestore/core/test/unit/local/local_store_test.cc +++ b/Firestore/core/test/unit/local/local_store_test.cc @@ -26,6 +26,7 @@ #include "Firestore/core/src/bundle/named_query.h" #include "Firestore/core/src/core/field_filter.h" #include "Firestore/core/src/credentials/user.h" +#include "Firestore/core/src/local/index_backfiller.h" #include "Firestore/core/src/local/local_view_changes.h" #include "Firestore/core/src/local/local_write_result.h" #include "Firestore/core/src/local/persistence.h" @@ -214,6 +215,10 @@ void LocalStoreTestBase::BackfillIndexes() { local_store_.Backfill(); } +void LocalStoreTestBase::SetBackfillerMaxDocumentsToProcess(size_t new_max) { + local_store_.index_backfiller()->SetMaxDocumentsToProcess(new_max); +} + void LocalStoreTestBase::UpdateViews(int target_id, bool from_cache) { NotifyLocalViewChanges(TestViewChanges(target_id, from_cache, {}, {})); } @@ -275,6 +280,22 @@ QueryResult LocalStoreTestBase::ExecuteQuery(const core::Query& query) { return last_query_result_; } +void LocalStoreTestBase::SetIndexAutoCreationEnabled(bool is_enabled) { + query_engine_.SetIndexAutoCreationEnabled(is_enabled); +} + +void LocalStoreTestBase::DeleteAllIndexes() const { + local_store_.DeleteAllFieldIndexes(); +} + +void LocalStoreTestBase::SetMinCollectionSizeToAutoCreateIndex(size_t new_min) { + query_engine_.SetIndexAutoCreationMinCollectionSize(new_min); +} + +void LocalStoreTestBase::SetRelativeIndexReadCostPerDocument(double new_cost) { + query_engine_.SetRelativeIndexReadCostPerDocument(new_cost); +} + void LocalStoreTestBase::ApplyBundledDocuments( const std::vector& documents) { last_changes_ = diff --git a/Firestore/core/test/unit/local/local_store_test.h b/Firestore/core/test/unit/local/local_store_test.h index 2a1cb0ac528..1271bc4fa1b 100644 --- a/Firestore/core/test/unit/local/local_store_test.h +++ b/Firestore/core/test/unit/local/local_store_test.h @@ -78,6 +78,7 @@ class LocalStoreTestBase : public testing::Test { void ApplyRemoteEvent(const remote::RemoteEvent& event); void NotifyLocalViewChanges(LocalViewChanges changes); void BackfillIndexes(); + void SetBackfillerMaxDocumentsToProcess(size_t new_max); void AcknowledgeMutationWithVersion( int64_t document_version, absl::optional> @@ -89,6 +90,10 @@ class LocalStoreTestBase : public testing::Test { model::TargetId AllocateQuery(core::Query query); local::TargetData GetTargetData(const core::Query& query); local::QueryResult ExecuteQuery(const core::Query& query); + void SetIndexAutoCreationEnabled(bool is_enabled); + void DeleteAllIndexes() const; + void SetMinCollectionSizeToAutoCreateIndex(size_t new_min); + void SetRelativeIndexReadCostPerDocument(double new_cost); void ApplyBundledDocuments( const std::vector& documents); diff --git a/Firestore/core/test/unit/model/target_index_matcher_test.cc b/Firestore/core/test/unit/model/target_index_matcher_test.cc index ea0d8c6d40c..f07b7bfc66d 100644 --- a/Firestore/core/test/unit/model/target_index_matcher_test.cc +++ b/Firestore/core/test/unit/model/target_index_matcher_test.cc @@ -20,6 +20,7 @@ #include #include "Firestore/core/src/core/query.h" +#include "Firestore/core/src/core/target.h" #include "Firestore/core/src/model/document_key.h" #include "Firestore/core/src/model/field_index.h" #include "Firestore/core/test/unit/testutil/testutil.h" @@ -59,6 +60,18 @@ std::vector QueriesWithArrayContains() { Filter("a", "array-contains-any", Array("a")))}; } +std::vector QueriesWithOrderBys() { + return {testutil::Query("collId").AddingOrderBy(OrderBy("a")), + testutil::Query("collId").AddingOrderBy(OrderBy("a", "desc")), + testutil::Query("collId").AddingOrderBy(OrderBy("a", "asc")), + testutil::Query("collId") + .AddingOrderBy(OrderBy("a")) + .AddingOrderBy(OrderBy("__name__")), + testutil::Query("collId") + .AddingFilter(Filter("a", "array-contains", "a")) + .AddingOrderBy(OrderBy("b"))}; +} + void ValidateServesTarget(const core::Query& query, const std::string& field, Segment::Kind kind) { @@ -123,6 +136,15 @@ void ValidateDoesNotServeTarget(const core::Query& query, EXPECT_FALSE(matcher.ServedByIndex(expected_index)); } +void ValidateBuildTargetIndexCreateFullMatchIndex(const core::Query& query) { + const core::Target& target = query.ToTarget(); + TargetIndexMatcher matcher(target); + FieldIndex expected_index = matcher.BuildTargetIndex(); + EXPECT_TRUE(matcher.ServedByIndex(expected_index)); + // Check the index created is a FULL MATCH index + EXPECT_TRUE(expected_index.segments().size() >= target.GetSegmentCount()); +} + TEST(TargetIndexMatcher, CanUseMergeJoin) { auto q = testutil::Query("collId") .AddingFilter(Filter("a", "==", 1)) @@ -557,6 +579,155 @@ TEST(TargetIndexMatcher, WithEqualityAndInequalityOnTheSameField) { "a", Segment::Kind::kDescending); } +TEST(TargetIndexMatcher, BuildTargetIndexWithQueriesWithEqualities) { + for (const auto& query : QueriesWithEqualities()) { + ValidateBuildTargetIndexCreateFullMatchIndex(query); + } +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithQueriesWithInequalities) { + for (const auto& query : QueriesWithInequalities()) { + ValidateBuildTargetIndexCreateFullMatchIndex(query); + } +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithQueriesWithArrayContains) { + for (const auto& query : QueriesWithArrayContains()) { + ValidateBuildTargetIndexCreateFullMatchIndex(query); + } +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithQueriesWithOrderBys) { + for (const auto& query : QueriesWithOrderBys()) { + ValidateBuildTargetIndexCreateFullMatchIndex(query); + } +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithInequalityUsesSingleFieldIndex) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", ">", 1)) + .AddingFilter(Filter("a", "<", 10)); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithCollection) { + auto query = testutil::Query("collId"); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithArrayContainsAndOrderBy) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", "array-contains", "a")) + .AddingFilter(Filter("a", ">", "b")) + .AddingOrderBy(OrderBy("a", "asc")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithEqualityAndDescendingOrder) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", "==", 1)) + .AddingOrderBy(OrderBy("__name__", "desc")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithMultipleEqualities) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a1", "==", "a")) + .AddingFilter(Filter("a2", "==", "b")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithMultipleEqualitiesAndInequality) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("equality1", "==", "a")) + .AddingFilter(Filter("equality2", "==", "b")) + .AddingFilter(Filter("inequality", ">=", "c")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); + query = testutil::Query("collId") + .AddingFilter(Filter("equality1", "==", "a")) + .AddingFilter(Filter("inequality", ">=", "c")) + .AddingFilter(Filter("equality2", "==", "b")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithMultipleFilters) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", "==", "a")) + .AddingFilter(Filter("b", ">", "b")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); + query = testutil::Query("collId") + .AddingFilter(Filter("a1", "==", "a")) + .AddingFilter(Filter("a2", ">", "b")) + .AddingOrderBy(OrderBy("a2", "asc")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); + query = testutil::Query("collId") + .AddingFilter(Filter("a", ">=", 1)) + .AddingFilter(Filter("a", "==", 5)) + .AddingFilter(Filter("a", "<=", 10)); + ValidateBuildTargetIndexCreateFullMatchIndex(query); + query = testutil::Query("collId") + .AddingFilter(Filter("a", "not-in", Array(1, 2, 3))) + .AddingFilter(Filter("a", ">=", 2)); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithMultipleOrderBys) { + auto query = testutil::Query("collId") + .AddingOrderBy(OrderBy("fff")) + .AddingOrderBy(OrderBy("bar", "desc")) + .AddingOrderBy(OrderBy("__name__")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); + query = testutil::Query("collId") + .AddingOrderBy(OrderBy("foo")) + .AddingOrderBy(OrderBy("bar")) + .AddingOrderBy(OrderBy("__name__", "desc")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithInAndNotIn) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", "not-in", Array(1, 2, 3))) + .AddingFilter(Filter("b", "in", Array(1, 2, 3))); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithEqualityAndDifferentOrderBy) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("foo", "==", "")) + .AddingFilter(Filter("bar", "==", "")) + .AddingOrderBy(OrderBy("qux")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); + query = testutil::Query("collId") + .AddingFilter(Filter("aaa", "==", "")) + .AddingFilter(Filter("qqq", "==", "")) + .AddingFilter(Filter("ccc", "==", "")) + .AddingOrderBy(OrderBy("fff", "desc")) + .AddingOrderBy(OrderBy("bbb")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithEqualsAndNotIn) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", "==", 1)) + .AddingFilter(Filter("b", "not-in", Array(1, 2, 3))); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithInAndOrderBy) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", "not-in", Array(1, 2, 3))) + .AddingOrderBy(OrderBy("a")) + .AddingOrderBy(OrderBy("b")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + +TEST(TargetIndexMatcher, BuildTargetIndexWithInAndOrderBySameField) { + auto query = testutil::Query("collId") + .AddingFilter(Filter("a", "in", Array(1, 2, 3))) + .AddingOrderBy(OrderBy("a")); + ValidateBuildTargetIndexCreateFullMatchIndex(query); +} + } // namespace } // namespace model } // namespace firestore