Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/migration-guide-12.1-to-13.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,23 @@ version does not insert it twice. History for another scope does not skip a step
in the current scope. Transaction rollback still applies to baseline schema and
history changes according to the selected transaction mode.

## Adding SQLite identity to an existing table
## SQLite defaults and identity

### SQLite GUID defaults

SQLite now renders a CLR `Guid` default as a blob using `Guid.ToByteArray()`,
matching GUID parameters inserted by the provider. Previously a GUID default
was text, so a defaulted foreign-key value did not match an explicitly inserted
parent GUID even when both represented the same identifier.

This fixes new table/column definitions, including backfilling a new column.
Existing text GUID defaults and data are preserved during unrelated rebuilds;
column inspection retains their SQL as `RawSql` so storage classes are not
silently converted. Databases already containing mixed text/blob GUIDs require
an explicit data migration that converts related keys consistently. A string
default remains text; use a CLR `Guid` when authoring a GUID default.

### SQLite identity columns

SQLite requires the identity column and its single-column primary key in the
same table definition. Separate `AddColumn` and `AddPrimaryKey` calls create an
Expand Down
77 changes: 77 additions & 0 deletions src/Migrator.Tests/SQLiteGuidDefaultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Data;
using DotNetProjects.Migrator;
using DotNetProjects.Migrator.Framework;
using DotNetProjects.Migrator.Providers;
using DotNetProjects.Migrator.Providers.Impl.SQLite;
using NUnit.Framework;
using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint;

namespace Migrator.Tests;

[Category("SQLite")]
public class SQLiteGuidDefaultTests
{
private static IDbConnection OpenConnection(bool systemData)
{
IDbConnection connection = systemData
? new System.Data.SQLite.SQLiteConnection("Data Source=:memory:")
: new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
connection.Open();
return connection;
}

[TestCase(false, "00000000-0000-0000-0000-000000000000")]
[TestCase(true, "00000000-0000-0000-0000-000000000000")]
[TestCase(false, "00112233-4455-6677-8899-aabbccddeeff")]
[TestCase(true, "00112233-4455-6677-8899-aabbccddeeff")]
public void GuidDefaultsMatchInsertedKeysBeforeAndAfterRebuild(bool systemData, string value)
{
using var connection = OpenConnection(systemData);
using var provider = (SQLiteTransformationProvider)ProviderFactory.Create(ProviderTypes.SQLite, connection, null);
var id = Guid.Parse(value);
provider.AddTable("Parents", new Column("Id", DbType.Guid), new PrimaryKeyConstraint("PK_Parents", "Id"));
provider.Insert("Parents", ["Id"], [id]);
provider.AddTable("Children", new Column("Id", DbType.Int32),
new Column("ParentId", DbType.Guid) { DefaultValue = id },
new ForeignKeyConstraint("FK_Children_Parents", "Parents", ["Id"], "Children", ["ParentId"]));
provider.Insert("Children", ["Id"], [1]);
Assert.That(provider.CheckForeignKeyIntegrity(), Is.True);
Assert.That(provider.ExecuteScalar("SELECT hex(ParentId) FROM Children"), Is.EqualTo(Convert.ToHexString(id.ToByteArray())));
provider.AddColumn("Children", new Column("Extra", DbType.Int32));
provider.Insert("Children", ["Id"], [2]);
Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Children JOIN Parents ON Children.ParentId = Parents.Id"), Is.EqualTo(2));
Assert.That(provider.CheckForeignKeyIntegrity(), Is.True);
}

[TestCase(false)]
[TestCase(true)]
public void AddingGuidColumnBackfillsTheSameRepresentationAsParameters(bool systemData)
{
using var connection = OpenConnection(systemData);
using var provider = (SQLiteTransformationProvider)ProviderFactory.Create(ProviderTypes.SQLite, connection, null);
var id = Guid.Parse("00112233-4455-6677-8899-aabbccddeeff");
provider.AddTable("Parents", new Column("Id", DbType.Guid), new PrimaryKeyConstraint("PK_Parents", "Id"));
provider.Insert("Parents", ["Id"], [id]);
provider.AddTable("Children", new Column("Id", DbType.Int32));
provider.Insert("Children", ["Id"], [1]);
provider.AddColumn("Children", new Column("ParentId", DbType.Guid) { DefaultValue = id, IsNullable = false });
provider.AddForeignKey("FK_Children_Parents", "Children", "ParentId", "Parents", "Id");
Assert.That(provider.CheckForeignKeyIntegrity(), Is.True);
Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Children JOIN Parents ON Children.ParentId = Parents.Id"), Is.EqualTo(1));
}

[TestCase(false)]
[TestCase(true)]
public void RebuildingLegacyTextGuidDefaultsPreservesTheirStorageRepresentation(bool systemData)
{
using var connection = OpenConnection(systemData);
using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null);
const string value = "00112233-4455-6677-8899-aabbccddeeff";
provider.ExecuteNonQuery("CREATE TABLE Legacy (Id INTEGER, Token UNIQUEIDENTIFIER DEFAULT '" + value + "')");
provider.Insert("Legacy", ["Id"], [1]);
provider.AddColumn("Legacy", new Column("Extra", DbType.Int32));
provider.Insert("Legacy", ["Id"], [2]);
Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Legacy WHERE typeof(Token) = 'text' AND Token = '" + value + "'"), Is.EqualTo(2));
}
}
3 changes: 3 additions & 0 deletions src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Data;
using DotNetProjects.Migrator.Framework;

Expand Down Expand Up @@ -66,6 +67,8 @@ public SQLiteDialect()
public override string Default(object defaultValue)
{
if (defaultValue is RawSql expression) return "DEFAULT (" + expression.Sql + ")";
// Match SQLiteTransformationProvider's GUID parameter representation, including byte order.
if (defaultValue is Guid guid) return "DEFAULT X'" + Convert.ToHexString(guid.ToByteArray()) + "'";
if (defaultValue is bool)
{
return string.Format("DEFAULT {0}", (bool)defaultValue ? "1" : "0");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@
}

[Obsolete]
public override void AddTable(string table, string engine, string columns)

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Unit)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (SQLite)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Firebird)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (MariaDB)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (PostgreSQL)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (MySQL)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Sybase)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Informix)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (SQLServer)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Db2)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Oracle)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'

Check warning on line 960 in src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Hana)

Obsolete member 'SQLiteTransformationProvider.AddTable(string, string, string)' overrides non-obsolete member 'TransformationProvider.AddTable(string, string, string)'
{
throw new NotSupportedException();
}
Expand Down Expand Up @@ -1137,8 +1137,11 @@

var defValue = pragmaTableInfoItem.DfltValue == DBNull.Value ? null : pragmaTableInfoItem.DfltValue;

// Keep legacy text GUID defaults as text during unrelated rebuilds. New Guid
// values render as blobs, so parsing an old SQL literal into Guid would change its storage class.
column.DefaultValue = defValue is string sqlDefault
? CatalogDefaultValue.Parse(sqlDefault, column.Type) : defValue;
? column.Type == DbType.Guid ? RawSql.Insert(sqlDefault) : CatalogDefaultValue.Parse(sqlDefault, column.Type)
: defValue;

var columnTableInfoItem = pragmaTableInfoItems.First(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase));

Expand Down
Loading