Skip to content

Commit 259ef1d

Browse files
Ja bist du narrischJa bist du narrisch
authored andcommitted
Updated GetForeignKeyConstraints
1 parent 4de399c commit 259ef1d

25 files changed

+986
-48
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Migrator.Tests.Database.DatabaseName.Interfaces;
5+
using Migrator.Tests.Database.Interfaces;
6+
using Migrator.Tests.Database.Models;
7+
using Migrator.Tests.Settings.Models;
8+
9+
namespace Migrator.Tests.Database;
10+
11+
public abstract class DatabaseIntegrationTestServiceBase(IDatabaseNameService databaseNameService) : IDatabaseIntegrationTestService
12+
{
13+
/// <summary>
14+
/// Deletes all integration test databases older than the given time span.
15+
/// </summary>
16+
// TODO CK time span!
17+
protected readonly TimeSpan MinTimeSpanBeforeDatabaseDeletion = TimeSpan.FromMinutes(1); // TimeSpan.FromMinutes(60);
18+
19+
protected IDatabaseNameService DatabaseNameService { get; private set; } = databaseNameService;
20+
21+
abstract public Task<DatabaseInfo> CreateTestDatabaseAsync(DatabaseConnectionConfig databaseConnectionConfig, CancellationToken cancellationToken);
22+
23+
abstract public Task DropDatabaseAsync(DatabaseInfo databaseInfo, CancellationToken cancellationToken);
24+
25+
protected DateTime ReadTimeStampFromDatabaseName(string name)
26+
{
27+
var creationDate = DatabaseNameService.ReadTimeStampFromString(name);
28+
29+
if (!creationDate.HasValue)
30+
{
31+
throw new Exception("You tried to drop a database that was not created by this service. For safety reasons we deny your request.");
32+
}
33+
34+
return creationDate.Value;
35+
}
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using DryIoc;
2+
using Migrator.Tests.Database.Interfaces;
3+
4+
namespace Migrator.Tests.Database;
5+
6+
public class DatabaseIntegrationTestServiceFactory(IResolver resolver) : IDatabaseIntegrationTestServiceFactory
7+
{
8+
public IDatabaseIntegrationTestService Create(DatabaseProviderType providerType)
9+
{
10+
return resolver.Resolve<IDatabaseIntegrationTestService>(serviceKey: providerType);
11+
}
12+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Migrator.Tests.Database.DatabaseName.Interfaces;
2+
using Migrator.Tests.Database.GuidServices.Interfaces;
3+
using Migrator.Tests.Database.GuidServices;
4+
using Migrator.Tests.Database.Interfaces;
5+
using Migrator.Tests.Database.DerivedDatabaseIntegrationTestServices;
6+
using System;
7+
using DryIoc;
8+
using Migrator.Test.Shared.Database;
9+
using Migrator.Tests.Settings.Interfaces;
10+
using Migrator.Tests.Settings;
11+
12+
namespace Migrator.Tests.Database;
13+
14+
public static class DatabaseCreationServiceRegistry
15+
{
16+
public static void RegisterDatabaseIntegrationTestService(this IRegistrator container)
17+
{
18+
container.Register<IDatabaseIntegrationTestServiceFactory, DatabaseIntegrationTestServiceFactory>(reuse: Reuse.Transient);
19+
container.Register<IDatabaseNameService, DatabaseNameService>(reuse: Reuse.Transient);
20+
container.RegisterInstance(TimeProvider.System, ifAlreadyRegistered: IfAlreadyRegistered.Keep);
21+
container.Register<IGuidService, GuidService>(reuse: Reuse.Transient, ifAlreadyRegistered: IfAlreadyRegistered.Keep);
22+
container.Register<IDatabaseIntegrationTestService, OracleDatabaseIntegrationTestService>(serviceKey: DatabaseProviderType.Oracle);
23+
container.Register<IDatabaseIntegrationTestService, SQLiteDatabaseIntegrationTestService>(serviceKey: DatabaseProviderType.SQLite);
24+
container.Register<IDatabaseIntegrationTestService, PostgreSqlDatabaseIntegrationTestService>(serviceKey: DatabaseProviderType.Postgres);
25+
container.Register<IDatabaseIntegrationTestService, SqlServerDatabaseIntegrationTestService>(serviceKey: DatabaseProviderType.SQLServer);
26+
container.Register<IConfigurationReader, ConfigurationReader>(reuse: Reuse.Singleton, ifAlreadyRegistered: IfAlreadyRegistered.Keep);
27+
}
28+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.Globalization;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text.RegularExpressions;
6+
using Migrator.Tests.Database.DatabaseName.Interfaces;
7+
using Migrator.Tests.Database.GuidServices.Interfaces;
8+
9+
namespace Migrator.Test.Shared.Database;
10+
11+
public partial class DatabaseNameService(TimeProvider timeProvider, IGuidService guidService) : IDatabaseNameService
12+
{
13+
private const string TestDatabaseString = "Test";
14+
private const string TimeStampPattern = "yyyyMMddHHmmssfff";
15+
16+
public DateTime? ReadTimeStampFromString(string name)
17+
{
18+
name = Path.GetFileNameWithoutExtension(name);
19+
20+
var regex = DateTimeRegex();
21+
var match = regex.Match(name);
22+
23+
if (match.Success && DateTime.TryParseExact(match.Value, TimeStampPattern, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var res))
24+
{
25+
return res;
26+
}
27+
28+
return null;
29+
}
30+
31+
public string CreateDatabaseName()
32+
{
33+
var dateTimePattern = timeProvider.GetUtcNow()
34+
.ToString(TimeStampPattern);
35+
36+
var randomString = string.Concat(guidService.NewGuid()
37+
.ToString("N")
38+
.Reverse()
39+
.Take(9));
40+
41+
return $"{dateTimePattern}{TestDatabaseString}{randomString}";
42+
}
43+
44+
[GeneratedRegex(@"^(\d+)(?=Test.{9}$)")]
45+
private static partial Regex DateTimeRegex();
46+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
3+
namespace Migrator.Tests.Database.DatabaseName.Interfaces;
4+
5+
/// <summary>
6+
/// Used for integration tests. During integration tests we need to create unique database names for parallel testing.
7+
/// </summary>
8+
public interface IDatabaseNameService
9+
{
10+
/// <summary>
11+
/// Reads the date time from the date part of the database or user name (in Oracle we use the user name/schema name).
12+
/// </summary>
13+
/// <param name="databaseName"></param>
14+
/// <returns></returns>
15+
DateTime? ReadTimeStampFromString(string name);
16+
17+
/// <summary>
18+
/// Creates a database name
19+
/// </summary>
20+
/// <returns></returns>
21+
string CreateDatabaseName();
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Migrator.Tests.Database;
2+
3+
public enum DatabaseProviderType
4+
{
5+
// Do not use in any case not even as default
6+
None = 0,
7+
8+
Unknown,
9+
10+
// Postgre SQL
11+
Postgres,
12+
13+
// SQL Server
14+
SQLServer,
15+
16+
// SQLite
17+
SQLite,
18+
19+
// Oracle
20+
Oracle
21+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using LinqToDB;
7+
using LinqToDB.Data;
8+
using Mapster;
9+
using Migrator.Tests.Database.DatabaseName.Interfaces;
10+
using Migrator.Tests.Database.Interfaces;
11+
using Migrator.Tests.Database.Models;
12+
using Migrator.Tests.Settings.Models;
13+
using Oracle.ManagedDataAccess.Client;
14+
15+
namespace Migrator.Tests.Database.DerivedDatabaseIntegrationTestServices;
16+
17+
public class OracleDatabaseIntegrationTestService(
18+
TimeProvider timeProvider,
19+
IDatabaseNameService databaseNameService
20+
// IImportExportMappingSchemaFactory importExportMappingSchemaFactory
21+
)
22+
: DatabaseIntegrationTestServiceBase(databaseNameService), IDatabaseIntegrationTestService
23+
{
24+
private const string UserStringKey = "User Id";
25+
private const string PasswordStringKey = "Password";
26+
private const string ReplaceString = "RandomStringThatIsNotQuotedByTheBuilderDoNotChange";
27+
// private readonly IImportExportMappingSchemaFactory _importExportMappingSchemaFactory = importExportMappingSchemaFactory;
28+
29+
/// <summary>
30+
/// Creates an oracle database for test purposes.
31+
/// </summary>
32+
/// <remarks>
33+
/// For the creation of the Oracle user used in this method follow these steps:
34+
///
35+
/// Use a SYSDBA user, connect or switch to the default PDB.
36+
/// On the free docker container the name of the default PDB is "FREEPDB1" use it as the service name or alternatively switch containers. For installations other than the "FREE" Oracle
37+
/// Docker image find out the (default) PDB and switch to it then create grant privileges listed below. Having all set you can create a connection string using the newly created user
38+
/// and password and add it to appsettings.Development (for dev environment)
39+
/// <list type="bullet">
40+
/// <item><description>ALTER SESSION SET CONTAINER = FREEPDB1</description></item>
41+
/// <item><description>CREATE USER myuser IDENTIFIED BY mypassword</description></item>
42+
/// <item><description>GRANT CREATE USER TO myuser</description></item>
43+
/// <item><description>GRANT DROP USER TO myuser</description></item>
44+
/// <item><description>GRANT CREATE SESSION TO myuser WITH ADMIN OPTION</description></item>
45+
/// <item><description>GRANT RESOURCE TO myuser WITH ADMIN OPTION</description></item>
46+
/// <item><description>GRANT CONNECT TO myuser WITH ADMIN OPTION</description></item>
47+
/// <item><description>GRANT UNLIMITED TABLESPACE TO myuser with ADMIN OPTION</description></item>
48+
/// <item><description>GRANT SELECT ON V_$SESSION TO myuser with GRANT OPTION</description></item>
49+
/// <item><description>GRANT ALTER SYSTEM TO myuser</description></item>
50+
/// </list>
51+
/// Having all set you can create a connection string using the newly created user and password and add it into appsettings.development
52+
/// </remarks>
53+
/// <param name="databaseConnectionConfig"></param>
54+
/// <param name="cancellationToken"></param>
55+
/// <returns></returns>
56+
/// <exception cref="NotImplementedException"></exception>
57+
public override async Task<DatabaseInfo> CreateTestDatabaseAsync(DatabaseConnectionConfig databaseConnectionConfig, CancellationToken cancellationToken)
58+
{
59+
DataConnection context;
60+
61+
var tempDatabaseConnectionConfig = databaseConnectionConfig.Adapt<DatabaseConnectionConfig>();
62+
63+
var connectionStringBuilder = new OracleConnectionStringBuilder()
64+
{
65+
ConnectionString = tempDatabaseConnectionConfig.ConnectionString
66+
};
67+
68+
if (!connectionStringBuilder.TryGetValue(UserStringKey, out var user))
69+
{
70+
throw new Exception($"Cannot find key '{UserStringKey}'");
71+
}
72+
73+
if (!connectionStringBuilder.TryGetValue(PasswordStringKey, out var password))
74+
{
75+
throw new Exception($"Cannot find key '{PasswordStringKey}'");
76+
}
77+
78+
var tempUserName = DatabaseNameService.CreateDatabaseName();
79+
80+
List<string> userNames;
81+
82+
var dataOptions = new DataOptions().UseOracle(databaseConnectionConfig.ConnectionString);
83+
84+
using (context = new DataConnection(dataOptions))
85+
{
86+
userNames = await context.QueryToListAsync<string>("SELECT username FROM all_users", cancellationToken);
87+
}
88+
89+
var toBeDeletedUsers = userNames.Where(x =>
90+
{
91+
var creationDate = DatabaseNameService.ReadTimeStampFromString(x);
92+
93+
return creationDate.HasValue && creationDate.Value < timeProvider.GetUtcNow().Subtract(MinTimeSpanBeforeDatabaseDeletion);
94+
}).ToList();
95+
96+
await Parallel.ForEachAsync(
97+
toBeDeletedUsers,
98+
new ParallelOptions { MaxDegreeOfParallelism = 3, CancellationToken = cancellationToken },
99+
async (x, cancellationTokenInner) =>
100+
{
101+
var databaseInfoToBeDeleted = new DatabaseInfo
102+
{
103+
DatabaseConnectionConfig = databaseConnectionConfig.Adapt<DatabaseConnectionConfig>(),
104+
DatabaseConnectionConfigMaster = databaseConnectionConfig.Adapt<DatabaseConnectionConfig>(),
105+
SchemaName = x
106+
};
107+
108+
await DropDatabaseAsync(databaseInfoToBeDeleted, cancellationTokenInner);
109+
110+
});
111+
112+
using (context = new DataConnection(dataOptions))
113+
{
114+
await context.ExecuteAsync($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\"", cancellationToken);
115+
116+
var privileges = new[]
117+
{
118+
"CONNECT",
119+
"CREATE SESSION",
120+
"RESOURCE",
121+
"UNLIMITED TABLESPACE"
122+
};
123+
124+
await context.ExecuteAsync($"GRANT {string.Join(", ", privileges)} TO \"{tempUserName}\"", cancellationToken);
125+
await context.ExecuteAsync($"GRANT SELECT ON SYS.V_$SESSION TO \"{tempUserName}\"", cancellationToken);
126+
}
127+
128+
connectionStringBuilder.Add(UserStringKey, ReplaceString);
129+
connectionStringBuilder.Add(PasswordStringKey, ReplaceString);
130+
131+
tempDatabaseConnectionConfig.ConnectionString = connectionStringBuilder.ConnectionString;
132+
tempDatabaseConnectionConfig.ConnectionString = tempDatabaseConnectionConfig.ConnectionString.Replace(ReplaceString, $"\"{tempUserName}\"");
133+
134+
var databaseInfo = new DatabaseInfo
135+
{
136+
DatabaseConnectionConfigMaster = databaseConnectionConfig.Adapt<DatabaseConnectionConfig>(),
137+
DatabaseConnectionConfig = tempDatabaseConnectionConfig,
138+
SchemaName = tempUserName,
139+
};
140+
141+
return databaseInfo;
142+
}
143+
144+
public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, CancellationToken cancellationToken)
145+
{
146+
var creationDate = ReadTimeStampFromDatabaseName(databaseInfo.SchemaName);
147+
148+
var dataOptions = new DataOptions().UseOracle(databaseInfo.DatabaseConnectionConfigMaster.ConnectionString);
149+
// .UseMappingSchema(_importExportMappingSchemaFactory.CreateOracleMappingSchema());
150+
151+
using var context = new DataConnection(dataOptions);
152+
153+
// var vSessions = await context.GetTable<VSession>()
154+
// .Where(x => x.UserName == databaseInfo.SchemaName)
155+
// .ToListAsync(cancellationToken);
156+
157+
// await Parallel.ForEachAsync(
158+
// vSessions,
159+
// new ParallelOptions { MaxDegreeOfParallelism = 3, CancellationToken = cancellationToken },
160+
// async (x, cancellationTokenInner) =>
161+
// {
162+
// using var killSessionContext = new DataConnection(dataOptions);
163+
164+
// var killStatement = $"ALTER SYSTEM KILL SESSION '{x.SID},{x.SerialHashTag}' IMMEDIATE";
165+
// try
166+
// {
167+
// await killSessionContext.ExecuteAsync(killStatement, cancellationToken);
168+
169+
// // Oracle does not close the session immediately as they pretend so we need to wait a while
170+
// // Since this happens only in very rare cases we accept waiting for a while.
171+
// // If nobody connects to the database this will never happen.
172+
// await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
173+
// }
174+
// catch
175+
// {
176+
// // Most probably killed by another parallel running integration test. If not, the DROP USER exception will show the details.
177+
// }
178+
// });
179+
180+
try
181+
{
182+
await context.ExecuteAsync($"DROP USER \"{databaseInfo.SchemaName}\" CASCADE", cancellationToken);
183+
}
184+
catch
185+
{
186+
await Task.Delay(2000, cancellationToken);
187+
188+
// In next Linq2db version this can be replaced by ...FromSql().First();
189+
// https://github.com/linq2db/linq2db/issues/2779
190+
// TODO CK create issue in Redmine and refer to it here
191+
var countList = await context.QueryToListAsync<int>($"SELECT COUNT(*) FROM all_users WHERE username = '{databaseInfo.SchemaName}'", cancellationToken);
192+
var count = countList.First();
193+
194+
if (count == 1)
195+
{
196+
throw;
197+
}
198+
else
199+
{
200+
// The user was removed by another asynchronously running test that kicked in earlier.
201+
// That's ok for us as we have achieved the goal.
202+
}
203+
}
204+
}
205+
}

0 commit comments

Comments
 (0)