Skip to content

Commit e2aae09

Browse files
JaBistDuNarrischJaBistDuNarrisch
authored andcommitted
Oracle: Handle "NULL" as default value
1 parent e81bba5 commit e2aae09

12 files changed

+325
-57
lines changed

src/Migrator.Tests/Database/DatabaseName/DatabaseNameService.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
using System;
22
using System.Globalization;
33
using System.IO;
4-
using System.Linq;
4+
using System.Security.Cryptography;
55
using System.Text.RegularExpressions;
66
using Migrator.Tests.Database.DatabaseName.Interfaces;
7-
using Migrator.Tests.Database.GuidServices.Interfaces;
87

98
namespace Migrator.Test.Shared.Database;
109

11-
public partial class DatabaseNameService(TimeProvider timeProvider, IGuidService guidService) : IDatabaseNameService
10+
public partial class DatabaseNameService(TimeProvider timeProvider) : IDatabaseNameService
1211
{
13-
private const string TestDatabaseString = "Test";
12+
private const string TestDatabaseString = "T";
1413
private const string TimeStampPattern = "yyyyMMddHHmmssfff";
1514

1615
public DateTime? ReadTimeStampFromString(string name)
@@ -33,14 +32,25 @@ public string CreateDatabaseName()
3332
var dateTimePattern = timeProvider.GetUtcNow()
3433
.ToString(TimeStampPattern);
3534

36-
var randomString = string.Concat(guidService.NewGuid()
37-
.ToString("N")
38-
.Reverse()
39-
.Take(9));
35+
var randomString = CreateRandomChars(7);
4036

4137
return $"{dateTimePattern}{TestDatabaseString}{randomString}";
4238
}
4339

44-
[GeneratedRegex(@"^(\d+)(?=Test.{9}$)")]
40+
private string CreateRandomChars(int length)
41+
{
42+
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
43+
var stringChars = new char[length];
44+
45+
for (var i = 0; i < length; i++)
46+
{
47+
var index = RandomNumberGenerator.GetInt32(chars.Length);
48+
stringChars[i] = chars[index];
49+
}
50+
51+
return new string(stringChars);
52+
}
53+
54+
[GeneratedRegex(@"^([\d]+)(?=T.{7}$)")]
4555
private static partial Regex DateTimeRegex();
46-
}
56+
}

src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs

Lines changed: 113 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Linq;
3+
using System.Text;
44
using System.Text.RegularExpressions;
55
using System.Threading;
66
using System.Threading.Tasks;
@@ -19,7 +19,7 @@
1919

2020
namespace Migrator.Tests.Database.DerivedDatabaseIntegrationTestServices;
2121

22-
public class OracleDatabaseIntegrationTestService(
22+
public partial class OracleDatabaseIntegrationTestService(
2323
TimeProvider timeProvider,
2424
IDatabaseNameService databaseNameService)
2525
: DatabaseIntegrationTestServiceBase(databaseNameService), IDatabaseIntegrationTestService
@@ -61,8 +61,6 @@ public class OracleDatabaseIntegrationTestService(
6161
/// <exception cref="NotImplementedException"></exception>
6262
public override async Task<DatabaseInfo> CreateTestDatabaseAsync(DatabaseConnectionConfig databaseConnectionConfig, CancellationToken cancellationToken)
6363
{
64-
DataConnection context;
65-
6664
var tempDatabaseConnectionConfig = databaseConnectionConfig.Adapt<DatabaseConnectionConfig>();
6765

6866
var connectionStringBuilder = new OracleConnectionStringBuilder()
@@ -82,15 +80,12 @@ public override async Task<DatabaseInfo> CreateTestDatabaseAsync(DatabaseConnect
8280

8381
var tempUserName = DatabaseNameService.CreateDatabaseName();
8482

85-
List<string> userNames;
86-
8783
var dataOptions = new DataOptions().UseOracle(databaseConnectionConfig.ConnectionString)
8884
.UseMappingSchema(_mappingSchema);
8985

90-
using (context = new DataConnection(dataOptions))
91-
{
92-
userNames = await context.QueryToListAsync<string>("SELECT username FROM all_users", cancellationToken);
93-
}
86+
using var context = new DataConnection(dataOptions);
87+
88+
var userNames = await context.GetTable<AllUsers>().Select(x => x.UserName).ToListAsync(cancellationToken);
9489

9590
var toBeDeletedUsers = userNames.Where(x =>
9691
{
@@ -112,49 +107,84 @@ await Parallel.ForEachAsync(
112107
};
113108

114109
await DropDatabaseAsync(databaseInfoToBeDeleted, cancellationTokenInner);
110+
});
111+
112+
// To be on the safe side we check for table spaces used in tests that have not been deleted for any reason (possible connection issues/concurrent deletion attempts - there is
113+
// no transaction for DDL in Oracle etc.).
114+
var tableSpaceNames = await context.GetTable<DBADataFiles>()
115+
.Select(x => x.TablespaceName)
116+
.ToListAsync(cancellationToken);
115117

118+
var toBeDeletedTableSpaces = tableSpaceNames
119+
.Where(x =>
120+
{
121+
var replacedTablespaceString = TableSpacePrefixRegex().Replace(x, "");
122+
var creationDate = DatabaseNameService.ReadTimeStampFromString(replacedTablespaceString);
123+
return creationDate.HasValue && creationDate.Value < timeProvider.GetUtcNow().Subtract(_MinTimeSpanBeforeDatabaseDeletion);
116124
});
117125

118-
using (context = new DataConnection(dataOptions))
126+
foreach (var toBeDeletedTableSpace in toBeDeletedTableSpaces)
119127
{
120-
// To be on the safe side we check for table spaces used in tests that have not been deleted for any reason (possible connection issues/concurrent deletion attempts - there is
121-
// no transaction for DDL in Oracle etc.).
122-
var tableSpaceNames = await context.GetTable<DBADataFiles>()
123-
.Select(x => x.TablespaceName)
124-
.ToListAsync(cancellationToken);
125-
126-
var toBeDeletedTableSpaces = tableSpaceNames
127-
.Where(x =>
128-
{
129-
var replacedTablespaceString = _tablespaceRegex.Replace(x, "");
130-
var creationDate = DatabaseNameService.ReadTimeStampFromString(replacedTablespaceString);
131-
return creationDate.HasValue && creationDate.Value < timeProvider.GetUtcNow().Subtract(_MinTimeSpanBeforeDatabaseDeletion);
132-
});
128+
var maxAttempts = 4;
129+
var delayBetweenAttempts = TimeSpan.FromSeconds(1);
133130

134-
foreach (var toBeDeletedTableSpace in toBeDeletedTableSpaces)
131+
for (var i = 0; i < maxAttempts; i++)
135132
{
136-
await context.ExecuteAsync($"DROP TABLESPACE {toBeDeletedTableSpace} INCLUDING CONTENTS AND DATAFILES", cancellationToken);
137-
}
133+
try
134+
{
135+
await context.ExecuteAsync($"DROP TABLESPACE {toBeDeletedTableSpace} INCLUDING CONTENTS AND DATAFILES", cancellationToken);
136+
}
137+
catch
138+
{
139+
var exists = await context.GetTable<DBADataFiles>().AnyAsync(x => x.TablespaceName == toBeDeletedTableSpace, cancellationToken);
138140

139-
await context.ExecuteAsync($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\"", cancellationToken);
141+
if (!exists)
142+
{
143+
break;
144+
}
140145

141-
var privileges = new[]
142-
{
143-
"CONNECT",
144-
"CREATE SESSION",
145-
"RESOURCE",
146-
"UNLIMITED TABLESPACE"
147-
};
148-
149-
await context.ExecuteAsync($"GRANT {string.Join(", ", privileges)} TO \"{tempUserName}\"", cancellationToken);
150-
await context.ExecuteAsync($"GRANT SELECT ON SYS.V_$SESSION TO \"{tempUserName}\"", cancellationToken);
146+
if (i + 1 == maxAttempts)
147+
{
148+
throw;
149+
}
150+
151+
await Task.Delay(delayBetweenAttempts, cancellationToken);
152+
153+
delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1));
154+
}
155+
}
151156
}
152157

158+
var tableSpaceName = $"{TableSpacePrefix}{tempUserName}";
159+
160+
var createTablespaceSql = $"CREATE TABLESPACE {tableSpaceName}";
161+
await context.ExecuteAsync(createTablespaceSql, cancellationToken: cancellationToken);
162+
163+
var stringBuilder = new StringBuilder();
164+
stringBuilder.Append($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\"");
165+
stringBuilder.AppendLine($"DEFAULT TABLESPACE {tableSpaceName}");
166+
stringBuilder.AppendLine($"TEMPORARY TABLESPACE TEMP");
167+
stringBuilder.AppendLine($"QUOTA UNLIMITED ON {tableSpaceName}");
168+
169+
await context.ExecuteAsync(stringBuilder.ToString(), cancellationToken);
170+
171+
var privileges = new[]
172+
{
173+
"CONNECT",
174+
"CREATE SESSION",
175+
"RESOURCE",
176+
"UNLIMITED TABLESPACE"
177+
};
178+
179+
await context.ExecuteAsync($"GRANT {string.Join(", ", privileges)} TO \"{tempUserName}\"", cancellationToken);
180+
await context.ExecuteAsync($"GRANT SELECT ON SYS.GV_$SESSION TO \"{tempUserName}\"", cancellationToken);
181+
153182
connectionStringBuilder.Add(UserStringKey, ReplaceString);
154183
connectionStringBuilder.Add(PasswordStringKey, ReplaceString);
155184

156185
tempDatabaseConnectionConfig.ConnectionString = connectionStringBuilder.ConnectionString;
157186
tempDatabaseConnectionConfig.ConnectionString = tempDatabaseConnectionConfig.ConnectionString.Replace(ReplaceString, $"\"{tempUserName}\"");
187+
tempDatabaseConnectionConfig.Schema = tempUserName;
158188

159189
var databaseInfo = new DatabaseInfo
160190
{
@@ -168,16 +198,18 @@ await Parallel.ForEachAsync(
168198

169199
public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, CancellationToken cancellationToken)
170200
{
201+
ArgumentNullException.ThrowIfNull(databaseInfo);
202+
171203
var creationDate = ReadTimeStampFromDatabaseName(databaseInfo.SchemaName);
172204

173205
var dataOptions = new DataOptions().UseOracle(databaseInfo.DatabaseConnectionConfigMaster.ConnectionString)
174206
.UseMappingSchema(_mappingSchema);
175207

208+
using var context = new DataConnection(dataOptions);
209+
176210
var maxAttempts = 4;
177211
var delayBetweenAttempts = TimeSpan.FromSeconds(1);
178212

179-
using var context = new DataConnection(dataOptions);
180-
181213
for (var i = 0; i < maxAttempts; i++)
182214
{
183215
try
@@ -192,6 +224,13 @@ public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, Cancella
192224
await context.ExecuteAsync(killStatement, cancellationToken);
193225
}
194226

227+
var userExists = context.GetTable<AllUsers>().Any(x => x.UserName == databaseInfo.SchemaName);
228+
229+
if (!userExists)
230+
{
231+
break;
232+
}
233+
195234
await context.ExecuteAsync($"DROP USER \"{databaseInfo.SchemaName}\" CASCADE", cancellationToken);
196235
}
197236
catch
@@ -207,19 +246,47 @@ public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, Cancella
207246
{
208247
break;
209248
}
210-
}
211249

212-
await Task.Delay(delayBetweenAttempts, cancellationToken);
250+
await Task.Delay(delayBetweenAttempts, cancellationToken);
213251

214-
delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1));
252+
delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1));
253+
}
215254
}
216255

217256
var tablespaceName = $"{TableSpacePrefix}{databaseInfo.SchemaName}";
218257

219-
var tablespaces = await context.GetTable<DBADataFiles>().ToListAsync(cancellationToken);
258+
maxAttempts = 4;
259+
delayBetweenAttempts = TimeSpan.FromSeconds(1);
260+
261+
for (var i = 0; i < maxAttempts; i++)
262+
{
263+
try
264+
{
265+
await context.ExecuteAsync($"DROP TABLESPACE {tablespaceName} INCLUDING CONTENTS AND DATAFILES", cancellationToken);
266+
}
267+
catch
268+
{
269+
var exists = await context.GetTable<DBADataFiles>().AnyAsync(x => x.TablespaceName == tablespaceName, cancellationToken);
270+
271+
if (!exists)
272+
{
273+
break;
274+
}
220275

221-
await context.ExecuteAsync($"DROP TABLESPACE {tablespaceName} INCLUDING CONTENTS AND DATAFILES", cancellationToken);
276+
if (i + 1 == maxAttempts)
277+
{
278+
throw;
279+
}
280+
281+
await Task.Delay(delayBetweenAttempts, cancellationToken);
282+
283+
delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1));
284+
}
285+
}
222286

223287
await context.ExecuteAsync($"PURGE RECYCLEBIN", cancellationToken);
224288
}
225-
}
289+
290+
[GeneratedRegex("^TS_TESTS_")]
291+
private static partial Regex TableSpacePrefixRegex();
292+
}

src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Collections.Generic;
12
using System.Data;
3+
using System.Linq;
24
using DotNetProjects.Migrator.Framework;
35
using Migrator.Tests.Providers.Base;
46
using NUnit.Framework;
@@ -30,6 +32,86 @@ public void AddTable_PrimaryKeyWithIdentity_Success()
3032
Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True);
3133
}
3234

35+
[Test]
36+
public void AddTable_PrimaryKeyAndIdentity_Success()
37+
{
38+
// Arrange
39+
var tableName = "TableName";
40+
var column1Name = "Column1";
41+
var column2Name = "Column2";
42+
43+
// Act
44+
Provider.AddTable(tableName,
45+
new Column(column1Name, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.PrimaryKey | ColumnProperty.Identity),
46+
new Column(column2Name, DbType.Int32, ColumnProperty.NotNull)
47+
);
48+
49+
// Assert
50+
var column1 = Provider.GetColumnByName(tableName, column1Name);
51+
var column2 = Provider.GetColumnByName(tableName, column2Name);
52+
53+
Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True);
54+
Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True);
55+
}
56+
57+
[Test]
58+
public void AddTable_PrimaryKeyAndIdentityWithInsertNull_Success()
59+
{
60+
// Arrange
61+
var tableName = "TableName";
62+
var column1Name = "Column1";
63+
var column2Name = "Column2";
64+
65+
// Act
66+
Provider.AddTable(tableName,
67+
new Column(column1Name, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.PrimaryKey | ColumnProperty.Identity),
68+
new Column(column2Name, DbType.Int32, ColumnProperty.NotNull)
69+
);
70+
71+
Provider.Insert(table: tableName, [column2Name], [999]);
72+
73+
// Assert
74+
var column1 = Provider.GetColumnByName(tableName, column1Name);
75+
var column2 = Provider.GetColumnByName(tableName, column2Name);
76+
77+
using var cmd = Provider.CreateCommand();
78+
using var reader = Provider.Select(cmd: cmd, table: tableName, columns: [column1Name, column2Name]);
79+
80+
List<(int, int)> records = [];
81+
82+
while (reader.Read())
83+
{
84+
records.Add((reader.GetInt32(0), reader.GetInt32(1)));
85+
}
86+
87+
Assert.That(records.Single().Item1, Is.EqualTo(1));
88+
89+
Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True);
90+
Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True);
91+
}
92+
93+
[Test]
94+
public void AddTable_PrimaryKeyAndIdentityWithoutNotNull_Success()
95+
{
96+
// Arrange
97+
var tableName = "TableName";
98+
var column1Name = "Column1";
99+
var column2Name = "Column2";
100+
101+
// Act
102+
Provider.AddTable(tableName,
103+
new Column(column1Name, DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.Identity),
104+
new Column(column2Name, DbType.Int32, ColumnProperty.NotNull)
105+
);
106+
107+
// Assert
108+
var column1 = Provider.GetColumnByName(tableName, column1Name);
109+
var column2 = Provider.GetColumnByName(tableName, column2Name);
110+
111+
Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True);
112+
Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True);
113+
}
114+
33115
[Test]
34116
public void AddTable_NotNull_Success()
35117
{

0 commit comments

Comments
 (0)