Skip to content

Commit

Permalink
Expose Multi-DB, and add tests. (#4015)
Browse files Browse the repository at this point in the history
* Expose Multi-DB, and add tests.

* Undo

* Add documentation and fix test.

* Make pretty

* Prevent public access by making multi-db methods package-private

* Revert "Prevent public access by making multi-db methods package-private"

This reverts commit 8fbc22f.

* Format

* API

* Fix method description

* Changelog

* Format
  • Loading branch information
tom-andersen committed Jun 9, 2023
1 parent c1be5f9 commit e9d0de9
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 6 deletions.
1 change: 1 addition & 0 deletions firebase-firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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})
Expand Down
2 changes: 2 additions & 0 deletions firebase-firestore/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<com.google.firebase.firestore.Query> 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[]);
Expand Down
2 changes: 2 additions & 0 deletions firebase-firestore/ktx/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package com.google.firebase.firestore.ktx {
method public static inline <reified T> kotlinx.coroutines.flow.Flow<? extends java.util.List<? extends T>> dataObjects(@NonNull com.google.firebase.firestore.Query, @NonNull com.google.firebase.firestore.MetadataChanges metadataChanges = com.google.firebase.firestore.MetadataChanges.EXCLUDE);
method public static inline <reified T> kotlinx.coroutines.flow.Flow<? extends T> 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<? super com.google.firebase.firestore.FirebaseFirestoreSettings.Builder,kotlin.Unit> init);
method public static inline <reified T> T getField(@NonNull com.google.firebase.firestore.DocumentSnapshot, @NonNull String field);
method public static inline <reified T> T getField(@NonNull com.google.firebase.firestore.DocumentSnapshot, @NonNull String field, @NonNull com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<DocumentSnapshot> task = docRef2.get(Source.SERVER);
DocumentSnapshot result = waitFor(task);
assertNull(result.getDocument());
}

{
Task<DocumentSnapshot> task = docRef2.get(Source.DEFAULT);
DocumentSnapshot result = waitFor(task);
assertNull(result.getDocument());
}
}

@Test
public void testNewOperationThrowsAfterFirestoreTerminate() {
FirebaseFirestore instance = testFirestore();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit e9d0de9

Please sign in to comment.