Implements SQL search parameter status data layer. (#1430)

Adds new schema version and refactors SQL initialization logic.
This commit is contained in:
Robin Todd 2020-11-11 15:01:26 -08:00 коммит произвёл GitHub
Родитель 5d8f5d601d
Коммит 40cda490a2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
37 изменённых файлов: 2999 добавлений и 296 удалений

Просмотреть файл

@ -31,7 +31,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search.Registry
private static readonly string ResourceQuery = "http://hl7.org/fhir/SearchParameter/Resource-query";
private readonly SearchParameterStatusManager _manager;
private readonly ISearchParameterRegistry _searchParameterRegistry;
private readonly ISearchParameterStatusDataStore _searchParameterStatusDataStore;
private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager;
private readonly IMediator _mediator;
private readonly SearchParameterInfo[] _searchParameterInfos;
@ -41,13 +41,13 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search.Registry
public SearchParameterStatusManagerTests()
{
_searchParameterRegistry = Substitute.For<ISearchParameterRegistry>();
_searchParameterStatusDataStore = Substitute.For<ISearchParameterStatusDataStore>();
_searchParameterDefinitionManager = Substitute.For<ISearchParameterDefinitionManager>();
_searchParameterSupportResolver = Substitute.For<ISearchParameterSupportResolver>();
_mediator = Substitute.For<IMediator>();
_manager = new SearchParameterStatusManager(
_searchParameterRegistry,
_searchParameterStatusDataStore,
_searchParameterDefinitionManager,
_searchParameterSupportResolver,
_mediator);
@ -81,7 +81,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search.Registry
},
};
_searchParameterRegistry.GetSearchParameterStatuses().Returns(_resourceSearchParameterStatuses);
_searchParameterStatusDataStore.GetSearchParameterStatuses().Returns(_resourceSearchParameterStatuses);
List<string> baseResourceTypes = new List<string>() { "Resource" };
List<string> targetResourceTypes = new List<string>() { "Patient" };
@ -161,9 +161,9 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search.Registry
{
await _manager.EnsureInitialized();
await _searchParameterRegistry
await _searchParameterStatusDataStore
.DidNotReceive()
.UpdateStatuses(Arg.Any<IEnumerable<ResourceSearchParameterStatus>>());
.UpsertStatuses(Arg.Any<List<ResourceSearchParameterStatus>>());
}
[Fact]

Просмотреть файл

@ -25,7 +25,6 @@ namespace Microsoft.Health.Fhir.Core.Features.Definition
private readonly IModelInfoProvider _modelInfoProvider;
private IDictionary<string, IDictionary<string, SearchParameterInfo>> _typeLookup;
private bool _started;
private ConcurrentDictionary<string, string> _resourceTypeSearchParameterHashMap;
public SearchParameterDefinitionManager(IModelInfoProvider modelInfoProvider)
@ -49,22 +48,14 @@ namespace Microsoft.Health.Fhir.Core.Features.Definition
public Task StartAsync(CancellationToken cancellationToken)
{
// This method is idempotent because dependent Start methods are not guaranteed to be executed in order.
if (!_started)
{
var builder = new SearchParameterDefinitionBuilder(
_modelInfoProvider,
"search-parameters.json");
var builder = new SearchParameterDefinitionBuilder(
_modelInfoProvider,
"search-parameters.json");
builder.Build();
builder.Build();
_typeLookup = builder.ResourceTypeDictionary;
UrlLookup = builder.UriDictionary;
List<string> list = UrlLookup.Values.Where(p => p.Type == ValueSets.SearchParamType.Composite).Select(p => string.Join("|", p.Component.Select(c => UrlLookup[c.DefinitionUrl].Type))).Distinct().ToList();
_started = true;
}
_typeLookup = builder.ResourceTypeDictionary;
UrlLookup = builder.UriDictionary;
return Task.CompletedTask;
}

Просмотреть файл

@ -16,13 +16,13 @@ using Newtonsoft.Json;
namespace Microsoft.Health.Fhir.Core.Features.Search.Registry
{
public class FilebasedSearchParameterRegistry : ISearchParameterRegistry
public class FilebasedSearchParameterStatusDataStore : ISearchParameterStatusDataStore
{
private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager;
private readonly IModelInfoProvider _modelInfoProvider;
private ResourceSearchParameterStatus[] _statusResults;
public FilebasedSearchParameterRegistry(
public FilebasedSearchParameterStatusDataStore(
ISearchParameterDefinitionManager searchParameterDefinitionManager,
IModelInfoProvider modelInfoProvider)
{
@ -32,7 +32,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search.Registry
_modelInfoProvider = modelInfoProvider;
}
public delegate ISearchParameterRegistry Resolver();
public delegate ISearchParameterStatusDataStore Resolver();
public Task<IReadOnlyCollection<ResourceSearchParameterStatus>> GetSearchParameterStatuses()
{
@ -76,7 +76,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search.Registry
return Task.FromResult<IReadOnlyCollection<ResourceSearchParameterStatus>>(_statusResults);
}
public Task UpdateStatuses(IEnumerable<ResourceSearchParameterStatus> statuses)
public Task UpsertStatuses(List<ResourceSearchParameterStatus> statuses)
{
// File based registry does not persist runtime updates
return Task.CompletedTask;

Просмотреть файл

@ -8,10 +8,10 @@ using System.Threading.Tasks;
namespace Microsoft.Health.Fhir.Core.Features.Search.Registry
{
public interface ISearchParameterRegistry
public interface ISearchParameterStatusDataStore
{
Task<IReadOnlyCollection<ResourceSearchParameterStatus>> GetSearchParameterStatuses();
Task UpdateStatuses(IEnumerable<ResourceSearchParameterStatus> statuses);
Task UpsertStatuses(List<ResourceSearchParameterStatus> statuses);
}
}

Просмотреть файл

@ -19,23 +19,23 @@ namespace Microsoft.Health.Fhir.Core.Features.Search.Registry
{
public class SearchParameterStatusManager : IRequireInitializationOnFirstRequest
{
private readonly ISearchParameterRegistry _searchParameterRegistry;
private readonly ISearchParameterStatusDataStore _searchParameterStatusDataStore;
private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager;
private readonly ISearchParameterSupportResolver _searchParameterSupportResolver;
private readonly IMediator _mediator;
public SearchParameterStatusManager(
ISearchParameterRegistry searchParameterRegistry,
ISearchParameterStatusDataStore searchParameterStatusDataStore,
ISearchParameterDefinitionManager searchParameterDefinitionManager,
ISearchParameterSupportResolver searchParameterSupportResolver,
IMediator mediator)
{
EnsureArg.IsNotNull(searchParameterRegistry, nameof(searchParameterRegistry));
EnsureArg.IsNotNull(searchParameterStatusDataStore, nameof(searchParameterStatusDataStore));
EnsureArg.IsNotNull(searchParameterDefinitionManager, nameof(searchParameterDefinitionManager));
EnsureArg.IsNotNull(searchParameterSupportResolver, nameof(searchParameterSupportResolver));
EnsureArg.IsNotNull(mediator, nameof(mediator));
_searchParameterRegistry = searchParameterRegistry;
_searchParameterStatusDataStore = searchParameterStatusDataStore;
_searchParameterDefinitionManager = searchParameterDefinitionManager;
_searchParameterSupportResolver = searchParameterSupportResolver;
_mediator = mediator;
@ -46,7 +46,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search.Registry
var updated = new List<SearchParameterInfo>();
var resourceTypeSearchParamStatusMap = new Dictionary<string, List<ResourceSearchParameterStatus>>();
var parameters = (await _searchParameterRegistry.GetSearchParameterStatuses())
var parameters = (await _searchParameterStatusDataStore.GetSearchParameterStatuses())
.ToDictionary(x => x.Uri);
// Set states of known parameters

Просмотреть файл

@ -18,23 +18,23 @@ using Xunit;
namespace Microsoft.Health.Fhir.CosmosDb.UnitTests.Features.Storage.Registry
{
public class CosmosDbStatusRegistryInitializerTests
public class CosmosDbSearchParameterStatusInitializerTests
{
private readonly CosmosDbStatusRegistryInitializer _initializer;
private readonly CosmosDbSearchParameterStatusInitializer _initializer;
private readonly ICosmosQueryFactory _cosmosDocumentQueryFactory;
private readonly Uri _testParameterUri;
public CosmosDbStatusRegistryInitializerTests()
public CosmosDbSearchParameterStatusInitializerTests()
{
ISearchParameterRegistry searchParameterRegistry = Substitute.For<ISearchParameterRegistry>();
ISearchParameterStatusDataStore searchParameterStatusDataStore = Substitute.For<ISearchParameterStatusDataStore>();
_cosmosDocumentQueryFactory = Substitute.For<ICosmosQueryFactory>();
_initializer = new CosmosDbStatusRegistryInitializer(
() => searchParameterRegistry,
_initializer = new CosmosDbSearchParameterStatusInitializer(
() => searchParameterStatusDataStore,
_cosmosDocumentQueryFactory);
_testParameterUri = new Uri("/test", UriKind.Relative);
searchParameterRegistry
searchParameterStatusDataStore
.GetSearchParameterStatuses()
.Returns(new[]
{

Просмотреть файл

@ -17,12 +17,12 @@ using Microsoft.Health.Fhir.CosmosDb.Configs;
namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage.Registry
{
public class CosmosDbStatusRegistry : ISearchParameterRegistry
public class CosmosDbSearchParameterStatusDataStore : ISearchParameterStatusDataStore
{
private readonly Func<IScoped<Container>> _containerScopeFactory;
private readonly ICosmosQueryFactory _queryFactory;
public CosmosDbStatusRegistry(
public CosmosDbSearchParameterStatusDataStore(
Func<IScoped<Container>> containerScopeFactory,
CosmosDataStoreConfiguration cosmosDataStoreConfiguration,
ICosmosQueryFactory queryFactory)
@ -35,8 +35,6 @@ namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage.Registry
_queryFactory = queryFactory;
}
public Uri CollectionUri { get; set; }
public async Task<IReadOnlyCollection<ResourceSearchParameterStatus>> GetSearchParameterStatuses()
{
using var cancellationSource = new CancellationTokenSource(TimeSpan.FromMinutes(1));
@ -72,10 +70,15 @@ namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage.Registry
return parameterStatus;
}
public async Task UpdateStatuses(IEnumerable<ResourceSearchParameterStatus> statuses)
public async Task UpsertStatuses(List<ResourceSearchParameterStatus> statuses)
{
EnsureArg.IsNotNull(statuses, nameof(statuses));
if (statuses.Count == 0)
{
return;
}
using var clientScope = _containerScopeFactory.Invoke();
var batch = clientScope.Value.CreateTransactionalBatch(new PartitionKey(SearchParameterStatusWrapper.SearchParameterStatusPartitionKey));

Просмотреть файл

@ -14,19 +14,19 @@ using Microsoft.Health.Fhir.CosmosDb.Features.Storage.Versioning;
namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage.Registry
{
public class CosmosDbStatusRegistryInitializer : ICollectionUpdater
public class CosmosDbSearchParameterStatusInitializer : ICollectionUpdater
{
private readonly ISearchParameterRegistry _filebasedRegistry;
private readonly ISearchParameterStatusDataStore _filebasedSearchParameterStatusDataStore;
private readonly ICosmosQueryFactory _queryFactory;
public CosmosDbStatusRegistryInitializer(
FilebasedSearchParameterRegistry.Resolver filebasedRegistry,
public CosmosDbSearchParameterStatusInitializer(
FilebasedSearchParameterStatusDataStore.Resolver filebasedSearchParameterStatusDataStoreResolver,
ICosmosQueryFactory queryFactory)
{
EnsureArg.IsNotNull(filebasedRegistry, nameof(filebasedRegistry));
EnsureArg.IsNotNull(filebasedSearchParameterStatusDataStoreResolver, nameof(filebasedSearchParameterStatusDataStoreResolver));
EnsureArg.IsNotNull(queryFactory, nameof(queryFactory));
_filebasedRegistry = filebasedRegistry.Invoke();
_filebasedSearchParameterStatusDataStore = filebasedSearchParameterStatusDataStoreResolver.Invoke();
_queryFactory = queryFactory;
}
@ -46,7 +46,7 @@ namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage.Registry
if (!results.Any())
{
var statuses = await _filebasedRegistry.GetSearchParameterStatuses();
var statuses = await _filebasedSearchParameterStatusDataStore.GetSearchParameterStatuses();
foreach (var batch in statuses.TakeBatch(100))
{

Просмотреть файл

@ -161,7 +161,7 @@ namespace Microsoft.Extensions.DependencyInjection
.Transient()
.AsService<ICollectionUpdater>();
services.Add<CosmosDbStatusRegistryInitializer>()
services.Add<CosmosDbSearchParameterStatusInitializer>()
.Transient()
.AsService<ICollectionUpdater>();
@ -185,10 +185,10 @@ namespace Microsoft.Extensions.DependencyInjection
.AsSelf()
.AsImplementedInterfaces();
services.Add<CosmosDbStatusRegistry>()
services.Add<CosmosDbSearchParameterStatusDataStore>()
.Singleton()
.AsSelf()
.ReplaceService<ISearchParameterRegistry>();
.ReplaceService<ISearchParameterStatusDataStore>();
// Each CosmosClient needs new instances of a RequestHandler
services.TypesInSameAssemblyAs<FhirCosmosClientInitializer>()

Просмотреть файл

@ -66,11 +66,11 @@ namespace Microsoft.Health.Fhir.Api.Modules
.AsSelf()
.AsImplementedInterfaces();
services.Add<FilebasedSearchParameterRegistry>()
services.Add<FilebasedSearchParameterStatusDataStore>()
.Transient()
.AsSelf()
.AsService<ISearchParameterRegistry>()
.AsDelegate<FilebasedSearchParameterRegistry.Resolver>();
.AsService<ISearchParameterStatusDataStore>()
.AsDelegate<FilebasedSearchParameterStatusDataStore.Resolver>();
services.Add<SearchParameterSupportResolver>()
.Singleton()

Просмотреть файл

@ -29,7 +29,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Operations.Reindex
private readonly ISearchIndexer _searchIndexer = Substitute.For<ISearchIndexer>();
private readonly ResourceDeserializer _resourceDeserializer = Deserializers.ResourceDeserializer;
private readonly ISupportedSearchParameterDefinitionManager _searchParameterDefinitionManager = Substitute.For<ISupportedSearchParameterDefinitionManager>();
private readonly ISearchParameterRegistry _searchParameterRegistry = Substitute.For<ISearchParameterRegistry>();
private readonly ISearchParameterStatusDataStore _searchParameterStatusDataStore = Substitute.For<ISearchParameterStatusDataStore>();
private readonly ITestOutputHelper _output;
private IReadOnlyDictionary<string, string> _searchParameterHashMap;
private readonly ReindexUtilities _reindexUtilities;
@ -39,7 +39,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Operations.Reindex
_output = output;
_searchParameterHashMap = new Dictionary<string, string>() { { "Patient", "hash1" } };
Func<Health.Extensions.DependencyInjection.IScoped<IFhirDataStore>> fhirDataStoreScope = () => _fhirDataStore.CreateMockScope();
_reindexUtilities = new ReindexUtilities(fhirDataStoreScope, _searchIndexer, _resourceDeserializer, _searchParameterDefinitionManager, _searchParameterRegistry);
_reindexUtilities = new ReindexUtilities(fhirDataStoreScope, _searchIndexer, _resourceDeserializer, _searchParameterDefinitionManager, _searchParameterStatusDataStore);
}
[Fact]

Просмотреть файл

@ -31,7 +31,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search
private static readonly string ResourceTest = "http://hl7.org/fhir/SearchParameter/Resource-test";
private readonly SearchParameterStatusManager _manager;
private readonly ISearchParameterRegistry _searchParameterRegistry;
private readonly ISearchParameterStatusDataStore _searchParameterStatusDataStore;
private readonly SearchParameterDefinitionManager _searchParameterDefinitionManager;
private readonly IMediator _mediator;
private readonly SearchParameterInfo[] _searchParameterInfos;
@ -45,18 +45,18 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search
{
_searchParameterSupportResolver = Substitute.For<ISearchParameterSupportResolver>();
_mediator = Substitute.For<IMediator>();
_searchParameterRegistry = Substitute.For<ISearchParameterRegistry>();
_searchParameterStatusDataStore = Substitute.For<ISearchParameterStatusDataStore>();
_searchParameterDefinitionManager = new SearchParameterDefinitionManager(ModelInfoProvider.Instance);
_fhirRequestContextAccessor = Substitute.For<IFhirRequestContextAccessor>();
_fhirRequestContextAccessor.FhirRequestContext.Returns(_fhirRequestContext);
_manager = new SearchParameterStatusManager(
_searchParameterRegistry,
_searchParameterStatusDataStore,
_searchParameterDefinitionManager,
_searchParameterSupportResolver,
_mediator);
_searchParameterRegistry.GetSearchParameterStatuses()
_searchParameterStatusDataStore.GetSearchParameterStatuses()
.Returns(new[]
{
new ResourceSearchParameterStatus

Просмотреть файл

@ -74,7 +74,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search
var definitionManager = new SearchParameterDefinitionManager(modelInfoProvider);
await definitionManager.StartAsync(CancellationToken.None);
var statusRegistry = new FilebasedSearchParameterRegistry(
var statusRegistry = new FilebasedSearchParameterStatusDataStore(
definitionManager,
modelInfoProvider);
var statusManager = new SearchParameterStatusManager(

Просмотреть файл

@ -22,26 +22,26 @@ namespace Microsoft.Health.Fhir.Core.Features.Operations.Reindex
private ISearchIndexer _searchIndexer;
private ResourceDeserializer _deserializer;
private readonly ISupportedSearchParameterDefinitionManager _searchParameterDefinitionManager;
private readonly ISearchParameterRegistry _searchParameterRegistry;
private readonly ISearchParameterStatusDataStore _searchParameterStatusDataStore;
public ReindexUtilities(
Func<IScoped<IFhirDataStore>> fhirDataStoreFactory,
ISearchIndexer searchIndexer,
ResourceDeserializer deserializer,
ISupportedSearchParameterDefinitionManager searchParameterDefinitionManager,
ISearchParameterRegistry searchParameterRegistry)
ISearchParameterStatusDataStore searchParameterStatusDataStore)
{
EnsureArg.IsNotNull(fhirDataStoreFactory, nameof(fhirDataStoreFactory));
EnsureArg.IsNotNull(searchIndexer, nameof(searchIndexer));
EnsureArg.IsNotNull(deserializer, nameof(deserializer));
EnsureArg.IsNotNull(searchParameterDefinitionManager, nameof(searchParameterDefinitionManager));
EnsureArg.IsNotNull(searchParameterRegistry, nameof(searchParameterRegistry));
EnsureArg.IsNotNull(searchParameterStatusDataStore, nameof(searchParameterStatusDataStore));
_fhirDataStoreFactory = fhirDataStoreFactory;
_searchIndexer = searchIndexer;
_deserializer = deserializer;
_searchParameterDefinitionManager = searchParameterDefinitionManager;
_searchParameterRegistry = searchParameterRegistry;
_searchParameterStatusDataStore = searchParameterStatusDataStore;
}
/// <summary>
@ -114,7 +114,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Operations.Reindex
});
}
await _searchParameterRegistry.UpdateStatuses(searchParameterStatusList);
await _searchParameterStatusDataStore.UpsertStatuses(searchParameterStatusList);
return (true, null);
}

Просмотреть файл

@ -0,0 +1,76 @@
/*************************************************************
Search Parameter Status Information
**************************************************************/
ALTER TABLE dbo.SearchParam
ADD
Status varchar(10) NULL,
LastUpdated datetimeoffset(7) NULL,
IsPartiallySupported bit NULL
-- We adopted this naming convention for table-valued parameters because they are immutable.
CREATE TYPE dbo.SearchParamTableType_1 AS TABLE
(
Uri varchar(128) COLLATE Latin1_General_100_CS_AS NOT NULL,
Status varchar(10) NOT NULL,
IsPartiallySupported bit NOT NULL
)
GO
/*************************************************************
Stored procedures for search parameter information
**************************************************************/
--
-- STORED PROCEDURE
-- GetSearchParamStatuses
--
-- DESCRIPTION
-- Gets all the search parameters and their statuses.
--
-- RETURN VALUE
-- The search parameters and their statuses.
--
CREATE PROCEDURE dbo.GetSearchParamStatuses
AS
SET NOCOUNT ON
SELECT Uri, Status, LastUpdated, IsPartiallySupported FROM dbo.SearchParam
GO
--
-- STORED PROCEDURE
-- UpsertSearchParams
--
-- DESCRIPTION
-- Given a set of search parameters, creates or updates the parameters.
--
-- PARAMETERS
-- @searchParams
-- * The updated existing search parameters or the new search parameters
--
CREATE PROCEDURE dbo.UpsertSearchParams
@searchParams dbo.SearchParamTableType_1 READONLY
AS
SET NOCOUNT ON
SET XACT_ABORT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
DECLARE @lastUpdated datetimeoffset(7) = SYSDATETIMEOFFSET()
-- Acquire and hold an exclusive table lock for the entire transaction to prevent parameters from being added or modified during upsert.
MERGE INTO dbo.SearchParam WITH (TABLOCKX) AS target
USING @searchParams AS source
ON target.Uri = source.Uri
WHEN MATCHED THEN
UPDATE
SET Status = source.Status, LastUpdated = @lastUpdated, IsPartiallySupported = source.IsPartiallySupported
WHEN NOT MATCHED BY target THEN
INSERT
(Uri, Status, LastUpdated, IsPartiallySupported)
VALUES (source.Uri, source.Status, @lastUpdated, source.IsPartiallySupported);
COMMIT TRANSACTION
GO

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -41,10 +41,12 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema.Model
internal readonly static CreateExportJobProcedure CreateExportJob = new CreateExportJobProcedure();
internal readonly static GetExportJobByHashProcedure GetExportJobByHash = new GetExportJobByHashProcedure();
internal readonly static GetExportJobByIdProcedure GetExportJobById = new GetExportJobByIdProcedure();
internal readonly static GetSearchParamStatusesProcedure GetSearchParamStatuses = new GetSearchParamStatusesProcedure();
internal readonly static HardDeleteResourceProcedure HardDeleteResource = new HardDeleteResourceProcedure();
internal readonly static ReadResourceProcedure ReadResource = new ReadResourceProcedure();
internal readonly static UpdateExportJobProcedure UpdateExportJob = new UpdateExportJobProcedure();
internal readonly static UpsertResourceProcedure UpsertResource = new UpsertResourceProcedure();
internal readonly static UpsertSearchParamsProcedure UpsertSearchParams = new UpsertSearchParamsProcedure();
internal class ClaimTypeTable : Table
{
internal ClaimTypeTable(): base("dbo.ClaimType")
@ -229,6 +231,9 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema.Model
internal readonly SmallIntColumn SearchParamId = new SmallIntColumn("SearchParamId");
internal readonly VarCharColumn Uri = new VarCharColumn("Uri", 128, "Latin1_General_100_CS_AS");
internal readonly NullableVarCharColumn Status = new NullableVarCharColumn("Status", 10);
internal readonly NullableDateTimeOffsetColumn LastUpdated = new NullableDateTimeOffsetColumn("LastUpdated", 7);
internal readonly NullableBitColumn IsPartiallySupported = new NullableBitColumn("IsPartiallySupported");
}
internal class StringSearchParamTable : Table
@ -452,6 +457,19 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema.Model
}
}
internal class GetSearchParamStatusesProcedure : StoredProcedure
{
internal GetSearchParamStatusesProcedure(): base("dbo.GetSearchParamStatuses")
{
}
public void PopulateCommand(SqlCommandWrapper command)
{
command.CommandType = global::System.Data.CommandType.StoredProcedure;
command.CommandText = "dbo.GetSearchParamStatuses";
}
}
internal class HardDeleteResourceProcedure : StoredProcedure
{
internal HardDeleteResourceProcedure(): base("dbo.HardDeleteResource")
@ -724,6 +742,53 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema.Model
}
}
internal class UpsertSearchParamsProcedure : StoredProcedure
{
internal UpsertSearchParamsProcedure(): base("dbo.UpsertSearchParams")
{
}
private readonly SearchParamTableTypeTableValuedParameterDefinition _searchParams = new SearchParamTableTypeTableValuedParameterDefinition("@searchParams");
public void PopulateCommand(SqlCommandWrapper command, global::System.Collections.Generic.IEnumerable<SearchParamTableTypeRow> searchParams)
{
command.CommandType = global::System.Data.CommandType.StoredProcedure;
command.CommandText = "dbo.UpsertSearchParams";
_searchParams.AddParameter(command.Parameters, searchParams);
}
public void PopulateCommand(SqlCommandWrapper command, UpsertSearchParamsTableValuedParameters tableValuedParameters)
{
PopulateCommand(command, searchParams: tableValuedParameters.SearchParams);
}
}
internal class UpsertSearchParamsTvpGenerator<TInput> : IStoredProcedureTableValuedParametersGenerator<TInput, UpsertSearchParamsTableValuedParameters>
{
public UpsertSearchParamsTvpGenerator(ITableValuedParameterRowGenerator<TInput, SearchParamTableTypeRow> SearchParamTableTypeRowGenerator)
{
this.SearchParamTableTypeRowGenerator = SearchParamTableTypeRowGenerator;
}
private readonly ITableValuedParameterRowGenerator<TInput, SearchParamTableTypeRow> SearchParamTableTypeRowGenerator;
public UpsertSearchParamsTableValuedParameters Generate(TInput input)
{
return new UpsertSearchParamsTableValuedParameters(SearchParamTableTypeRowGenerator.GenerateRows(input));
}
}
internal struct UpsertSearchParamsTableValuedParameters
{
internal UpsertSearchParamsTableValuedParameters(global::System.Collections.Generic.IEnumerable<SearchParamTableTypeRow> SearchParams)
{
this.SearchParams = SearchParams;
}
internal global::System.Collections.Generic.IEnumerable<SearchParamTableTypeRow> SearchParams
{
get;
}
}
private class CompartmentAssignmentTableTypeTableValuedParameterDefinition : TableValuedParameterDefinition<CompartmentAssignmentTableTypeRow>
{
internal CompartmentAssignmentTableTypeTableValuedParameterDefinition(System.String parameterName): base(parameterName, "dbo.CompartmentAssignmentTableType_1")
@ -1097,6 +1162,49 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema.Model
}
}
private class SearchParamTableTypeTableValuedParameterDefinition : TableValuedParameterDefinition<SearchParamTableTypeRow>
{
internal SearchParamTableTypeTableValuedParameterDefinition(System.String parameterName): base(parameterName, "dbo.SearchParamTableType_1")
{
}
internal readonly VarCharColumn Uri = new VarCharColumn("Uri", 128, "Latin1_General_100_CS_AS");
internal readonly VarCharColumn Status = new VarCharColumn("Status", 10);
internal readonly BitColumn IsPartiallySupported = new BitColumn("IsPartiallySupported");
protected override global::System.Collections.Generic.IEnumerable<Column> Columns => new Column[]{Uri, Status, IsPartiallySupported};
protected override void FillSqlDataRecord(global::Microsoft.Data.SqlClient.Server.SqlDataRecord record, SearchParamTableTypeRow rowData)
{
Uri.Set(record, 0, rowData.Uri);
Status.Set(record, 1, rowData.Status);
IsPartiallySupported.Set(record, 2, rowData.IsPartiallySupported);
}
}
internal struct SearchParamTableTypeRow
{
internal SearchParamTableTypeRow(System.String Uri, System.String Status, System.Boolean IsPartiallySupported)
{
this.Uri = Uri;
this.Status = Status;
this.IsPartiallySupported = IsPartiallySupported;
}
internal System.String Uri
{
get;
}
internal System.String Status
{
get;
}
internal System.Boolean IsPartiallySupported
{
get;
}
}
private class StringSearchParamTableTypeTableValuedParameterDefinition : TableValuedParameterDefinition<StringSearchParamTableTypeRow>
{
internal StringSearchParamTableTypeTableValuedParameterDefinition(System.String parameterName): base(parameterName, "dbo.StringSearchParamTableType_1")
@ -1620,4 +1728,4 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema.Model
}
}
}
}
}

Просмотреть файл

@ -15,5 +15,6 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema
V3 = 3,
V4 = 4,
V5 = 5,
V6 = 6,
}
}

Просмотреть файл

@ -7,7 +7,8 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Schema
{
public static class SchemaVersionConstants
{
public const int Max = (int)SchemaVersion.V5;
public const int Min = (int)SchemaVersion.V4;
public const int Max = (int)SchemaVersion.V6;
public const int SearchParameterStatusSchemaVersion = (int)SchemaVersion.V6;
}
}

Просмотреть файл

@ -0,0 +1,32 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
using System.Collections.Generic;
using System.Data;
using Microsoft.Data.SqlClient.Server;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
namespace Microsoft.Health.Fhir.SqlServer.Features.Storage.Registry
{
public class SearchParameterStatusCollection : List<ResourceSearchParameterStatus>, IEnumerable<SqlDataRecord>
{
IEnumerator<SqlDataRecord> IEnumerable<SqlDataRecord>.GetEnumerator()
{
var sqlRow = new SqlDataRecord(
new SqlMetaData("Uri", SqlDbType.VarChar, 128),
new SqlMetaData("Status", SqlDbType.VarChar, 10),
new SqlMetaData("IsPartiallySupported", SqlDbType.Bit));
foreach (ResourceSearchParameterStatus status in this)
{
sqlRow.SetString(0, status.Uri.ToString());
sqlRow.SetString(1, status.Status.ToString());
sqlRow.SetSqlBoolean(2, status.IsPartiallySupported);
yield return sqlRow;
}
}
}
}

Просмотреть файл

@ -0,0 +1,127 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EnsureThat;
using Microsoft.Health.Extensions.DependencyInjection;
using Microsoft.Health.Fhir.Core.Features.Persistence;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.SqlServer.Features.Schema;
using Microsoft.Health.Fhir.SqlServer.Features.Schema.Model;
using Microsoft.Health.SqlServer.Features.Client;
using Microsoft.Health.SqlServer.Features.Schema;
using Microsoft.Health.SqlServer.Features.Storage;
using SqlDataReader = Microsoft.Data.SqlClient.SqlDataReader;
namespace Microsoft.Health.Fhir.SqlServer.Features.Storage.Registry
{
internal class SqlServerSearchParameterStatusDataStore : ISearchParameterStatusDataStore
{
private readonly Func<IScoped<SqlConnectionWrapperFactory>> _scopedSqlConnectionWrapperFactory;
private readonly VLatest.UpsertSearchParamsTvpGenerator<List<ResourceSearchParameterStatus>> _updateSearchParamsTvpGenerator;
private readonly ISearchParameterStatusDataStore _filebasedSearchParameterStatusDataStore;
private readonly SchemaInformation _schemaInformation;
public SqlServerSearchParameterStatusDataStore(
Func<IScoped<SqlConnectionWrapperFactory>> scopedSqlConnectionWrapperFactory,
VLatest.UpsertSearchParamsTvpGenerator<List<ResourceSearchParameterStatus>> updateSearchParamsTvpGenerator,
FilebasedSearchParameterStatusDataStore.Resolver filebasedRegistry,
SchemaInformation schemaInformation)
{
EnsureArg.IsNotNull(scopedSqlConnectionWrapperFactory, nameof(scopedSqlConnectionWrapperFactory));
EnsureArg.IsNotNull(updateSearchParamsTvpGenerator, nameof(updateSearchParamsTvpGenerator));
EnsureArg.IsNotNull(filebasedRegistry, nameof(filebasedRegistry));
EnsureArg.IsNotNull(schemaInformation, nameof(schemaInformation));
_scopedSqlConnectionWrapperFactory = scopedSqlConnectionWrapperFactory;
_updateSearchParamsTvpGenerator = updateSearchParamsTvpGenerator;
_filebasedSearchParameterStatusDataStore = filebasedRegistry.Invoke();
_schemaInformation = schemaInformation;
}
// TODO: Make cancellation token an input.
public async Task<IReadOnlyCollection<ResourceSearchParameterStatus>> GetSearchParameterStatuses()
{
// If the search parameter table in SQL does not yet contain status columns
if (_schemaInformation.Current < SchemaVersionConstants.SearchParameterStatusSchemaVersion)
{
// Get status information from file.
return await _filebasedSearchParameterStatusDataStore.GetSearchParameterStatuses();
}
using (IScoped<SqlConnectionWrapperFactory> scopedSqlConnectionWrapperFactory = _scopedSqlConnectionWrapperFactory())
using (SqlConnectionWrapper sqlConnectionWrapper = await scopedSqlConnectionWrapperFactory.Value.ObtainSqlConnectionWrapperAsync(CancellationToken.None, true))
using (SqlCommandWrapper sqlCommandWrapper = sqlConnectionWrapper.CreateSqlCommand())
{
VLatest.GetSearchParamStatuses.PopulateCommand(sqlCommandWrapper);
var parameterStatuses = new List<ResourceSearchParameterStatus>();
using (SqlDataReader sqlDataReader = await sqlCommandWrapper.ExecuteReaderAsync(CommandBehavior.SequentialAccess, CancellationToken.None))
{
while (await sqlDataReader.ReadAsync())
{
(string uri, string stringStatus, DateTimeOffset? lastUpdated, bool? isPartiallySupported) = sqlDataReader.ReadRow(
VLatest.SearchParam.Uri,
VLatest.SearchParam.Status,
VLatest.SearchParam.LastUpdated,
VLatest.SearchParam.IsPartiallySupported);
if (string.IsNullOrEmpty(stringStatus) || lastUpdated == null || isPartiallySupported == null)
{
// These columns are nullable because they are added to dbo.SearchParam in a later schema version.
// They should be populated as soon as they are added to the table and should never be null.
throw new NullReferenceException(Resources.SearchParameterStatusShouldNotBeNull);
}
var status = Enum.Parse<SearchParameterStatus>(stringStatus, true);
var resourceSearchParameterStatus = new ResourceSearchParameterStatus()
{
Uri = new Uri(uri),
Status = status,
IsPartiallySupported = (bool)isPartiallySupported,
LastUpdated = (DateTimeOffset)lastUpdated,
};
parameterStatuses.Add(resourceSearchParameterStatus);
}
}
return parameterStatuses;
}
}
// TODO: Make cancellation token an input.
public async Task UpsertStatuses(List<ResourceSearchParameterStatus> statuses)
{
EnsureArg.IsNotNull(statuses, nameof(statuses));
if (!statuses.Any())
{
return;
}
if (_schemaInformation.Current < SchemaVersionConstants.SearchParameterStatusSchemaVersion)
{
throw new BadRequestException(Resources.SchemaVersionNeedsToBeUpgraded);
}
using (IScoped<SqlConnectionWrapperFactory> scopedSqlConnectionWrapperFactory = _scopedSqlConnectionWrapperFactory())
using (SqlConnectionWrapper sqlConnectionWrapper = await scopedSqlConnectionWrapperFactory.Value.ObtainSqlConnectionWrapperAsync(CancellationToken.None, true))
using (SqlCommandWrapper sqlCommandWrapper = sqlConnectionWrapper.CreateSqlCommand())
{
VLatest.UpsertSearchParams.PopulateCommand(sqlCommandWrapper, _updateSearchParamsTvpGenerator.Generate(statuses));
await sqlCommandWrapper.ExecuteNonQueryAsync(CancellationToken.None);
}
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
using System.Threading;
using System.Threading.Tasks;
using EnsureThat;
using MediatR;
using Microsoft.Health.SqlServer.Features.Schema.Messages.Notifications;
namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
{
public class SchemaUpgradedHandler : INotificationHandler<SchemaUpgradedNotification>
{
private SqlServerFhirModel _sqlServerFhirModel;
public SchemaUpgradedHandler(SqlServerFhirModel sqlServerFhirModel)
{
EnsureArg.IsNotNull(sqlServerFhirModel, nameof(sqlServerFhirModel));
_sqlServerFhirModel = sqlServerFhirModel;
}
public Task Handle(SchemaUpgradedNotification notification, CancellationToken cancellationToken)
{
EnsureArg.IsNotNull(notification, nameof(notification));
// If it is a snapshot upgrade, then we need to run initialization for all schema versions up to the current version.
_sqlServerFhirModel.Initialize(notification.Version, notification.IsFullSchemaSnapshot);
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -8,18 +8,20 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EnsureThat;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Health.Abstractions.Exceptions;
using Microsoft.Health.Extensions.DependencyInjection;
using Microsoft.Health.Fhir.Core.Configs;
using Microsoft.Health.Fhir.Core.Features.Definition;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.Core.Models;
using Microsoft.Health.Fhir.SqlServer.Features.Schema;
using Microsoft.Health.Fhir.SqlServer.Features.Schema.Model;
using Microsoft.Health.Fhir.SqlServer.Features.Storage.Registry;
using Microsoft.Health.SqlServer.Configs;
using Microsoft.Health.SqlServer.Features.Schema;
using Microsoft.Health.SqlServer.Features.Schema.Model;
@ -34,11 +36,12 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
/// many many times in the database. For more compact storage, we use IDs instead of the strings when referencing these.
/// Also, because the number of distinct values is small, we can maintain all values in memory and avoid joins when querying.
/// </summary>
public sealed class SqlServerFhirModel : IHostedService
public sealed class SqlServerFhirModel : IRequireInitializationOnFirstRequest
{
private readonly SqlServerDataStoreConfiguration _configuration;
private readonly SchemaInitializer _schemaInitializer;
private readonly SchemaInformation _schemaInformation;
private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager;
private readonly ISearchParameterStatusDataStore _filebasedSearchParameterStatusDataStore;
private readonly SecurityConfiguration _securityConfiguration;
private readonly ILogger<SqlServerFhirModel> _logger;
private Dictionary<string, short> _resourceTypeToId;
@ -48,73 +51,76 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
private ConcurrentDictionary<string, int> _quantityCodeToId;
private Dictionary<string, byte> _claimNameToId;
private Dictionary<string, byte> _compartmentTypeToId;
private bool _started;
private int _highestInitializedVersion;
public SqlServerFhirModel(
SqlServerDataStoreConfiguration configuration,
SchemaInitializer schemaInitializer,
SchemaInformation schemaInformation,
ISearchParameterDefinitionManager searchParameterDefinitionManager,
FilebasedSearchParameterStatusDataStore.Resolver filebasedRegistry,
IOptions<SecurityConfiguration> securityConfiguration,
ILogger<SqlServerFhirModel> logger)
{
EnsureArg.IsNotNull(configuration, nameof(configuration));
EnsureArg.IsNotNull(schemaInitializer, nameof(schemaInitializer));
EnsureArg.IsNotNull(schemaInformation, nameof(schemaInformation));
EnsureArg.IsNotNull(searchParameterDefinitionManager, nameof(searchParameterDefinitionManager));
EnsureArg.IsNotNull(filebasedRegistry, nameof(filebasedRegistry));
EnsureArg.IsNotNull(securityConfiguration?.Value, nameof(securityConfiguration));
EnsureArg.IsNotNull(logger, nameof(logger));
_configuration = configuration;
_schemaInitializer = schemaInitializer;
_schemaInformation = schemaInformation;
_searchParameterDefinitionManager = searchParameterDefinitionManager;
_filebasedSearchParameterStatusDataStore = filebasedRegistry.Invoke();
_securityConfiguration = securityConfiguration.Value;
_logger = logger;
}
public short GetResourceTypeId(string resourceTypeName)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _resourceTypeToId[resourceTypeName];
}
public bool TryGetResourceTypeId(string resourceTypeName, out short id)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _resourceTypeToId.TryGetValue(resourceTypeName, out id);
}
public string GetResourceTypeName(short resourceTypeId)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _resourceTypeIdToTypeName[resourceTypeId];
}
public byte GetClaimTypeId(string claimTypeName)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _claimNameToId[claimTypeName];
}
public short GetSearchParamId(Uri searchParamUri)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _searchParamUriToId[searchParamUri];
}
public byte GetCompartmentTypeId(string compartmentType)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _compartmentTypeToId[compartmentType];
}
public bool TryGetSystemId(string system, out int systemId)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _systemToId.TryGetValue(system, out systemId);
}
public int GetSystemId(string system)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
VLatest.SystemTable systemTable = VLatest.System;
return GetStringId(_systemToId, system, systemTable, systemTable.SystemId, systemTable.Value);
@ -122,7 +128,7 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
public int GetQuantityCodeId(string code)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
VLatest.QuantityCodeTable quantityCodeTable = VLatest.QuantityCode;
return GetStringId(_quantityCodeToId, code, quantityCodeTable, quantityCodeTable.QuantityCodeId, quantityCodeTable.Value);
@ -130,156 +136,229 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
public bool TryGetQuantityCodeId(string code, out int quantityCodeId)
{
ThrowIfNotStarted();
ThrowIfNotInitialized();
return _quantityCodeToId.TryGetValue(code, out quantityCodeId);
}
public async Task StartAsync(CancellationToken cancellationToken)
public Task EnsureInitialized()
{
await _schemaInitializer.StartAsync(cancellationToken);
ThrowIfCurrentSchemaVersionIsNull();
if (_searchParameterDefinitionManager is IHostedService hostedService)
// If the fhir-server is just starting up, synchronize the fhir-server dictionaries with the SQL database
Initialize((int)_schemaInformation.Current, true);
return Task.CompletedTask;
}
public void Initialize(int version, bool runAllInitialization)
{
if (_highestInitializedVersion == version)
{
await hostedService.StartAsync(cancellationToken);
return;
}
var connectionStringBuilder = new SqlConnectionStringBuilder(_configuration.ConnectionString);
_logger.LogInformation("Initializing {Server} {Database} to version {Version}", connectionStringBuilder.DataSource, connectionStringBuilder.InitialCatalog, version);
_logger.LogInformation("Initializing {Server} {Database}", connectionStringBuilder.DataSource, connectionStringBuilder.InitialCatalog);
await using var connection = new SqlConnection(_configuration.ConnectionString);
await connection.OpenAsync(cancellationToken);
// Synchronous calls are used because this code is executed on startup and doesn't need to be async.
// Additionally, XUnit task scheduler constraints prevent async calls from being easily tested.
await using SqlCommand sqlCommand = connection.CreateCommand();
sqlCommand.CommandText = @"
SET XACT_ABORT ON
BEGIN TRANSACTION
INSERT INTO dbo.ResourceType (Name)
SELECT value FROM string_split(@resourceTypes, ',')
EXCEPT SELECT Name FROM dbo.ResourceType WITH (TABLOCKX);
-- result set 1
SELECT ResourceTypeId, Name FROM dbo.ResourceType;
INSERT INTO dbo.SearchParam (Uri)
SELECT * FROM OPENJSON (@searchParams)
WITH (Uri varchar(128) '$.Uri')
EXCEPT SELECT Uri FROM dbo.SearchParam;
-- result set 2
SELECT Uri, SearchParamId FROM dbo.SearchParam;
INSERT INTO dbo.ClaimType (Name)
SELECT value FROM string_split(@claimTypes, ',')
EXCEPT SELECT Name FROM dbo.ClaimType;
-- result set 3
SELECT ClaimTypeId, Name FROM dbo.ClaimType;
INSERT INTO dbo.CompartmentType (Name)
SELECT value FROM string_split(@compartmentTypes, ',')
EXCEPT SELECT Name FROM dbo.CompartmentType;
-- result set 4
SELECT CompartmentTypeId, Name FROM dbo.CompartmentType;
COMMIT TRANSACTION
-- result set 5
SELECT Value, SystemId from dbo.System;
-- result set 6
SELECT Value, QuantityCodeId FROM dbo.QuantityCode";
string commaSeparatedResourceTypes = string.Join(",", ModelInfoProvider.GetResourceTypeNames());
string searchParametersJson = JsonConvert.SerializeObject(_searchParameterDefinitionManager.AllSearchParameters.Select(p => new { Name = p.Name, Uri = p.Url }));
string commaSeparatedClaimTypes = string.Join(',', _securityConfiguration.PrincipalClaims);
string commaSeparatedCompartmentTypes = string.Join(',', ModelInfoProvider.GetCompartmentTypeNames());
sqlCommand.Parameters.AddWithValue("@resourceTypes", commaSeparatedResourceTypes);
sqlCommand.Parameters.AddWithValue("@searchParams", searchParametersJson);
sqlCommand.Parameters.AddWithValue("@claimTypes", commaSeparatedClaimTypes);
sqlCommand.Parameters.AddWithValue("@compartmentTypes", commaSeparatedCompartmentTypes);
await using SqlDataReader reader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken);
var resourceTypeToId = new Dictionary<string, short>(StringComparer.Ordinal);
var resourceTypeIdToTypeName = new Dictionary<short, string>();
var searchParamUriToId = new Dictionary<Uri, short>();
var systemToId = new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var quantityCodeToId = new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var claimNameToId = new Dictionary<string, byte>(StringComparer.Ordinal);
var compartmentTypeToId = new Dictionary<string, byte>();
// result set 1
while (await reader.ReadAsync(cancellationToken))
// If we are applying a full snap shot schema file, or if the server is just starting up
if (runAllInitialization || _highestInitializedVersion == 0)
{
(short id, string resourceTypeName) = reader.ReadRow(VLatest.ResourceType.ResourceTypeId, VLatest.ResourceType.Name);
// Run the schema initialization required for all schema versions, from the minimum version to the current version.
InitializeBase();
resourceTypeToId.Add(resourceTypeName, id);
resourceTypeIdToTypeName.Add(id, resourceTypeName);
if (version >= SchemaVersionConstants.SearchParameterStatusSchemaVersion)
{
InitializeSearchParameterStatuses();
}
}
else
{
// Only run the schema initialization required for the current version
if (version == SchemaVersionConstants.SearchParameterStatusSchemaVersion)
{
InitializeSearchParameterStatuses();
}
}
// result set 2
await reader.NextResultAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
(string uri, short searchParamId) = reader.ReadRow(VLatest.SearchParam.Uri, VLatest.SearchParam.SearchParamId);
searchParamUriToId.Add(new Uri(uri), searchParamId);
}
// result set 3
await reader.NextResultAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
(byte id, string claimTypeName) = reader.ReadRow(VLatest.ClaimType.ClaimTypeId, VLatest.ClaimType.Name);
claimNameToId.Add(claimTypeName, id);
}
// result set 4
await reader.NextResultAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
(byte id, string compartmentName) = reader.ReadRow(VLatest.CompartmentType.CompartmentTypeId, VLatest.CompartmentType.Name);
compartmentTypeToId.Add(compartmentName, id);
}
// result set 5
await reader.NextResultAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
var (value, systemId) = reader.ReadRow(VLatest.System.Value, VLatest.System.SystemId);
systemToId.TryAdd(value, systemId);
}
// result set 6
await reader.NextResultAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
(string value, int quantityCodeId) = reader.ReadRow(VLatest.QuantityCode.Value, VLatest.QuantityCode.QuantityCodeId);
quantityCodeToId.TryAdd(value, quantityCodeId);
}
_resourceTypeToId = resourceTypeToId;
_resourceTypeIdToTypeName = resourceTypeIdToTypeName;
_searchParamUriToId = searchParamUriToId;
_systemToId = systemToId;
_quantityCodeToId = quantityCodeToId;
_claimNameToId = claimNameToId;
_compartmentTypeToId = compartmentTypeToId;
_started = true;
_highestInitializedVersion = version;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private void InitializeBase()
{
using (var connection = new SqlConnection(_configuration.ConnectionString))
{
connection.Open();
// Synchronous calls are used because this code is executed on startup and doesn't need to be async.
// Additionally, XUnit task scheduler constraints prevent async calls from being easily tested.
using (SqlCommand sqlCommand = connection.CreateCommand())
{
sqlCommand.CommandText = @"
SET XACT_ABORT ON
BEGIN TRANSACTION
INSERT INTO dbo.ResourceType (Name)
SELECT value FROM string_split(@resourceTypes, ',')
EXCEPT SELECT Name FROM dbo.ResourceType WITH (TABLOCKX);
-- result set 1
SELECT ResourceTypeId, Name FROM dbo.ResourceType;
INSERT INTO dbo.SearchParam (Uri)
SELECT * FROM OPENJSON (@searchParams)
WITH (Uri varchar(128) '$.Uri')
EXCEPT SELECT Uri FROM dbo.SearchParam;
-- result set 2
SELECT Uri, SearchParamId FROM dbo.SearchParam;
INSERT INTO dbo.ClaimType (Name)
SELECT value FROM string_split(@claimTypes, ',')
EXCEPT SELECT Name FROM dbo.ClaimType;
-- result set 3
SELECT ClaimTypeId, Name FROM dbo.ClaimType;
INSERT INTO dbo.CompartmentType (Name)
SELECT value FROM string_split(@compartmentTypes, ',')
EXCEPT SELECT Name FROM dbo.CompartmentType;
-- result set 4
SELECT CompartmentTypeId, Name FROM dbo.CompartmentType;
COMMIT TRANSACTION
-- result set 5
SELECT Value, SystemId from dbo.System;
-- result set 6
SELECT Value, QuantityCodeId FROM dbo.QuantityCode";
string searchParametersJson = JsonConvert.SerializeObject(_searchParameterDefinitionManager.AllSearchParameters.Select(p => new { Uri = p.Url }));
string commaSeparatedResourceTypes = string.Join(",", ModelInfoProvider.GetResourceTypeNames());
string commaSeparatedClaimTypes = string.Join(',', _securityConfiguration.PrincipalClaims);
string commaSeparatedCompartmentTypes = string.Join(',', ModelInfoProvider.GetCompartmentTypeNames());
sqlCommand.Parameters.AddWithValue("@searchParams", searchParametersJson);
sqlCommand.Parameters.AddWithValue("@resourceTypes", commaSeparatedResourceTypes);
sqlCommand.Parameters.AddWithValue("@claimTypes", commaSeparatedClaimTypes);
sqlCommand.Parameters.AddWithValue("@compartmentTypes", commaSeparatedCompartmentTypes);
using (SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.SequentialAccess))
{
var resourceTypeToId = new Dictionary<string, short>(StringComparer.Ordinal);
var resourceTypeIdToTypeName = new Dictionary<short, string>();
var searchParamUriToId = new Dictionary<Uri, short>();
var systemToId = new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var quantityCodeToId = new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var claimNameToId = new Dictionary<string, byte>(StringComparer.Ordinal);
var compartmentTypeToId = new Dictionary<string, byte>();
// result set 1
while (reader.Read())
{
(short id, string resourceTypeName) = reader.ReadRow(VLatest.ResourceType.ResourceTypeId, VLatest.ResourceType.Name);
resourceTypeToId.Add(resourceTypeName, id);
resourceTypeIdToTypeName.Add(id, resourceTypeName);
}
// result set 2
reader.NextResult();
while (reader.Read())
{
(string uri, short searchParamId) = reader.ReadRow(VLatest.SearchParam.Uri, VLatest.SearchParam.SearchParamId);
searchParamUriToId.Add(new Uri(uri), searchParamId);
}
// result set 3
reader.NextResult();
while (reader.Read())
{
(byte id, string claimTypeName) = reader.ReadRow(VLatest.ClaimType.ClaimTypeId, VLatest.ClaimType.Name);
claimNameToId.Add(claimTypeName, id);
}
// result set 4
reader.NextResult();
while (reader.Read())
{
(byte id, string compartmentName) = reader.ReadRow(VLatest.CompartmentType.CompartmentTypeId, VLatest.CompartmentType.Name);
compartmentTypeToId.Add(compartmentName, id);
}
// result set 5
reader.NextResult();
while (reader.Read())
{
var (value, systemId) = reader.ReadRow(VLatest.System.Value, VLatest.System.SystemId);
systemToId.TryAdd(value, systemId);
}
// result set 6
reader.NextResult();
while (reader.Read())
{
(string value, int quantityCodeId) = reader.ReadRow(VLatest.QuantityCode.Value, VLatest.QuantityCode.QuantityCodeId);
quantityCodeToId.TryAdd(value, quantityCodeId);
}
_resourceTypeToId = resourceTypeToId;
_resourceTypeIdToTypeName = resourceTypeIdToTypeName;
_searchParamUriToId = searchParamUriToId;
_systemToId = systemToId;
_quantityCodeToId = quantityCodeToId;
_claimNameToId = claimNameToId;
_compartmentTypeToId = compartmentTypeToId;
}
}
}
}
private void InitializeSearchParameterStatuses()
{
using (var connection = new SqlConnection(_configuration.ConnectionString))
{
connection.Open();
using (SqlCommand sqlCommand = connection.CreateCommand())
{
sqlCommand.CommandText = @"
SET XACT_ABORT ON
BEGIN TRANSACTION
DECLARE @lastUpdated datetimeoffset(7) = SYSDATETIMEOFFSET()
UPDATE dbo.SearchParam
SET Status = sps.Status, LastUpdated = @lastUpdated, IsPartiallySupported = sps.IsPartiallySupported
FROM dbo.SearchParam INNER JOIN @searchParamStatuses as sps
ON dbo.SearchParam.Uri = sps.Uri
COMMIT TRANSACTION";
IEnumerable<ResourceSearchParameterStatus> statuses = _filebasedSearchParameterStatusDataStore
.GetSearchParameterStatuses().GetAwaiter().GetResult();
var collection = new SearchParameterStatusCollection();
collection.AddRange(statuses);
var tableValuedParameter = new SqlParameter
{
ParameterName = "searchParamStatuses",
SqlDbType = SqlDbType.Structured,
Value = collection,
Direction = ParameterDirection.Input,
TypeName = "dbo.SearchParamTableType_1",
};
sqlCommand.Parameters.Add(tableValuedParameter);
sqlCommand.ExecuteNonQuery();
}
}
}
private int GetStringId(ConcurrentDictionary<string, int> cache, string stringValue, Table table, Column<int> idColumn, Column<string> stringColumn)
{
@ -324,13 +403,23 @@ namespace Microsoft.Health.Fhir.SqlServer.Features.Storage
}
}
private void ThrowIfNotStarted()
private void ThrowIfNotInitialized()
{
if (!_started)
ThrowIfCurrentSchemaVersionIsNull();
if (_highestInitializedVersion < _schemaInformation.Current)
{
_logger.LogError($"The {nameof(SqlServerFhirModel)} instance has not been initialized.");
_logger.LogError($"The {nameof(SqlServerFhirModel)} instance has not run the initialization required for the current schema version");
throw new ServiceUnavailableException();
}
}
private void ThrowIfCurrentSchemaVersionIsNull()
{
if (_schemaInformation.Current == null)
{
throw new InvalidOperationException(Resources.SchemaVersionShouldNotBeNull);
}
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
using System.Collections.Generic;
using System.Linq;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.SqlServer.Features.Schema.Model;
using Microsoft.Health.SqlServer.Features.Schema.Model;
namespace Microsoft.Health.Fhir.SqlServer.Features.Storage.TvpRowGeneration
{
internal class SearchParameterStatusRowGenerator : ITableValuedParameterRowGenerator<List<ResourceSearchParameterStatus>, VLatest.SearchParamTableTypeRow>
{
public IEnumerable<VLatest.SearchParamTableTypeRow> GenerateRows(List<ResourceSearchParameterStatus> searchParameterStatuses)
{
return searchParameterStatuses.Select(searchParameterStatus => new VLatest.SearchParamTableTypeRow(
searchParameterStatus.Uri.ToString(),
searchParameterStatus.Status.ToString(),
searchParameterStatus.IsPartiallySupported))
.ToList();
}
}
}

Просмотреть файл

@ -7,17 +7,23 @@
<None Remove="Features\Schema\Migrations\3.diff.sql" />
<None Remove="Features\Schema\Migrations\4.diff.sql" />
<None Remove="Features\Schema\Migrations\4.sql" />
<None Remove="Features\Schema\Migrations\5.diff.sql" />
<None Remove="Features\Schema\Migrations\5.sql" />
<None Remove="Features\Schema\Migrations\6.diff.sql" />
<None Remove="Features\Schema\Migrations\6.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\2.diff.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\3.diff.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\4.diff.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\4.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\5.diff.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\5.sql" />
<GenerateFilesInputs Include="Features\Schema\Migrations\5.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\6.diff.sql" />
<EmbeddedResource Include="Features\Schema\Migrations\6.sql" />
<GenerateFilesInputs Include="Features\Schema\Migrations\6.sql" />
<Generated Include="Features\Schema\Model\VLatest.Generated.cs">
<Generator>SqlModelGenerator</Generator>
<Namespace>Microsoft.Health.Fhir.SqlServer.Features.Schema.Model</Namespace>
<Args>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\Features\Schema\Migrations\5.sql'))</Args>
<Args>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\Features\Schema\Migrations\6.sql'))</Args>
</Generated>
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -8,13 +8,16 @@ using System.Linq;
using EnsureThat;
using Microsoft.Extensions.Configuration;
using Microsoft.Health.Extensions.DependencyInjection;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.Core.Registration;
using Microsoft.Health.Fhir.SqlServer.Features.Schema;
using Microsoft.Health.Fhir.SqlServer.Features.Search;
using Microsoft.Health.Fhir.SqlServer.Features.Search.Expressions.Visitors;
using Microsoft.Health.Fhir.SqlServer.Features.Storage;
using Microsoft.Health.Fhir.SqlServer.Features.Storage.Registry;
using Microsoft.Health.SqlServer.Api.Registration;
using Microsoft.Health.SqlServer.Configs;
using Microsoft.Health.SqlServer.Features.Client;
using Microsoft.Health.SqlServer.Features.Schema;
using Microsoft.Health.SqlServer.Features.Schema.Model;
using Microsoft.Health.SqlServer.Registration;
@ -36,6 +39,11 @@ namespace Microsoft.Extensions.DependencyInjection
.AsSelf()
.AsImplementedInterfaces();
services.Add<SqlServerSearchParameterStatusDataStore>()
.Singleton()
.AsSelf()
.ReplaceService<ISearchParameterStatusDataStore>();
services.Add<SqlServerFhirModel>()
.Singleton()
.AsSelf()
@ -87,6 +95,12 @@ namespace Microsoft.Extensions.DependencyInjection
.AsSelf()
.AsImplementedInterfaces();
services.AddFactory<IScoped<SqlConnectionWrapperFactory>>();
services.Add<SchemaUpgradedHandler>()
.Transient()
.AsImplementedInterfaces();
return fhirServerBuilder;
}

Просмотреть файл

@ -19,7 +19,7 @@ namespace Microsoft.Health.Fhir.SqlServer {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@ -60,6 +60,15 @@ namespace Microsoft.Health.Fhir.SqlServer {
}
}
/// <summary>
/// Looks up a localized string similar to Cyclic include iterate queries are not supported..
/// </summary>
internal static string CyclicIncludeIterateNotSupported {
get {
return ResourceManager.GetString("CyclicIncludeIterateNotSupported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The provided continuation token is not valid..
/// </summary>
@ -69,6 +78,33 @@ namespace Microsoft.Health.Fhir.SqlServer {
}
}
/// <summary>
/// Looks up a localized string similar to Cannot carry out the SQL datastore operation because the SQL schema needs to be upgraded..
/// </summary>
internal static string SchemaVersionNeedsToBeUpgraded {
get {
return ResourceManager.GetString("SchemaVersionNeedsToBeUpgraded", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The current schema version should not be null..
/// </summary>
internal static string SchemaVersionShouldNotBeNull {
get {
return ResourceManager.GetString("SchemaVersionShouldNotBeNull", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search parameter status information should not be null..
/// </summary>
internal static string SearchParameterStatusShouldNotBeNull {
get {
return ResourceManager.GetString("SearchParameterStatusShouldNotBeNull", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There was an internal server error while processing the transaction..
/// </summary>
@ -77,14 +113,5 @@ namespace Microsoft.Health.Fhir.SqlServer {
return ResourceManager.GetString("TransactionProcessingException", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cyclic Include Iterate Not Supported..
/// </summary>
internal static string CyclicIncludeIterateNotSupported {
get {
return ResourceManager.GetString("CyclicIncludeIterateNotSupported", resourceCulture);
}
}
}
}

Просмотреть файл

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -126,4 +126,13 @@
<data name="CyclicIncludeIterateNotSupported" xml:space="preserve">
<value>Cyclic include iterate queries are not supported.</value>
</data>
</root>
<data name="SchemaVersionNeedsToBeUpgraded" xml:space="preserve">
<value>Cannot carry out the SQL datastore operation because the SQL schema needs to be upgraded.</value>
</data>
<data name="SchemaVersionShouldNotBeNull" xml:space="preserve">
<value>The current schema version should not be null.</value>
</data>
<data name="SearchParameterStatusShouldNotBeNull" xml:space="preserve">
<value>Search parameter status information should not be null.</value>
</data>
</root>

Просмотреть файл

@ -52,7 +52,7 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Features.Operations.Reindex
private readonly ISearchIndexer _searchIndexer = Substitute.For<ISearchIndexer>();
private ISupportedSearchParameterDefinitionManager _supportedSearchParameterDefinitionManager;
private SearchableSearchParameterDefinitionManager _searchableSearchParameterDefinitionManager;
private readonly ISearchParameterRegistry _searchParameterRegistry = Substitute.For<ISearchParameterRegistry>();
private readonly ISearchParameterStatusDataStore _searchParameterStatusDataStore = Substitute.For<ISearchParameterStatusDataStore>();
private readonly IOptions<CoreFeatureConfiguration> coreOptions = Substitute.For<IOptions<CoreFeatureConfiguration>>();
private ReindexJobWorker _reindexJobWorker;
@ -91,7 +91,7 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Features.Operations.Reindex
_searchIndexer,
Deserializers.ResourceDeserializer,
_supportedSearchParameterDefinitionManager,
_searchParameterRegistry);
_searchParameterStatusDataStore);
coreOptions.Value.Returns(new CoreFeatureConfiguration());

Просмотреть файл

@ -14,6 +14,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Features\Operations\Export\CreateExportRequestHandlerTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Operations\FhirOperationTestConstants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Operations\Reindex\ReindexJobTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\SearchParameterStatusDataStoreTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\SqlServerSchemaUpgradeTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\CosmosDbFhirStorageTestHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\CosmosDbFhirStorageTestsFixture.cs" />

Просмотреть файл

@ -7,6 +7,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.Health.Core.Extensions;
using Microsoft.Health.Fhir.CosmosDb.Features.Storage.Registry;
using Newtonsoft.Json.Linq;
using Xunit;
@ -18,17 +20,10 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
private const string ReindexJobPartitionKey = "ReindexJob";
private readonly Container _documentClient;
private readonly string _databaseId;
private readonly string _collectionId;
public CosmosDbFhirStorageTestHelper(
Container documentClient,
string databaseId,
string collectionId)
public CosmosDbFhirStorageTestHelper(Container documentClient)
{
_documentClient = documentClient;
_databaseId = databaseId;
_collectionId = collectionId;
}
public async Task DeleteAllExportJobRecordsAsync(CancellationToken cancellationToken = default)
@ -41,6 +36,11 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
await _documentClient.DeleteItemStreamAsync(id, new PartitionKey(ExportJobPartitionKey), cancellationToken: cancellationToken);
}
public async Task DeleteSearchParameterStatusAsync(string uri, CancellationToken cancellationToken = default)
{
await _documentClient.DeleteItemStreamAsync(uri.ComputeHash(), new PartitionKey(SearchParameterStatusWrapper.SearchParameterStatusPartitionKey), cancellationToken: cancellationToken);
}
public async Task DeleteAllReindexJobRecordsAsync(CancellationToken cancellationToken = default)
{
await DeleteAllRecordsAsync(ReindexJobPartitionKey, cancellationToken);

Просмотреть файл

@ -44,7 +44,8 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
private IFhirDataStore _fhirDataStore;
private IFhirOperationDataStore _fhirOperationDataStore;
private IFhirStorageTestHelper _fhirStorageTestHelper;
private FilebasedSearchParameterRegistry _filebasedSearchParameterRegistry;
private FilebasedSearchParameterStatusDataStore _filebasedSearchParameterStatusDataStore;
private ISearchParameterStatusDataStore _searchParameterStatusDataStore;
private CosmosClient _cosmosClient;
public CosmosDbFhirStorageTestsFixture()
@ -79,14 +80,14 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
var searchParameterDefinitionManager = new SearchParameterDefinitionManager(ModelInfoProvider.Instance);
await searchParameterDefinitionManager.StartAsync(CancellationToken.None);
_filebasedSearchParameterRegistry = new FilebasedSearchParameterRegistry(searchParameterDefinitionManager, ModelInfoProvider.Instance);
_filebasedSearchParameterStatusDataStore = new FilebasedSearchParameterStatusDataStore(searchParameterDefinitionManager, ModelInfoProvider.Instance);
var updaters = new ICollectionUpdater[]
{
new FhirCollectionSettingsUpdater(_cosmosDataStoreConfiguration, optionsMonitor, NullLogger<FhirCollectionSettingsUpdater>.Instance),
new StoredProcedureInstaller(fhirStoredProcs),
new CosmosDbStatusRegistryInitializer(
() => _filebasedSearchParameterRegistry,
new CosmosDbSearchParameterStatusInitializer(
() => _filebasedSearchParameterStatusDataStore,
new CosmosQueryFactory(
new CosmosResponseProcessor(Substitute.For<IFhirRequestContextAccessor>(), Substitute.For<IMediator>(), NullLogger<CosmosResponseProcessor>.Instance),
NullFhirCosmosQueryLogger.Instance)),
@ -124,6 +125,11 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
var documentClient = new NonDisposingScope(_container);
_searchParameterStatusDataStore = new CosmosDbSearchParameterStatusDataStore(
() => documentClient,
_cosmosDataStoreConfiguration,
cosmosDocumentQueryFactory);
_fhirDataStore = new CosmosFhirDataStore(
documentClient,
_cosmosDataStoreConfiguration,
@ -142,10 +148,7 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
new CosmosQueryFactory(responseProcessor, new NullFhirCosmosQueryLogger()),
NullLogger<CosmosFhirOperationDataStore>.Instance);
_fhirStorageTestHelper = new CosmosDbFhirStorageTestHelper(
_container,
_cosmosDataStoreConfiguration.DatabaseId,
_cosmosCollectionConfiguration.CollectionId);
_fhirStorageTestHelper = new CosmosDbFhirStorageTestHelper(_container);
}
public async Task DisposeAsync()
@ -180,6 +183,16 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
return this;
}
if (serviceType == typeof(ISearchParameterStatusDataStore))
{
return _searchParameterStatusDataStore;
}
if (serviceType == typeof(FilebasedSearchParameterStatusDataStore))
{
return _filebasedSearchParameterStatusDataStore;
}
return null;
}
}

Просмотреть файл

@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Health.Abstractions.Features.Transactions;
using Microsoft.Health.Fhir.Core.Features.Operations;
using Microsoft.Health.Fhir.Core.Features.Persistence;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.Tests.Common.FixtureParameters;
using Xunit;
@ -41,6 +42,10 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
public ITransactionHandler TransactionHandler => _fixture.GetRequiredService<ITransactionHandler>();
public ISearchParameterStatusDataStore SearchParameterStatusDataStore => _fixture.GetRequiredService<ISearchParameterStatusDataStore>();
public FilebasedSearchParameterStatusDataStore FilebasedSearchParameterStatusDataStore => _fixture.GetRequiredService<FilebasedSearchParameterStatusDataStore>();
void IDisposable.Dispose()
{
(_fixture as IDisposable)?.Dispose();

Просмотреть файл

@ -25,6 +25,14 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
/// <returns>A task.</returns>
Task DeleteExportJobRecordAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes specified search parameter statuses from the database.
/// </summary>
/// <param name="uri">The string URI of the status to be deleted.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task.</returns>
Task DeleteSearchParameterStatusAsync(string uri, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes all reindex job records from the database.
/// </summary>

Просмотреть файл

@ -0,0 +1,141 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.Tests.Common.FixtureParameters;
using Xunit;
namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
{
[FhirStorageTestsFixtureArgumentSets(DataStore.All)]
public class SearchParameterStatusDataStoreTests : IClassFixture<FhirStorageTestsFixture>
{
private readonly FhirStorageTestsFixture _fixture;
private readonly IFhirStorageTestHelper _testHelper;
public SearchParameterStatusDataStoreTests(FhirStorageTestsFixture fixture)
{
_fixture = fixture;
_testHelper = fixture.TestHelper;
}
[Fact]
public async Task GivenAStatusRegistry_WhenGettingStatuses_ThenTheStatusesAreRetrieved()
{
IReadOnlyCollection<ResourceSearchParameterStatus> expectedStatuses = await _fixture.FilebasedSearchParameterStatusDataStore.GetSearchParameterStatuses();
IReadOnlyCollection<ResourceSearchParameterStatus> actualStatuses = await _fixture.SearchParameterStatusDataStore.GetSearchParameterStatuses();
ValidateSearchParameterStatuses(expectedStatuses, actualStatuses);
}
[Fact]
public async Task GivenAStatusRegistry_WhenUpsertingNewStatuses_ThenTheStatusesAreAdded()
{
string statusName1 = "http://hl7.org/fhir/SearchParameter/Test-1";
string statusName2 = "http://hl7.org/fhir/SearchParameter/Test-2";
var status1 = new ResourceSearchParameterStatus
{
Uri = new Uri(statusName1), Status = SearchParameterStatus.Disabled, IsPartiallySupported = false,
};
var status2 = new ResourceSearchParameterStatus
{
Uri = new Uri(statusName2), Status = SearchParameterStatus.Disabled, IsPartiallySupported = false,
};
IReadOnlyCollection<ResourceSearchParameterStatus> readonlyStatusesBeforeUpsert = await _fixture.SearchParameterStatusDataStore.GetSearchParameterStatuses();
var expectedStatuses = readonlyStatusesBeforeUpsert.ToList();
expectedStatuses.Add(status1);
expectedStatuses.Add(status2);
var statusesToUpsert = new List<ResourceSearchParameterStatus> { status1, status2 };
try
{
await _fixture.SearchParameterStatusDataStore.UpsertStatuses(statusesToUpsert);
IReadOnlyCollection<ResourceSearchParameterStatus> actualStatuses = await _fixture.SearchParameterStatusDataStore.GetSearchParameterStatuses();
ValidateSearchParameterStatuses(expectedStatuses, actualStatuses);
}
finally
{
await _testHelper.DeleteSearchParameterStatusAsync(statusName1);
await _testHelper.DeleteSearchParameterStatusAsync(statusName2);
}
}
[Fact]
public async Task GivenAStatusRegistry_WhenUpsertingExistingStatuses_ThenTheExistingStatusesAreUpdated()
{
IReadOnlyCollection<ResourceSearchParameterStatus> statusesBeforeUpdate = await _fixture.SearchParameterStatusDataStore.GetSearchParameterStatuses();
// Get two existing statuses.
ResourceSearchParameterStatus expectedStatus1 = statusesBeforeUpdate.First();
ResourceSearchParameterStatus expectedStatus2 = statusesBeforeUpdate.Last();
// Modify them in some way.
expectedStatus1.IsPartiallySupported = !expectedStatus1.IsPartiallySupported;
expectedStatus2.IsPartiallySupported = !expectedStatus2.IsPartiallySupported;
var statusesToUpsert = new List<ResourceSearchParameterStatus> { expectedStatus1, expectedStatus2 };
try
{
// Upsert the two existing, modified statuses.
await _fixture.SearchParameterStatusDataStore.UpsertStatuses(statusesToUpsert);
IReadOnlyCollection<ResourceSearchParameterStatus> statusesAfterUpdate =
await _fixture.SearchParameterStatusDataStore.GetSearchParameterStatuses();
Assert.Equal(statusesBeforeUpdate.Count, statusesAfterUpdate.Count);
ResourceSearchParameterStatus actualStatus1 = statusesAfterUpdate.FirstOrDefault(s => s.Uri.Equals(expectedStatus1.Uri));
ResourceSearchParameterStatus actualStatus2 = statusesAfterUpdate.FirstOrDefault(s => s.Uri.Equals(expectedStatus2.Uri));
Assert.NotNull(actualStatus1);
Assert.NotNull(actualStatus2);
Assert.Equal(expectedStatus1.Status, actualStatus1.Status);
Assert.Equal(expectedStatus1.IsPartiallySupported, actualStatus1.IsPartiallySupported);
Assert.Equal(expectedStatus2.Status, actualStatus2.Status);
Assert.Equal(expectedStatus2.IsPartiallySupported, actualStatus2.IsPartiallySupported);
}
finally
{
// Reset changes made.
expectedStatus1.IsPartiallySupported = !expectedStatus1.IsPartiallySupported;
expectedStatus2.IsPartiallySupported = !expectedStatus2.IsPartiallySupported;
statusesToUpsert = new List<ResourceSearchParameterStatus> { expectedStatus1, expectedStatus2 };
await _fixture.SearchParameterStatusDataStore.UpsertStatuses(statusesToUpsert);
}
}
private static void ValidateSearchParameterStatuses(IReadOnlyCollection<ResourceSearchParameterStatus> expectedStatuses, IReadOnlyCollection<ResourceSearchParameterStatus> actualStatuses)
{
Assert.NotEmpty(expectedStatuses);
var sortedExpected = expectedStatuses.OrderBy(status => status.Uri.ToString()).ToList();
var sortedActual = actualStatuses.OrderBy(status => status.Uri.ToString()).ToList();
Assert.Equal(sortedExpected.Count, sortedActual.Count);
for (int i = 0; i < sortedExpected.Count; i++)
{
Assert.Equal(sortedExpected[i].Uri, sortedActual[i].Uri);
Assert.Equal(sortedExpected[i].Status, sortedActual[i].Status);
Assert.Equal(sortedExpected[i].IsPartiallySupported, sortedActual[i].IsPartiallySupported);
}
}
}
}

Просмотреть файл

@ -11,6 +11,7 @@ using EnsureThat;
using MediatR;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Health.Fhir.Core.Features.Definition;
using Microsoft.Health.Fhir.SqlServer.Features.Schema;
using Microsoft.Health.Fhir.SqlServer.Features.Storage;
using Microsoft.Health.SqlServer;
@ -29,15 +30,24 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
{
private readonly string _masterDatabaseName;
private readonly string _initialConnectionString;
private readonly SearchParameterDefinitionManager _searchParameterDefinitionManager;
private readonly SqlServerFhirModel _sqlServerFhirModel;
private readonly ISqlConnectionFactory _sqlConnectionFactory;
public SqlServerFhirStorageTestHelper(string initialConnectionString, string masterDatabaseName, SqlServerFhirModel sqlServerFhirModel, ISqlConnectionFactory sqlConnectionFactory)
public SqlServerFhirStorageTestHelper(
string initialConnectionString,
string masterDatabaseName,
SearchParameterDefinitionManager searchParameterDefinitionManager,
SqlServerFhirModel sqlServerFhirModel,
ISqlConnectionFactory sqlConnectionFactory)
{
EnsureArg.IsNotNull(searchParameterDefinitionManager, nameof(searchParameterDefinitionManager));
EnsureArg.IsNotNull(sqlServerFhirModel, nameof(sqlServerFhirModel));
EnsureArg.IsNotNull(sqlConnectionFactory, nameof(sqlConnectionFactory));
_masterDatabaseName = masterDatabaseName;
_initialConnectionString = initialConnectionString;
_searchParameterDefinitionManager = searchParameterDefinitionManager;
_sqlServerFhirModel = sqlServerFhirModel;
_sqlConnectionFactory = sqlConnectionFactory;
}
@ -80,7 +90,8 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
});
await schemaInitializer.InitializeAsync(forceIncrementalSchemaUpgrade, cancellationToken);
await _sqlServerFhirModel.StartAsync(cancellationToken);
await _searchParameterDefinitionManager.StartAsync(CancellationToken.None);
_sqlServerFhirModel.Initialize(SchemaVersionConstants.Max, true);
}
public async Task DeleteDatabase(string databaseName, CancellationToken cancellationToken = default)
@ -137,6 +148,18 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
}
}
public async Task DeleteSearchParameterStatusAsync(string uri, CancellationToken cancellationToken = default)
{
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync())
{
var command = new SqlCommand("DELETE FROM dbo.SearchParam WHERE Uri = @uri", connection);
command.Parameters.AddWithValue("@uri", uri);
await command.Connection.OpenAsync(cancellationToken);
await command.ExecuteNonQueryAsync(cancellationToken);
}
}
public Task DeleteAllReindexJobRecordsAsync(CancellationToken cancellationToken = default)
{
throw new NotImplementedException();

Просмотреть файл

@ -4,9 +4,9 @@
// -------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using Hl7.Fhir.Model;
using MediatR;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
@ -14,14 +14,16 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Health.Abstractions.Features.Transactions;
using Microsoft.Health.Fhir.Core.Configs;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Definition;
using Microsoft.Health.Fhir.Core.Features.Operations;
using Microsoft.Health.Fhir.Core.Features.Persistence;
using Microsoft.Health.Fhir.Core.Features.Search;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.Core.Models;
using Microsoft.Health.Fhir.Core.UnitTests.Extensions;
using Microsoft.Health.Fhir.SqlServer.Features.Schema;
using Microsoft.Health.Fhir.SqlServer.Features.Schema.Model;
using Microsoft.Health.Fhir.SqlServer.Features.Storage;
using Microsoft.Health.Fhir.SqlServer.Features.Storage.Registry;
using Microsoft.Health.SqlServer;
using Microsoft.Health.SqlServer.Configs;
using Microsoft.Health.SqlServer.Features.Client;
@ -43,6 +45,7 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
private readonly IFhirOperationDataStore _fhirOperationDataStore;
private readonly SqlServerFhirStorageTestHelper _testHelper;
private readonly SchemaInitializer _schemaInitializer;
private readonly FilebasedSearchParameterStatusDataStore _filebasedSearchParameterStatusDataStore;
public SqlServerFhirStorageTestsFixture()
{
@ -63,16 +66,19 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
var schemaUpgradeRunner = new SchemaUpgradeRunner(scriptProvider, baseScriptProvider, mediator, NullLogger<SchemaUpgradeRunner>.Instance, sqlConnectionFactory);
_schemaInitializer = new SchemaInitializer(config, schemaUpgradeRunner, schemaInformation, sqlConnectionFactory, NullLogger<SchemaInitializer>.Instance);
var searchParameterDefinitionManager = Substitute.For<ISearchParameterDefinitionManager>();
searchParameterDefinitionManager.AllSearchParameters.Returns(new[]
{
new SearchParameter { Name = SearchParameterNames.Id, Type = SearchParamType.Token, Url = SearchParameterNames.IdUri.ToString() }.ToInfo(),
new SearchParameter { Name = SearchParameterNames.LastUpdated, Type = SearchParamType.Date, Url = SearchParameterNames.LastUpdatedUri.ToString() }.ToInfo(),
});
var searchParameterDefinitionManager = new SearchParameterDefinitionManager(ModelInfoProvider.Instance);
_filebasedSearchParameterStatusDataStore = new FilebasedSearchParameterStatusDataStore(searchParameterDefinitionManager, ModelInfoProvider.Instance);
var securityConfiguration = new SecurityConfiguration { PrincipalClaims = { "oid" } };
var sqlServerFhirModel = new SqlServerFhirModel(config, _schemaInitializer, searchParameterDefinitionManager, Options.Create(securityConfiguration), NullLogger<SqlServerFhirModel>.Instance);
var sqlServerFhirModel = new SqlServerFhirModel(
config,
schemaInformation,
searchParameterDefinitionManager,
() => _filebasedSearchParameterStatusDataStore,
Options.Create(securityConfiguration),
NullLogger<SqlServerFhirModel>.Instance);
var serviceCollection = new ServiceCollection();
serviceCollection.AddSqlServerTableRowParameterGenerators();
@ -81,17 +87,24 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
var upsertResourceTvpGenerator = serviceProvider.GetRequiredService<VLatest.UpsertResourceTvpGenerator<ResourceMetadata>>();
var upsertSearchParamsTvpGenerator = serviceProvider.GetRequiredService<VLatest.UpsertSearchParamsTvpGenerator<List<ResourceSearchParameterStatus>>>();
var searchParameterToSearchValueTypeMap = new SearchParameterToSearchValueTypeMap(new SupportedSearchParameterDefinitionManager(searchParameterDefinitionManager));
SqlTransactionHandler = new SqlTransactionHandler();
SqlConnectionWrapperFactory = new SqlConnectionWrapperFactory(SqlTransactionHandler, new SqlCommandWrapperFactory(), sqlConnectionFactory);
SqlServerSearchParameterStatusDataStore = new SqlServerSearchParameterStatusDataStore(
() => SqlConnectionWrapperFactory.CreateMockScope(),
upsertSearchParamsTvpGenerator,
() => _filebasedSearchParameterStatusDataStore,
schemaInformation);
_fhirDataStore = new SqlServerFhirDataStore(config, sqlServerFhirModel, searchParameterToSearchValueTypeMap, upsertResourceTvpGenerator, Options.Create(new CoreFeatureConfiguration()), SqlConnectionWrapperFactory, NullLogger<SqlServerFhirDataStore>.Instance, schemaInformation);
_fhirOperationDataStore = new SqlServerFhirOperationDataStore(SqlConnectionWrapperFactory, NullLogger<SqlServerFhirOperationDataStore>.Instance);
_testHelper = new SqlServerFhirStorageTestHelper(initialConnectionString, MasterDatabaseName, sqlServerFhirModel, sqlConnectionFactory);
_testHelper = new SqlServerFhirStorageTestHelper(initialConnectionString, MasterDatabaseName, searchParameterDefinitionManager, sqlServerFhirModel, sqlConnectionFactory);
}
public string TestConnectionString { get; }
@ -100,6 +113,8 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
internal SqlConnectionWrapperFactory SqlConnectionWrapperFactory { get; }
internal SqlServerSearchParameterStatusDataStore SqlServerSearchParameterStatusDataStore { get; }
public async Task InitializeAsync()
{
await _testHelper.CreateAndInitializeDatabase(_databaseName, forceIncrementalSchemaUpgrade: false, _schemaInitializer, CancellationToken.None);
@ -137,6 +152,16 @@ namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
return SqlTransactionHandler;
}
if (serviceType == typeof(ISearchParameterStatusDataStore))
{
return SqlServerSearchParameterStatusDataStore;
}
if (serviceType == typeof(FilebasedSearchParameterStatusDataStore))
{
return _filebasedSearchParameterStatusDataStore;
}
return null;
}
}