chore(claude-do): merge test(data): assert the migrated schema matches the EF model
ClaudeDo-Task: 9b8d47d7-e42f-4ea5-8c86-fd10a420e7f6
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Tests;
|
||||||
|
|
||||||
|
// Sibling migrations branched off the same parent have silently dropped each other's columns
|
||||||
|
// during a SQLite table rebuild before (see docs/explore-notes). Every other Data.Tests fixture
|
||||||
|
// uses EnsureCreated, which skips the migration chain entirely, so nothing else in the suite
|
||||||
|
// would catch that. This compares the schema the migration chain actually produces against the
|
||||||
|
// schema EnsureCreated builds from today's model.
|
||||||
|
public sealed class MigrationSchemaDriftTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _migratedDbPath;
|
||||||
|
private readonly string _modelDbPath;
|
||||||
|
|
||||||
|
public MigrationSchemaDriftTests()
|
||||||
|
{
|
||||||
|
_migratedDbPath = Path.Combine(Path.GetTempPath(), $"claudedo_drift_migrated_{Guid.NewGuid():N}.db");
|
||||||
|
_modelDbPath = Path.Combine(Path.GetTempPath(), $"claudedo_drift_model_{Guid.NewGuid():N}.db");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
foreach (var path in new[] { _migratedDbPath, _modelDbPath })
|
||||||
|
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
||||||
|
try { File.Delete(path + suffix); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Migrated_schema_has_the_same_columns_as_the_current_model()
|
||||||
|
{
|
||||||
|
using (var migrated = new ClaudeDoDbContext(OptionsFor(_migratedDbPath)))
|
||||||
|
ClaudeDoDbContext.MigrateAndConfigure(migrated);
|
||||||
|
using (var model = new ClaudeDoDbContext(OptionsFor(_modelDbPath)))
|
||||||
|
model.Database.EnsureCreated();
|
||||||
|
|
||||||
|
var migratedColumns = ReadColumnsByTable(_migratedDbPath);
|
||||||
|
var modelColumns = ReadColumnsByTable(_modelDbPath);
|
||||||
|
|
||||||
|
var mismatches = new List<string>();
|
||||||
|
foreach (var (table, expectedColumns) in modelColumns)
|
||||||
|
{
|
||||||
|
if (!migratedColumns.TryGetValue(table, out var actualColumns))
|
||||||
|
{
|
||||||
|
mismatches.Add($"{table}: missing from migrated schema entirely");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingInMigrated = expectedColumns.Except(actualColumns).ToList();
|
||||||
|
var extraInMigrated = actualColumns.Except(expectedColumns).ToList();
|
||||||
|
if (missingInMigrated.Count > 0 || extraInMigrated.Count > 0)
|
||||||
|
{
|
||||||
|
mismatches.Add(
|
||||||
|
$"{table}: missing-in-migrated=[{string.Join(", ", missingInMigrated)}] " +
|
||||||
|
$"extra-in-migrated=[{string.Join(", ", extraInMigrated)}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(mismatches.Count == 0, "Schema drift between migrated DB and current model:\n" + string.Join("\n", mismatches));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DbContextOptions<ClaudeDoDbContext> OptionsFor(string dbPath) =>
|
||||||
|
new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||||
|
.UseSqlite($"Data Source={dbPath}")
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
private static Dictionary<string, HashSet<string>> ReadColumnsByTable(string dbPath)
|
||||||
|
{
|
||||||
|
using var conn = new SqliteConnection($"Data Source={dbPath}");
|
||||||
|
conn.Open();
|
||||||
|
|
||||||
|
var tables = new List<string>();
|
||||||
|
using (var cmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name != '__EFMigrationsHistory' AND name NOT LIKE 'sqlite_%'";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
tables.Add(reader.GetString(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Dictionary<string, HashSet<string>>();
|
||||||
|
foreach (var table in tables)
|
||||||
|
{
|
||||||
|
var columns = new HashSet<string>();
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = $"PRAGMA table_info(\"{table}\")";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
columns.Add(reader.GetString(reader.GetOrdinal("name")));
|
||||||
|
result[table] = columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user