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