diff --git a/.github/scripts/render-homepage-tests.py b/.github/scripts/render-homepage-tests.py new file mode 100644 index 00000000..f3e4ea59 --- /dev/null +++ b/.github/scripts/render-homepage-tests.py @@ -0,0 +1,48 @@ +"""Render a dated CI snapshot from downloaded TRX artifacts into the Pages artifact.""" +import html +import json +import pathlib +import sys +import xml.etree.ElementTree as ET + +EXPECTED = {"Unit", "SQLite", "SQLServer", "PostgreSQL", "Oracle", "MySQL", + "MariaDB", "Firebird", "Db2", "Informix", "Sybase", "Hana"} + + +def render(results, run): + suites = {} + for path in pathlib.Path(results).rglob("*.trx"): + name = path.stem + if name not in EXPECTED or name in suites: + raise ValueError(f"Unexpected or duplicate suite: {name}") + root = ET.parse(path).getroot() + counter = root.find(".//{*}ResultSummary/{*}Counters") + if counter is None: + raise ValueError(f"Missing counters: {name}") + values = {k: int(counter.attrib[k]) for k in ("total", "executed", "passed", "failed")} + if not 0 <= values["passed"] + values["failed"] <= values["executed"] <= values["total"]: + raise ValueError(f"Invalid counters: {name}") + suites[name] = values + url = html.escape(run["html_url"], quote=True) + sha = html.escape(run["head_sha"][:7]) + date = html.escape(run["updated_at"]) + conclusion = html.escape(run["conclusion"] or "unknown") + source = f'

CI run · {sha} · {date} · workflow: {conclusion}. Latest completed master run at deployment time.

' + if set(suites) != EXPECTED: + return source + f'

Incomplete test results: {len(suites)} of {len(EXPECTED)} suites available. Full totals unavailable; inspect the run for failures or missing artifacts.

' + totals = {key: sum(s[key] for s in suites.values()) for key in ("total", "executed", "passed", "failed")} + totals["skipped"] = totals["total"] - totals["executed"] + totals["other"] = totals["executed"] - totals["passed"] - totals["failed"] + counts = '' + return counts + source + + +if __name__ == "__main__": + results, metadata, page = sys.argv[1:] + target = pathlib.Path(page) + content = target.read_text(encoding="utf-8") + start = "" + end = "" + before, remainder = content.split(start, 1) + _, after = remainder.split(end, 1) + target.write_text(before + start + "\n" + render(results, json.loads(pathlib.Path(metadata).read_text(encoding="utf-8"))) + "\n" + end + after, encoding="utf-8") diff --git a/.github/scripts/test-homepage-tests.py b/.github/scripts/test-homepage-tests.py new file mode 100644 index 00000000..b39a5acc --- /dev/null +++ b/.github/scripts/test-homepage-tests.py @@ -0,0 +1,61 @@ +"""Run with: python .github/scripts/test-homepage-tests.py""" +import importlib.util +import pathlib +import tempfile +import unittest + +spec = importlib.util.spec_from_file_location("renderer", pathlib.Path(__file__).with_name("render-homepage-tests.py")) +renderer = importlib.util.module_from_spec(spec) +spec.loader.exec_module(renderer) + + +class HomepageCountsTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = pathlib.Path(self.temp.name) + self.run = dict(html_url="https://github.com/dotnetprojects/Migrator.NET/actions/runs/1", + head_sha="abc123456", updated_at="2026-09-22T12:00:00Z", conclusion="failure") + + def populate(self, passed=3, failed=1): + for suite in renderer.EXPECTED: + (self.root / f"{suite}.trx").write_text( + '' + f'' + '') + + def test_counts_and_failure_provenance(self): + self.populate() + result = renderer.render(self.root, self.run) + for text in ('48executed', '36passed', '12failed', + '12skipped', '0other', 'abc1234', 'workflow: failure'): + self.assertIn(text, result) + + def test_success(self): + self.populate(passed=4, failed=0) + self.run['conclusion'] = 'success' + self.assertIn('48passed', renderer.render(self.root, self.run)) + + def test_missing_suite_does_not_show_partial_totals(self): + self.populate() + (self.root / 'Unit.trx').unlink() + result = renderer.render(self.root, self.run) + self.assertIn('Incomplete test results: 11 of 12', result) + self.assertNotIn('test-counts', result) + + def test_duplicate_suite_rejected(self): + self.populate() + duplicate = self.root / 'duplicate' + duplicate.mkdir() + (duplicate / 'Unit.trx').write_text((self.root / 'Unit.trx').read_text()) + with self.assertRaises(ValueError): + renderer.render(self.root, self.run) + + def test_invalid_counts_rejected(self): + self.populate(passed=8) + with self.assertRaises(ValueError): + renderer.render(self.root, self.run) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 7569a3b6..55a413a6 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -6,10 +6,17 @@ on: paths: - "docs/**" - ".github/workflows/pages.yml" + - ".github/scripts/render-homepage-tests.py" + - ".github/scripts/test-homepage-tests.py" + workflow_run: + workflows: [".NET Pull Request"] + types: [completed] + branches: [master] workflow_dispatch: permissions: contents: read + actions: read concurrency: group: github-pages @@ -20,6 +27,8 @@ jobs: if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest permissions: + contents: read + actions: read pages: write id-token: write environment: @@ -27,6 +36,36 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/checkout@v6 + with: + ref: master + - name: Verify test-count renderer + run: python3 .github/scripts/test-homepage-tests.py + - name: Select latest completed master test run + id: tests + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const { data } = await github.rest.actions.listWorkflowRuns({ + ...context.repo, workflow_id: 'dotnetpull.yml', branch: 'master', + event: 'push', status: 'completed', per_page: 1 + }); + const run = data.workflow_runs[0]; + if (run) { + fs.writeFileSync('ci-run.json', JSON.stringify(run)); + core.setOutput('run-id', String(run.id)); + } + - name: Download test results from that run + if: steps.tests.outputs.run-id != '' + uses: actions/download-artifact@v4 + with: + github-token: ${{ github.token }} + run-id: ${{ steps.tests.outputs.run-id }} + pattern: test-results-* + path: ci-results + - name: Render test counts and provenance + if: steps.tests.outputs.run-id != '' + run: python3 .github/scripts/render-homepage-tests.py ci-results ci-run.json docs/index.html - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v4 with: diff --git a/README.md b/README.md index 9e43a63b..34678016 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Source target: .NET 9](https://img.shields.io/badge/source_target-.NET_9-512BD4)](src/Migrator/DotNetProjects.Migrator.csproj) [![License: MPL-1.1](https://img.shields.io/badge/license-MPL--1.1-blue.svg)](https://www.mozilla.org/en-US/MPL/1.1/) -[Homepage & documentation](https://dotnetprojects.github.io/Migrator.NET/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) +[Homepage & documentation](https://dotnetprojects.github.io/Migrator.NET/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) · [CI test counts](https://dotnetprojects.github.io/Migrator.NET/#test-results) DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratordotnet/Migrator.NET). Write each schema change as a numbered C# class, commit it alongside your application, and use the runner to bring a database to the required version. The database records which migrations have already been applied. @@ -20,6 +20,7 @@ DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratord - [Quick start](#quick-start) - [Migration versions and rollback](#migration-versions-and-rollback) - [Multiple modules and migration scopes](#multiple-modules-and-migration-scopes) +- [Fluent API and deployment tooling](#fluent-api-and-deployment-tooling) - [Schema and data operations](#schema-and-data-operations) - [Database providers](#database-providers) - [Comparison with other .NET frameworks](#comparison-with-other-net-frameworks) @@ -30,7 +31,7 @@ DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratord ## Why use it? -- **Explicit C# migrations.** Define forward and reverse changes with `Up()` and `Down()`; review them like application code. +- **Imperative or fluent C# migrations.** Use `Migration.Up/Down` or v13’s `FluentMigration.BuildUp/BuildDown`; review both like application code. - **No ORM dependency.** Use it alongside EF, Dapper, another data layer, or plain ADO.NET. - **Database transformation API.** Work with tables, columns, keys, indexes and data, with raw SQL available for provider-specific operations. - **Version tracking.** Apply pending migrations or target a specific version using database-backed history. @@ -38,7 +39,7 @@ DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratord - **Bring your database driver.** The library does not directly reference database-driver packages; supply an ADO.NET connection or configure the driver factory. - **SQLite schema handling.** This fork includes schema inspection and table-recreation logic for operations SQLite cannot perform directly. -The source upgrade adds a structured fluent API, runner filtering/lifecycle options, SQL-preview subset, native locking, a CLI project and optional Microsoft DI/logging integration. These changes are under review and **are not a released NuGet feature claim**. See the [runner and fluent guide](docs/runner-guide.md) and [detailed framework comparison](docs/migration-framework-comparison.md). EF-style model scaffolding and migration-content checksums remain outside the implementation. +The source upgrade adds a structured fluent API, runner filtering/lifecycle options, SQL-preview subset, native locking, a CLI project and optional Microsoft DI/logging integration. These changes are merged in source and **are not a released NuGet feature claim**. See the [runner and fluent guide](docs/runner-guide.md) and [detailed framework comparison](docs/migration-framework-comparison.md). EF-style model scaffolding and migration-content checksums remain outside the implementation. ## Installation and requirements @@ -58,7 +59,7 @@ Building the `.slnx` solution requires an SDK that understands that format, such ## Quick start -This example targets **unreleased v13 source**. Clone/check out the upgrade branch before running these commands from the repository root. For published 12.1, follow its version-specific API; see the [migration guide](docs/migration-guide-12.1-to-13.md). +This example targets **unreleased v13 source**. Clone/check out this repository before running these commands from the repository root. For published 12.1, follow its version-specific API; see the [migration guide](docs/migration-guide-12.1-to-13.md). ### 1. Create a migration host @@ -152,7 +153,7 @@ Keep applied migration classes in source control. Change the schema with a new m With the runner above, `migrator.MigrateTo(0)` reverses all applied migrations in its set. In this example that drops `Users`, including its data. A `Down()` implementation is a reverse schema operation, not a backup restore. -Migration execution starts a transaction for each migration and attempts rollback on failure. Actual atomicity depends on the database, driver and operation; some databases implicitly commit DDL. `AfterUp()` and `AfterDown()` run **after commit**, so a failure in those hooks cannot undo the committed migration. +By default, migration execution starts a transaction for each migration and attempts rollback on failure. V13 also offers `None` and `WholeSession` transaction modes; whole-session support is limited to SQLite, PostgreSQL and SQL Server. Actual atomicity depends on the database, driver and operation; some databases implicitly commit DDL. `AfterUp()` and `AfterDown()` run **after commit**, so a failure in those hooks cannot undo the committed migration. For deployment, run a dedicated migration host before the application needs the new schema. Coordinate it so competing instances do not migrate the same database concurrently. Review and test both directions against your actual database engine. @@ -191,6 +192,32 @@ See [ProviderFactory](src/Migrator/ProviderFactory.cs), [MigrationLoader](src/Mi ## Fluent API and deployment tooling +For v13 source, replace the quick start’s `CreateUsers.cs` with this fluent equivalent; keep the same runner. Use one version-1 class, not both examples together. + +```csharp +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Fluent; + +[Migration(1)] +public class CreateUsers : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + { + migration.Create.Table("Users") + .WithColumn("Id").AsInt32().NotNullable() + .WithPrimaryKey("PK_Users", "Id") + .WithColumn("Name").AsString(255); + } + + public override void BuildDown(MigrationBuilder migration) + { + migration.Delete.Table("Users"); + } +} +``` + +`FluentMigration` collects operations in `BuildUp` and uses your explicit `BuildDown`. `AutoReversingMigration` derives reverse operations for supported create/rename changes; it cannot recover deleted data. + Run the [compiled fluent example](examples/FluentQuickStart/Program.cs): ```sh @@ -253,18 +280,18 @@ Reviewed **22 September 2026**. Migrator's column describes this repository; the | Capability | Migrator.NET (this fork) | FluentMigrator | EF Core | DbUp | Evolve | | ---------------------------- | --------------------------------- | -------------------------------------------- | ------------------------------------ | -------------------------- | --------------------------------- | -| Authoring | Handwritten C# transformation API | Handwritten C# fluent DSL | C# scaffolded from model differences | SQL or C# scripts | Versioned SQL files | +| Authoring | Imperative C# + structured fluent API | Handwritten C# fluent DSL | C# scaffolded from model differences | SQL or C# scripts | Versioned SQL files | | ORM-independent workflow | Yes | Yes | Uses EF model / DbContext | Yes | Yes | | Model-difference scaffolding | No built-in generator | Hand-authored | Yes, with model snapshots | Hand-authored | Hand-authored | -| Downgrade applied migrations | Authored `Down()` | `Down()`; supported auto-reverse expressions | Generated/editable `Down()` | Custom undo or forward fix | Forward fix; no Down command | +| Downgrade applied migrations | Authored `Down()` / `BuildDown()`; supported automatic reversal | `Down()`; supported auto-reverse expressions | Generated/editable `Down()` | Custom undo or forward fix | Forward fix; no Down command | | Separate histories | Scope + selected assembly/types | Custom version table + filtering | Contexts + custom history table | Journals + script filters | Metadata table/schema + locations | -| Execution | Library / custom host | Library + CLI | CLI, scripts, bundles, runtime | Library / custom host | Library, .NET tool, CLI | -| Recurring work | Custom code | Maintenance migrations / profiles | Seeding APIs (EF 9+) | `RunAlways` scripts | Checksum-based repeatable SQL | +| Execution | Library / source CLI (unreleased) | Library + CLI | CLI, scripts, bundles, runtime | Library / custom host | Library, .NET tool, CLI | +| Recurring work | Ordered maintenance / named profiles | Maintenance migrations / profiles | Seeding APIs (EF 9+) | `RunAlways` scripts | Checksum-based repeatable SQL | -All five can execute raw SQL. Transaction support depends on database capabilities: Migrator starts one per migration; DbUp makes transactions opt-in; the others have configurable transaction behavior. Reversing a completed migration is different from rolling back a failed transaction. Evolve's checksum-based repeatables also differ from always-run scripts or lifecycle hooks. +All five can execute raw SQL. Transaction support depends on database capabilities: Migrator defaults to per-migration transactions, with none or whole-session options (SQLite, PostgreSQL and SQL Server); DbUp makes transactions opt-in; the others have configurable transaction behavior. Reversing a completed migration is different from rolling back a failed transaction. Evolve's checksum-based repeatables also differ from always-run scripts or lifecycle hooks. -- Choose **Migrator** for direct C# schema operations, scoped history and integration with your own host. -- Consider **FluentMigrator** for its fluent authoring API, packaged runners, tags and profiles. +- Choose **Migrator** for imperative or fluent C# schema operations, scoped history, tags/profiles and a source CLI or your own host. +- **FluentMigrator** also offers fluent C# authoring, tags and profiles. Compare its published runner packages and provider behavior with Migrator’s v13 source tooling; fluent syntax alone is not a reason to switch. - Consider **EF Core migrations** when your EF model drives the schema and you want scaffolding and deployment artifacts. - Consider **DbUp** for a SQL-oriented runner composed in .NET, or **Evolve** for convention-based SQL with checksum validation and repeatables. @@ -321,7 +348,7 @@ The package declares **Mozilla Public License 1.1 (MPL-1.1)** in its [project me ### Version 13 source changes -The unreleased v13 stack separates columns from named table constraints and removes the old column flags and duplicate fluent builder. See the [12.1-to-13 migration guide](docs/migration-guide-12.1-to-13.md) before recompiling migrations. These source features are not claims about the published 12.1 NuGet package. +The v13 source preview separates columns from named table constraints and removes the old column flags and duplicate fluent builder. See the [12.1-to-13 migration guide](docs/migration-guide-12.1-to-13.md) before recompiling migrations. These source features are not claims about the published 12.1 NuGet package. ### SQL expressions and collations in v13 diff --git a/docs/README.md b/docs/README.md index 4c809518..f620b7b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,3 +27,17 @@ All site assets use relative URLs, so the repository subpath works without a cus The comparison distinguishes source capabilities from guarantees about released packages or database compatibility. Update the review date and source links together when reviewing it. Avoid equating transaction rollback with reversing completed migrations, treating a provider enum as a support guarantee, or assuming scopes isolate physical tables. The v13 runner filters explicitly scoped migrations and lets unscoped migrations inherit its effective scope. The quick start targets the unreleased v13 source's .NET 9 API and references the source project. Check the selected NuGet release's target frameworks. The SQLite driver version matches the repository test dependency. Validate authoring and runner snippets together when changing them. + +## Status badges and test counts + +The homepage links to the master CI badge, stable NuGet version and MPL-1.1 license. +Pages also redeploys when master CI completes. During deployment it selects the latest +completed master push run (including failures), downloads its TRX artifacts, and renders +executed/passed/failed/skipped/other counts with the run URL, commit and timestamp. +Counts are a deployment snapshot, not code coverage or a guarantee about every provider. +Missing suites produce an incomplete-results message, never a partial success total. +The committed/local page shows a fallback link until deployment supplies results. +The renderer only changes the uploaded Pages artifact; it does not commit generated counts. + +Keep the homepage, README summary and detailed comparison aligned. The fluent homepage +example replaces the imperative version-1 class and uses the same quick-start runner. diff --git a/docs/assets/site.css b/docs/assets/site.css index 1c2286ab..82be1fde 100644 --- a/docs/assets/site.css +++ b/docs/assets/site.css @@ -800,3 +800,9 @@ footer .brand { #version-13 .snippet { margin: 1rem 0; } #version-13 p + p { margin-top: 1rem; } + +.project-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 24px; } +.project-badges a { display: inline-flex; } +.project-badges img { height: 20px; max-width: 100%; } +.test-counts { display: flex; flex-wrap: wrap; gap: 12px 24px; padding: 0; list-style: none; } +.test-counts strong { display: block; font-size: 1.8rem; } diff --git a/docs/index.html b/docs/index.html index 516efd0e..b4ae8b20 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,7 +5,7 @@ Migrator.NET — Database changes, in your code. @@ -22,7 +22,7 @@ >Migrator.NET