From 2dc5d762f0ff44a50a6d4e0656b0a645d75e2bd9 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 12 Jan 2026 16:49:55 +0100 Subject: [PATCH] Set NOT NULL implicitly for PKs in SQLite but only for non-composite PKs --- ...teTransformationProvider_AddColumnTests.cs | 7 ++- ...ansformationProvider_AddPrimaryKeyTests.cs | 58 ++++++++++++++++++- ...iteTransformationProvider_AddTableTests.cs | 57 +++++++++++++++++- ...ionProvider_PropertyColumnIdentityTests.cs | 2 +- .../SQLite/SQLiteTransformationProvider.cs | 7 +++ .../SqlServerTransformationProvider.cs | 2 - 6 files changed, 126 insertions(+), 7 deletions(-) diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs index b9d32893..4b9c6453 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs @@ -59,8 +59,11 @@ public void AddColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() CollectionAssert.AreEquivalent(indexAfter.KeyColumns, new string[] { propertyName1, propertyName2 }); } + /// + /// NOT NULL is implicitly set by the migrator for non-composite primary key + /// [Test] - public void AddColumn_HavingNullInPrimaryKey_HasNULLAfterAddAnotherColumn() + public void AddColumn_HavingNullInPrimaryKey_HasNotNullAfterAddAnotherColumn() { // Arrange/Act Provider.ExecuteNonQuery("CREATE TABLE Common_Language (LanguageID TEXT PRIMARY KEY)"); @@ -73,7 +76,7 @@ public void AddColumn_HavingNullInPrimaryKey_HasNULLAfterAddAnotherColumn() var columnProperty = tableInfo.Columns.Single(x => x.Name == "LanguageID").ColumnProperty; // Assert - Assert.That(script, Does.Contain("LanguageID TEXT NULL PRIMARY KEY")); + Assert.That(script, Does.Contain("LanguageID TEXT NOT NULL PRIMARY KEY")); } [Test] diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs index ccde9404..23c217e8 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs @@ -1,4 +1,6 @@ +using System; using System.Data; +using System.Data.SQLite; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Impl.SQLite; @@ -27,7 +29,10 @@ public void AddPrimaryKey_ColumnsInOtherOrderThanInColumnsList_Success() const string tableName = "TestTable"; const string primaryKeyName = $"PK_{tableName}"; - Provider.AddTable(tableName, new Column(columnName1, DbType.String), new Column(columnName2, DbType.Int32), new Column(columnName3, DbType.Int32)); + Provider.AddTable(tableName, + new Column(columnName1, DbType.String), + new Column(columnName2, DbType.Int32), + new Column(columnName3, DbType.Int32)); // Act Provider.AddPrimaryKey(name: primaryKeyName, table: tableName, columns: [columnName3, columnName2]); @@ -37,4 +42,55 @@ public void AddPrimaryKey_ColumnsInOtherOrderThanInColumnsList_Success() Assert.That(createTableScript, Does.Contain("PRIMARY KEY (TestColumn3, TestColumn2))")); } + + [Test] + public void AddPrimaryKey_ColumnGuidNonComposite_ThrowsOnDuplicatesAndNulls() + { + const string tableName = "MyTableName"; + const string columnName1 = "Column1"; + var guid = Guid.NewGuid(); + + // Arrange/Act + Provider.AddTable(tableName, + new Column(columnName1, DbType.Guid, ColumnProperty.PrimaryKey) + ); + + Provider.Insert(tableName, [columnName1], [guid]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1], [guid])); + Assert.Throws(() => Provider.Insert(tableName, [columnName1], [null])); + } + + [Test] + public void AddPrimaryKey_ColumnGuidComposite_ThrowsOnDuplicatesAndNulls() + { + // Arrange + const string columnName1 = "TestColumn1"; + const string columnName2 = "TestColumn2"; + const string tableName = "TestTable"; + const string primaryKeyName = $"PK_{tableName}"; + var guid = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Guid), + new Column(columnName2, DbType.Guid)); + + // Act + Provider.AddPrimaryKey(name: primaryKeyName, table: tableName, columns: [columnName1, columnName2]); + + // This is a normal SQLite behavior! + // NULL != NULL + // (A, NULL) != (A, NULL) + // Duplicates! You need to set NotNull if you want to prevent it! + Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + + Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + + Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); + Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); + + Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2])); + } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs index 2b9eee06..1cc48749 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs @@ -1,3 +1,4 @@ +using System; using System.Data.SQLite; using System.Linq; using System.Threading.Tasks; @@ -89,7 +90,7 @@ public void AddTable_SinglePrimaryKey_ContainsNull() var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); // In SQLite an INTEGER PRIMARY KEY column is NOT NULL implicitly (see insert asserts above) - Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER PRIMARY KEY, Column2 INTEGER NOT NULL)")); + Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER NOT NULL PRIMARY KEY, Column2 INTEGER NOT NULL)")); var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); Assert.That(sqliteInfo.Columns.First().Name, Is.EqualTo(columnName1)); @@ -124,4 +125,58 @@ public void AddTable_MiscellaneousColumns_Succeeds() Assert.That(sqliteInfo.Columns.First().Name, Is.EqualTo(columnName1)); Assert.That(sqliteInfo.Columns[1].Name, Is.EqualTo(columnName2)); } + + /// + /// NOT NULL is implicitly set by SQLite + /// + [Test] + public void AddTable_GuidPrimaryKeyOneColumnPKImplicitlyUsingNotNull_ThrowsOnNullAndOnDuplicates() + { + const string tableName = "MyTableName"; + const string columnName1 = "Column1"; + var guid = Guid.NewGuid(); + + // Arrange/Act + Provider.AddTable(tableName, + new Column(columnName1, System.Data.DbType.Guid, ColumnProperty.PrimaryKey) + ); + + Provider.Insert(tableName, [columnName1], [guid]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1], [guid])); + + // The migrator sets NotNull on PrimaryKey (non composite) so this line throws. + Assert.Throws(() => Provider.Insert(tableName, [columnName1], [null])); + } + + /// + /// Composite PK with Guids + /// + [Test] + public void AddTable_GuidPrimaryKeyCompositeWithGuid_DoesNotThrowOnDuplicateNULLEntries() + { + const string tableName = "MyTableName"; + const string columnName1 = "Column1"; + const string columnName2 = "Column2"; + var guid = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + // Arrange/Act + Provider.AddTable(tableName, + new Column(columnName1, System.Data.DbType.Guid, ColumnProperty.PrimaryKey), + new Column(columnName2, System.Data.DbType.Guid, ColumnProperty.PrimaryKey) + ); + + // This is a normal SQLite behavior! + // NULL != NULL + // (A, NULL) != (A, NULL) + // Duplicates! You need to set NotNull if you want to prevent it! + Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + + Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); + Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); + + Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2])); + } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs index ad841b31..7ba49518 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs @@ -26,6 +26,6 @@ public void AddPrimaryIdentity_Succeeds() var sql = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(testTableName); // NOT NULL implicitly set in SQLite - Assert.That(sql, Does.Contain("Color1 INTEGER PRIMARY KEY")); + Assert.That(sql, Does.Contain("Color1 INTEGER NOT NULL PRIMARY KEY")); } } diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index b3fb46a2..7697b05f 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1390,6 +1390,13 @@ public override void AddTable(string name, string engine, params IDbField[] fiel foreach (var column in columns) { + if (!hasCompoundPrimaryKey && column.IsPrimaryKey) + { + // We implicitly set NOT NULL for non-composite primary keys like in other RDBMS. + column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.Null); + column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.NotNull); + } + if (hasCompoundPrimaryKey && column.IsPrimaryKey) { // We remove PrimaryKey here and readd it as compound later ("...PRIMARY KEY(column1,column2)"); diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 9aac8aa8..871c2cb8 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -391,8 +391,6 @@ ORDER BY var schemaNameOrdinal = reader.GetOrdinal("SchemaName"); var tableNameOrdinal = reader.GetOrdinal("TableName"); - - while (reader.Read()) { var indexItem = new IndexItem