2019-05-11 02:18:58 +03:00
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
|
|
|
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
|
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
|
|
2020-02-10 20:36:48 +03:00
|
|
|
|
using System;
|
2020-12-12 04:13:43 +03:00
|
|
|
|
using System.Linq;
|
2019-05-11 02:18:58 +03:00
|
|
|
|
using System.Text;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
using System.Threading;
|
2019-05-11 02:18:58 +03:00
|
|
|
|
using System.Threading.Tasks;
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using EnsureThat;
|
2020-09-16 17:34:26 +03:00
|
|
|
|
using MediatR;
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using Microsoft.Data.SqlClient;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
|
using Microsoft.Health.Fhir.SqlServer.Features.Schema;
|
2020-06-26 23:40:54 +03:00
|
|
|
|
using Microsoft.Health.Fhir.SqlServer.Features.Storage;
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using Microsoft.Health.SqlServer;
|
2020-03-28 01:19:05 +03:00
|
|
|
|
using Microsoft.Health.SqlServer.Configs;
|
|
|
|
|
using Microsoft.Health.SqlServer.Features.Schema;
|
2021-02-17 02:10:18 +03:00
|
|
|
|
using Microsoft.Health.SqlServer.Features.Schema.Manager;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
using Microsoft.SqlServer.Dac.Compare;
|
2020-09-16 17:34:26 +03:00
|
|
|
|
using NSubstitute;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
using Polly;
|
2019-05-11 02:18:58 +03:00
|
|
|
|
using Xunit;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
using Task = System.Threading.Tasks.Task;
|
|
|
|
|
|
2019-05-11 02:18:58 +03:00
|
|
|
|
namespace Microsoft.Health.Fhir.Tests.Integration.Persistence
|
|
|
|
|
{
|
2020-02-10 20:36:48 +03:00
|
|
|
|
public class SqlServerFhirStorageTestHelper : IFhirStorageTestHelper, ISqlServerFhirStorageTestHelper
|
2019-05-11 02:18:58 +03:00
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
private readonly string _masterDatabaseName;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
private readonly string _initialConnectionString;
|
2020-06-26 23:40:54 +03:00
|
|
|
|
private readonly SqlServerFhirModel _sqlServerFhirModel;
|
2020-10-15 19:40:36 +03:00
|
|
|
|
private readonly ISqlConnectionFactory _sqlConnectionFactory;
|
2019-05-11 02:18:58 +03:00
|
|
|
|
|
2020-11-12 02:01:26 +03:00
|
|
|
|
public SqlServerFhirStorageTestHelper(
|
|
|
|
|
string initialConnectionString,
|
|
|
|
|
string masterDatabaseName,
|
|
|
|
|
SqlServerFhirModel sqlServerFhirModel,
|
|
|
|
|
ISqlConnectionFactory sqlConnectionFactory)
|
2019-05-11 02:18:58 +03:00
|
|
|
|
{
|
2020-11-12 02:01:26 +03:00
|
|
|
|
EnsureArg.IsNotNull(sqlServerFhirModel, nameof(sqlServerFhirModel));
|
2020-10-15 19:40:36 +03:00
|
|
|
|
EnsureArg.IsNotNull(sqlConnectionFactory, nameof(sqlConnectionFactory));
|
|
|
|
|
|
|
|
|
|
_masterDatabaseName = masterDatabaseName;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
_initialConnectionString = initialConnectionString;
|
2020-06-26 23:40:54 +03:00
|
|
|
|
_sqlServerFhirModel = sqlServerFhirModel;
|
2020-10-15 19:40:36 +03:00
|
|
|
|
_sqlConnectionFactory = sqlConnectionFactory;
|
2020-02-10 20:36:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 17:16:56 +03:00
|
|
|
|
public async Task CreateAndInitializeDatabase(string databaseName, int maximumSupportedSchemaVersion, bool forceIncrementalSchemaUpgrade, SchemaInitializer schemaInitializer = null, CancellationToken cancellationToken = default)
|
2020-02-10 20:36:48 +03:00
|
|
|
|
{
|
|
|
|
|
var testConnectionString = new SqlConnectionStringBuilder(_initialConnectionString) { InitialCatalog = databaseName }.ToString();
|
|
|
|
|
schemaInitializer = schemaInitializer ?? CreateSchemaInitializer(testConnectionString);
|
|
|
|
|
|
|
|
|
|
// Create the database.
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync(_masterDatabaseName, cancellationToken))
|
2020-02-10 20:36:48 +03:00
|
|
|
|
{
|
|
|
|
|
await connection.OpenAsync(cancellationToken);
|
|
|
|
|
|
|
|
|
|
using (SqlCommand command = connection.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
command.CommandTimeout = 600;
|
2021-01-21 17:16:56 +03:00
|
|
|
|
command.CommandText = @$"
|
|
|
|
|
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '{databaseName}')
|
|
|
|
|
BEGIN
|
|
|
|
|
CREATE DATABASE {databaseName};
|
|
|
|
|
END";
|
2020-02-10 20:36:48 +03:00
|
|
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify that we can connect to the new database. This sometimes does not work right away with Azure SQL.
|
|
|
|
|
await Policy
|
|
|
|
|
.Handle<SqlException>()
|
|
|
|
|
.WaitAndRetryAsync(
|
|
|
|
|
retryCount: 7,
|
|
|
|
|
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
|
|
|
|
|
.ExecuteAsync(async () =>
|
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync(databaseName, cancellationToken))
|
2020-02-10 20:36:48 +03:00
|
|
|
|
{
|
|
|
|
|
await connection.OpenAsync(cancellationToken);
|
|
|
|
|
using (SqlCommand sqlCommand = connection.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
sqlCommand.CommandText = "SELECT 1";
|
|
|
|
|
await sqlCommand.ExecuteScalarAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2020-10-15 19:40:36 +03:00
|
|
|
|
await schemaInitializer.InitializeAsync(forceIncrementalSchemaUpgrade, cancellationToken);
|
2021-02-17 02:10:18 +03:00
|
|
|
|
await _sqlServerFhirModel.Initialize(maximumSupportedSchemaVersion, true, cancellationToken);
|
2020-02-10 20:36:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task DeleteDatabase(string databaseName, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync(_masterDatabaseName, cancellationToken))
|
2020-02-10 20:36:48 +03:00
|
|
|
|
{
|
|
|
|
|
await connection.OpenAsync(cancellationToken);
|
|
|
|
|
SqlConnection.ClearAllPools();
|
|
|
|
|
|
|
|
|
|
using (SqlCommand command = connection.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
command.CommandTimeout = 600;
|
|
|
|
|
command.CommandText = $"DROP DATABASE IF EXISTS {databaseName}";
|
|
|
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool CompareDatabaseSchemas(string databaseName1, string databaseName2)
|
|
|
|
|
{
|
|
|
|
|
var testConnectionString1 = new SqlConnectionStringBuilder(_initialConnectionString) { InitialCatalog = databaseName1 }.ToString();
|
|
|
|
|
var testConnectionString2 = new SqlConnectionStringBuilder(_initialConnectionString) { InitialCatalog = databaseName2 }.ToString();
|
|
|
|
|
|
|
|
|
|
var source = new SchemaCompareDatabaseEndpoint(testConnectionString1);
|
|
|
|
|
var target = new SchemaCompareDatabaseEndpoint(testConnectionString2);
|
|
|
|
|
var comparison = new SchemaComparison(source, target);
|
2020-12-12 04:13:43 +03:00
|
|
|
|
|
2020-02-10 20:36:48 +03:00
|
|
|
|
SchemaComparisonResult result = comparison.Compare();
|
|
|
|
|
|
2020-12-12 04:13:43 +03:00
|
|
|
|
// These types were introduced in earlier schema versions but are no longer used in newer versions.
|
|
|
|
|
// They are not removed so as to no break compatibility with instances requiring an older schema version.
|
|
|
|
|
// Exclude them from the schema comparison differences.
|
|
|
|
|
(string type, string name)[] deprecatedObjectToIgnore =
|
|
|
|
|
{
|
|
|
|
|
("Procedure", "[dbo].[UpsertResource]"),
|
2021-04-02 08:01:44 +03:00
|
|
|
|
("Procedure", "[dbo].[UpsertResource_2]"),
|
2020-12-12 04:13:43 +03:00
|
|
|
|
("TableType", "[dbo].[ReferenceSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[ReferenceTokenCompositeSearchParamTableType_1]"),
|
2021-04-02 08:01:44 +03:00
|
|
|
|
("TableType", "[dbo].[ResourceWriteClaimTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[CompartmentAssignmentTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[ReferenceSearchParamTableType_2]"),
|
|
|
|
|
("TableType", "[dbo].[TokenSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[TokenTextTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[StringSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[UriSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[NumberSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[QuantitySearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[DateTimeSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[ReferenceTokenCompositeSearchParamTableType_2]"),
|
|
|
|
|
("TableType", "[dbo].[TokenTokenCompositeSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[TokenDateTimeCompositeSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[TokenQuantityCompositeSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[TokenStringCompositeSearchParamTableType_1]"),
|
|
|
|
|
("TableType", "[dbo].[TokenNumberNumberCompositeSearchParamTableType_1]"),
|
2020-12-12 04:13:43 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var remainingDifferences = result.Differences.Where(
|
|
|
|
|
d => !deprecatedObjectToIgnore.Any(
|
|
|
|
|
i =>
|
|
|
|
|
(d.SourceObject?.ObjectType.Name == i.type && d.SourceObject?.Name?.ToString() == i.name) ||
|
|
|
|
|
(d.TargetObject?.ObjectType.Name == i.type && d.TargetObject?.Name?.ToString() == i.name)))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
return remainingDifferences.Count == 0;
|
2019-05-11 02:18:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-02-10 20:36:48 +03:00
|
|
|
|
public async Task DeleteAllExportJobRecordsAsync(CancellationToken cancellationToken = default)
|
2019-05-11 02:18:58 +03:00
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync())
|
2020-02-10 20:36:48 +03:00
|
|
|
|
{
|
|
|
|
|
var command = new SqlCommand("DELETE FROM dbo.ExportJob", connection);
|
|
|
|
|
|
|
|
|
|
await command.Connection.OpenAsync(cancellationToken);
|
|
|
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task DeleteExportJobRecordAsync(string id, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync())
|
2020-02-10 20:36:48 +03:00
|
|
|
|
{
|
|
|
|
|
var command = new SqlCommand("DELETE FROM dbo.ExportJob WHERE Id = @id", connection);
|
|
|
|
|
|
|
|
|
|
var parameter = new SqlParameter { ParameterName = "@id", Value = id };
|
|
|
|
|
command.Parameters.Add(parameter);
|
|
|
|
|
|
|
|
|
|
await command.Connection.OpenAsync(cancellationToken);
|
|
|
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
|
}
|
2019-05-11 02:18:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-12 02:01:26 +03:00
|
|
|
|
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);
|
|
|
|
|
}
|
2021-04-02 08:01:44 +03:00
|
|
|
|
|
|
|
|
|
_sqlServerFhirModel.RemoveSearchParamIdToUriMapping(uri);
|
2020-11-12 02:01:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-02-18 04:50:52 +03:00
|
|
|
|
public async Task DeleteAllReindexJobRecordsAsync(CancellationToken cancellationToken = default)
|
2020-07-31 20:34:30 +03:00
|
|
|
|
{
|
2021-02-18 04:50:52 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync())
|
|
|
|
|
{
|
|
|
|
|
var command = new SqlCommand("DELETE FROM dbo.ReindexJob", connection);
|
|
|
|
|
|
|
|
|
|
await command.Connection.OpenAsync(cancellationToken);
|
|
|
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task DeleteReindexJobRecordAsync(string id, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync())
|
|
|
|
|
{
|
|
|
|
|
var command = new SqlCommand("DELETE FROM dbo.ReindexJob WHERE Id = @id", connection);
|
|
|
|
|
|
|
|
|
|
var parameter = new SqlParameter { ParameterName = "@id", Value = id };
|
|
|
|
|
command.Parameters.Add(parameter);
|
|
|
|
|
|
|
|
|
|
await command.Connection.OpenAsync(cancellationToken);
|
|
|
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
|
}
|
2020-07-31 20:34:30 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 02:18:58 +03:00
|
|
|
|
async Task<object> IFhirStorageTestHelper.GetSnapshotToken()
|
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync())
|
2019-05-11 02:18:58 +03:00
|
|
|
|
{
|
|
|
|
|
await connection.OpenAsync();
|
|
|
|
|
|
|
|
|
|
SqlCommand command = connection.CreateCommand();
|
|
|
|
|
command.CommandText = "SELECT MAX(ResourceSurrogateId) FROM dbo.Resource";
|
|
|
|
|
return await command.ExecuteScalarAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task IFhirStorageTestHelper.ValidateSnapshotTokenIsCurrent(object snapshotToken)
|
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
using (var connection = await _sqlConnectionFactory.GetSqlConnectionAsync())
|
2019-05-11 02:18:58 +03:00
|
|
|
|
{
|
|
|
|
|
await connection.OpenAsync();
|
|
|
|
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
using (SqlCommand outerCommand = connection.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
outerCommand.CommandText = @"
|
|
|
|
|
SELECT t.name
|
|
|
|
|
FROM sys.tables t
|
|
|
|
|
INNER JOIN sys.columns c ON c.object_id = t.object_id
|
|
|
|
|
WHERE c.name = 'ResourceSurrogateId'";
|
|
|
|
|
|
|
|
|
|
using (SqlDataReader reader = await outerCommand.ExecuteReaderAsync())
|
|
|
|
|
{
|
|
|
|
|
while (reader.Read())
|
|
|
|
|
{
|
|
|
|
|
if (sb.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine("UNION ALL");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string tableName = reader.GetString(0);
|
|
|
|
|
sb.AppendLine($"SELECT '{tableName}' as TableName, MAX(ResourceSurrogateId) as MaxResourceSurrogateId FROM dbo.{tableName}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var command = connection.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
command.CommandText = sb.ToString();
|
|
|
|
|
using (var reader = await command.ExecuteReaderAsync())
|
|
|
|
|
{
|
2020-10-15 19:40:36 +03:00
|
|
|
|
while (await reader.ReadAsync())
|
2019-05-11 02:18:58 +03:00
|
|
|
|
{
|
|
|
|
|
Assert.True(reader.IsDBNull(1) || reader.GetInt64(1) <= (long)snapshotToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-10 20:36:48 +03:00
|
|
|
|
|
|
|
|
|
private SchemaInitializer CreateSchemaInitializer(string testConnectionString)
|
|
|
|
|
{
|
2020-05-21 20:02:52 +03:00
|
|
|
|
var schemaOptions = new SqlServerSchemaOptions { AutomaticUpdatesEnabled = true };
|
|
|
|
|
var config = new SqlServerDataStoreConfiguration { ConnectionString = testConnectionString, Initialize = true, SchemaOptions = schemaOptions };
|
2020-09-24 21:48:07 +03:00
|
|
|
|
var schemaInformation = new SchemaInformation(SchemaVersionConstants.Min, SchemaVersionConstants.Max);
|
2020-03-28 01:19:05 +03:00
|
|
|
|
var scriptProvider = new ScriptProvider<SchemaVersion>();
|
2020-09-16 17:34:26 +03:00
|
|
|
|
var baseScriptProvider = new BaseScriptProvider();
|
|
|
|
|
var mediator = Substitute.For<IMediator>();
|
2021-02-17 02:10:18 +03:00
|
|
|
|
var sqlConnectionStringProvider = new DefaultSqlConnectionStringProvider(config);
|
|
|
|
|
var sqlConnectionFactory = new DefaultSqlConnectionFactory(sqlConnectionStringProvider);
|
|
|
|
|
var schemaManagerDataStore = new SchemaManagerDataStore(sqlConnectionFactory);
|
|
|
|
|
var schemaUpgradeRunner = new SchemaUpgradeRunner(scriptProvider, baseScriptProvider, mediator, NullLogger<SchemaUpgradeRunner>.Instance, sqlConnectionFactory, schemaManagerDataStore);
|
2020-02-10 20:36:48 +03:00
|
|
|
|
|
2021-02-17 02:10:18 +03:00
|
|
|
|
return new SchemaInitializer(config, schemaUpgradeRunner, schemaInformation, sqlConnectionFactory, sqlConnectionStringProvider, NullLogger<SchemaInitializer>.Instance);
|
2020-02-10 20:36:48 +03:00
|
|
|
|
}
|
2019-05-11 02:18:58 +03:00
|
|
|
|
}
|
|
|
|
|
}
|