Skip to content

Commit

Permalink
feat: add support for multiple databases
Browse files Browse the repository at this point in the history
  • Loading branch information
hemanshv authored and jskeet committed Mar 10, 2023
1 parent 00967cb commit 3a40583
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 40 deletions.
@@ -0,0 +1,75 @@
// Copyright 2022, 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
//
// https://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.

using Google.Cloud.ClientTesting;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.Firestore.IntegrationTests;

[Collection(nameof(MultipleFirestoreDbFixture))]
public class FirestoreMultipleDbTest
{
private readonly MultipleFirestoreDbFixture _fixture;
public FirestoreMultipleDbTest(MultipleFirestoreDbFixture multipleFirestoreDbFixture) =>
_fixture = multipleFirestoreDbFixture;

[Fact]
public async Task MultipleDbReadWriteTest()
{
string dbIdPrefix = IdGenerator.FromGuid(prefix: "db", separator: "-");
string correctDbId = $"{dbIdPrefix}-correct";
string incorrectDbId = $"{dbIdPrefix}-incorrect";
string databaseLocationId = "us-east1";
string databaseType = "FIRESTORE_NATIVE";

// Create two databases one for reading and writing, one for reading to validate that correct database should be referenced.
await _fixture.CreateDatabaseAsync(correctDbId, databaseLocationId, databaseType);
await _fixture.CreateDatabaseAsync(incorrectDbId, databaseLocationId, databaseType);

// Write data to the first database; referenced as correct database.
FirestoreDb correctDbWriter = new FirestoreDbBuilder
{
DatabaseId = correctDbId,
ProjectId = _fixture.ProjectId
}.Build();
await correctDbWriter.Collection("cities").Document("SF").SetAsync(new Dictionary<string, object>
{
{ "Name", "San Francisco" },
{ "State", "CA" },
{ "Country", "USA" },
{ "Capital", false },
{ "Population", 860000 }
});

// Read data from database that was referenced while writing.
FirestoreDb correctDbReader = new FirestoreDbBuilder
{
DatabaseId = correctDbId,
ProjectId = _fixture.ProjectId
}.Build();
DocumentReference readerDocRef = correctDbReader.Collection("cities").Document("SF");
DocumentSnapshot correctDbSnapshot = await readerDocRef.GetSnapshotAsync();
Dictionary<string, object> items = correctDbSnapshot.ToDictionary();
Assert.True(correctDbSnapshot.Exists);
Assert.Equal(5, items.Count);

// Try to read data from random incorrect database, to validate that the snapshot and data do not exist.
FirestoreDb incorrectDbReader = new FirestoreDbBuilder { DatabaseId = incorrectDbId, ProjectId = _fixture.ProjectId }.Build();
DocumentReference incorrectDbDocRef = incorrectDbReader.Collection("cities").Document("SF");
DocumentSnapshot incorrectDbSnapshot = await incorrectDbDocRef.GetSnapshotAsync();
Assert.False(incorrectDbSnapshot.Exists);
}
}
@@ -0,0 +1,59 @@
// Copyright 2022, Google Inc. All rights reserved.
//
// 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.

using Google.Apis.Auth.OAuth2;
using Google.Apis.Http;
using Google.Cloud.ClientTesting;
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.Firestore.IntegrationTests;

[CollectionDefinition(nameof(MultipleFirestoreDbFixture))]
public class MultipleFirestoreDbFixture : CloudProjectFixtureBase, ICollectionFixture<MultipleFirestoreDbFixture>
{
private const string RestApiUrlBase = "https://firestore.googleapis.com/v1";
private const string ProjectEnvironmentVariable = "FIRESTORE_TEST_PROJECT";
private readonly HttpClient _httpClient;

public MultipleFirestoreDbFixture() : base(ProjectEnvironmentVariable)
{
// Scope used for the REST API to create databases.
string scope = "https://www.googleapis.com/auth/datastore";
string credentialsPath = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS");

// Initalize gcloud credentials to be used with REST API calls.
GoogleCredential googleCredential = GoogleCredential.FromFile(credentialsPath).CreateScoped(scope);
_httpClient = new HttpClientFactory()
.CreateHttpClient(new CreateHttpClientArgs { Initializers = { googleCredential } });
}

// TODO: Use Google.Cloud.Firestore.Admin.V1 when that supports this operation.

/// <summary>
/// Creates a new Firestore database using the REST API.
/// </summary>
public async Task CreateDatabaseAsync(string databaseId, string locationId, string databaseType)
{
var createDatabaseEndpoint = new Uri($"{RestApiUrlBase}/projects/{ProjectId}/databases?database_id={databaseId}");
var data = new { locationId = locationId, type = databaseType };
var httpData = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(createDatabaseEndpoint, httpData);
response.EnsureSuccessStatusCode();
}
}
Expand Up @@ -19,48 +19,73 @@
using System;
using Xunit;

namespace Google.Cloud.Firestore.Tests
namespace Google.Cloud.Firestore.Tests;

public class FirestoreDbBuilderTest
{
public class FirestoreDbBuilderTest
[Fact]
public void MaximalConfiguration()
{
string warning = null;
var builder = new FirestoreDbBuilder
{
ProjectId = "proj",
DatabaseId = "db",
CallInvoker = new FakeCallInvoker(),
WarningLogger = text => warning = text,
Settings = new FirestoreSettings { Clock = new FakeClock() },
ConverterRegistry = new ConverterRegistry { new SerializationTestData.GuidConverter() }
};
var db = builder.Build();
Assert.Equal("proj", db.ProjectId);
Assert.Equal("db", db.DatabaseId);
db.LogWarning("Test warning");
Assert.Equal("Test warning", warning);
Assert.IsType<FakeClock>(db.Client.Settings.Clock);
Assert.IsType<CustomConverter<Guid>>(db.SerializationContext.GetConverter(typeof(Guid)));
}

[Fact]
public void DefaultDatabase()
{
[Fact]
public void MaximalConfiguration()
string projectId = "testProjectId";
FirestoreDb defaultDb = new FirestoreDbBuilder
{
string warning = null;
var builder = new FirestoreDbBuilder
{
ProjectId = "proj",
DatabaseId = "db",
CallInvoker = new FakeCallInvoker(),
WarningLogger = text => warning = text,
Settings = new FirestoreSettings { Clock = new FakeClock() },
ConverterRegistry = new ConverterRegistry { new SerializationTestData.GuidConverter() }
};
var db = builder.Build();
Assert.Equal("proj", db.ProjectId);
Assert.Equal("db", db.DatabaseId);
db.LogWarning("Test warning");
Assert.Equal("Test warning", warning);
Assert.IsType<FakeClock>(db.Client.Settings.Clock);
Assert.IsType<CustomConverter<Guid>>(db.SerializationContext.GetConverter(typeof(Guid)));
}
ProjectId = projectId,
CallInvoker = new FakeCallInvoker()
}.Build();
Assert.Equal("(default)", defaultDb.DatabaseId);
}

private class FakeCallInvoker : CallInvoker
[Fact]
public void NonDefaultDatabase()
{
string databaseId = "testDatabase";
string projectId = "testProjectId";
FirestoreDb nonDefaultDb = new FirestoreDbBuilder
{
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options) =>
throw new NotImplementedException();
ProjectId = projectId,
DatabaseId = databaseId,
CallInvoker = new FakeCallInvoker()
}.Build();
Assert.Equal(databaseId, nonDefaultDb.DatabaseId);
}

private class FakeCallInvoker : CallInvoker
{
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options) =>
throw new NotImplementedException();

public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options) =>
throw new NotImplementedException();
public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options) =>
throw new NotImplementedException();

public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) =>
throw new NotImplementedException();
public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) =>
throw new NotImplementedException();

public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) =>
throw new NotImplementedException();
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) =>
throw new NotImplementedException();

public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) =>
throw new NotImplementedException();
}
public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) =>
throw new NotImplementedException();
}
}
@@ -1,4 +1,4 @@
// Copyright 2017, Google Inc. All rights reserved.
// Copyright 2017, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,11 +43,10 @@ public sealed class FirestoreDb
/// </summary>
public string ProjectId { get; }

// Note: currently internal as only the default database ID is supported.
/// <summary>
/// The database ID associated with this database.
/// The database ID associated with this database.The value will be "(default)" for the project-default database.
/// </summary>
internal string DatabaseId { get; }
public string DatabaseId { get; }

/// <summary>
/// The resource name of the database, in the form "projects/{project_id}/databases/{database_id}".
Expand Down
Expand Up @@ -50,11 +50,10 @@ public FirestoreDbBuilder() : base(FirestoreClient.ServiceMetadata)
/// </summary>
public string ProjectId { get; set; }

// Note: currently internal as only the default database is supported.
/// <summary>
/// The ID of the database within the project. May be null, in which case the default database will be used.
/// </summary>
internal string DatabaseId { get; set; }
public string DatabaseId { get; set; }

/// <summary>
/// Action to receive warning messages. May be null, in which case warnings will be ignored.
Expand Down

0 comments on commit 3a40583

Please sign in to comment.