Skip to content

Commit 97280c2

Browse files
committed
Add scoped migration histories
- support independent migration scopes - preserve global migration application order - store scope-local migration sequences - validate migration histories independently for each scope - support multiple scopes within the same transaction - expand migration tests and documentation
1 parent 0126d77 commit 97280c2

4 files changed

Lines changed: 500 additions & 44 deletions

File tree

‎README.md‎

Lines changed: 43 additions & 9 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, records migration history with optional version metadata, and validates that the applied history matches the configured migration sequence.
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.
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 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 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,24 +83,58 @@ internal sealed class CreateMoviesSchema_0001 : SQLiteDbMigration
8383
}
8484
}
8585

86+
internal sealed class CreateVideoSchema_0001 : SQLiteDbMigration
87+
{
88+
public override string Id => "0001_CreateVideoSchema";
89+
90+
public override void Up(SQLiteDbMigrationContext context)
91+
{
92+
context.ExecuteNonQuery(
93+
"""
94+
CREATE TABLE Videos (
95+
Id INTEGER PRIMARY KEY,
96+
MovieId INTEGER NOT NULL,
97+
FileName TEXT NOT NULL
98+
);
99+
""");
100+
}
101+
}
102+
86103
SQLiteDbMigration[] migrations =
87104
[
88105
new CreateMoviesSchema_0001()
89106
];
90107

91108
migrations.Migrate(CreateConnection, "1.0.0", cancellationToken);
109+
110+
SQLiteDbMigration[] videoModuleMigrations =
111+
[
112+
new CreateVideoSchema_0001()
113+
];
114+
115+
videoModuleMigrations.MigrateScope(
116+
CreateConnection,
117+
"module.video",
118+
"1.0.0",
119+
cancellationToken);
92120
```
93121

94-
Migrations are applied in the order in which they are registered. Applied migrations are recorded in the `__nuext_migrations` table together with their application time and optional version information. 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 can also run within an existing `SQLiteDbContext`, allowing migration and application operations to share the same caller-owned transaction.
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.
123+
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.
125+
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.
127+
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.
95129

96130
## Commonly Used Types
97131

98-
- **`System.Data.SQLite.SQLiteDalBase`**: Base class for SQLite DAL implementations with synchronized provider-managed context execution and access to connection creation for custom scenarios.
99-
- **`System.Data.SQLite.SQLiteDbConnection`**: Wraps SQLite connections with process-local synchronization, transaction coordination, command execution, schema inspection, and common data access utilities.
100-
- **`System.Data.SQLite.SQLiteDbContext`**: Manages connection state and transaction lifetime, with optional validation that the connection lock remains held for the lifetime of the context.
101-
- **`System.Data.SQLite.SQLiteDbMigration`**: Base class for defining ordered SQLite schema and data migrations.
102-
- **`System.Data.SQLite.SQLiteDbMigrationContext`**: Provides the execution context used by SQLite migrations.
103-
- **`System.Data.SQLite.SQLiteDbTransaction`**: Wraps SQLite transactions with explicit state tracking, commit and rollback operations, and transaction lifetime diagnostics.
132+
- **`SQLiteDalBase`**: Base class for SQLite DAL implementations with synchronized provider-managed context execution and access to connection creation for custom scenarios.
133+
- **`SQLiteDbConnection`**: Wraps SQLite connections with process-local synchronization, transaction coordination, command execution, schema inspection, and common data access utilities.
134+
- **`SQLiteDbTransaction`**: Wraps SQLite transactions with explicit state tracking, commit and rollback operations, and transaction lifetime diagnostics.
135+
- **`SQLiteDbContext`**: Manages connection state and transaction lifetime, with optional validation that the connection lock remains held for the lifetime of the context.
136+
- **`SQLiteDbMigration`**: Base class for defining forward-only SQLite schema and data migrations within ordered, independently validated scopes.
137+
- **`SQLiteDbMigrationContext`**: Provides the execution context used by SQLite migrations.
104138

105139
## Installation
106140

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

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,69 @@
66
public static class SQLiteDbMigrationExtensions
77
{
88
/// <summary>
9-
/// Applies all pending migrations to a SQLite database.
9+
/// Applies all pending migrations to the default migration scope of a SQLite database.
1010
/// </summary>
1111
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
1212
/// <param name="createConnection">A function that creates a SQLite database connection.</param>
1313
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
1414
/// <exception cref="ArgumentNullException"><paramref name="migrations"/> or <paramref name="createConnection"/> is <see langword="null"/>.</exception>
1515
public static void Migrate(this IEnumerable<SQLiteDbMigration> migrations, Func<SQLiteDbConnection> createConnection, CancellationToken cancellationToken = default)
1616
{
17-
Migrate(migrations, createConnection, null, cancellationToken);
17+
MigrateScope(migrations, createConnection, SQLiteMigrationHistory.DefaultScope, null, cancellationToken);
1818
}
1919

2020
/// <summary>
21-
/// Applies all pending migrations to a SQLite database.
21+
/// Applies all pending migrations to the default migration scope of a SQLite database.
2222
/// </summary>
2323
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
2424
/// <param name="createConnection">A function that creates a SQLite database connection.</param>
2525
/// <param name="version">Optional version information to record for each applied migration.</param>
2626
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
2727
/// <exception cref="ArgumentNullException"><paramref name="migrations"/> or <paramref name="createConnection"/> is <see langword="null"/>.</exception>
2828
public static void Migrate(this IEnumerable<SQLiteDbMigration> migrations, Func<SQLiteDbConnection> createConnection, string? version, CancellationToken cancellationToken = default)
29+
{
30+
MigrateScope(migrations, createConnection, SQLiteMigrationHistory.DefaultScope, version, cancellationToken);
31+
}
32+
33+
/// <summary>
34+
/// Applies all pending migrations to the specified migration scope of a SQLite database.
35+
/// </summary>
36+
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
37+
/// <param name="createConnection">A function that creates a SQLite database connection.</param>
38+
/// <param name="scope">The stable migration scope identifier.</param>
39+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
40+
/// <exception cref="ArgumentNullException"><paramref name="migrations"/> or <paramref name="createConnection"/> is <see langword="null"/>.</exception>
41+
/// <exception cref="ArgumentException"><paramref name="scope"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
42+
public static void MigrateScope(this IEnumerable<SQLiteDbMigration> migrations, Func<SQLiteDbConnection> createConnection, string scope, CancellationToken cancellationToken = default)
43+
{
44+
MigrateScope(migrations, createConnection, scope, null, cancellationToken);
45+
}
46+
47+
/// <summary>
48+
/// Applies all pending migrations to the specified migration scope of a SQLite database.
49+
/// </summary>
50+
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
51+
/// <param name="createConnection">A function that creates a SQLite database connection.</param>
52+
/// <param name="scope">The stable migration scope identifier.</param>
53+
/// <param name="version">Optional version information to record for each applied migration.</param>
54+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
55+
/// <exception cref="ArgumentNullException"><paramref name="migrations"/> or <paramref name="createConnection"/> is <see langword="null"/>.</exception>
56+
/// <exception cref="ArgumentException"><paramref name="scope"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
57+
public static void MigrateScope(this IEnumerable<SQLiteDbMigration> migrations, Func<SQLiteDbConnection> createConnection, string scope, string? version, CancellationToken cancellationToken = default)
2958
{
3059
ArgumentNullException.ThrowIfNull(migrations);
3160
ArgumentNullException.ThrowIfNull(createConnection);
61+
ArgumentException.ThrowIfNullOrWhiteSpace(scope);
3262
cancellationToken.ThrowIfCancellationRequested();
3363

3464
var executor = new SQLiteMigrationExecutor(createConnection);
35-
var history = new SQLiteMigrationHistory();
65+
var history = new SQLiteMigrationHistory(scope);
3666
var migrator = new DbMigrator<SQLiteDbMigrationContext>(executor, history, migrations, version);
3767
migrator.Migrate(cancellationToken);
3868
}
3969

4070
/// <summary>
41-
/// Applies all pending migrations using an existing SQLite database context.
71+
/// Applies all pending migrations to the default migration scope using an existing SQLite database context.
4272
/// </summary>
4373
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
4474
/// <param name="dbContext">The existing SQLite database context.</param>
@@ -49,11 +79,11 @@ public static void Migrate(this IEnumerable<SQLiteDbMigration> migrations, Func<
4979
/// </remarks>
5080
public static void Migrate(this IEnumerable<SQLiteDbMigration> migrations, SQLiteDbContext dbContext, CancellationToken cancellationToken = default)
5181
{
52-
Migrate(migrations, dbContext, null, cancellationToken);
82+
MigrateScope(migrations, dbContext, SQLiteMigrationHistory.DefaultScope, null, cancellationToken);
5383
}
5484

5585
/// <summary>
56-
/// Applies all pending migrations using an existing SQLite database context.
86+
/// Applies all pending migrations to the default migration scope using an existing SQLite database context.
5787
/// </summary>
5888
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
5989
/// <param name="dbContext">The existing SQLite database context.</param>
@@ -64,13 +94,49 @@ public static void Migrate(this IEnumerable<SQLiteDbMigration> migrations, SQLit
6494
/// The caller owns the database context and is responsible for committing or rolling back its transaction.
6595
/// </remarks>
6696
public static void Migrate(this IEnumerable<SQLiteDbMigration> migrations, SQLiteDbContext dbContext, string? version, CancellationToken cancellationToken = default)
97+
{
98+
MigrateScope(migrations, dbContext, SQLiteMigrationHistory.DefaultScope, version, cancellationToken);
99+
}
100+
101+
/// <summary>
102+
/// Applies all pending migrations to the specified migration scope using an existing SQLite database context.
103+
/// </summary>
104+
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
105+
/// <param name="dbContext">The existing SQLite database context.</param>
106+
/// <param name="scope">The stable migration scope identifier.</param>
107+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
108+
/// <exception cref="ArgumentNullException"><paramref name="migrations"/> or <paramref name="dbContext"/> is <see langword="null"/>.</exception>
109+
/// <exception cref="ArgumentException"><paramref name="scope"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
110+
/// <remarks>
111+
/// The caller owns the database context and is responsible for committing or rolling back its transaction.
112+
/// </remarks>
113+
public static void MigrateScope(this IEnumerable<SQLiteDbMigration> migrations, SQLiteDbContext dbContext, string scope, CancellationToken cancellationToken = default)
114+
{
115+
MigrateScope(migrations, dbContext, scope, null, cancellationToken);
116+
}
117+
118+
/// <summary>
119+
/// Applies all pending migrations to the specified migration scope using an existing SQLite database context.
120+
/// </summary>
121+
/// <param name="migrations">The migrations in the order in which they must be applied.</param>
122+
/// <param name="dbContext">The existing SQLite database context.</param>
123+
/// <param name="scope">The stable migration scope identifier.</param>
124+
/// <param name="version">Optional version information to record for each applied migration.</param>
125+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
126+
/// <exception cref="ArgumentNullException"><paramref name="migrations"/> or <paramref name="dbContext"/> is <see langword="null"/>.</exception>
127+
/// <exception cref="ArgumentException"><paramref name="scope"/> is <see langword="null"/>, empty, or consists only of white-space characters.</exception>
128+
/// <remarks>
129+
/// The caller owns the database context and is responsible for committing or rolling back its transaction.
130+
/// </remarks>
131+
public static void MigrateScope(this IEnumerable<SQLiteDbMigration> migrations, SQLiteDbContext dbContext, string scope, string? version, CancellationToken cancellationToken = default)
67132
{
68133
ArgumentNullException.ThrowIfNull(migrations);
69134
ArgumentNullException.ThrowIfNull(dbContext);
135+
ArgumentException.ThrowIfNullOrWhiteSpace(scope);
70136
cancellationToken.ThrowIfCancellationRequested();
71137

72138
var executor = new SQLiteDbContextMigrationExecutor(dbContext);
73-
var history = new SQLiteMigrationHistory();
139+
var history = new SQLiteMigrationHistory(scope);
74140
var migrator = new DbMigrator<SQLiteDbMigrationContext>(executor, history, migrations, version);
75141
migrator.Migrate(cancellationToken);
76142
}

0 commit comments

Comments
 (0)