Skip to content

Commit b793bca

Browse files
committed
Add transactional SQLite migrations
- add SQLite-specific migration and execution context - add synchronized transactional migration executor - add SQLite migration history storage - record migration identifiers, timestamps, and optional version metadata - validate and apply pending migrations atomically - add migration integration and concurrency tests - migrate MoviesAppSample from converters to migrations - remove the legacy converter mechanism - update package description and README
1 parent 0972907 commit b793bca

21 files changed

Lines changed: 839 additions & 383 deletions

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: CI
22

33
on:
4+
workflow_dispatch: {}
45
push:
56
branches: [ main ]
67
pull_request:
@@ -33,8 +34,8 @@ jobs:
3334
- name: Restore
3435
run: dotnet restore --nologo
3536

36-
- name: Build (Release)
37-
run: dotnet build --configuration Release --nologo -warnaserror
37+
- name: Build
38+
run: dotnet build -c Release --no-restore --nologo -warnaserror
3839

39-
- name: Test (Release)
40-
run: dotnet test --configuration Release --nologo --no-build
40+
- name: Test
41+
run: dotnet test --configuration Release --no-build

‎Directory.Build.targets‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
<CsFiles Include="**\*.*.cs" />
44
</ItemGroup>
55
<ItemGroup>
6-
<Compile Update="@(CsFiles)">
7-
<DependentUpon>$([System.Text.RegularExpressions.Regex]::Replace('%(Filename)', '\..*$', '.cs'))</DependentUpon>
8-
</Compile>
96
<Compile Update="**\*Extensions.cs">
107
<DependentUpon>$([System.String]::Copy('%(Filename)').Replace('Extensions', '.cs'))</DependentUpon>
118
</Compile>
129
<Compile Update="**\*Factory.cs">
1310
<DependentUpon>$([System.String]::Copy('%(Filename)').Replace('Factory', '.cs'))</DependentUpon>
1411
</Compile>
12+
<Compile Update="@(CsFiles)">
13+
<DependentUpon>$([System.Text.RegularExpressions.Regex]::Replace('%(Filename)', '\..*$', '.cs'))</DependentUpon>
14+
</Compile>
1515
</ItemGroup>
1616
</Project>

‎README.md‎

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
- **Transaction Scope Management**: Provides explicit transaction state tracking, prevents nested transactions, and supports coordinated execution through reentrant connection locks.
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.
17-
- **Schema Versioning**: Supports controlled database schema updates through the existing converter-based mechanism.
17+
- **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.
1818

1919
### Important Considerations
2020

@@ -48,23 +48,53 @@ connection.ExecuteInTransaction(transaction =>
4848

4949
The callback is responsible for committing the transaction or explicitly rolling it back when the operation completes normally. If the callback throws while the transaction is still active, the transaction is rolled back automatically without replacing the original exception with a rollback error.
5050

51+
#### Migration Example
52+
53+
```csharp
54+
internal sealed class CreateMoviesSchema_0001 : SQLiteDbMigration
55+
{
56+
public override string Id => "0001_CreateMoviesSchema";
57+
58+
public override void Up(SQLiteDbMigrationContext context)
59+
{
60+
context.ExecuteNonQuery(
61+
"""
62+
CREATE TABLE Movies (
63+
Id INTEGER PRIMARY KEY,
64+
Title TEXT NOT NULL
65+
);
66+
""");
67+
}
68+
}
69+
70+
SQLiteDbMigration[] migrations =
71+
[
72+
new CreateMoviesSchema_0001()
73+
];
74+
75+
migrations.Migrate(CreateConnection, "1.0.0", cancellationToken);
76+
```
77+
78+
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. The complete migration batch is executed within a synchronized transaction and is rolled back if any migration fails.
79+
5180
### Commonly Used Types
5281

53-
- **`System.Data.SQLite.SQLiteDalBase`**: Base class for SQLite-specific Data Access Layer (DAL) operations.
5482
- **`System.Data.SQLite.SQLiteDbConnection`**: Wraps SQLite connections with synchronized process-local access, transaction coordination, and utilities for common database operations.
55-
- **`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.
56-
- **`System.Data.SQLite.SQLiteDbConverter`**: Base class for applying updates to the SQLite database schema.
5783
- **`System.Data.SQLite.SQLiteDbTransaction`**: Wraps SQLite transactions with explicit state tracking, commit and rollback operations, and transaction lifetime diagnostics.
84+
- **`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.
85+
- **`System.Data.SQLite.SQLiteDalBase`**: Base class for SQLite-specific Data Access Layer (DAL) operations.
86+
- **`System.Data.SQLite.SQLiteDbMigration`**: Base class for defining ordered SQLite schema and data migrations.
87+
- **`System.Data.SQLite.SQLiteDbMigrationContext`**: Provides the execution context used by SQLite migrations.
5888

5989
### Installation
6090

61-
You can install `NuExt.System.Data.SQLite` via [NuGet](https://www.nuget.org/):
91+
Via [NuGet](https://www.nuget.org/):
6292

6393
```sh
6494
dotnet add package NuExt.System.Data.SQLite
6595
```
6696

67-
Or through the Visual Studio package manager:
97+
Or via Visual Studio:
6898

6999
1. Go to `Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution...`.
70100
2. Search for `NuExt.System.Data.SQLite`.
@@ -74,10 +104,20 @@ Or through the Visual Studio package manager:
74104

75105
For examples of how to use these classes, see the [samples](samples). These samples provide practical guidance on implementing the library's features in real-world scenarios.
76106

77-
### Contributing
107+
## Ecosystem
108+
109+
- [NuExt.Minimal.Mvvm](https://github.com/nu-ext/NuExt.Minimal.Mvvm)
110+
- [NuExt.Minimal.Mvvm.SourceGenerator](https://github.com/nu-ext/NuExt.Minimal.Mvvm.SourceGenerator)
111+
- [NuExt.Minimal.Behaviors.Wpf](https://github.com/nu-ext/NuExt.Minimal.Behaviors.Wpf)
112+
- [NuExt.Minimal.Mvvm.Wpf](https://github.com/nu-ext/NuExt.Minimal.Mvvm.Wpf)
113+
- [NuExt.Minimal.Mvvm.MahApps.Metro](https://github.com/nu-ext/NuExt.Minimal.Mvvm.MahApps.Metro)
114+
- [NuExt.System](https://github.com/nu-ext/NuExt.System)
115+
- [NuExt.System.Data](https://github.com/nu-ext/NuExt.System.Data)
116+
117+
## Contributing
78118

79-
Contributions are welcome! Feel free to submit issues, fork the repository, and send pull requests. Your feedback and suggestions for improvement are highly appreciated.
119+
Issues and PRs are welcome. Keep changes minimal and performance-conscious.
80120

81-
### License
121+
## License
82122

83-
Licensed under the MIT License. See the LICENSE file for details.
123+
MIT. See LICENSE.

‎samples/MoviesAppSample/Converters/MoviesConverter10.cs‎

Lines changed: 0 additions & 41 deletions
This file was deleted.

‎samples/MoviesAppSample/Converters/MoviesConverter11.cs‎

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Data.SQLite;
2+
3+
namespace MoviesAppSample.Migrations;
4+
5+
internal sealed class CreateMoviesSchema_0001 : SQLiteDbMigration
6+
{
7+
public override string Id => "0001_CreateMoviesSchema";
8+
9+
public override void Up(SQLiteDbMigrationContext context)
10+
{
11+
context.ExecuteNonQuery(
12+
"""
13+
CREATE TABLE Movies (
14+
Id INTEGER PRIMARY KEY,
15+
Title TEXT NOT NULL,
16+
Description TEXT,
17+
DateReleased INTEGER NOT NULL
18+
);
19+
20+
CREATE TABLE Persons (
21+
Id INTEGER PRIMARY KEY,
22+
Name TEXT NOT NULL UNIQUE
23+
);
24+
""");
25+
}
26+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Data.SQLite;
2+
3+
namespace MoviesAppSample.Migrations;
4+
5+
internal sealed class CreateMovieRelationships_0002 : SQLiteDbMigration
6+
{
7+
public override string Id => "0002_CreateMovieRelationships";
8+
9+
public override void Up(SQLiteDbMigrationContext context)
10+
{
11+
context.ExecuteNonQuery(
12+
"""
13+
CREATE TABLE MovieCasts (
14+
MovieId INTEGER NOT NULL,
15+
PersonId INTEGER NOT NULL,
16+
FOREIGN KEY (MovieId) REFERENCES Movies(Id),
17+
FOREIGN KEY (PersonId) REFERENCES Persons(Id),
18+
PRIMARY KEY (MovieId, PersonId)
19+
);
20+
21+
CREATE TABLE MovieDirectors (
22+
MovieId INTEGER NOT NULL,
23+
PersonId INTEGER NOT NULL,
24+
FOREIGN KEY (MovieId) REFERENCES Movies(Id),
25+
FOREIGN KEY (PersonId) REFERENCES Persons(Id),
26+
PRIMARY KEY (MovieId, PersonId)
27+
);
28+
29+
CREATE TABLE MovieWriters (
30+
MovieId INTEGER NOT NULL,
31+
PersonId INTEGER NOT NULL,
32+
FOREIGN KEY (MovieId) REFERENCES Movies(Id),
33+
FOREIGN KEY (PersonId) REFERENCES Persons(Id),
34+
PRIMARY KEY (MovieId, PersonId)
35+
);
36+
""");
37+
}
38+
}

‎samples/MoviesAppSample/Services/MoviesService.cs‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
using MoviesAppSample.Converters;
1+
using MoviesAppSample.Migrations;
22
using MoviesAppSample.DataAccess;
33
using MoviesAppSample.Models;
44
using System.Data;
55
using System.Data.SQLite;
6+
using System.Reflection;
67

78
namespace MoviesAppSample.Services;
89

910
internal class MoviesService(string dataBasePath)
1011
{
11-
private static readonly SQLiteDbConverter[] s_moviesConverters = [new MoviesConverter10(), new MoviesConverter11()
12+
private static readonly SQLiteDbMigration[] s_migrations =
13+
[
14+
new CreateMoviesSchema_0001(),
15+
new CreateMovieRelationships_0002()
1216
];
1317

1418
#region Properties
@@ -42,7 +46,6 @@ public async ValueTask<List<Movie>> GetAllMoviesAsync(CancellationToken cancella
4246
using var dbContext = new SQLiteDbContext(connection);
4347
try
4448
{
45-
s_moviesConverters.Initialize(dbContext, cancellationToken);
4649
using var movieDal = new MovieDal(CreateConnection);
4750
using var personDal = new PersonDal(CreateConnection);
4851
var movieDtos = await movieDal.LoadMoviesAsync(dbContext, cancellationToken);
@@ -86,7 +89,8 @@ private string GetMoviesConnectionString()
8689

8790
public ValueTask InitializeAsync(CancellationToken cancellationToken)
8891
{
89-
s_moviesConverters.Initialize(CreateConnection, cancellationToken);
92+
var info = new AssemblyInfo();
93+
s_migrations.Migrate(CreateConnection, info.Version?.ToString(), cancellationToken);
9094
return default;
9195
}
9296

@@ -99,7 +103,6 @@ public async ValueTask<bool> SaveMovieAsync(Movie movie, CancellationToken cance
99103
using var dbContext = new SQLiteDbContext(connection);
100104
try
101105
{
102-
s_moviesConverters.Initialize(dbContext, cancellationToken);
103106
using var movieDal = new MovieDal(CreateConnection);
104107
using var personDal = new PersonDal(CreateConnection);
105108

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFrameworks>netstandard2.1;netstandard2.0;net10.0;net9.0;net8.0;net471;net462</TargetFrameworks>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
7-
<Description>High‑performance SQLite data access utilities for .NET, providing thread‑safe connection handling, reliable transaction management, and a consistent DAL foundation. Includes context and converter abstractions to simplify schema updates, coordinate concurrent operations, and ensure predictable behavior across SQLite’s locking model.</Description>
7+
<Description>High-performance SQLite data access utilities for .NET, providing synchronized process-local connection access, explicit transaction management, a consistent DAL foundation, and transactional database migrations with validated history tracking.</Description>
88
<RootNamespace />
99
<GenerateDocumentationFile>True</GenerateDocumentationFile>
1010
<NoWarn>$(NoWarn);1591;NETSDK1233</NoWarn>
@@ -23,4 +23,4 @@
2323
<ProjectReference Include="..\..\NuExt.System.Data\src\NuExt.System.Data.csproj" />
2424
</ItemGroup>
2525

26-
</Project>
26+
</Project>

‎src/System/Data/SQLite/SQLiteDbConnection.Asserts.cs‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.ComponentModel;
2-
using System.Diagnostics;
32

43
namespace System.Data.SQLite;
54

@@ -24,7 +23,7 @@ public void Dispose()
2423
if (connection is not null)
2524
{
2625
int count = Interlocked.Decrement(ref connection._assertsSuspendCount);
27-
Debug.Assert(count >= 0, $"{nameof(AssertsSuspender)} was disposed without a matching suspension.");
26+
System.Diagnostics.Debug.Assert(count >= 0, $"{nameof(AssertsSuspender)} was disposed without a matching suspension.");
2827
}
2928
}
3029
}
@@ -50,19 +49,6 @@ public void Dispose()
5049

5150
#region Methods
5251

53-
[Conditional("DEBUG")]
54-
internal void DisableAsserts()
55-
{
56-
Interlocked.Increment(ref _assertsSuspendCount);
57-
}
58-
59-
[Conditional("DEBUG")]
60-
internal void RestoreAsserts()
61-
{
62-
int count = Interlocked.Decrement(ref _assertsSuspendCount);
63-
Debug.Assert(count >= 0, $"{nameof(RestoreAsserts)} called without a matching {nameof(DisableAsserts)}.");
64-
}
65-
6652
/// <summary>
6753
/// Temporarily disables diagnostic assertions until the returned object is disposed.
6854
/// </summary>

0 commit comments

Comments
 (0)