Skip to content

Commit 92c5680

Browse files
feat(efcore): inverted schema-comparison validation harness (#379)
* feat(efcore): Weasel model → EF MigrationOperation translation layer Closes #365. First implementation phase of the EF Core migration generation epic (#371), building on the #364 spike results. - New MigrationOperationTranslation in Weasel.EntityFrameworkCore: walks the provider-neutral surface (ITable/ITableColumn/ITableIndex/ ForeignKeyBase/SequenceBase) and produces EF Core MigrationOperation instances — the reverse of MapToTable. Raw store type strings (ColumnType) everywhere so EF's CLR mapping is bypassed and DDL matches Weasel exactly; the CLR type is a best-effort inverse used only for the Column<T>() generic in emitted C# - CreateTable with nested columns / primary key / check constraints / foreign keys, one CreateIndex per index, EnsureSchema per non-default schema (deduplicated; default public/dbo emitted as null Schema like EF's own scaffolding), CreateSequence from SequenceBase - Provider specifics: identity → Npgsql:ValueGenerationStrategy or SqlServer:Identity annotations; computed columns → ComputedColumnSql + IsStored (always stored on PG); index includes/method annotations; CascadeAction → ReferentialAction with SQL Server Restrict ≡ NoAction mirroring mapDeleteBehavior - Raw-SQL fallback: non-table/non-sequence objects (functions, sprocs, table types) and anything matched by the ForceRawSql hook (e.g. partitioned tables) are wrapped in SqlOperation carrying the object's own WriteCreateStatement DDL; expression indexes throw with guidance to use the hook - ToDropMigrationOperations for Down() bodies: reverse-order DropTable / DropSequence / raw drops; schemas never dropped (may be shared with Marten/Wolverine) - Weasel.Core additions: ITable.Columns and ITableIndex.Columns expose the column collections on the neutral surface (implicitly satisfied by every provider's concrete types) 13 new DB-free unit tests; all provider suites green locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(efcore): C# migration file emitter + stub DbContext Closes #366. Second implementation phase of the EF Core migration generation epic (#371), rendering the #365 operation lists into compilable attribute-only migration files. - EfMigrationFileEmitter.EmitMigration renders Up/Down operation lists over the stable public MigrationBuilder surface — deliberately not EF's pubternal CSharpMigrationsGenerator (dotnet/efcore#23595). Generated migrations carry [DbContext]/[Migration] attributes and no BuildTargetModel body, per the #364 spike verification - Renders EnsureSchema, CreateTable (nested columns with raw store types, PK incl. composite, check constraints, FKs with referential actions), CreateIndex (unique/filter + annotations), CreateSequence, Sql (verbatim strings), DropTable, DropSequence; the Npgsql:ValueGenerationStrategy annotation is rendered as the real NpgsqlValueGenerationStrategy enum literal with the using added on demand; unknown operations/annotations throw rather than emitting wrong code - Column names map to anonymous-type members with @-escaping for reserved words and name:-argument fallback for non-identifier names - Migration ids are yyyyMMddHHmmss_Name UTC with a monotonicity guard: LastMigrationId bumps the timestamp until the new id sorts strictly after (EF orders by plain string sort) - EmitStubContext generates the no-entity host context: provider configured, history table relocated into the critter-stack schema, EF 9+ PendingModelChangesWarning suppressed, registration snippet in the XML docs, plus an IDesignTimeDbContextFactory reading WEASEL_EF_CONNECTION so dotnet ef update/script/bundle work without an application host Testing: the generated sample files (from a Weasel schema with sequence, identity, checks, FK, filtered index) are CHECKED IN and compiled as part of the test project — the "generated files compile" acceptance — with a drift-guard test proving they are byte-for-byte emitter output, and an end-to-end test applying them through the real EF runtime against PostgreSQL, round-tripping the schema against Weasel's own delta detection (no changes), and migrating back down to zero. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(efcore): incremental migrations — serialized snapshot + differ Closes #367. Third implementation phase of the EF Core migration generation epic (#371). - EfSchemaSnapshot: JSON-serialized design-time snapshot of the Weasel typed model (tables/columns/indexes/FKs/checks/PK, sequences, and raw-SQL objects captured as their CREATE/DROP DDL) — Weasel's analog of EF's ModelSnapshot, written beside the generated migrations and never compiled. The Sable lesson without the shadow database - The snapshot DTOs are now the canonical IR: the #365 ITable translation routes through SnapshotTable.From(...) into shared operation builders, so first-migration translation and the incremental differ can never drift apart - EfSnapshotDiffer.Diff(baseline, target): in-memory diff producing incremental Up/Down operations — Add/Alter/DropColumn (Alter carries the old definition), Create/DropIndex (recreate on change), Add/DropForeignKey, Add/DropCheckConstraint, Drop+AddPrimaryKey, Create/Drop/AlterSequence, EnsureSchema for new schemas (never dropped), and raw-object add/remove via Sql(). Down runs in reverse order of Up. Changed raw-SQL objects are refused with guidance — the snapshot diff cannot infer a safe transform for partitioned tables or function bodies - EfSnapshotDiffer.DiffAgainstDatabaseAsync: the live-database baseline mode — Weasel's own CreateMigrationAsync SQL (updates + rollbacks) wrapped in Sql() operations, covering everything the snapshot diff refuses (partition additive/rebuild, function changes) - Emitter renders the incremental operations: AddColumn/AlterColumn/ DropColumn, DropIndex, AddForeignKey/DropForeignKey standalone, Add/DropPrimaryKey, Add/DropCheckConstraint, AlterSequence Renames are deliberately not inferred (the model carries no rename intent); the seam arrives with the CLI phase where renames can be declared explicitly. Tests: snapshot JSON round-trip yields a zero diff; add-column / changed-index / new-table+FK / altered-column scenarios; changed raw object refusal; incremental ops render through the emitter with the id monotonicity guard; and an end-to-end acceptance test that applies the initial generated migration via EF, diffs a changed model against the snapshot, executes the incremental operations through the real Npgsql migrations SQL generator, has Weasel's own delta detection report None, then rolls back down and round-trips again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(efcore): db-ef-migration add | script | baseline CLI command Closes #368. Fourth implementation phase of the EF Core migration generation epic (#371). - New db-ef-migration JasperFx command in Weasel.EntityFrameworkCore (discovered via [assembly: JasperFxAssembly]; Weasel.Core stays EF-free), using the same WeaselInput / TryChooseSingleDatabase database-selection machinery as db-patch — IDatabase is the single source of schema objects, so Marten/Wolverine/Polecat tables all flow in through one door - `add <Name>`: first run scaffolds the stub context (history table relocated into the first non-default schema of the database's objects), the initial create-everything migration, and the JSON snapshot; later runs diff against the snapshot (or the live database with --against-database) and emit an incremental migration with the id monotonicity guard. --output/--namespace/--context/ --history-schema flags - `script`: documents the canonical EF toolchain path (dotnet ef migrations script --idempotent / bundle) verified by the #364 spike — idempotent scripting needs the compiled migrations, which only exist in the consuming project - `baseline`: adopts a pre-existing database by inserting __EFMigrationsHistory rows (create-if-missing relocated history table) for every generated migration file without executing them — the EF-sanctioned baselining technique, idempotent across runs - EfMigrationGenerator is the testable engine behind the command: provider detection from the Migrator type, structural partition detection as the default ForceRawSql routing (no provider references), connection resolution via IConnectionSource Tests: provider detection, partition detection, first-run scaffold → no-change no-op → model change → incremental add with ordered ids, and baselining against live PostgreSQL (rows recorded once, idempotent second pass, verified in the relocated history table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(efcore): inverted schema-comparison validation harness Closes #369. Fifth implementation phase of the EF Core migration generation epic (#371). - InvertedComparisonHarness: the reverse of SchemaComparisonHarness — schemas defined as Weasel objects, the generated migration chain (initial + snapshot-diffed incrementals) COMPILED WITH ROSLYN and applied through the real EF runtime via Migrate(), then validated by (a) catalog-level SchemaComparer parity against a Weasel-created schema using the existing neutral introspectors and (b) Weasel's own SchemaMigration.DetermineAsync reporting None against the EF-migrated database. PostgreSQL and SQL Server variants - Scenarios: baseline conventions (identity, defaults, varchar facets, unique+filtered index, FK cascade, check constraint), computed columns, raw-SQL fallback objects (list-partitioned table + plpgsql function via Sql() blocks + sequence), a two-migration incremental chain (add column + index + new table), coexistence of two generated migration sets with separate schemas/history tables in one database, and a SQL Server baseline - Two generator fixes surfaced by the harness: - generated migration files now emit `using System;` (they must be self-contained rather than relying on ImplicitUsings) - SQL Server unique indexes without an explicit predicate are emitted as raw CREATE UNIQUE INDEX DDL — EF's SqlServer generator auto-appends a WHERE col IS NOT NULL filter whenever the (empty) target model cannot prove the columns non-nullable, which would diverge from Weasel's index - CI: the new suites live in Weasel.EntityFrameworkCore.Tests, which ci-build-efcore.yml already runs against PostgreSQL + SQL Server on net9.0/net10.0 — no workflow change needed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): resolve SampleGenerated path from output dir, not CallerFilePath Deterministic CI builds rewrite [CallerFilePath] to the virtual /_/ source root, which does not exist on disk — the drift-guard tests failed on CI with an IO error. Walk up from AppContext.BaseDirectory to the repo root instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 8bb070a commit 92c5680

8 files changed

Lines changed: 605 additions & 2 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
2121
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
2222
<PackageVersion Include="MySqlConnector" Version="2.4.0" />
23+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
2324
<PackageVersion Include="Npgsql" Version="9.0.4" />
2425
<!-- Npgsql EF Core is framework-conditional - see below -->
2526
<PackageVersion Include="Npgsql.NetTopologySuite" Version="9.0.4" />
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
using System.Reflection;
2+
using JasperFx;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.EntityFrameworkCore;
6+
using Npgsql;
7+
using Weasel.Core;
8+
using Weasel.EntityFrameworkCore.Tests.SchemaComparison;
9+
using Weasel.Postgresql;
10+
11+
namespace Weasel.EntityFrameworkCore.Tests.MigrationOperations;
12+
13+
/// <summary>
14+
/// The inverse of <see cref="SchemaComparisonHarness" /> (#369): the schema
15+
/// is defined as WEASEL objects, the generated migration files are compiled
16+
/// with Roslyn and applied through the real EF runtime (Migrate()), and the
17+
/// result must satisfy both the catalog-level comparison against a
18+
/// Weasel-created schema and Weasel's own delta detection.
19+
/// </summary>
20+
public static class InvertedComparisonHarness
21+
{
22+
/// <summary>
23+
/// Run the inverted flow on PostgreSQL. <paramref name="models" /> is the
24+
/// migration chain: the first entry generates the initial migration, each
25+
/// later entry generates an incremental migration via the snapshot diff.
26+
/// Each invocation must return a FRESH object graph.
27+
/// </summary>
28+
public static async Task<SchemaComparisonResult> RunPostgresqlAsync(
29+
string schemaName,
30+
params Func<ISchemaObject[]>[] models)
31+
{
32+
if (models.Length == 0)
33+
{
34+
throw new ArgumentException("At least one model is required", nameof(models));
35+
}
36+
37+
var migrator = new PostgresqlMigrator();
38+
var options = new MigrationOperationTranslationOptions(EfMigrationProvider.PostgreSql)
39+
{
40+
Migrator = migrator, ForceRawSql = CommandLine.EfMigrationGenerator.IsPartitioned
41+
};
42+
43+
var contextName = sanitize(schemaName) + "InvCtx";
44+
var ns = $"Weasel.Generated.{sanitize(schemaName)}";
45+
46+
// ---- generate the migration chain --------------------------------
47+
var sources = new List<string>
48+
{
49+
EfMigrationFileEmitter.EmitStubContext(EfMigrationProvider.PostgreSql,
50+
new EfMigrationEmissionOptions(contextName) { Namespace = ns }, schemaName)
51+
};
52+
53+
string? lastId = null;
54+
EfSchemaSnapshot? baseline = null;
55+
var timestamp = new DateTime(2026, 7, 18, 12, 0, 0, DateTimeKind.Utc);
56+
57+
for (var i = 0; i < models.Length; i++)
58+
{
59+
var objects = models[i]();
60+
var target = EfSchemaSnapshot.FromSchemaObjects(objects, options);
61+
62+
var emission = new EfMigrationEmissionOptions(contextName)
63+
{
64+
Namespace = ns, TimestampUtc = timestamp, LastMigrationId = lastId
65+
};
66+
67+
EfMigrationFile migration;
68+
if (baseline == null)
69+
{
70+
migration = EfMigrationFileEmitter.EmitMigration(
71+
$"Step{i}",
72+
objects.ToMigrationOperations(options),
73+
objects.ToDropMigrationOperations(options),
74+
emission);
75+
}
76+
else
77+
{
78+
var diff = EfSnapshotDiffer.Diff(baseline, target, options);
79+
migration = EfMigrationFileEmitter.EmitMigration(
80+
$"Step{i}", diff.UpOperations, diff.DownOperations, emission);
81+
}
82+
83+
sources.Add(migration.Code);
84+
lastId = migration.MigrationId;
85+
baseline = target;
86+
}
87+
88+
// ---- compile with Roslyn and apply through the EF runtime --------
89+
var connectionString = Postgresql.PostgresqlDbContext.ConnectionString;
90+
var assembly = Compile($"{ns}.Generated", sources);
91+
92+
await using var conn = new NpgsqlConnection(connectionString);
93+
await conn.OpenAsync();
94+
await executeAsync(conn, $"drop schema if exists \"{schemaName}\" cascade;");
95+
96+
await MigrateAsync(assembly, contextName, connectionString);
97+
98+
var finalObjects = models[^1]();
99+
var efSnapshot = await PostgresqlSchemaIntrospector.SnapshotAsync(conn, schemaName);
100+
101+
// ---- Weasel's own delta detection must find nothing to do --------
102+
var deltaAgainstEf = await SchemaMigration.DetermineAsync(conn, default, finalObjects);
103+
var deltaSql = string.Empty;
104+
if (deltaAgainstEf.Difference != SchemaPatchDifference.None)
105+
{
106+
var writer = new StringWriter();
107+
deltaAgainstEf.WriteAllUpdates(writer, migrator, AutoCreate.CreateOrUpdate);
108+
deltaSql = writer.ToString();
109+
}
110+
111+
// ---- Weasel creates the same schema; snapshots must match --------
112+
await executeAsync(conn, $"drop schema if exists \"{schemaName}\" cascade;");
113+
var creation = await SchemaMigration.DetermineAsync(conn, default, models[^1]());
114+
await migrator.ApplyAllAsync(conn, creation, AutoCreate.CreateOrUpdate);
115+
116+
var weaselSnapshot = await PostgresqlSchemaIntrospector.SnapshotAsync(conn, schemaName);
117+
var deltaAfterWeasel = await SchemaMigration.DetermineAsync(conn, default, models[^1]());
118+
119+
return new SchemaComparisonResult
120+
{
121+
// the EF-migrated catalog also contains the relocated history
122+
// table; exclude it from the comparison
123+
EfSchema = withoutHistoryTable(efSnapshot),
124+
WeaselSchema = weaselSnapshot,
125+
Differences = SchemaComparer.Compare(withoutHistoryTable(efSnapshot), weaselSnapshot),
126+
DeltaAgainstEfSchema = deltaAgainstEf.Difference,
127+
DeltaUpdateSql = deltaSql,
128+
DeltaAfterWeaselCreate = deltaAfterWeasel.Difference
129+
};
130+
}
131+
132+
/// <summary>
133+
/// The SQL Server variant of the inverted flow.
134+
/// </summary>
135+
public static async Task<SchemaComparisonResult> RunSqlServerAsync(
136+
string schemaName,
137+
params Func<ISchemaObject[]>[] models)
138+
{
139+
if (models.Length == 0)
140+
{
141+
throw new ArgumentException("At least one model is required", nameof(models));
142+
}
143+
144+
var migrator = new Weasel.SqlServer.SqlServerMigrator();
145+
var options = new MigrationOperationTranslationOptions(EfMigrationProvider.SqlServer)
146+
{
147+
Migrator = migrator, ForceRawSql = CommandLine.EfMigrationGenerator.IsPartitioned
148+
};
149+
150+
var contextName = sanitize(schemaName) + "InvCtx";
151+
var ns = $"Weasel.Generated.{sanitize(schemaName)}";
152+
153+
var sources = new List<string>
154+
{
155+
EfMigrationFileEmitter.EmitStubContext(EfMigrationProvider.SqlServer,
156+
new EfMigrationEmissionOptions(contextName) { Namespace = ns }, schemaName)
157+
};
158+
159+
string? lastId = null;
160+
EfSchemaSnapshot? baseline = null;
161+
var timestamp = new DateTime(2026, 7, 18, 12, 0, 0, DateTimeKind.Utc);
162+
163+
for (var i = 0; i < models.Length; i++)
164+
{
165+
var objects = models[i]();
166+
var target = EfSchemaSnapshot.FromSchemaObjects(objects, options);
167+
168+
var emission = new EfMigrationEmissionOptions(contextName)
169+
{
170+
Namespace = ns, TimestampUtc = timestamp, LastMigrationId = lastId
171+
};
172+
173+
var migration = baseline == null
174+
? EfMigrationFileEmitter.EmitMigration($"Step{i}",
175+
objects.ToMigrationOperations(options),
176+
objects.ToDropMigrationOperations(options), emission)
177+
: EfMigrationFileEmitter.EmitMigration($"Step{i}",
178+
EfSnapshotDiffer.Diff(baseline, target, options).UpOperations,
179+
EfSnapshotDiffer.Diff(baseline, target, options).DownOperations, emission);
180+
181+
sources.Add(migration.Code);
182+
lastId = migration.MigrationId;
183+
baseline = target;
184+
}
185+
186+
var connectionString = SqlServer.SqlServerDbContext.ConnectionString;
187+
await SqlServer.SqlServerDatabaseBootstrap.EnsureDatabaseExistsAsync(connectionString);
188+
var assembly = Compile($"{ns}.Generated", sources);
189+
190+
await using var conn = new Microsoft.Data.SqlClient.SqlConnection(connectionString);
191+
await conn.OpenAsync();
192+
await dropSqlServerSchemaAsync(conn, schemaName);
193+
194+
await MigrateAsync(assembly, contextName, connectionString);
195+
196+
var efSnapshot = await SqlServerSchemaIntrospector.SnapshotAsync(conn, schemaName);
197+
198+
var deltaAgainstEf = await SchemaMigration.DetermineAsync(conn, default, models[^1]());
199+
var deltaSql = string.Empty;
200+
if (deltaAgainstEf.Difference != SchemaPatchDifference.None)
201+
{
202+
var writer = new StringWriter();
203+
deltaAgainstEf.WriteAllUpdates(writer, migrator, AutoCreate.CreateOrUpdate);
204+
deltaSql = writer.ToString();
205+
}
206+
207+
await dropSqlServerSchemaAsync(conn, schemaName);
208+
var creation = await SchemaMigration.DetermineAsync(conn, default, models[^1]());
209+
await migrator.ApplyAllAsync(conn, creation, AutoCreate.CreateOrUpdate);
210+
211+
var weaselSnapshot = await SqlServerSchemaIntrospector.SnapshotAsync(conn, schemaName);
212+
var deltaAfterWeasel = await SchemaMigration.DetermineAsync(conn, default, models[^1]());
213+
214+
return new SchemaComparisonResult
215+
{
216+
EfSchema = withoutHistoryTable(efSnapshot),
217+
WeaselSchema = weaselSnapshot,
218+
Differences = SchemaComparer.Compare(withoutHistoryTable(efSnapshot), weaselSnapshot),
219+
DeltaAgainstEfSchema = deltaAgainstEf.Difference,
220+
DeltaUpdateSql = deltaSql,
221+
DeltaAfterWeaselCreate = deltaAfterWeasel.Difference
222+
};
223+
}
224+
225+
private static async Task dropSqlServerSchemaAsync(Microsoft.Data.SqlClient.SqlConnection conn, string schemaName)
226+
{
227+
await using var cmd = conn.CreateCommand();
228+
cmd.CommandText = $@"
229+
IF SCHEMA_ID('{schemaName}') IS NOT NULL
230+
BEGIN
231+
DECLARE @sql NVARCHAR(MAX) = N'';
232+
SELECT @sql += N'ALTER TABLE ' + QUOTENAME(s.name) + N'.' + QUOTENAME(t.name) + N' DROP CONSTRAINT ' + QUOTENAME(fk.name) + N';'
233+
FROM sys.foreign_keys fk
234+
JOIN sys.tables t ON fk.parent_object_id = t.object_id
235+
JOIN sys.schemas s ON t.schema_id = s.schema_id
236+
WHERE s.name = '{schemaName}';
237+
SELECT @sql += N'DROP TABLE ' + QUOTENAME(s.name) + N'.' + QUOTENAME(t.name) + N';'
238+
FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id
239+
WHERE s.name = '{schemaName}';
240+
SELECT @sql += N'DROP SEQUENCE ' + QUOTENAME(s.name) + N'.' + QUOTENAME(sq.name) + N';'
241+
FROM sys.sequences sq JOIN sys.schemas s ON sq.schema_id = s.schema_id
242+
WHERE s.name = '{schemaName}';
243+
EXEC sp_executesql @sql;
244+
EXEC('DROP SCHEMA [{schemaName}]');
245+
END";
246+
await cmd.ExecuteNonQueryAsync();
247+
}
248+
249+
// ------------------------------------------------------------------
250+
// compile + run
251+
// ------------------------------------------------------------------
252+
253+
public static Assembly Compile(string assemblyName, IEnumerable<string> sources)
254+
{
255+
// the assemblies the generated code needs, referenced explicitly so we
256+
// don't depend on what happens to be loaded in the test host yet
257+
var required = new[]
258+
{
259+
typeof(object).Assembly,
260+
typeof(Enumerable).Assembly,
261+
typeof(DbContext).Assembly,
262+
typeof(Microsoft.EntityFrameworkCore.Migrations.Migration).Assembly,
263+
typeof(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId).Assembly,
264+
typeof(NpgsqlConnection).Assembly,
265+
typeof(NpgsqlDbContextOptionsBuilderExtensions).Assembly,
266+
typeof(Microsoft.Data.SqlClient.SqlConnection).Assembly,
267+
typeof(SqlServerDbContextOptionsExtensions).Assembly,
268+
typeof(Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.NpgsqlValueGenerationStrategy).Assembly
269+
};
270+
271+
var locations = AppDomain.CurrentDomain.GetAssemblies()
272+
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
273+
.Select(a => a.Location)
274+
.Concat(required.Select(a => a.Location))
275+
.Distinct(StringComparer.OrdinalIgnoreCase);
276+
277+
var references = locations
278+
.Select(l => (MetadataReference)MetadataReference.CreateFromFile(l))
279+
.ToList();
280+
281+
var compilation = CSharpCompilation.Create(
282+
assemblyName,
283+
sources.Select(s => CSharpSyntaxTree.ParseText(s)),
284+
references,
285+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable));
286+
287+
using var stream = new MemoryStream();
288+
var result = compilation.Emit(stream);
289+
290+
if (!result.Success)
291+
{
292+
var errors = result.Diagnostics
293+
.Where(d => d.Severity == DiagnosticSeverity.Error)
294+
.Select(d => d.ToString());
295+
throw new InvalidOperationException(
296+
"Generated migration sources failed to compile:\n" + string.Join("\n", errors) +
297+
"\n\n---- sources ----\n" + string.Join("\n\n", sources));
298+
}
299+
300+
stream.Position = 0;
301+
return Assembly.Load(stream.ToArray());
302+
}
303+
304+
public static async Task MigrateAsync(Assembly assembly, string contextName, string connectionString)
305+
{
306+
var contextType = assembly.GetTypes()
307+
.Single(t => typeof(DbContext).IsAssignableFrom(t) && t.Name == contextName);
308+
309+
contextType.GetProperty("ConnectionString", BindingFlags.Public | BindingFlags.Static)!
310+
.SetValue(null, connectionString);
311+
312+
await using var context = (DbContext)Activator.CreateInstance(contextType)!;
313+
await context.Database.MigrateAsync();
314+
}
315+
316+
// ------------------------------------------------------------------
317+
// helpers
318+
// ------------------------------------------------------------------
319+
320+
private static SchemaSnapshot withoutHistoryTable(SchemaSnapshot snapshot)
321+
{
322+
var filtered = snapshot.Tables
323+
.Where(t => t.Name != CommandLine.EfMigrationGenerator.HistoryTableName)
324+
.ToList();
325+
return new SchemaSnapshot(snapshot.SchemaName, filtered, snapshot.Sequences);
326+
}
327+
328+
private static string sanitize(string name)
329+
=> new(name.Select(c => char.IsLetterOrDigit(c) ? c : '_').ToArray());
330+
331+
private static async Task executeAsync(NpgsqlConnection conn, string sql)
332+
{
333+
await using var cmd = conn.CreateCommand();
334+
cmd.CommandText = sql;
335+
await cmd.ExecuteNonQueryAsync();
336+
}
337+
}

src/Weasel.EntityFrameworkCore.Tests/MigrationOperations/SampleGenerated/20260718120000_WeaselSampleSchema.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.EntityFrameworkCore.Infrastructure;
22
using Microsoft.EntityFrameworkCore.Migrations;
33
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
4+
using System;
45

56
namespace Weasel.EntityFrameworkCore.Tests.MigrationOperations.SampleGenerated;
67

src/Weasel.EntityFrameworkCore.Tests/MigrationOperations/SampleGenerated/WeaselSampleDbContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using Microsoft.EntityFrameworkCore;
23
using Microsoft.EntityFrameworkCore.Design;
34
using Microsoft.EntityFrameworkCore.Diagnostics;

0 commit comments

Comments
 (0)