Skip to content

Commit

Permalink
Multiple db support for REST scenario (#2169)
Browse files Browse the repository at this point in the history
## Why make this change?
This change introduces REST support for multiple DB Scenario.
Issue: #1753
## What is this change?
1. Instead of using the default db name to dispatch queries, we are
using the db name based on entity and then dispatching queries and
mutations.

## How was this tested?
1. Existing unit tests should cover backward compatibility scenarios of
all single db rest support.
2. Existing unit tests cover the flow once the query is dispatched to
the queryEngine/query executor. We need to add the tests for the
integration scenario for other calls in rest.
3. Added test for rest scenario dispatch routing.

Integration test:
1. Two db's one has publishers entity and one has books:
Get publishers

![image](https://github.com/Azure/data-api-builder/assets/124841904/8f1fd2d6-50f0-410a-b096-ad946434ba94)

Get books

![image](https://github.com/Azure/data-api-builder/assets/124841904/3c0e680c-4c30-46dd-8bdc-575c673ecdaf)
  • Loading branch information
rohkhann committed May 8, 2024
1 parent 3488493 commit 910ba2a
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 95 deletions.
15 changes: 14 additions & 1 deletion src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
using System.Net;
using System.Text.Json;
Expand Down Expand Up @@ -135,7 +136,9 @@ public bool AllowIntrospection

private Dictionary<string, DataSource> _dataSourceNameToDataSource;

private Dictionary<string, string> _entityNameToDataSourceName;
private Dictionary<string, string> _entityNameToDataSourceName = new();

private Dictionary<string, string> _entityPathNameToEntityName = new();

/// <summary>
/// List of all datasources.
Expand All @@ -154,6 +157,16 @@ public IEnumerable<DataSource> ListAllDataSources()
return _dataSourceNameToDataSource.AsEnumerable();
}

public bool TryAddEntityPathNameToEntityName(string entityPathName, string entityName)
{
return _entityPathNameToEntityName.TryAdd(entityPathName, entityName);
}

public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)] out string? entityName)
{
return _entityPathNameToEntityName.TryGetValue(entityPathName, out entityName);
}

/// <summary>
/// Constructor for runtimeConfig.
/// To be used when setting up from cli json scenario.
Expand Down
3 changes: 1 addition & 2 deletions src/Core/Resolvers/SqlQueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,7 @@ public class SqlQueryEngine : IQueryEngine
// </summary>
public async Task<JsonDocument?> ExecuteAsync(FindRequestContext context)
{
// for REST API scenarios, use the default datasource
string dataSourceName = _runtimeConfigProvider.GetConfig().DefaultDataSourceName;
string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(context.EntityName);

ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName);
SqlQueryStructure structure = new(
Expand Down
9 changes: 0 additions & 9 deletions src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,6 @@ public interface ISqlMetadataProvider
/// <returns>True if exists, false otherwise.</returns>
bool TryGetBackingColumn(string entityName, string field, [NotNullWhen(true)] out string? name);

/// <summary>
/// Try to obtain the name of the Entity that has the provided Path. If It
/// exists save in out param, and return true, otherwise return false.
/// </summary>
/// <param name="entityPathName">Entity's path as seen in a request.</param>
/// <param name="entityName">Name of the associated entity.</param>
/// <returns>True if exists, otherwise false.</returns>
bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)] out string? entityName);

/// <summary>
/// Obtains the underlying database type.
/// </summary>
Expand Down
11 changes: 2 additions & 9 deletions src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ public abstract class SqlMetadataProvider<ConnectionT, DataAdapterT, CommandT> :

private Dictionary<string, Dictionary<string, string>> EntityExposedNamesToBackingColumnNames { get; } = new();

private Dictionary<string, string> EntityPathToEntityName { get; } = new();

protected IAbstractQueryManagerFactory QueryManagerFactory { get; init; }

/// <summary>
Expand Down Expand Up @@ -220,12 +218,6 @@ public bool TryGetBackingColumn(string entityName, string field, [NotNullWhen(tr
return EntityExposedNamesToBackingColumnNames[entityName].TryGetValue(field, out name);
}

/// <inheritdoc />
public virtual bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)] out string? entityName)
{
return EntityPathToEntityName.TryGetValue(entityPathName, out entityName);
}

/// <inheritdoc />
public IReadOnlyDictionary<string, DatabaseObject> GetEntityNamesAndDbObjects()
{
Expand Down Expand Up @@ -503,7 +495,8 @@ private void GenerateRestPathToEntityMap()

if (!string.IsNullOrEmpty(path))
{
EntityPathToEntityName[path] = entityName;
// add the entity path name to the entity name mapping to the runtime config for multi-db resolution.
runtimeConfig.TryAddEntityPathNameToEntityName(path, entityName);
}
}
catch (Exception e)
Expand Down
20 changes: 10 additions & 10 deletions src/Core/Services/RestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,18 @@ RequestValidator requestValidator
private async Task<IActionResult> DispatchQuery(RestRequestContext context, DatabaseType databaseType)
{
IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(databaseType);
string defaultDataSourceName = _runtimeConfigProvider.GetConfig().DefaultDataSourceName;

string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(context.EntityName);

if (context is FindRequestContext findRequestContext)
{
using JsonDocument? restApiResponse = await queryEngine.ExecuteAsync(findRequestContext);
return restApiResponse is null ? SqlResponseHelpers.FormatFindResult(JsonDocument.Parse("[]").RootElement.Clone(), findRequestContext, _sqlMetadataProviderFactory.GetMetadataProvider(defaultDataSourceName), _runtimeConfigProvider.GetConfig(), GetHttpContext())
: SqlResponseHelpers.FormatFindResult(restApiResponse.RootElement.Clone(), findRequestContext, _sqlMetadataProviderFactory.GetMetadataProvider(defaultDataSourceName), _runtimeConfigProvider.GetConfig(), GetHttpContext());
return restApiResponse is null ? SqlResponseHelpers.FormatFindResult(JsonDocument.Parse("[]").RootElement.Clone(), findRequestContext, _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName), _runtimeConfigProvider.GetConfig(), GetHttpContext())
: SqlResponseHelpers.FormatFindResult(restApiResponse.RootElement.Clone(), findRequestContext, _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName), _runtimeConfigProvider.GetConfig(), GetHttpContext());
}
else if (context is StoredProcedureRequestContext storedProcedureRequestContext)
{
return await queryEngine.ExecuteAsync(storedProcedureRequestContext, defaultDataSourceName);
return await queryEngine.ExecuteAsync(storedProcedureRequestContext, dataSourceName);
}
else
{
Expand All @@ -237,10 +238,10 @@ private async Task<IActionResult> DispatchQuery(RestRequestContext context, Data
private Task<IActionResult?> DispatchMutation(RestRequestContext context, DatabaseType databaseType)
{
IMutationEngine mutationEngine = _mutationEngineFactory.GetMutationEngine(databaseType);
string defaultDataSourceName = _runtimeConfigProvider.GetConfig().DefaultDataSourceName;
string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(context.EntityName);
return context switch
{
StoredProcedureRequestContext => mutationEngine.ExecuteAsync((StoredProcedureRequestContext)context, defaultDataSourceName),
StoredProcedureRequestContext => mutationEngine.ExecuteAsync((StoredProcedureRequestContext)context, dataSourceName),
_ => mutationEngine.ExecuteAsync(context)
};
}
Expand Down Expand Up @@ -437,8 +438,7 @@ public bool TryGetRestRouteFromConfig([NotNullWhen(true)] out string? configured
public (string, string) GetEntityNameAndPrimaryKeyRouteFromRoute(string routeAfterPathBase)
{

string dataSourceName = _runtimeConfigProvider.GetConfig().DefaultDataSourceName;
ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName);
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();

// Split routeAfterPath on the first occurrence of '/', if we get back 2 elements
// this means we have a non empty primary key route which we save. Otherwise, save
Expand All @@ -451,15 +451,15 @@ public bool TryGetRestRouteFromConfig([NotNullWhen(true)] out string? configured
string entityPath = entityPathAndPKRoute[0];
string primaryKeyRoute = entityPathAndPKRoute.Length == maxNumberOfElementsFromSplit ? entityPathAndPKRoute[1] : string.Empty;

if (!sqlMetadataProvider.TryGetEntityNameFromPath(entityPath, out string? entityName))
if (!runtimeConfig.TryGetEntityNameFromPath(entityPath, out string? entityName))
{
throw new DataApiBuilderException(
message: $"Invalid Entity path: {entityPath}.",
statusCode: HttpStatusCode.NotFound,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

return (entityName, primaryKeyRoute);
return (entityName!, primaryKeyRoute);
}

/// <summary>
Expand Down

0 comments on commit 910ba2a

Please sign in to comment.