Skip to content

Commit c5ac15f

Browse files
Merge pull request #147 from dotnetprojects/add-primary-key
Oracle: Handle "NULL" as default value
2 parents 491978c + 8f56721 commit c5ac15f

14 files changed

+274
-69
lines changed

.github/workflows/sql/oracle.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ grant resource to k with admin option;
1414
grant connect to k with admin option;
1515
grant unlimited tablespace to k with admin option;
1616
grant select on v_$session to k with grant option;
17+
grant select on sys.gv_$session to k with grant option;
1718
grant alter system to k;
1819

1920
exit;

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: 41 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Linq;
4-
using System.Text.RegularExpressions;
3+
using System.Text;
54
using System.Threading;
65
using System.Threading.Tasks;
76
using DotNetProjects.Migrator.Framework.Data.Common;
@@ -19,17 +18,21 @@
1918

2019
namespace Migrator.Tests.Database.DerivedDatabaseIntegrationTestServices;
2120

21+
22+
/// <summary>
23+
/// We use the tablespace users since the server container is recreated before the test runs (once per github workflow run)
24+
/// </summary>
25+
/// <param name="timeProvider"></param>
26+
/// <param name="databaseNameService"></param>
2227
public class OracleDatabaseIntegrationTestService(
2328
TimeProvider timeProvider,
2429
IDatabaseNameService databaseNameService)
2530
: DatabaseIntegrationTestServiceBase(databaseNameService), IDatabaseIntegrationTestService
2631
{
27-
private const string TableSpacePrefix = "TS_";
2832
private const string UserStringKey = "User Id";
2933
private const string PasswordStringKey = "Password";
3034
private const string ReplaceString = "RandomStringThatIsNotQuotedByTheBuilderDoNotChange";
3135
private readonly MappingSchema _mappingSchema = new MappingSchemaFactory().CreateOracleMappingSchema();
32-
private Regex _tablespaceRegex = new("^TS_TESTS_");
3336

3437
/// <summary>
3538
/// Creates an oracle database for test purposes.
@@ -61,8 +64,6 @@ public class OracleDatabaseIntegrationTestService(
6164
/// <exception cref="NotImplementedException"></exception>
6265
public override async Task<DatabaseInfo> CreateTestDatabaseAsync(DatabaseConnectionConfig databaseConnectionConfig, CancellationToken cancellationToken)
6366
{
64-
DataConnection context;
65-
6667
var tempDatabaseConnectionConfig = databaseConnectionConfig.Adapt<DatabaseConnectionConfig>();
6768

6869
var connectionStringBuilder = new OracleConnectionStringBuilder()
@@ -82,15 +83,12 @@ public override async Task<DatabaseInfo> CreateTestDatabaseAsync(DatabaseConnect
8283

8384
var tempUserName = DatabaseNameService.CreateDatabaseName();
8485

85-
List<string> userNames;
86-
8786
var dataOptions = new DataOptions().UseOracle(databaseConnectionConfig.ConnectionString)
8887
.UseMappingSchema(_mappingSchema);
8988

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

9593
var toBeDeletedUsers = userNames.Where(x =>
9694
{
@@ -112,49 +110,33 @@ await Parallel.ForEachAsync(
112110
};
113111

114112
await DropDatabaseAsync(databaseInfoToBeDeleted, cancellationTokenInner);
115-
116113
});
117114

118-
using (context = new DataConnection(dataOptions))
119-
{
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-
});
115+
var stringBuilder = new StringBuilder();
116+
stringBuilder.Append($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\"");
117+
stringBuilder.AppendLine($"DEFAULT TABLESPACE users");
118+
stringBuilder.AppendLine($"TEMPORARY TABLESPACE TEMP");
119+
stringBuilder.AppendLine($"QUOTA UNLIMITED ON users");
133120

134-
foreach (var toBeDeletedTableSpace in toBeDeletedTableSpaces)
135-
{
136-
await context.ExecuteAsync($"DROP TABLESPACE {toBeDeletedTableSpace} INCLUDING CONTENTS AND DATAFILES", cancellationToken);
137-
}
121+
await context.ExecuteAsync(stringBuilder.ToString(), cancellationToken);
138122

139-
await context.ExecuteAsync($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\"", cancellationToken);
123+
var privileges = new[]
124+
{
125+
"CONNECT",
126+
"CREATE SESSION",
127+
"RESOURCE",
128+
"UNLIMITED TABLESPACE"
129+
};
140130

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);
151-
}
131+
await context.ExecuteAsync($"GRANT {string.Join(", ", privileges)} TO \"{tempUserName}\"", cancellationToken);
132+
await context.ExecuteAsync($"GRANT SELECT ON SYS.GV_$SESSION TO \"{tempUserName}\"", cancellationToken);
152133

153134
connectionStringBuilder.Add(UserStringKey, ReplaceString);
154135
connectionStringBuilder.Add(PasswordStringKey, ReplaceString);
155136

156137
tempDatabaseConnectionConfig.ConnectionString = connectionStringBuilder.ConnectionString;
157138
tempDatabaseConnectionConfig.ConnectionString = tempDatabaseConnectionConfig.ConnectionString.Replace(ReplaceString, $"\"{tempUserName}\"");
139+
tempDatabaseConnectionConfig.Schema = tempUserName;
158140

159141
var databaseInfo = new DatabaseInfo
160142
{
@@ -168,16 +150,18 @@ await Parallel.ForEachAsync(
168150

169151
public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, CancellationToken cancellationToken)
170152
{
153+
ArgumentNullException.ThrowIfNull(databaseInfo);
154+
171155
var creationDate = ReadTimeStampFromDatabaseName(databaseInfo.SchemaName);
172156

173157
var dataOptions = new DataOptions().UseOracle(databaseInfo.DatabaseConnectionConfigMaster.ConnectionString)
174158
.UseMappingSchema(_mappingSchema);
175159

160+
using var context = new DataConnection(dataOptions);
161+
176162
var maxAttempts = 4;
177163
var delayBetweenAttempts = TimeSpan.FromSeconds(1);
178164

179-
using var context = new DataConnection(dataOptions);
180-
181165
for (var i = 0; i < maxAttempts; i++)
182166
{
183167
try
@@ -192,6 +176,13 @@ public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, Cancella
192176
await context.ExecuteAsync(killStatement, cancellationToken);
193177
}
194178

179+
var userExists = context.GetTable<AllUsers>().Any(x => x.UserName == databaseInfo.SchemaName);
180+
181+
if (!userExists)
182+
{
183+
break;
184+
}
185+
195186
await context.ExecuteAsync($"DROP USER \"{databaseInfo.SchemaName}\" CASCADE", cancellationToken);
196187
}
197188
catch
@@ -207,19 +198,13 @@ public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, Cancella
207198
{
208199
break;
209200
}
210-
}
211201

212-
await Task.Delay(delayBetweenAttempts, cancellationToken);
202+
await Task.Delay(delayBetweenAttempts, cancellationToken);
213203

214-
delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1));
204+
delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1));
205+
}
215206
}
216207

217-
var tablespaceName = $"{TableSpacePrefix}{databaseInfo.SchemaName}";
218-
219-
var tablespaces = await context.GetTable<DBADataFiles>().ToListAsync(cancellationToken);
220-
221-
await context.ExecuteAsync($"DROP TABLESPACE {tablespaceName} INCLUDING CONTENTS AND DATAFILES", cancellationToken);
222-
223208
await context.ExecuteAsync($"PURGE RECYCLEBIN", cancellationToken);
224209
}
225-
}
210+
}

src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ protected void DropTestTables()
6464
}
6565
}
6666

67-
6867
protected async Task BeginOracleTransactionAsync()
6968
{
70-
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
69+
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
7170
var configReader = new ConfigurationReader();
7271

7372
var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.OracleId);

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)