Skip to content

Commit 1a7a68e

Browse files
Merge pull request #71 from dotnetprojects/Sqlite
Retrieve Unique constraint names using RegEx in SQLite (from create table script)
2 parents fef817f + fa5cdf5 commit 1a7a68e

File tree

4 files changed

+162
-13
lines changed

4 files changed

+162
-13
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Data;
2+
using System.Linq;
3+
using DotNetProjects.Migrator.Providers.Impl.SQLite;
4+
using Migrator.Framework;
5+
using Migrator.Tests.Providers.SQLite.Base;
6+
using NUnit.Framework;
7+
8+
namespace Migrator.Tests.Providers.SQLite;
9+
10+
[TestFixture]
11+
[Category("SQLite")]
12+
public class SQLiteTransformationProvider_GetUniquesTests : SQLiteTransformationProviderTestBase
13+
{
14+
[Test]
15+
public void GetUniques_Success()
16+
{
17+
// Arrange
18+
const string tableNameA = "TableA";
19+
const string property1 = "Property1";
20+
const string property2 = "Property2";
21+
const string property3 = "Property3";
22+
const string property4 = "Property4";
23+
const string property5 = "Property5";
24+
const string uniqueConstraintName1 = "UniqueConstraint1";
25+
const string uniqueConstraintName2 = "UniqueConstraint2";
26+
27+
Provider.AddTable(tableNameA,
28+
new Column(property1, DbType.Int32, ColumnProperty.PrimaryKey),
29+
new Column(property2, DbType.Int32, ColumnProperty.Unique),
30+
new Column(property3, DbType.Int32),
31+
new Column(property4, DbType.Int32),
32+
new Column(property5, DbType.Int32)
33+
);
34+
35+
Provider.AddUniqueConstraint(uniqueConstraintName1, tableNameA, property3);
36+
Provider.AddUniqueConstraint(uniqueConstraintName2, tableNameA, property4, property5);
37+
38+
// Act
39+
var uniqueConstraints = ((SQLiteTransformationProvider)Provider).GetUniques(tableNameA);
40+
41+
// Assert
42+
var sql = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableNameA);
43+
44+
Assert.That(uniqueConstraints.Count, Is.EqualTo(3));
45+
Assert.That(uniqueConstraints.Single(x => x.Name == uniqueConstraintName1).KeyColumns, Is.EqualTo([property3]));
46+
Assert.That(uniqueConstraints.Single(x => x.Name == uniqueConstraintName2).KeyColumns, Is.EqualTo([property4, property5]));
47+
48+
Assert.That(sql, Does.Contain("CONSTRAINT UniqueConstraint1 UNIQUE (Property3)"));
49+
Assert.That(sql, Does.Contain("CONSTRAINT UniqueConstraint2 UNIQUE (Property4, Property5)"));
50+
Assert.That(sql, Does.Contain("CONSTRAINT sqlite_autoindex_TableA_1 UNIQUE (Property2)"));
51+
}
52+
}

src/Migrator/Framework/ColumnProperty.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ public enum ColumnProperty
2626
Identity = 4,
2727

2828
/// <summary>
29-
/// Unique Column
29+
/// Unique Column. This is marked being obsolete since you cannot add a name for the constraint which makes it difficult to remove the constraint again.
3030
/// </summary>
31+
[Obsolete("Use method 'AddUniqueConstraint' instead. This is marked being obsolete since you cannot add a name for the constraint which makes it difficult to remove the constraint again.")]
3132
Unique = 8,
3233

3334
/// <summary>

src/Migrator/Framework/ITransformationProvider.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,29 +488,33 @@ public interface ITransformationProvider : IDisposable
488488
void RemoveColumn(string table, string column);
489489

490490
/// <summary>
491-
/// Remove an existing foreign key constraint
491+
/// Remove an existing foreign key constraint.
492492
/// </summary>
493493
/// <param name="table">The table that contains the foreign key.</param>
494494
/// <param name="name">The name of the foreign key to remove</param>
495495
void RemoveForeignKey(string table, string name);
496496

497497
/// <summary>
498-
/// Remove an existing constraint
498+
/// Remove an existing constraint.
499499
/// </summary>
500500
/// <param name="table">The table that contains the foreign key.</param>
501501
/// <param name="name">The name of the constraint to remove</param>
502502
void RemoveConstraint(string table, string name);
503503

504+
/// <summary>
505+
/// Removes PK, FKs, Unique and CHECK constraints.
506+
/// </summary>
507+
/// <param name="table"></param>
504508
void RemoveAllConstraints(string table);
505509

506510
/// <summary>
507-
/// Remove an existing primary key
511+
/// Remove an existing primary key.
508512
/// </summary>
509513
/// <param name="table">The table that contains the primary key.</param>
510514
void RemovePrimaryKey(string table);
511515

512516
/// <summary>
513-
/// Remove an existing table
517+
/// Drops an existing table.
514518
/// </summary>
515519
/// <param name="tableName">The name of the table</param>
516520
void RemoveTable(string tableName);

src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,31 @@ public override void AddForeignKey(
5353
string[] parentColumns,
5454
ForeignKeyConstraintType constraint)
5555
{
56+
if (string.IsNullOrWhiteSpace(name))
57+
{
58+
throw new Exception("A FK name is mandatory");
59+
}
60+
5661
var sqliteTableInfo = GetSQLiteTableInfo(childTable);
5762

63+
// Get all unique constraint names if available
64+
var uniqueConstraintNames = sqliteTableInfo.Uniques.Select(x => x.Name).ToList();
65+
66+
// Get all FK constraint names if available
67+
var foreignKeyNames = sqliteTableInfo.ForeignKeys.Select(x => x.Name).ToList();
68+
69+
var names = uniqueConstraintNames.Concat(foreignKeyNames)
70+
.Distinct()
71+
.Where(x => !string.IsNullOrWhiteSpace(x))
72+
.ToList();
73+
74+
if (names.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)))
75+
{
76+
throw new Exception($"Constraint name {name} already exists");
77+
}
78+
5879
var foreignKey = new ForeignKeyConstraint
5980
{
60-
// SQLite does not support FK names
6181
ChildColumns = childColumns,
6282
ChildTable = childTable,
6383
Name = name,
@@ -149,6 +169,7 @@ public override ForeignKeyConstraint[] GetForeignKeyConstraints(string tableName
149169
var foreignKeyExtract = new ForeignKeyExtract()
150170
{
151171
ChildColumnNames = parenthesisContents[0].Split(',').Select(x => x.Trim()).ToList(),
172+
ForeignKeyString = fkPart,
152173
ParentColumnNames = parenthesisContents[1].Split(',').Select(x => x.Trim()).ToList(),
153174
};
154175

@@ -816,12 +837,40 @@ public override List<string> GetDatabases()
816837

817838
public override bool ConstraintExists(string table, string name)
818839
{
819-
throw new NotSupportedException("SQLite does not offer constraint names e.g. for unique, check constraints. You need to use alternative ways.");
840+
var constraintNames = GetConstraints(table);
841+
842+
var exists = constraintNames.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase));
843+
844+
return exists;
820845
}
821846

822847
public override string[] GetConstraints(string table)
823848
{
824-
throw new NotSupportedException("SQLite does not offer constraint names e.g. for unique, check constraints You need to drop them using alternative ways.");
849+
var sqliteInfo = GetSQLiteTableInfo(table);
850+
851+
var foreignKeyNames = sqliteInfo.ForeignKeys
852+
.Select(x => x.Name)
853+
.ToList();
854+
855+
var uniqueConstraints = sqliteInfo.Uniques
856+
.Select(x => x.Name)
857+
.ToList();
858+
859+
// TODO add PK and CHECK
860+
861+
var names = foreignKeyNames.Concat(uniqueConstraints)
862+
.Where(x => !string.IsNullOrWhiteSpace(x))
863+
.ToArray();
864+
865+
var distinctNames = names.Distinct(StringComparer.OrdinalIgnoreCase)
866+
.ToArray();
867+
868+
if (names.Length != distinctNames.Length)
869+
{
870+
throw new Exception($"There are duplicate constraint names in table {table}'");
871+
}
872+
873+
return distinctNames;
825874
}
826875

827876
public override string[] GetTables()
@@ -1059,11 +1108,15 @@ public override void AddTable(string name, string engine, params IDbField[] fiel
10591108
{
10601109
if (!string.IsNullOrEmpty(u.Name))
10611110
{
1062-
stringBuilder.Append($" CONSTRAINT {u.Name}");
1111+
stringBuilder.Append($", CONSTRAINT {u.Name}");
1112+
}
1113+
else
1114+
{
1115+
stringBuilder.Append(", ");
10631116
}
10641117

10651118
var uniqueColumnsCommaSeparated = string.Join(", ", u.KeyColumns);
1066-
stringBuilder.Append($", UNIQUE ({uniqueColumnsCommaSeparated})");
1119+
stringBuilder.Append($" UNIQUE ({uniqueColumnsCommaSeparated})");
10671120
}
10681121

10691122
var foreignKeys = fields.Where(x => x is ForeignKeyConstraint).Cast<ForeignKeyConstraint>().ToArray();
@@ -1169,14 +1222,18 @@ public override void RemoveAllIndexes(string tableName)
11691222

11701223
public List<Unique> GetUniques(string tableName)
11711224
{
1225+
var regEx = new Regex(@"(?<=,)\s*(CONSTRAINT\s+\w+\s+)?UNIQUE\s*\(\s*[\w\s,]+\s*\)\s*(?=,|\s*\))");
1226+
var regExConstraintName = new Regex(@"(?<=CONSTRAINT\s+)\w+(?=\s+)");
1227+
var regExParenthesis = new Regex(@"(?<=\().+(?=\))");
1228+
11721229
List<Unique> uniques = [];
11731230

11741231
var pragmaIndexListItems = GetPragmaIndexListItems(tableName);
11751232

11761233
// Here we filter for origin u and unique while in "GetIndexes()" we exclude them.
1177-
// If pk is set then it was added by using a primary key. If so this is handled by "GetColumns()".
1178-
// If c is set it was created by using CREATE INDEX. At this moment in time this migrator does not support UNIQUE indexes but only normal indexes
1179-
// so u should never be set 30.06.2025).
1234+
// If "pk" is set then it was added by using a primary key. If so this is handled by "GetColumns()".
1235+
// If "c" is set it was created by using CREATE INDEX. At this moment in time this migrator does not support UNIQUE indexes but only normal indexes
1236+
// so "u" should never be set 30.06.2025).
11801237
var uniqueConstraints = pragmaIndexListItems.Where(x => x.Unique && x.Origin == "u")
11811238
.ToList();
11821239

@@ -1197,6 +1254,41 @@ public List<Unique> GetUniques(string tableName)
11971254
uniques.Add(unique);
11981255
}
11991256

1257+
var createScript = GetSqlCreateTableScript(tableName);
1258+
1259+
var matches = regEx.Matches(createScript).Cast<Match>().Where(x => x.Success).Select(x => x.Value.Trim()).ToList();
1260+
1261+
// We can only use the ones containing a starting with CONSTRAINT
1262+
var matchesHavingName = matches.Where(x => x.StartsWith("CONSTRAINT")).ToList();
1263+
1264+
foreach (var constraintString in matchesHavingName)
1265+
{
1266+
var constraintNameMatch = regExConstraintName.Match(constraintString);
1267+
1268+
if (!constraintNameMatch.Success)
1269+
{
1270+
throw new Exception("Cannot extract constraint name - severe issue. Please file an issue");
1271+
}
1272+
1273+
var constraintName = constraintNameMatch.Value;
1274+
1275+
var parenthesisMatch = regExParenthesis.Match(constraintString);
1276+
1277+
if (!parenthesisMatch.Success)
1278+
{
1279+
throw new Exception("Cannot extract parenthesis content for UNIQUE constraint - severe issue. Please file an issue");
1280+
}
1281+
1282+
var columns = parenthesisMatch.Value.Split(',').Select(x => x.Trim()).ToList();
1283+
1284+
var unique = uniques.Where(x => x.KeyColumns.SequenceEqual(columns)).SingleOrDefault();
1285+
1286+
if (unique != null)
1287+
{
1288+
unique.Name = constraintName;
1289+
}
1290+
}
1291+
12001292
return uniques;
12011293
}
12021294

0 commit comments

Comments
 (0)