feat(persistence): SQLite factory + versioned Migrator
This commit is contained in:
9
src/VolcanicCore.Persistence/Migrations/IMigration.cs
Normal file
9
src/VolcanicCore.Persistence/Migrations/IMigration.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence.Migrations;
|
||||
|
||||
public interface IMigration
|
||||
{
|
||||
int Version { get; } // starts at 1, strictly monotonic
|
||||
void Apply(SqliteConnection conn);
|
||||
}
|
||||
42
src/VolcanicCore.Persistence/Migrator.cs
Normal file
42
src/VolcanicCore.Persistence/Migrator.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using VolcanicCore.Persistence.Migrations;
|
||||
|
||||
namespace VolcanicCore.Persistence;
|
||||
|
||||
public sealed class Migrator
|
||||
{
|
||||
private readonly IReadOnlyList<IMigration> _migrations;
|
||||
|
||||
public Migrator(IEnumerable<IMigration> migrations)
|
||||
{
|
||||
_migrations = migrations.OrderBy(m => m.Version).ToArray();
|
||||
for (var i = 1; i < _migrations.Count; i++)
|
||||
if (_migrations[i].Version == _migrations[i - 1].Version)
|
||||
throw new InvalidOperationException($"Duplicate migration version {_migrations[i].Version}");
|
||||
}
|
||||
|
||||
public void Apply(SqliteConnection conn)
|
||||
{
|
||||
var current = ReadUserVersion(conn);
|
||||
foreach (var m in _migrations)
|
||||
{
|
||||
if (m.Version <= current) continue;
|
||||
using var tx = conn.BeginTransaction();
|
||||
m.Apply(conn);
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText = $"PRAGMA user_version = {m.Version};";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private static int ReadUserVersion(SqliteConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA user_version;";
|
||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
||||
}
|
||||
}
|
||||
30
src/VolcanicCore.Persistence/SqliteConnectionFactory.cs
Normal file
30
src/VolcanicCore.Persistence/SqliteConnectionFactory.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence;
|
||||
|
||||
public sealed class SqliteConnectionFactory
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public SqliteConnectionFactory(string filePath)
|
||||
{
|
||||
_connectionString = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = filePath,
|
||||
Cache = SqliteCacheMode.Shared,
|
||||
Pooling = true
|
||||
}.ToString();
|
||||
}
|
||||
|
||||
public SqliteConnection Open()
|
||||
{
|
||||
var conn = new SqliteConnection(_connectionString);
|
||||
conn.Open();
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
}
|
||||
52
tests/VolcanicCore.Tests/MigratorTests.cs
Normal file
52
tests/VolcanicCore.Tests/MigratorTests.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using VolcanicCore.Persistence;
|
||||
using VolcanicCore.Persistence.Migrations;
|
||||
|
||||
namespace VolcanicCore.Tests;
|
||||
|
||||
public class MigratorTests
|
||||
{
|
||||
private sealed class M1 : IMigration
|
||||
{
|
||||
public int Version => 1;
|
||||
public void Apply(SqliteConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "CREATE TABLE t (id INTEGER PRIMARY KEY);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class M2 : IMigration
|
||||
{
|
||||
public int Version => 2;
|
||||
public void Apply(SqliteConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "ALTER TABLE t ADD COLUMN name TEXT;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Migrator_applies_missing_migrations_in_order_and_sets_user_version()
|
||||
{
|
||||
using var conn = new SqliteConnection("Data Source=:memory:");
|
||||
conn.Open();
|
||||
var mig = new Migrator(new IMigration[] { new M2(), new M1() });
|
||||
|
||||
mig.Apply(conn);
|
||||
|
||||
Assert.Equal(2, ReadUserVersion(conn));
|
||||
// Re-applying is a no-op.
|
||||
mig.Apply(conn);
|
||||
Assert.Equal(2, ReadUserVersion(conn));
|
||||
}
|
||||
|
||||
private static int ReadUserVersion(SqliteConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA user_version;";
|
||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user