Skip to content

Commit b11243e

Browse files
committed
Add cross-scope migration planning
- add migration dependencies between scopes - add transactional multi-scope migration plans - resolve pending migrations in dependency order - preserve configured ordering within each scope - detect missing dependencies, unknown migrations, and cycles - optimize planning with cached history and pooled execution state - add migration plan tests and documentation
1 parent 820a4f6 commit b11243e

8 files changed

Lines changed: 1132 additions & 36 deletions

‎README.md‎

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
- **Database Context Management**: Manages connection state, transaction lifetime, commit, rollback, and deterministic resource cleanup.
1616
- **ADO.NET Utilities**: Provides helpers for commands, parameters, readers, data adapters, schema inspection, and common SQLite operations.
1717
- **Allocation-Conscious APIs**: Provides span-based parameters, `ValueTask`-based coordination, and explicit-state callbacks that help avoid closure allocations.
18-
- **Database Migrations**: Applies ordered schema and data migrations transactionally, supports independent migration scopes, preserves global application order, records optional version metadata, and strictly validates each configured migration sequence.
18+
- **Database Migrations**: Applies schema and data migrations transactionally, supports independent scopes and cross-scope dependencies, preserves global application order, and strictly validates each migration sequence.
1919

2020
## Why NuExt.System.Data.SQLite?
2121

2222
`NuExt.System.Data.SQLite` is designed as a performance-oriented infrastructure layer over ADO.NET rather than an ORM. It keeps SQL, commands, parameters, readers, and transactions explicit while removing repetitive connection, transaction, and resource lifecycle code. It also preserves SQLite's synchronous execution model instead of wrapping database operations in artificial asynchronous work.
2323

24-
The library supports both standalone DAL operations and coordinated units of work, protects complete database operation scopes during concurrent and reentrant execution, and provides forward-only migrations with deterministic ordering, independent scopes, and strict history validation.
24+
The library supports both standalone DAL operations and coordinated units of work, protects complete database operation scopes during concurrent and reentrant execution, and provides forward-only migrations with deterministic dependency ordering, independent scopes, and strict history validation.
2525

2626
The result is a lightweight data access foundation that adds predictable resource ownership, transaction coordination, and schema evolution without introducing change tracking, SQL generation, or a custom query language.
2727

@@ -83,10 +83,28 @@ internal sealed class CreateMoviesSchema_0001 : SQLiteDbMigration
8383
}
8484
}
8585

86+
SQLiteDbMigration[] migrations =
87+
[
88+
new CreateMoviesSchema_0001()
89+
];
90+
91+
migrations.Migrate(CreateConnection, "1.0.0", cancellationToken);
92+
```
93+
94+
`Migrate` applies a single ordered migration sequence within the default scope. The complete migration batch runs within a synchronized transaction and is rolled back if any migration fails.
95+
96+
### Multiple Migration Scopes
97+
98+
```csharp
8699
internal sealed class CreateVideoSchema_0001 : SQLiteDbMigration
87100
{
88101
public override string Id => "0001_CreateVideoSchema";
89102

103+
public override IReadOnlyList<SQLiteDbMigrationDependency> Dependencies =>
104+
[
105+
new("core", "0001_CreateMoviesSchema")
106+
];
107+
90108
public override void Up(SQLiteDbMigrationContext context)
91109
{
92110
context.ExecuteNonQuery(
@@ -100,32 +118,34 @@ internal sealed class CreateVideoSchema_0001 : SQLiteDbMigration
100118
}
101119
}
102120

103-
SQLiteDbMigration[] migrations =
121+
SQLiteDbMigration[] coreMigrations =
104122
[
105123
new CreateMoviesSchema_0001()
106124
];
107125

108-
migrations.Migrate(CreateConnection, "1.0.0", cancellationToken);
109-
110126
SQLiteDbMigration[] videoModuleMigrations =
111127
[
112128
new CreateVideoSchema_0001()
113129
];
114130

115-
videoModuleMigrations.MigrateScope(
116-
CreateConnection,
117-
"module.video",
118-
"1.0.0",
119-
cancellationToken);
131+
var plan = new SQLiteDbMigrationPlan()
132+
.AddScope("core", coreMigrations, "1.0.0")
133+
.AddScope("module.video", videoModuleMigrations, "1.0.0");
134+
135+
plan.Migrate(CreateConnection, cancellationToken);
120136
```
121137

122-
Migrations are applied in the order in which they are registered. Each migration scope maintains an independent ordered sequence, and its applied history must be an exact prefix of the configured migration sequence.
138+
Use `MigrateScope` for an independent named migration sequence. Use `SQLiteDbMigrationPlan` only when migrations from multiple scopes depend on one another.
123139

124-
Applied migrations are recorded in the `__nuext_migrations` table with a global history identifier, scope-local sequence, application time, and optional version information. This preserves the complete migration chronology while allowing the application core and independently installed modules to maintain separate migration chains.
140+
Migrations are applied in the order in which they are registered within each scope. Each migration scope maintains an independent ordered sequence, and its applied history must be an exact prefix of the configured migration sequence.
141+
142+
`SQLiteDbMigrationPlan` coordinates multiple scopes and automatically orders pending migrations according to their declared dependencies while preserving the configured order within each scope. The plan detects missing dependencies, unknown migrations, and dependency cycles before incompatible schema changes can be applied.
125143

126-
`Migrate` uses the default migration scope, while `MigrateScope` applies migrations to an explicitly named scope. Scope identifiers must remain stable after deployment because changing a scope creates a separate migration history.
144+
Dependencies may reference required scopes or optional scopes. An optional-scope dependency is ignored only when the scope is neither registered in the plan nor present in migration history. This prevents migrations from ignoring schema changes left by a module that was previously installed but is not currently loaded.
145+
146+
Applied migrations are recorded in the `__nuext_migrations` table with a global history identifier, scope-local sequence, application time, and optional version information. This preserves the complete migration chronology while allowing the application core and independently installed modules to maintain separate migration chains.
127147

128-
When a connection factory is used, the complete migration batch is executed within a synchronized transaction and is rolled back if any migration fails. Migrations from multiple scopes can also run within an existing `SQLiteDbContext`, allowing core, module, and application operations to share the same caller-owned transaction.
148+
When a connection factory is used, the complete migration plan is executed within a synchronized transaction and is rolled back if any migration fails. A plan can also run within an existing `SQLiteDbContext`, allowing migrations and application operations to share the same caller-owned transaction.
129149

130150
## Commonly Used Types
131151

@@ -134,6 +154,8 @@ When a connection factory is used, the complete migration batch is executed with
134154
- **`SQLiteDbTransaction`**: Wraps SQLite transactions with explicit state tracking, commit and rollback operations, and transaction lifetime diagnostics.
135155
- **`SQLiteDbContext`**: Manages connection state and transaction lifetime, with optional validation that the connection lock remains held for the lifetime of the context.
136156
- **`SQLiteDbMigration`**: Base class for defining forward-only SQLite schema and data migrations within ordered, independently validated scopes.
157+
- **`SQLiteDbMigrationDependency`**: Describes a required migration from another scope, including optional-scope dependencies.
158+
- **`SQLiteDbMigrationPlan`**: Coordinates multiple migration scopes, resolves cross-scope dependencies, and applies pending migrations transactionally.
137159
- **`SQLiteDbMigrationContext`**: Provides the execution context used by SQLite migrations.
138160

139161
## Installation

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@
55
/// </summary>
66
public abstract class SQLiteDbMigration : DbMigration<SQLiteDbMigrationContext>
77
{
8+
/// <summary>
9+
/// Gets the migrations that must be applied before this migration.
10+
/// </summary>
11+
public virtual IReadOnlyList<SQLiteDbMigrationDependency> Dependencies => [];
812
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace System.Data.SQLite;
2+
3+
/// <summary>
4+
/// Represents a dependency on a migration from another migration scope.
5+
/// </summary>
6+
public sealed record SQLiteDbMigrationDependency
7+
{
8+
/// <summary>
9+
/// Initializes a new instance of the <see cref="SQLiteDbMigrationDependency"/> class.
10+
/// </summary>
11+
/// <param name="scope">The stable migration scope identifier.</param>
12+
/// <param name="migrationId">The required migration identifier.</param>
13+
/// <param name="optionalScope">
14+
/// <see langword="true"/> if the dependency is ignored when the scope is neither registered nor present in migration history;
15+
/// otherwise, <see langword="false"/>.
16+
/// </param>
17+
public SQLiteDbMigrationDependency(string scope, string migrationId, bool optionalScope = false)
18+
{
19+
ArgumentException.ThrowIfNullOrWhiteSpace(scope);
20+
ArgumentException.ThrowIfNullOrWhiteSpace(migrationId);
21+
22+
Scope = scope;
23+
MigrationId = migrationId;
24+
OptionalScope = optionalScope;
25+
}
26+
27+
/// <summary>
28+
/// Gets the stable migration scope identifier.
29+
/// </summary>
30+
public string Scope { get; }
31+
32+
/// <summary>
33+
/// Gets the required migration identifier.
34+
/// </summary>
35+
public string MigrationId { get; }
36+
37+
/// <summary>
38+
/// Gets a value indicating whether the dependency is ignored when the migration scope is absent.
39+
/// </summary>
40+
public bool OptionalScope { get; }
41+
}

0 commit comments

Comments
 (0)