diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 8a1cea385ce..9204d7a1865 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [feature] Expose MultiDb support in API. [#4015](//github.com/firebase/firebase-android-sdk/issues/4015) # 24.6.1 * [feature] Implemented an optimization in the local cache synchronization logic that reduces the number of billed document reads when documents were deleted on the server while the client was not actively listening to the query (e.g. while the client was offline). (GitHub [#4982](//github.com/firebase/firebase-android-sdk/pull/4982){: .external}) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 64e0c639166..6b54d4140db 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -186,6 +186,8 @@ package com.google.firebase.firestore { method @NonNull public com.google.firebase.firestore.FirebaseFirestoreSettings getFirestoreSettings(); method @NonNull public static com.google.firebase.firestore.FirebaseFirestore getInstance(); method @NonNull public static com.google.firebase.firestore.FirebaseFirestore getInstance(@NonNull com.google.firebase.FirebaseApp); + method @NonNull public static com.google.firebase.firestore.FirebaseFirestore getInstance(@NonNull String); + method @NonNull public static com.google.firebase.firestore.FirebaseFirestore getInstance(@NonNull com.google.firebase.FirebaseApp, @NonNull String); method @NonNull public com.google.android.gms.tasks.Task getNamedQuery(@NonNull String); method @NonNull public com.google.firebase.firestore.LoadBundleTask loadBundle(@NonNull java.io.InputStream); method @NonNull public com.google.firebase.firestore.LoadBundleTask loadBundle(@NonNull byte[]); diff --git a/firebase-firestore/ktx/api.txt b/firebase-firestore/ktx/api.txt index 2adeaede3ff..3307ecb1215 100644 --- a/firebase-firestore/ktx/api.txt +++ b/firebase-firestore/ktx/api.txt @@ -5,6 +5,8 @@ package com.google.firebase.firestore.ktx { method public static inline kotlinx.coroutines.flow.Flow> dataObjects(@NonNull com.google.firebase.firestore.Query, @NonNull com.google.firebase.firestore.MetadataChanges metadataChanges = com.google.firebase.firestore.MetadataChanges.EXCLUDE); method public static inline kotlinx.coroutines.flow.Flow dataObjects(@NonNull com.google.firebase.firestore.DocumentReference, @NonNull com.google.firebase.firestore.MetadataChanges metadataChanges = com.google.firebase.firestore.MetadataChanges.EXCLUDE); method @NonNull public static com.google.firebase.firestore.FirebaseFirestore firestore(@NonNull com.google.firebase.ktx.Firebase, @NonNull com.google.firebase.FirebaseApp app); + method @NonNull public static com.google.firebase.firestore.FirebaseFirestore firestore(@NonNull com.google.firebase.ktx.Firebase, @NonNull com.google.firebase.FirebaseApp app, @NonNull String database); + method @NonNull public static com.google.firebase.firestore.FirebaseFirestore firestore(@NonNull com.google.firebase.ktx.Firebase, @NonNull String database); method @NonNull public static com.google.firebase.firestore.FirebaseFirestoreSettings firestoreSettings(@NonNull kotlin.jvm.functions.Function1 init); method public static inline T getField(@NonNull com.google.firebase.firestore.DocumentSnapshot, @NonNull String field); method public static inline T getField(@NonNull com.google.firebase.firestore.DocumentSnapshot, @NonNull String field, @NonNull com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior); diff --git a/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt b/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt index 733b8f44162..0523beea7f9 100644 --- a/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt +++ b/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt @@ -36,6 +36,16 @@ val Firebase.firestore: FirebaseFirestore /** Returns the [FirebaseFirestore] instance of a given [FirebaseApp]. */ fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFirestore.getInstance(app) +/** Returns the [FirebaseFirestore] instance of a given [FirebaseApp] and database name. */ +fun Firebase.firestore(app: FirebaseApp, database: String): FirebaseFirestore = + FirebaseFirestore.getInstance(app, database) + +/** + * Returns the [FirebaseFirestore] instance of the default [FirebaseApp], given the database name. + */ +fun Firebase.firestore(database: String): FirebaseFirestore = + FirebaseFirestore.getInstance(database) + /** * Returns the contents of the document converted to a POJO or null if the document doesn't exist. * diff --git a/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt b/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt index 605ebdbc4ca..3116d4aaf28 100644 --- a/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt +++ b/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt @@ -79,12 +79,24 @@ class FirestoreTests : BaseTestCase() { assertThat(Firebase.firestore).isSameInstanceAs(FirebaseFirestore.getInstance()) } + @Test + fun `Database#firestore should delegate to FirebaseFirestore#getInstance(Database)`() { + assertThat(Firebase.firestore("name")).isSameInstanceAs(FirebaseFirestore.getInstance("name")) + } + @Test fun `FirebaseApp#firestore should delegate to FirebaseFirestore#getInstance(FirebaseApp)`() { val app = Firebase.app(EXISTING_APP) assertThat(Firebase.firestore(app)).isSameInstanceAs(FirebaseFirestore.getInstance(app)) } + @Test + fun `FirebaseApp#Database#firestore should delegate to FirebaseFirestore#getInstance(FirebaseApp,Database)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.firestore(app, "name")) + .isSameInstanceAs(FirebaseFirestore.getInstance(app, "name")) + } + @Test fun `FirebaseFirestoreSettings builder works`() { val host = "http://10.0.2.2:8080" diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java index 65da03d51d8..fff35290d98 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java @@ -15,6 +15,7 @@ package com.google.firebase.firestore; import static com.google.firebase.firestore.AccessHelper.getAsyncQueue; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.isRunningAgainstEmulator; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.newTestSettings; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.provider; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testChangeUserTo; @@ -38,6 +39,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.tasks.Task; @@ -47,6 +49,7 @@ import com.google.firebase.firestore.FirebaseFirestoreException.Code; import com.google.firebase.firestore.Query.Direction; import com.google.firebase.firestore.auth.User; +import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.testutil.EventAccumulator; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.AsyncQueue.TimerId; @@ -1132,6 +1135,85 @@ public void testAppDeleteLeadsToFirestoreTerminate() { assertTrue(instance.getClient().isTerminated()); } + @Test + public void testDefaultNamedDbIsSame() { + FirebaseApp app = FirebaseApp.getInstance(); + FirebaseFirestore db1 = FirebaseFirestore.getInstance(); + FirebaseFirestore db2 = FirebaseFirestore.getInstance(app); + FirebaseFirestore db3 = FirebaseFirestore.getInstance(app, "(default)"); + FirebaseFirestore db4 = FirebaseFirestore.getInstance("(default)"); + + assertSame(db1, db2); + assertSame(db1, db3); + assertSame(db1, db4); + } + + @Test + public void testSameNamedDbIsSame() { + FirebaseApp app = FirebaseApp.getInstance(); + FirebaseFirestore db1 = FirebaseFirestore.getInstance(app, "myDb"); + FirebaseFirestore db2 = FirebaseFirestore.getInstance("myDb"); + + assertSame(db1, db2); + } + + @Test + public void testDifferentDbNamesAreDifferent() { + FirebaseFirestore db1 = FirebaseFirestore.getInstance(); + FirebaseFirestore db2 = FirebaseFirestore.getInstance("db1"); + FirebaseFirestore db3 = FirebaseFirestore.getInstance("db2"); + + assertNotSame(db1, db2); + assertNotSame(db1, db3); + assertNotSame(db2, db3); + } + + @Test + public void testNamedDbHaveDifferentPersistence() { + // TODO: Have backend with named databases created beforehand. + // Emulator doesn't care if database was created beforehand. + assumeTrue(isRunningAgainstEmulator()); + + // FirebaseFirestore db1 = FirebaseFirestore.getInstance(); + String projectId = provider().projectId(); + FirebaseFirestore db1 = + testFirestore( + DatabaseId.forDatabase(projectId, "db1"), + Level.DEBUG, + newTestSettings(), + "dbPersistenceKey"); + FirebaseFirestore db2 = + testFirestore( + DatabaseId.forDatabase(projectId, "db2"), + Level.DEBUG, + newTestSettings(), + "dbPersistenceKey"); + + DocumentReference docRef = db1.collection("col1").document("doc1"); + waitFor(docRef.set(Collections.singletonMap("foo", "bar"))); + assertEquals(waitFor(docRef.get(Source.SERVER)).get("foo"), "bar"); + + String path = docRef.getPath(); + DocumentReference docRef2 = db2.document(path); + + { + Exception e = waitForException(docRef2.get(Source.CACHE)); + assertEquals(Code.UNAVAILABLE, ((FirebaseFirestoreException) e).getCode()); + } + + { + Task task = docRef2.get(Source.SERVER); + DocumentSnapshot result = waitFor(task); + assertNull(result.getDocument()); + } + + { + Task task = docRef2.get(Source.DEFAULT); + DocumentSnapshot result = waitFor(task); + assertNull(result.getDocument()); + } + } + @Test public void testNewOperationThrowsAfterFirestoreTerminate() { FirebaseFirestore instance = testFirestore(); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java index f6edabfcb74..8812fbab14d 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java @@ -95,7 +95,15 @@ public void disableSslWithoutSettingHostFails() { @Test public void firestoreGetInstanceWithNullAppFails() { expectError( - () -> FirebaseFirestore.getInstance(null), "Provided FirebaseApp must not be null."); + () -> FirebaseFirestore.getInstance((FirebaseApp) null), + "Provided FirebaseApp must not be null."); + } + + @Test + public void firestoreGetInstanceWithNullDbNamepFails() { + expectError( + () -> FirebaseFirestore.getInstance((String) null), + "Provided database name must not be null."); } @Test diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index 3f0196e9d05..53e8149d845 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -282,11 +282,22 @@ public static FirebaseFirestore testFirestore( Logger.Level logLevel, FirebaseFirestoreSettings settings, String persistenceKey) { + return testFirestore( + DatabaseId.forDatabase(projectId, DatabaseId.DEFAULT_DATABASE_ID), + logLevel, + settings, + persistenceKey); + } + + public static FirebaseFirestore testFirestore( + DatabaseId databaseId, + Logger.Level logLevel, + FirebaseFirestoreSettings settings, + String persistenceKey) { // This unfortunately is a global setting that affects existing Firestore clients. Logger.setLogLevel(logLevel); Context context = ApplicationProvider.getApplicationContext(); - DatabaseId databaseId = DatabaseId.forDatabase(projectId, DatabaseId.DEFAULT_DATABASE_ID); ensureStrictMode(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index add8ab6983c..d0dea0a7ca4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -105,23 +105,68 @@ public interface InstanceRegistry { private final GrpcMetadataProvider metadataProvider; @NonNull - public static FirebaseFirestore getInstance() { + private static FirebaseApp getDefaultFirebaseApp() { FirebaseApp app = FirebaseApp.getInstance(); if (app == null) { throw new IllegalStateException("You must call FirebaseApp.initializeApp first."); } - return getInstance(app, DatabaseId.DEFAULT_DATABASE_ID); + return app; + } + + /** + * Returns the default {@link FirebaseFirestore} instance associated with the default {@link + * FirebaseApp}. Returns the same instance for all invocations. If no instance exists, initializes + * a new instance with default settings. + * + * @returns The {@link FirebaseFirestore} instance. + */ + @NonNull + public static FirebaseFirestore getInstance() { + return getInstance(getDefaultFirebaseApp(), DatabaseId.DEFAULT_DATABASE_ID); } + /** + * Returns the default {@link FirebaseFirestore} instance that is associated with the provided + * {@link FirebaseApp}. For a given {@link FirebaseApp}, invocation always returns the same + * instance. If no instance exists, initializes a new instance with default settings. + * + * @param app - The {@link FirebaseApp} instance that the returned {@link FirebaseFirestore} + * instance is associated with. + * @returns The {@link FirebaseFirestore} instance. + */ @NonNull public static FirebaseFirestore getInstance(@NonNull FirebaseApp app) { return getInstance(app, DatabaseId.DEFAULT_DATABASE_ID); } - // TODO: make this public + /** + * Returns the {@link FirebaseFirestore} instance that is associated with the default {@link + * FirebaseApp}. Returns the same instance for all invocations given the same database parameter. + * If no instance exists, initializes a new instance with default settings. + * + * @param database - The name of database. + * @returns The {@link FirebaseFirestore} instance. + */ + @NonNull + public static FirebaseFirestore getInstance(@NonNull String database) { + return getInstance(getDefaultFirebaseApp(), database); + } + + /** + * Returns the {@link FirebaseFirestore} instance that is associated with the provided {@link + * FirebaseApp}. Returns the same instance for all invocations given the same {@link FirebaseApp} + * and database parameter. If no instance exists, initializes a new instance with default + * settings. + * + * @param app - The {@link FirebaseApp} instance that the returned {@link FirebaseFirestore} + * instance is associated with. + * @param database - The name of database. + * @returns The {@link FirebaseFirestore} instance. + */ @NonNull - private static FirebaseFirestore getInstance(@NonNull FirebaseApp app, @NonNull String database) { + public static FirebaseFirestore getInstance(@NonNull FirebaseApp app, @NonNull String database) { checkNotNull(app, "Provided FirebaseApp must not be null."); + checkNotNull(database, "Provided database name must not be null."); FirestoreMultiDbComponent component = app.get(FirestoreMultiDbComponent.class); checkNotNull(component, "Firestore component is not present."); return component.get(database);