Skip to content

Commit 820a4f6

Browse files
committed
Add migration dependency checks
- add APIs for checking migration scopes and applied migrations - add required and optional-scope dependency guards - prevent migrations from running against incompatible schema states - roll back migration batches when dependencies are not satisfied - add dependency and interleaved migration tests
1 parent e272806 commit 820a4f6

5 files changed

Lines changed: 459 additions & 1 deletion

File tree

‎src/NuExt.System.Data.SQLite.csproj‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@
2323
<ProjectReference Include="..\..\NuExt.System.Data\src\NuExt.System.Data.csproj" />
2424
</ItemGroup>
2525

26+
<ItemGroup>
27+
<InternalsVisibleTo Include="$(MSBuildProjectName).Tests" Key="0024000004800000940000000602000000240000525341310004000001000100b789a2ca53134810a04b9629da17625466eca3b89758bedfe43ebb1ed321c10eb969c265dbfe73e8fa34a63a30246654a4db84f23112d88b7f2a61573784f338af6d225b994a3ff0aa17db7faadb09edda35a28a5569a7e389fa501a23215d30987200702b22b86b36becc3780770256448a230c38bbdc0ba30fb21faeae5fbc" />
28+
</ItemGroup>
29+
2630
</Project>

‎src/System/Data/SQLite/SQLiteDbMigrationContext.cs‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,57 @@ public int ExecuteNonQuery(string commandText)
2525
{
2626
return Connection.ExecuteNonQuery(commandText, CancellationToken);
2727
}
28-
}
28+
29+
/// <summary>
30+
/// Determines whether the specified migration scope has applied migrations.
31+
/// </summary>
32+
/// <param name="scope">The stable migration scope identifier.</param>
33+
/// <returns><see langword="true"/> if the migration scope has applied migrations; otherwise, <see langword="false"/>.</returns>
34+
/// <exception cref="ArgumentException"><paramref name="scope"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
35+
public bool MigrationScopeExists(string scope)
36+
{
37+
return SQLiteMigrationHistory.MigrationScopeExists(this, scope);
38+
}
39+
40+
/// <summary>
41+
/// Determines whether the specified migration has been applied within the specified migration scope.
42+
/// </summary>
43+
/// <param name="scope">The stable migration scope identifier.</param>
44+
/// <param name="migrationId">The migration identifier.</param>
45+
/// <returns><see langword="true"/> if the migration has been applied; otherwise, <see langword="false"/>.</returns>
46+
/// <exception cref="ArgumentException"><paramref name="scope"/> or <paramref name="migrationId"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
47+
public bool IsMigrationApplied(string scope, string migrationId)
48+
{
49+
return SQLiteMigrationHistory.IsMigrationApplied(this, scope, migrationId);
50+
}
51+
52+
/// <summary>
53+
/// Requires the specified migration to have been applied within the specified migration scope.
54+
/// </summary>
55+
/// <param name="scope">The stable migration scope identifier.</param>
56+
/// <param name="migrationId">The required migration identifier.</param>
57+
/// <exception cref="ArgumentException"><paramref name="scope"/> or <paramref name="migrationId"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
58+
/// <exception cref="DbMigrationException">The required migration has not been applied.</exception>
59+
public void RequireAppliedMigration(string scope, string migrationId)
60+
{
61+
if (!IsMigrationApplied(scope, migrationId))
62+
{
63+
throw new DbMigrationException($"Migration '{scope}/{migrationId}' must be applied before this migration.");
64+
}
65+
}
66+
67+
/// <summary>
68+
/// Requires the specified migration when the migration scope has applied migrations.
69+
/// </summary>
70+
/// <param name="scope">The stable migration scope identifier.</param>
71+
/// <param name="migrationId">The required migration identifier.</param>
72+
/// <exception cref="ArgumentException"><paramref name="scope"/> or <paramref name="migrationId"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
73+
/// <exception cref="DbMigrationException">The migration scope exists, but the required migration has not been applied.</exception>
74+
public void RequireAppliedMigrationIfScopeExists(string scope, string migrationId)
75+
{
76+
if (MigrationScopeExists(scope))
77+
{
78+
RequireAppliedMigration(scope, migrationId);
79+
}
80+
}
81+
}

‎src/System/Data/SQLite/SQLiteMigrationHistory.cs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ migration_id TEXT NOT NULL CHECK(migration_id <> ''),
4141
@version,
4242
@applied_at
4343
);
44+
""";
45+
46+
private const string MigrationScopeExistsCommand = $"""
47+
SELECT EXISTS (
48+
SELECT 1
49+
FROM {MigrationHistoryTableName}
50+
WHERE scope = @scope
51+
);
52+
""";
53+
54+
private const string IsMigrationAppliedCommand = $"""
55+
SELECT EXISTS (
56+
SELECT 1
57+
FROM {MigrationHistoryTableName}
58+
WHERE scope = @scope AND migration_id = @migration_id
59+
);
4460
""";
4561

4662
private readonly string _scope;
@@ -96,4 +112,30 @@ public void Add(SQLiteDbMigrationContext context, AppliedDbMigration migration)
96112
DbType.String.CreateInputParam("@version", migration.Version),
97113
DbType.Int64.CreateInputParam("@applied_at", migration.AppliedAt.ToUnixTimeMilliseconds()));
98114
}
115+
116+
internal static bool MigrationScopeExists(SQLiteDbMigrationContext context, string scope)
117+
{
118+
ArgumentNullException.ThrowIfNull(context);
119+
ArgumentException.ThrowIfNullOrWhiteSpace(scope);
120+
121+
var result = context.Connection.ExecuteScalar(
122+
MigrationScopeExistsCommand,
123+
context.CancellationToken,
124+
DbType.String.CreateInputParam("@scope", scope));
125+
return Convert.ToInt64(result) != 0;
126+
}
127+
128+
internal static bool IsMigrationApplied(SQLiteDbMigrationContext context, string scope, string migrationId)
129+
{
130+
ArgumentNullException.ThrowIfNull(context);
131+
ArgumentException.ThrowIfNullOrWhiteSpace(scope);
132+
ArgumentException.ThrowIfNullOrWhiteSpace(migrationId);
133+
134+
var result = context.Connection.ExecuteScalar(
135+
IsMigrationAppliedCommand,
136+
context.CancellationToken,
137+
DbType.String.CreateInputParam("@scope", scope),
138+
DbType.String.CreateInputParam("@migration_id", migrationId));
139+
return Convert.ToInt64(result) != 0;
140+
}
99141
}

‎tests/Directory.Build.props‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project>
2+
3+
<!-- Sign assembly -->
4+
<PropertyGroup Condition="!Exists('$(MSBuildThisFileDirectory)\..\..\MyKey.snk')">
5+
<SignAssembly>true</SignAssembly>
6+
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\..\MyPublicKey.snk</AssemblyOriginatorKeyFile>
7+
<PublicSign>true</PublicSign>
8+
</PropertyGroup>
9+
<PropertyGroup Condition="Exists('$(MSBuildThisFileDirectory)\..\..\MyKey.snk')">
10+
<SignAssembly>true</SignAssembly>
11+
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\..\..\MyKey.snk</AssemblyOriginatorKeyFile>
12+
<PublicSign>false</PublicSign>
13+
</PropertyGroup>
14+
15+
<!-- Imports -->
16+
<Import Project="$(MSBuildThisFileDirectory)\..\Directory.Build.props" />
17+
18+
</Project>

0 commit comments

Comments
 (0)