From 559b34b84b25cf70add9675bce9fcc1f38b84fec Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 23 Sep 2026 09:01:06 +0200 Subject: [PATCH 1/2] Build detailed Classic and Fluent documentation and redesign homepage --- .gitattributes | 3 + .github/scripts/build-docs.py | 140 ++ .github/scripts/verify-docs.py | 161 ++ .github/workflows/docs.yml | 31 + .github/workflows/pages.yml | 6 + README.md | 108 +- docs/README.md | 39 +- docs/_src/comparison.html | 369 ++++ docs/_src/content.py | 713 ++++++++ docs/_src/home.html | 34 + docs/assets/favicon.svg | 2 +- docs/assets/search-index.json | 268 +++ docs/assets/site.css | 2320 +++++++++++++++++--------- docs/assets/site.js | 147 +- docs/guide/altering-tables.html | 22 + docs/guide/api-map.html | 8 + docs/guide/auto-reversing.html | 33 + docs/guide/cli.html | 30 + docs/guide/columns.html | 32 + docs/guide/conditional.html | 13 + docs/guide/configuration.html | 28 + docs/guide/connections.html | 33 + docs/guide/constraints.html | 18 + docs/guide/contributing.html | 8 + docs/guide/creating-tables.html | 58 + docs/guide/data.html | 38 + docs/guide/defaults-collations.html | 26 + docs/guide/dependency-injection.html | 40 + docs/guide/extensions.html | 35 + docs/guide/faq.html | 43 + docs/guide/foreign-keys.html | 22 + docs/guide/index.html | 8 + docs/guide/indexes.html | 22 + docs/guide/installation.html | 18 + docs/guide/maintenance.html | 36 + docs/guide/mysql.html | 16 + docs/guide/oracle.html | 18 + docs/guide/other-providers.html | 43 + docs/guide/postgresql.html | 16 + docs/guide/preview.html | 26 + docs/guide/profiles.html | 44 + docs/guide/providers.html | 18 + docs/guide/quick-start.html | 77 + docs/guide/runners.html | 32 + docs/guide/schema.html | 26 + docs/guide/sql-server.html | 12 + docs/guide/sql.html | 20 + docs/guide/sqlite.html | 18 + docs/guide/tags.html | 43 + docs/guide/testing.html | 20 + docs/guide/transactions.html | 22 + docs/guide/upgrading.html | 43 + docs/guide/versioning.html | 39 + docs/index.html | 565 +------ docs/migration-guide-12.1-to-13.md | 12 +- docs/runner-guide.md | 2 + 56 files changed, 4652 insertions(+), 1372 deletions(-) create mode 100644 .github/scripts/build-docs.py create mode 100644 .github/scripts/verify-docs.py create mode 100644 .github/workflows/docs.yml create mode 100644 docs/_src/comparison.html create mode 100644 docs/_src/content.py create mode 100644 docs/_src/home.html create mode 100644 docs/assets/search-index.json create mode 100644 docs/guide/altering-tables.html create mode 100644 docs/guide/api-map.html create mode 100644 docs/guide/auto-reversing.html create mode 100644 docs/guide/cli.html create mode 100644 docs/guide/columns.html create mode 100644 docs/guide/conditional.html create mode 100644 docs/guide/configuration.html create mode 100644 docs/guide/connections.html create mode 100644 docs/guide/constraints.html create mode 100644 docs/guide/contributing.html create mode 100644 docs/guide/creating-tables.html create mode 100644 docs/guide/data.html create mode 100644 docs/guide/defaults-collations.html create mode 100644 docs/guide/dependency-injection.html create mode 100644 docs/guide/extensions.html create mode 100644 docs/guide/faq.html create mode 100644 docs/guide/foreign-keys.html create mode 100644 docs/guide/index.html create mode 100644 docs/guide/indexes.html create mode 100644 docs/guide/installation.html create mode 100644 docs/guide/maintenance.html create mode 100644 docs/guide/mysql.html create mode 100644 docs/guide/oracle.html create mode 100644 docs/guide/other-providers.html create mode 100644 docs/guide/postgresql.html create mode 100644 docs/guide/preview.html create mode 100644 docs/guide/profiles.html create mode 100644 docs/guide/providers.html create mode 100644 docs/guide/quick-start.html create mode 100644 docs/guide/runners.html create mode 100644 docs/guide/schema.html create mode 100644 docs/guide/sql-server.html create mode 100644 docs/guide/sql.html create mode 100644 docs/guide/sqlite.html create mode 100644 docs/guide/tags.html create mode 100644 docs/guide/testing.html create mode 100644 docs/guide/transactions.html create mode 100644 docs/guide/upgrading.html create mode 100644 docs/guide/versioning.html diff --git a/.gitattributes b/.gitattributes index dfdb8b77..e77eddf1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ *.sh text eol=lf +docs/index.html linguist-generated=true +docs/guide/*.html linguist-generated=true +docs/assets/search-index.json linguist-generated=true diff --git a/.github/scripts/build-docs.py b/.github/scripts/build-docs.py new file mode 100644 index 00000000..58cd0aba --- /dev/null +++ b/.github/scripts/build-docs.py @@ -0,0 +1,140 @@ +"""Build the dependency-free documentation site. Generated HTML is committed for easy preview.""" +import argparse +import html +import hashlib +import importlib.util +import json +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parents[2] +DOCS = ROOT / "docs" +spec = importlib.util.spec_from_file_location("documentation_content", DOCS / "_src/content.py") +content = importlib.util.module_from_spec(spec) +spec.loader.exec_module(content) + + +def slug(value): + return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") + + +def highlight(code, kind): + if kind == "shell": + return html.escape(code) + tokens = re.compile(r'//[^\n]*|"(?:\\.|[^"\\])*"|\b(?:using|var|new|public|class|static|override|void|if|foreach|in|return|throw|false|true|null|typeof|params|string|object|int)\b|\b\d+\b') + result, end = [], 0 + for match in tokens.finditer(code): + result.append(html.escape(code[end:match.start()])) + value = match.group() + css = "comment" if value.startswith("//") else "string" if value.startswith('"') else "number" if value.isdigit() else "keyword" + result.append(f'{html.escape(value)}') + end = match.end() + result.append(html.escape(code[end:])) + return ''.join(result) + + +def example_html(example, instance): + eid = slug(instance + "-" + example["id"]) + label = "Inside Up() / BuildUp(MigrationBuilder migration)" if example["kind"] == "body" else "Shared commands · both styles" if example["kind"] == "shell" else "Shared host · both styles" if example["classic"] == example["fluent"] else "Choose one authoring style" + controls = ''.join(f'' for style in ("classic", "fluent")) + panels = [] + for style in ("classic", "fluent"): + panels.append(f'''
+

{style.title()}

+
{highlight(example[style], example['kind'])}
''') + return f'''
{html.escape(example['title'])}
{''.join(panels)}

{label}

''' + + +def header(prefix, guide=False): + return f''' +''' + + +def footer(prefix): + return f'''''' + + +def document(title, description, body, prefix="", body_class=""): + css_version = hashlib.sha256((DOCS / "assets/site.css").read_text(encoding="utf-8").encode()).hexdigest()[:12] + js_version = hashlib.sha256((DOCS / "assets/site.js").read_text(encoding="utf-8").encode()).hexdigest()[:12] + return f''' + +{html.escape(title)} · Migrator.NET{body} +''' + + +def navigation(current): + groups = list(dict.fromkeys(page['group'] for page in content.PAGES)) + result = ['' + + +def render_page(page, index): + sections = [] + for si, section in enumerate(page['sections']): + blocks = [example_html(block, f'{page["slug"]}-{si}-{bi}') if isinstance(block, dict) else block for bi, block in enumerate(section['blocks'])] + sections.append(f'

{html.escape(section["title"])}

{"".join(blocks)}
') + toc = '' + links = [] + for label, pos in (("Previous", index-1), ("Next", index+1)): + if 0 <= pos < len(content.PAGES): + target = content.PAGES[pos] + links.append(f'{label}{html.escape(target["title"])} {"→" if label == "Next" else ""}') + article = f'''

{html.escape(page['title'])}

{html.escape(page['summary'])}

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

{''.join(sections)}
''' + return document(page['title'], page['summary'], header('../', True) + '
' + navigation(page['slug']) + article + toc + '
' + footer('../'), '../', 'documentation') + + +def render_index(): + groups = list(dict.fromkeys(page['group'] for page in content.PAGES)) + items = [] + for i, group in enumerate(groups): + items.append(f'

{i+1:02}{html.escape(group)}

') + main = f'

DOTNETPROJECTS / THE MIGRATION MANUAL

Know what changes.
Know how it runs.

From your first table to deployment locks and SQLite reconstruction. Practical guides with a Classic and Fluent example for every authoring task.

Start with a working example ↗
{"".join(items)}
' + return document('The migration manual', 'Detailed C# database migration documentation with Classic and Fluent examples.', header('../', True) + main + footer('../'), '../', 'documentation') + + +def outputs(): + result = {DOCS / 'guide/index.html': render_index()} + search = [] + for i, page in enumerate(content.PAGES): + result[DOCS / 'guide' / (page['slug'] + '.html')] = render_page(page, i) + prose = ' '.join(block if isinstance(block, str) else block['title'] + ' ' + block['classic'] + ' ' + block['fluent'] for section in page['sections'] for block in section['blocks']) + search.append(dict(title=page['title'], group=page['group'], summary=page['summary'], url='guide/' + page['slug'] + '.html', text=html.unescape(re.sub('<[^>]+>', ' ', prose)))) + result[DOCS / 'assets/search-index.json'] = json.dumps(search, ensure_ascii=False, indent=2) + '\n' + home = (DOCS / '_src/home.html').read_text(encoding='utf-8') + for key, value in dict(header=header(''), footer=footer(''), hero=example_html(content.CREATE_USERS, 'hero'), install=example_html(content.INSTALL, 'install'), sqlite=example_html(content.SQLITE_ALTER, 'sqlite'), comparison=(DOCS / '_src/comparison.html').read_text(encoding='utf-8')).items(): + home = home.replace('{{' + key + '}}', value) + result[DOCS / 'index.html'] = document('Database changes, written in C#', 'Classic and Fluent C# migrations. Automatic SQLite schema reconstruction. A complete manual for your next database change.', home, body_class='homepage') + return result + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--check', action='store_true', help='Fail if generated files are stale.') + args = parser.parse_args() + stale = [] + for path, text in outputs().items(): + if args.check: + if not path.exists() or path.read_text(encoding='utf-8') != text: + stale.append(str(path.relative_to(ROOT))) + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding='utf-8', newline='\n') + if stale: + raise SystemExit('Regenerate docs: ' + ', '.join(stale)) + print(f'{"Checked" if args.check else "Built"} {len(content.PAGES)} chapters, documentation index, homepage and search index.') diff --git a/.github/scripts/verify-docs.py b/.github/scripts/verify-docs.py new file mode 100644 index 00000000..ec7447c2 --- /dev/null +++ b/.github/scripts/verify-docs.py @@ -0,0 +1,161 @@ +"""Check generated links/pairs; optionally compile every C# sample and run SQLite parity checks.""" +import argparse +from html.parser import HTMLParser +import importlib.util +import json +from pathlib import Path +import re +import subprocess +import tempfile +from urllib.parse import unquote, urlsplit +from xml.sax.saxutils import escape + +ROOT = Path(__file__).resolve().parents[2] +DOCS = ROOT / "docs" +spec = importlib.util.spec_from_file_location("documentation_content", DOCS / "_src/content.py") +content = importlib.util.module_from_spec(spec) +spec.loader.exec_module(content) + + +class Page(HTMLParser): + def __init__(self, path): + super().__init__() + self.path, self.ids, self.links, self.references, self.panels = path, set(), [], [], [] + self.feed(path.read_text(encoding="utf-8")) + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if "id" in attrs: + assert attrs["id"] not in self.ids, f"Duplicate ID: {self.path}: {attrs['id']}" + self.ids.add(attrs["id"]) + for name in ("href", "src"): + if name in attrs: + self.links.append(attrs[name]) + for name in ("aria-controls", "aria-labelledby", "aria-describedby", "data-copy"): + self.references.extend(attrs.get(name, "").split()) + if "data-code-style" in attrs: + assert "hidden" not in attrs, f"No-JS example hidden: {self.path}" + self.panels.append(attrs["data-code-style"]) + + +def check_site(): + pages = {path.resolve(): Page(path) for path in [DOCS / "index.html", *sorted((DOCS / "guide").glob("*.html"))]} + for path, page in pages.items(): + assert page.panels == [style for _ in range(len(page.panels) // 2) for style in ("classic", "fluent")], f"Unpaired samples: {path}" + for ref in page.references: + assert ref in page.ids, f"Broken control reference: {path}: {ref}" + for link in page.links: + parsed = urlsplit(link) + if parsed.scheme or parsed.netloc: + continue + target = (path.parent / unquote(parsed.path)).resolve() if parsed.path else path + assert target.exists(), f"Broken link: {path}: {link}" + if parsed.fragment and target in pages: + assert unquote(parsed.fragment) in pages[target].ids, f"Broken anchor: {path}: {link}" + for page in content.PAGES: + assert (ROOT / page["source"]).exists(), f"Missing implementation reference: {page['source']}" + entries = json.loads((DOCS / "assets/search-index.json").read_text(encoding="utf-8")) + assert len(entries) == len(content.PAGES) + for entry in entries: + assert (DOCS / entry["url"]).resolve() in pages + print(f"Checked {len(pages)} HTML pages: local links, anchors, control references, search entries and paired samples.") + + +USINGS = """global using System; +global using System.Data; +global using DotNetProjects.Migrator; +global using DotNetProjects.Migrator.Framework; +global using DotNetProjects.Migrator.Framework.Fluent; +global using DotNetProjects.Migrator.Providers; +global using DotNetProjects.Migrator.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Data.Sqlite; +""" +STUB = "public class CreateUsers : Migration { public override void Up() {} public override void Down() {} }" + + +def compile_samples(): + with tempfile.TemporaryDirectory(prefix="migrator-docs-") as directory: + folder = Path(directory) + project = 'Exenet9.0enabledisable' + for path in ("src/Migrator/DotNetProjects.Migrator.csproj", "src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj"): + project += f'' + project += '' + (folder / "Examples.csproj").write_text(project, encoding="utf-8") + (folder / "Usings.cs").write_text(USINGS, encoding="utf-8") + cases, count = [], 0 + for i, example in enumerate(content.EXAMPLES): + if example["kind"] == "shell": + continue + types = [] + for style in ("classic", "fluent"): + namespace = f"Example{i}_{style}" + code = example[style] + directives = re.findall(r"^using [\w.]+;\s*$", code, flags=re.M) + code = re.sub(r"^using [\w.]+;\s*$", "", code, flags=re.M).strip() + if example["kind"] == "body": + base = "Migration" if style == "classic" else "FluentMigration" + signature = "Up()" if style == "classic" else "BuildUp(MigrationBuilder migration)" + reverse = "public override void Down() {}" if style == "classic" else "public override void BuildDown(MigrationBuilder migration) {}" + code = f"public class Sample : {base} {{ public override void {signature} {{\n{code}\n}} {reverse} }}" + classname = "Sample" + elif example["kind"] in ("host", "program"): + parameters = "IDbConnection connection, ITransformationProvider provider, Migrator runner, IServiceCollection services" if example["kind"] == "host" else "" + code = f"public static class Host {{ public static void Run({parameters}) {{\n{code}\n}} }}\n{STUB}" + classname = "Host" + else: + classname = re.search(r"class (\w+)", code).group(1) + source = f"namespace {namespace} {{\n" + '\n'.join(directives) + f'\n#line 1 "{example["id"]}-{style}.cs"\n{code}\n}}' + (folder / f"{namespace}.cs").write_text(source, encoding="utf-8") + types.append(f"{namespace}.{classname}") + count += 1 + if example["smoke"]: + cases.append(f'Check("{example["id"]}", new {types[0]}(), new {types[1]}(), {str(example["kind"] == "class").lower()});') + program = r''' +__CASES__ +Console.WriteLine("SQLite Classic/Fluent schema parity and reverse checks passed."); + +static void Check(string label, IMigration classic, IMigration fluent, bool reverse) +{ + var left = Run(classic, reverse); + var right = Run(fluent, reverse); + if (left != right) throw new Exception(label + " schema mismatch:\n" + left + "\n" + right); +} +static string Run(IMigration migration, bool reverse) +{ + using var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); + migration.Database = provider; + migration.Up(); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name"; + var definitions = new System.Text.StringBuilder(); + using (var reader = command.ExecuteReader()) + while (reader.Read()) definitions.AppendLine(reader.GetString(0) + ":" + reader.GetString(1)); + if (definitions.Length == 0) throw new Exception("Creation example did not create a table"); + if (reverse) + { + migration.Down(); + command.CommandText = "SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"; + if (Convert.ToInt32(command.ExecuteScalar()) != 0) throw new Exception("Reverse left an example table behind"); + } + return definitions.ToString(); +} +'''.replace("__CASES__", "\n".join(cases)) + (folder / "Program.cs").write_text(program, encoding="utf-8") + result = subprocess.run(["dotnet", "run", "--project", str(folder / "Examples.csproj"), "--verbosity", "quiet"], cwd=ROOT, capture_output=True, text=True) + if result.returncode: + print(result.stdout) + print(result.stderr) + raise SystemExit(result.returncode) + print(f"Compiled {count} published C# samples and ran {len(cases)} Classic/Fluent SQLite schema parity checks (including authored/automatic reversal).") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--compile", action="store_true", help="Compile all C# examples and execute SQLite creation/reversal pairs.") + args = parser.parse_args() + check_site() + if args.compile: + compile_samples() diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..a74932a2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,31 @@ +name: Documentation + +on: + push: + branches: [master] + paths: ["docs/**", "README.md", "src/Migrator/**", "src/Migrator.Extensions.DependencyInjection/**", ".github/scripts/*docs.py", ".github/scripts/*homepage-tests.py", ".github/workflows/docs.yml"] + pull_request: + paths: ["docs/**", "README.md", "src/Migrator/**", "src/Migrator.Extensions.DependencyInjection/**", ".github/scripts/*docs.py", ".github/scripts/*homepage-tests.py", ".github/workflows/docs.yml"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + documentation: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Check generated pages + run: python -B .github/scripts/build-docs.py --check + - name: Validate links and compile all C# examples + run: python -B .github/scripts/verify-docs.py --compile + - name: Verify CI-count rendering + run: python -B .github/scripts/test-homepage-tests.py diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 55a413a6..7d3192af 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -8,6 +8,8 @@ on: - ".github/workflows/pages.yml" - ".github/scripts/render-homepage-tests.py" - ".github/scripts/test-homepage-tests.py" + - ".github/scripts/build-docs.py" + - ".github/scripts/verify-docs.py" workflow_run: workflows: [".NET Pull Request"] types: [completed] @@ -38,6 +40,10 @@ jobs: - uses: actions/checkout@v6 with: ref: master + - name: Verify generated documentation and links + run: | + python3 -B .github/scripts/build-docs.py --check + python3 -B .github/scripts/verify-docs.py - name: Verify test-count renderer run: python3 .github/scripts/test-homepage-tests.py - name: Select latest completed master test run diff --git a/README.md b/README.md index 5aefb5dd..321a55b2 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) · [CI test counts](https://dotnetprojects.github.io/Migrator.NET/#test-results) +[Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Documentation](https://dotnetprojects.github.io/Migrator.NET/guide/) · [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. @@ -55,26 +55,28 @@ Install the ADO.NET driver for your database separately. For the SQLite example dotnet add package Microsoft.Data.Sqlite --version 9.0.7 ``` -The **current source targets `net9.0`**. Check the [NuGet package's framework list](https://www.nuget.org/packages/DotNetProjects.Migrator/#supportedframeworks-body-tab) for the particular release you install; older package releases may target different frameworks. The SQLite driver version above matches the repository's test dependency. +The library targets **.NET 9**. The SQLite driver version above matches the repository's test dependency. See the [installation guide](https://dotnetprojects.github.io/Migrator.NET/guide/installation.html) for driver choices and optional packages. -Building the `.slnx` solution requires an SDK that understands that format, such as .NET SDK 9.0.200 or later. The runtime required by the current source is .NET 9. +Building the `.slnx` solution requires an SDK that understands that format, such as .NET SDK 9.0.200 or later, and the .NET 9 runtime. ## Quick start -This example uses the current repository API. Clone/check out this repository before running these commands from the repository root. When updating older migrations, see the [migration guide](docs/migration-guide-12.1-to-13.md). +Create a host with the .NET 9 SDK. Choose one of the two migration styles below; both use the same runner. The [interactive quick start](https://dotnetprojects.github.io/Migrator.NET/guide/quick-start.html) has Classic/Fluent tabs, and the [manual](https://dotnetprojects.github.io/Migrator.NET/guide/) covers each operation in both styles. When updating older migrations, see the [migration guide](docs/migration-guide-12.1-to-13.md). ### 1. Create a migration host ```sh dotnet new console -n MigrationDemo -f net9.0 cd MigrationDemo -dotnet add reference ../src/Migrator/DotNetProjects.Migrator.csproj +dotnet add package DotNetProjects.Migrator dotnet add package Microsoft.Data.Sqlite --version 9.0.7 ``` ### 2. Add `CreateUsers.cs` -Migrations must be public classes implementing the migration contract, decorated with `[Migration(version)]`. Each version must be unique within the set loaded by one runner. +Migrations must be public classes implementing the migration contract, decorated with `[Migration(version)]`. Each version must be unique within the set loaded by one runner. Copy either the Classic or Fluent class, not both. + +**Classic** ```csharp using System.Data; @@ -98,7 +100,29 @@ public class CreateUsers : Migration } ``` -### 3. Replace `Program.cs` +**Fluent — the equivalent `CreateUsers.cs`** + +```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() + .WithColumn("Name").AsString(255) + .WithPrimaryKey("PK_Users", "Id"); + } + + public override void BuildDown(MigrationBuilder migration) + => migration.Delete.Table("Users"); +} +``` + +### 3. Replace `Program.cs` (shared host for both styles) ```csharp using DotNetProjects.Migrator; @@ -136,7 +160,7 @@ The example supplies an **open** `IDbConnection`. The caller owns that connectio ## Migration versions and rollback -Use increasing numeric versions, or the attribute's date-based constructor: +Both Classic and Fluent classes use increasing numeric versions, or the attribute's date-based constructor: ```csharp [Migration(2026, 9, 22, 12, 0, 0)] @@ -163,7 +187,7 @@ For deployment, run a dedicated migration host before the application needs the The default history table is `SchemaInfo`, with version, scope and timestamp information. The default scope is `"default"`. You can use separate scopes for modules sharing a database. -Within a host with an open `connection`, select the module's migration types explicitly: +In the shared Classic/Fluent host with an open `connection`, select the module's migration types explicitly: ```csharp using var billingProvider = ProviderFactory.Create( @@ -196,30 +220,6 @@ See [ProviderFactory](src/Migrator/ProviderFactory.cs), [MigrationLoader](src/Mi ## Fluent API and deployment tooling -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): @@ -241,7 +241,9 @@ Inside a migration, `Database` implements [`ITransformationProvider`](src/Migrat | Schema inspection | `TableExists`, `ColumnExists`, `GetTables`, `GetColumns` | | Data and SQL | `Insert`, `Update`, `Delete`, `ExecuteNonQuery`, `ExecuteQuery`, `ExecuteScalar` | -For example, a new migration can add a column: +For example, a new migration can add a column. + +**Classic** ```csharp public override void Up() @@ -255,19 +257,41 @@ public override void Down() } ``` -Provider implementations determine which operations are available and how they map to SQL. Use `Database.ExecuteNonQuery(...)` for custom SQL and keep dialect-specific statements explicit. The source also includes the [MigrationBuilder fluent API](src/Migrator/Framework/Fluent/MigrationBuilder.cs). +**Fluent** + +```csharp +public override void BuildUp(MigrationBuilder migration) +{ + migration.Create.Column("Email", "Users").AsString(320); +} + +public override void BuildDown(MigrationBuilder migration) +{ + migration.Delete.Column("Email", "Users"); +} +``` + +Provider implementations determine which operations are available and how they map to SQL. Use `Database.ExecuteNonQuery(...)` or `migration.Execute.Sql(...)` for custom SQL and keep dialect-specific statements explicit. The [Classic/Fluent API map](https://dotnetprojects.github.io/Migrator.NET/guide/api-map.html) lists the corresponding operations. ### Explicit constraints, SQL defaults and collations Columns describe type, size, precision, nullability and identity. Define primary, unique, foreign-key and check constraints as named table objects; inspect them with `GetTableConstraints`. Changing a column preserves explicit constraints. `RawSql.Insert` marks a trusted SQL default expression, while `Collation` provides semantic presets and installed provider names. +**Classic — inside `Up()`** + ```csharp -new Column("Id", DbType.String, 27) { DefaultValue = RawSql.Insert("ksuid_new()") }; -builder.Create.Table("Events").WithColumn("Id").AsString(27) - .WithDefaultValue(RawSql.Insert("ksuid_new()")); +Database.AddTable("Events", new Column("Id", DbType.String, 27) + { DefaultValue = RawSql.Insert("ksuid_new()") }); +Database.AddTable("Names", new Column("Name", DbType.String, 100) + { Collation = Collation.AsciiIgnoreCase }); +``` + +**Fluent — inside `BuildUp(MigrationBuilder migration)`** -new Column("Name", DbType.String, 100) { Collation = Collation.AsciiIgnoreCase }; -builder.Create.Table("Names").WithColumn("Name").AsString(100) +```csharp +migration.Create.Table("Events").WithColumn("Id").AsString(27) + .WithDefaultValue(RawSql.Insert("ksuid_new()")); +migration.Create.Table("Names").WithColumn("Name").AsString(100) .WithCollation(Collation.AsciiIgnoreCase); ``` @@ -359,7 +383,9 @@ See [live database testing](docs/live-database-tests.md) for the CI matrix, pinn ## Documentation and GitHub Pages -The homepage in [`docs/`](docs/README.md) includes installation, a runnable quick start, provider information and a sourced feature comparison. It uses plain HTML, CSS and JavaScript with no build dependencies. +The [migration manual](https://dotnetprojects.github.io/Migrator.NET/guide/) includes 38 chapters with paired Classic/Fluent examples, chapter search, provider details and deployment guidance. Start with [tables](https://dotnetprojects.github.io/Migrator.NET/guide/creating-tables.html), [runner configuration](https://dotnetprojects.github.io/Migrator.NET/guide/configuration.html), or [SQLite](https://dotnetprojects.github.io/Migrator.NET/guide/sqlite.html). The homepage includes a sourced feature comparison. + +The site uses static HTML, CSS and JavaScript. A Python standard-library generator builds it from `docs/_src/`; generated pages are committed. See the [site maintenance guide](docs/README.md) for regeneration and sample validation. Preview locally from the repository root: diff --git a/docs/README.md b/docs/README.md index cac5786f..341d9d91 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,23 @@ -# Migrator.NET homepage +# Migrator.NET website and migration manual -Static GitHub Pages site, with no build tools, external fonts or client-side dependencies. `index.html` contains the homepage, quick start and sourced framework comparison. Styling and progressive enhancements live in `assets/`. +The static GitHub Pages site contains the homepage, sourced framework comparison and 38 documentation chapters. Every example has Classic/Fluent tabs; shared host and shell commands appear in both tabs. The choice persists across pages when local storage is available. Without JavaScript both examples remain visible and chapter navigation still works. No external fonts or client-side libraries are required. + +## Edit and build + +The generator uses only Python's standard library. Edit `_src/content.py` for chapters and paired code examples, `_src/home.html` for the homepage, and `_src/comparison.html` for its comparison table. Styling and progressive enhancements live in `assets/site.css` and `assets/site.js`. Commit generated `guide/*.html`, `index.html` and `assets/search-index.json` with their sources. + +From the repository root: + +```sh +python -B .github/scripts/build-docs.py +python -B .github/scripts/build-docs.py --check +python -B .github/scripts/verify-docs.py --compile +python -B .github/scripts/test-homepage-tests.py +``` + +`--compile` requires the .NET 9 SDK/runtime and restores sample dependencies. It compiles every C# example against the source projects and executes SQLite schema-parity/reversal checks for creation pairs. The verifier also checks local links, fragments, source references, unique IDs, control references and paired examples. Temporary projects are removed after validation. + +Use the `page`, `section` and `pair` helpers for content. Complete migration-class samples include imports. Body fragments belong inside `Up()` or `BuildUp(MigrationBuilder migration)`; host fragments use the context described in their chapter. Shell and shared host samples are rendered in both tabs. ## Preview @@ -10,36 +27,40 @@ From the repository root: python -m http.server 8766 --directory docs --bind 127.0.0.1 ``` -Open http://localhost:8766. Content and navigation work without JavaScript. Copy buttons require a secure context (HTTPS or localhost). +Open [localhost:8766](http://localhost:8766). Check desktop and narrow screens, keyboard tab selection (Left/Right, Home/End), preference persistence, copying, search, mobile chapter navigation and horizontal table scrolling. Copy buttons need HTTPS or localhost; search needs HTTP serving. Content and navigation work without JavaScript. ## Publish on GitHub Pages 1. In the repository's **Settings → Pages → Build and deployment**, set **Source** to **GitHub Actions**. 2. Merge the site and `.github/workflows/pages.yml` into `master`. -3. The workflow publishes only `docs/`. After enabling Pages, you can also run **Deploy homepage to GitHub Pages** manually on `master`. +3. The workflow validates generated pages and links, then publishes `docs/`. After enabling Pages, you can also run **Deploy homepage to GitHub Pages** manually on `master`. The [documentation workflow](../.github/workflows/docs.yml) checks generation, links, C# samples and the CI-count renderer on pull requests. Expected project URL: https://dotnetprojects.github.io/Migrator.NET/ -All site assets use relative URLs, so the repository subpath works without a custom domain. No deployment or repository setting changes are performed by a local preview. GitHub may require an environment approval if the repository has deployment protection rules. +All site assets and search-result links use relative URLs, so the repository subpath works without a custom domain. Asset fingerprints refresh cached CSS and JavaScript after changes. No deployment or repository setting changes are performed by a local preview. ## Keep the comparison accurate 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 runner filters explicitly scoped migrations and lets unscoped migrations inherit its effective scope. -The quick start targets the current repository’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. +The quick start targets .NET 9 and installs the core library and SQLite driver through NuGet. Sample validation uses local project references to catch API drift. The SQLite driver version matches the repository test dependency. ## Status badges and test counts -The homepage links to the master CI badge, stable NuGet version and MPL-1.1 license. +The homepage links to master CI results and NuGet, and identifies the 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. +The renderer only changes the uploaded Pages artifact; it does not commit generated counts. Preserve `TEST_RESULTS_START` and `TEST_RESULTS_END` in `_src/home.html`. 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. +example replaces the Classic version-1 class and uses the same quick-start runner. Present capabilities as ordinary features, without version-specific preview banners. Keep version numbers in the upgrade guide where they explain compatibility changes. Highlight automatic live-schema SQLite reconstruction in the homepage and README, distinguishing it from EF Core's model-based rebuilds and SQL runners' author-written scripts. Retain preservation limits and links to the operation matrix. + +## Design references + +The learning path follows [FluentMigrator's documentation](https://fluentmigrator.github.io/intro/quick-start.html), adapted to this API. Visual references are [Resend](https://resend.com/) for typography and code tabs and [Gel](https://www.geldata.com/) for code walkthroughs. The paper/rust palette, migration-history motif, layout and copy are specific to this project. diff --git a/docs/_src/comparison.html b/docs/_src/comparison.html new file mode 100644 index 00000000..79d5d6f0 --- /dev/null +++ b/docs/_src/comparison.html @@ -0,0 +1,369 @@ +
+
+
+
+

THE .NET MIGRATION LANDSCAPE

+

Choose by how you work.

+
+

+ Feature comparison · Reviewed 23 September 2026
Read the sources and qualifications ↓ +

+
+

+ Use fluent operations, version planning, a SQL-preview subset, runner options, + native locks and the CLI. + Read the runner guide and provider limits. +

+

+ Migrator fits applications that want explicit C# migrations and + scoped history without coupling schema changes to an ORM. Other + tools offer different authoring and deployment workflows. +

+

+ Read the detailed feature comparison (Markdown) →
+ Explore SQLite emulation, preservation limits and framework + differences → +

+

+ Scroll horizontally to compare all five frameworks on smaller + screens. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Built-in capabilities and documented workflows. “Custom” means + application code or configuration is needed. +
Capability + Migrator.NET DotNetProjects forkSource [1] + + FluentMigrator Sources [2] + + EF Core Sources [3] + + DbUp Sources [4] + + Evolve Sources [5] +
Authoring styleImperative C# + structured fluent APIHandwritten C#
Fluent DSL
C# generated from model changes; editableSQL scripts; C# scripts also supportedVersioned SQL files
ORM-independent workflowYesYesUses EF model and DbContextYesYes
+ Generate migrations from model differences + No built-in generatorHand-authoredYes — model snapshotsHand-authoredHand-authored
Raw SQLExecuteNonQuery / fluent Execute.Sql / scriptsExecute.Sql / scriptsmigrationBuilder.SqlPrimary workflowPrimary workflow
Downgrade an applied version + Authored Down() or supported automatic reversal + + Down(); auto-reverse for supported expressions + Down(); target an earlier migrationForward fixes; custom undo workflowForward fixes; no Down command
History / module separation + Scope-filtered discovery + history + Custom version tables + migration filtering + Separate contexts / migrations + custom history tables + Separate journals + script filteringMetadata table/schema + script locations
TransactionsPer migration by default; none or whole session (SQLite, PostgreSQL, SQL Server)Per migration by default; configurableMost migrations wrapped automaticallyOpt-in per script or whole run; none by defaultPer migration by default; whole-run option
Execution / deploymentLibrary + CLIIn-process runner + CLICLI, SQL scripts, bundles, runtime APILibrary; host in a console app or application.NET library, .NET tool, CLI
Database abstractionProvider dialects for schema operationsProvider-specific SQL generators + Relational providers; migrations may differ by provider + Database integrations; you write dialect-specific SQLDatabase integrations; you write dialect-specific SQL
Repeatable / recurring workOrdered maintenance + named profiles; no checksum repeatablesMaintenance migrations / profilesSeeding APIs (EF 9+); custom codeRunAlways scriptsRepeatable SQL reruns on checksum change
Automatic SQLite reconstructionLive-schema rebuilds; no ORM modelManual for general column and foreign-key alterationsRebuilds for model-represented artifactsAuthor scriptsAuthor scripts
Planning and SQL previewRead-only version plan; connected/offline SQL subsetPreview/outputGenerated SQL scriptsAuthored SQL / pending scriptsAuthored SQL
Deployment coordinationOpt-in native locks: SQL Server, PostgreSQL, MySQL/MariaDBApplication-lock pattern / deployment orchestrationMigration locking; execution-path dependentHost/provider concernCluster setting; provider-dependent
+
+
+

+ Rollback has two meanings. Reversing an already + applied migration uses authored reverse operations. Rolling back a + failed transaction depends on the database’s DDL support. Neither + restores data removed by a successful destructive migration. +

+

+ Recurring work is not the same as change detection. + Evolve stores script checksums and validates changes; Migrator + records versions and scopes without built-in content checksum + validation. Maintenance hooks, seeding and RunAlways have + different execution rules. +

+
+
+
+

Keep migrations in C#

+

+ Migrator: imperative and fluent schema operations, scoped + history, tags/profiles, and the CLI or your own host. + FluentMigrator: a fluent DSL with packaged + runners, tags and profiles. +

+
+
+

Let the model drive changes

+

+ EF Core: a natural fit when an EF model defines + your schema and you want migration scaffolding, SQL generation + and deployment bundles. +

+
+
+

Keep SQL as the source

+

+ DbUp: compose a script runner in .NET. + Evolve: convention-based versioned SQL, + checksum validation and repeatable scripts. +

+
+
+
+ Sources & comparison methodology +

+ Our column is based on the current repository source, which + targets net9.0. Other columns summarize official + documentation reviewed on 22 September 2026, with SQLite comparisons rechecked on 23 September, rather than claiming + parity across every released package. Check your chosen release, + provider and database version. Suitability notes are our + interpretation of these documented capabilities. +

+
    +
  1. + DotNetProjects.Migrator: + target framework, + runner, + execution and transactions, + history and schema operations, + migration discovery. +
  2. +
  3. + FluentMigrator: + quick start and runners, + configuration and version tables, + auto-reversing migrations, + maintenance migrations, + profiles, + SQLite generator, + authoring and providers. +
  4. +
  5. + EF Core: + model snapshots, + authoring and transactions, + scripts, bundles and downgrade, + custom history tables, + multiple providers, + seeding, + SQLite rebuilds and locking. +
  6. +
  7. + DbUp: + execution, + transactions, + journaling, + script types, + forward-change philosophy, + SQL and C# script providers. +
  8. +
  9. + Evolve: + commands, checksums, repeatables and transactions, + configuration, + execution options. +
  10. +
+
+
+
diff --git a/docs/_src/content.py b/docs/_src/content.py new file mode 100644 index 00000000..4111e9fb --- /dev/null +++ b/docs/_src/content.py @@ -0,0 +1,713 @@ +"""Documentation source. HTML prose is authored here; C# pairs are compiled by verify-docs.py.""" +from textwrap import dedent +from html import escape + +PAGES = [] +EXAMPLES = [] + + +def pair(title, classic, fluent=None, kind="body", smoke=False): + example = dict(id=f"example-{len(EXAMPLES) + 1}", title=title, + classic=dedent(classic).strip(), fluent=dedent(fluent if fluent is not None else classic).strip(), + kind=kind, smoke=smoke) + if kind == "class": + for style in ("classic", "fluent"): + if not example[style].startswith("using "): + imports = "using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\n" + if style == "fluent": + imports += "using DotNetProjects.Migrator.Framework.Fluent;\n" + example[style] = imports + "\n" + example[style] + EXAMPLES.append(example) + return example + + +def section(title, *blocks): + return dict(title=title, blocks=list(blocks)) + + +def table(headers, rows): + return '
' + ''.join(f'' for h in headers) + '' + ''.join('' + ''.join(f'' for v in row) + '' for row in rows) + '
{escape(h)}
{v}
' + + +def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ITransformationProvider.cs"): + PAGES.append(dict(group=group, slug=slug, title=title, summary=summary, sections=list(sections), source=source)) + + +CREATE_USERS = pair("CreateUsers.cs", ''' +using System.Data; +using DotNetProjects.Migrator.Framework; + +[Migration(1)] +public class CreateUsers : Migration +{ + public override void Up() + { + Database.AddTable("Users", + new Column("Id", DbType.Int32) { IsNullable = false }, + new Column("Name", DbType.String, 255), + new PrimaryKeyConstraint("PK_Users", "Id")); + } + + public override void Down() => Database.RemoveTable("Users"); +} +''', ''' +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() + .WithColumn("Name").AsString(255) + .WithPrimaryKey("PK_Users", "Id"); + } + + public override void BuildDown(MigrationBuilder migration) + => migration.Delete.Table("Users"); +} +''', kind="class", smoke=True) + +INSTALL = pair("Terminal · either authoring style", ''' +dotnet new console -n MigrationDemo -f net9.0 +cd MigrationDemo +dotnet add package DotNetProjects.Migrator +dotnet add package Microsoft.Data.Sqlite --version 9.0.7 +''', kind="shell") + +HOST = pair("Program.cs · shared runner", ''' +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Providers; +using Microsoft.Data.Sqlite; + +using var connection = new SqliteConnection("Data Source=app.db"); +connection.Open(); +using var provider = ProviderFactory.Create( + ProviderTypes.SQLite, connection, defaultSchema: null); + +var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false); +runner.MigrateToLastVersion(); +''', kind="program") + +page("Introduction", "quick-start", "Your first migration", "Create a SQLite database, apply a versioned change, and write its reverse. Choose either C# style; the runner is the same.", + section("Install the library", '

Start with the .NET 9 SDK and a console application. The core package supplies schema operations; your ADO.NET package connects to the database. SQLite needs no separate database server for this example.

', INSTALL), + section("Write the change", '

Add CreateUsers.cs. Choose one tab and copy that class. Each public migration has a numeric version; do not put both versions of the same example into one assembly. Classic migrations execute provider methods in Up and Down. Fluent migrations collect structured operations in BuildUp and BuildDown.

', CREATE_USERS), + section("Run it", '

Replace Program.cs with the shared host below and run dotnet run. The open connection belongs to this host and is disposed after the provider. The runner discovers public migration classes in the selected assembly.

', HOST, '

The result is an app.db file containing Users and SchemaInfo. Run again: version 1 is already recorded, so it is skipped. Add a class with [Migration(2)] for your next change.

'), + section("Reverse a change", '

Call runner.MigrateTo(0) to execute the reverse methods for this migration set. Here that drops Users and its data. A reverse migration is a schema operation, not a restore of deleted rows. Test both directions on a disposable database before deployment.

Continue with creating tables, or configure scopes, filters and transaction behavior.

'), source="examples/FluentQuickStart/Program.cs") + +page("Introduction", "installation", "Installation", "The core library, database driver, optional DI integration and CLI each have a distinct job.", + section("Choose your packages", table(["Package", "Purpose"], [["DotNetProjects.Migrator", "Migration classes, providers, runner and fluent operations."], ["An ADO.NET driver", "Install the driver for the database your host opens."], ["DotNetProjects.Migrator.Extensions.DependencyInjection", "Optional scoped runner, constructor injection and Microsoft logging."], ["DotNetProjects.Migrator.Tool", "The migrator command-line tool."]]), INSTALL), + section("Choose a driver", '

Common choices are Microsoft.Data.Sqlite, Microsoft.Data.SqlClient, Npgsql, MySql.Data, Oracle.ManagedDataAccess.Core and FirebirdSql.Data.FirebirdClient. The core library does not directly reference these packages. Passing an open connection makes driver selection explicit and keeps connection ownership with your application.

Read the provider overview for database families, aliases and CI coverage. A provider name is not a guarantee that every native operation has the same behavior on every server.

'), + section("Use the repository", '

To develop against a checkout, replace the core package reference with a project reference to src/Migrator/DotNetProjects.Migrator.csproj. The solution targets .NET 9. Building the .slnx solution requires an SDK that understands that format, such as SDK 9.0.200 or later.

Keep the library, CLI and optional DI integration on compatible versions. Recompile old migration assemblies when updating a breaking API; the upgrade guide explains the column and constraint changes.

'), source="src/Migrator/DotNetProjects.Migrator.csproj") + +page("Introduction", "configuration", "Configuration", "Select the connection, migration set and history scope first, then set runner options before executing.", + section("Connect and select a scope", '

This host fragment assumes an open ADO.NET connection. The provider scope partitions history and selects explicitly scoped classes. An unscoped migration inherits the provider scope. Scopes do not create separate database objects: two modules can still conflict on a table name.

', pair("Host configuration · both styles", ''' +using var billingProvider = ProviderFactory.Create( + ProviderTypes.SQLite, connection, defaultSchema: null, scope: "billing"); +billingProvider.CommandTimeout = 60; +var billing = new Migrator(billingProvider, typeof(CreateUsers).Assembly, false); +billing.SchemaInfoTableName = "BillingSchemaInfo"; +billing.Options.Tags.Add("core"); +billing.Options.TagMatch = TagMatchMode.All; +billing.Options.TransactionMode = MigrationTransactionMode.WholeSession; +billing.MigrateToLastVersion(); +''', kind="host")), + section("Runner options", table(["Option", "Behavior"], [["Tags / TagMatch", "Case-sensitive ordinal tags; match Any or All. No filter selects all versioned migrations."], ["Profiles", "Explicit profile names; selected profiles run after versioned migrations."], ["TransactionMode", "PerMigration, None or WholeSession. WholeSession supports SQLite, PostgreSQL and SQL Server."], ["Activator", "Optional delegate for creating migration instances."], ["Lock / LockTimeout", "Optional cross-process lease acquired before reading history; default timeout is 30 seconds."]])), + section("History and ownership", '

Set the history table before accessing history or running migrations. Its default name is SchemaInfo; the default scope is default. Give each runner its intended assembly or explicit migration types. Duplicate versions within an effective scope fail discovery.

Load connection strings from application configuration or environment variables. The CLI reads MIGRATOR_CONNECTION by default. Do not store production credentials in migration classes.

'), source="src/Migrator/RunnerOptions.cs") + +page("Introduction", "faq", "Frequently asked questions", "Decisions to make before adopting the library or moving an existing migration project.", + section("Do I need an ORM?", '

No. Migrations operate on an ADO.NET connection through the transformation provider. Use EF, Dapper, another data layer or direct SQL in the rest of your application. Migrator does not scaffold schema changes from an object model.

'), + section("Can I mix Classic and Fluent?", '

Yes. Both implement the same migration contract, run through the same loader and share history. Keep each version unique. The tabs throughout these guides show equivalent choices, not two classes to install together. The authoring method names differ: Up/Down versus BuildUp/BuildDown.

', CREATE_USERS), + section("Why does SQLite rebuilding matter?", '

SQLite does not implement every ALTER TABLE operation. Migrator reads the live schema and reconstructs a supported table when a column or constraint change needs it. This is useful without an ORM model. See SQLite for preserved objects, foreign-key checks and reconstruction boundaries.

'), + section("Does rollback recover data?", '

A transaction can roll back a failed migration when its database operations are transactional. Downgrading a completed version executes your reverse method. Neither mechanism recovers rows already deleted by a successful migration. Use an explicit recovery design and backups for that case.

'), + section("Can I edit an applied migration?", '

The journal records versions, scopes and timestamps, not a content checksum. Editing an applied class will not make it rerun. Add a new migration for a change. Consolidated baselines are an explicit history operation; read versioning and history.

'), + section("Why does SQL preview reject my migration?", '

Preview renders a structured subset. A provider callback, unsupported constraint alteration or schema dependency after raw SQL cannot be represented reliably and raises an error. Read planning and SQL preview rather than treating preview as a full execution simulation.

')) + +page("Operations", "creating-tables", "Creating tables", "Describe a complete table: columns first, with explicit named keys and constraints.", + section("Create a table with a key", '

The table definition groups related schema objects into one operation. Primary-key columns are emitted as non-nullable. In the fluent API a complete table is collected before execution, so keys can refer to columns declared in the same chain.

', CREATE_USERS), + section("Composite keys and uniqueness", '

Use the declared key order consistently in both primary and foreign keys. A composite unique constraint applies to the tuple; it does not make each column unique separately. The fully qualified constraint type below avoids the name collision with System.Data.UniqueConstraint.

', pair("A table with an ordered composite key", ''' +Database.AddTable("Subscriptions", + new Column("TenantId", DbType.Int32), + new Column("UserId", DbType.Int32), + new Column("Email", DbType.String, 255), + new PrimaryKeyConstraint("PK_Subscriptions", "TenantId", "UserId"), + new DotNetProjects.Migrator.Framework.UniqueConstraint( + "UQ_Subscriptions_Email", "TenantId", "Email")); +''', ''' +migration.Create.Table("Subscriptions") + .WithColumn("TenantId").AsInt32() + .WithColumn("UserId").AsInt32() + .WithColumn("Email").AsString(255) + .WithPrimaryKey("PK_Subscriptions", "TenantId", "UserId") + .WithUniqueConstraint("UQ_Subscriptions_Email", "TenantId", "Email"); +''', smoke=True)), + section("Identity and removal", '

Identity generation is a column attribute, separate from primary-key membership. SQLite requires an INTEGER identity column and its single-column primary key in the same definition. Use a complete Create.Table/AddTable operation to satisfy that rule. To reverse creation use Database.RemoveTable or migration.Delete.Table; dropping a table also removes its rows.

For supported creation operations, automatic reversal can derive the reverse operation. Explicitly author reverse behavior for destructive changes.

')) + +page("Operations", "altering-tables", "Altering tables", "Rename objects and evolve populated tables while preserving the schema details you still need.", + section("Rename a table and column", '

Use explicit old and new names. The column rename signature is table, old name, new name in both APIs. A table rename does not rename explicit constraints or their backing indexes. Reusing the original key name for a replacement table may collide on SQL Server or PostgreSQL.

', pair("Rename existing objects", ''' +Database.RenameTable("Users", "Members"); +Database.RenameColumn("Members", "Name", "DisplayName"); +''', ''' +migration.Rename.Table("Users", "Members"); +migration.Rename.Column("Members", "Name", "DisplayName"); +''')), + section("Change a complete column definition", '

Supply the type, length, nullability, default and collation you intend to retain. ChangeColumn replaces the column definition; it does not infer that table constraints should be created or removed. Existing rows must remain valid for the new definition.

', pair("Widen a required display name", ''' +Database.ChangeColumn("Users", new Column("Name", DbType.String, 500) +{ + IsNullable = false +}); +''', ''' +migration.Alter.Column("Name", "Users") + .AsString(500).NotNullable(); +''')), + section("A deployment sequence for populated data", '

Add a nullable column, deploy code that can read both forms, backfill values, then enforce the final requirement in a later migration. Large data copies and index creation can hold locks for substantial time; test them against a representative dataset.

On SQLite, a supported alteration may recreate the table and copy rows. On Oracle and some other engines, DDL may commit implicitly. Review the transaction guide and your provider page before choosing the deployment boundary.

')) + +page("Operations", "columns", "Columns and data types", "Type, size, precision, nullability, defaults, identity and collation are explicit column attributes.", + section("Add and remove a column", '

Column builders take column name followed by table name. Classic AddColumn takes table name first. New nullable columns accept existing rows without a backfill. A required column usually needs a compatible default or a staged data migration.

', pair("Add an optional email address", ''' +Database.AddColumn("Users", new Column("Email", DbType.String, 320)); +''', ''' +migration.Create.Column("Email", "Users").AsString(320).Nullable(); +'''), pair("Remove the email column", ''' +Database.RemoveColumn("Users", "Email"); +''', ''' +migration.Delete.Column("Email", "Users"); +''')), + section("Precision and defaults", '

For decimal values specify precision and scale. In a Column constructor an integer after the type is the size, not a numeric default. Set DefaultValue explicitly to avoid overload ambiguity. Plain strings are values; trusted SQL expressions use RawSql.Insert.

', pair("An amount with four decimal places", ''' +Database.AddColumn("Orders", new Column("Amount", DbType.Decimal) +{ + Precision = 12, Scale = 4, IsNullable = false, DefaultValue = 0m +}); +''', ''' +migration.Create.Column("Amount", "Orders").OfType(DbType.Decimal) + .WithPrecision(12, 4).NotNullable().WithDefaultValue(0m); +''')), + section("Time of day and durations", '

Use TimeOnly for time-of-day values and TimeSpan for intervals. A TimeSpan is a duration, including negative and multi-day values, so a TimeSpan default on a Time column is rejected. PostgreSQL and Oracle have native intervals; SQLite, SQL Server and MySQL/MariaDB store intervals as signed .NET ticks.

', pair("Clock time and elapsed time", ''' +Database.AddTable("Jobs", + new Column("RunAt", DbType.Time) { DefaultValue = new TimeOnly(9, 30) }, + new Column("Elapsed", MigratorDbType.Interval) { DefaultValue = TimeSpan.Zero }); +''', ''' +migration.Create.Table("Jobs") + .WithColumn("RunAt").OfType(DbType.Time).WithDefaultValue(new TimeOnly(9, 30)) + .WithColumn("Elapsed").OfType(MigratorDbType.Interval).WithDefaultValue(TimeSpan.Zero); +''', smoke=True)), + section("Database storage differs", '

SQLite does not enforce declared string lengths or decimal precision. UInt64 values above Int64.MaxValue are rejected there. Oracle character empty strings become NULL; Informix and Sybase have their own trimming and range behavior. Consult the type support and boundary matrix for supported mappings and live-test scope. A shared DbType does not imply identical native storage.

')) + +page("Operations", "data", "Data operations", "Insert, update and delete using explicit column/value arrays. Keep predicates separate from changed values.", + section("Insert rows", '

Column and value arrays must have the same length. The provider binds values using its driver-specific parameter mappings. For multiple rows issue multiple operations; the fluent Row method describes one row, not an accumulated collection of rows.

', pair("Insert a user", ''' +Database.Insert("Users", new[] { "Id", "Name" }, new object[] { 1, "Ada" }); +''', ''' +migration.Insert.IntoTable("Users") + .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" }); +''')), + section("Update and delete with predicates", '

Without a predicate, update/delete affects every row. Supply predicate columns and values deliberately. Fluent WhereSql is available for updates only; its text is trusted SQL, not an escaped user input.

', pair("Update one user", ''' +Database.Update("Users", new[] { "Name" }, new object[] { "Ada Lovelace" }, + new[] { "Id" }, new object[] { 1 }); +''', ''' +migration.Update.Table("Users") + .Set(new[] { "Name" }, new object[] { "Ada Lovelace" }) + .Where(new[] { "Id" }, new object[] { 1 }); +'''), pair("Delete one user", ''' +Database.Delete("Users", new[] { "Id" }, new object[] { 1 }); +''', ''' +migration.Delete.FromTable("Users").Where(new[] { "Id" }, new object[] { 1 }); +''')), + section("Conditional seed data", '

Use an explicit identifying predicate when a seed should exist only once. This is distinct from a migration version: a named profile can run repeatedly without a history entry. Coordinate competing writers; a check-then-insert helper is not a substitute for a database unique key.

', pair("Insert a missing seed", ''' +Database.InsertIfNotExists("Users", new[] { "Id", "Name" }, + new object[] { 1, "Ada" }, new[] { "Id" }, new object[] { 1 }); +''', ''' +migration.Insert.IntoTable("Users") + .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" }) + .IfNotExists(new[] { "Id" }, new object[] { 1 }); +''')), + section("Copying and reversal", '

Use the provider CopyDataFromTableToTable helper or fluent Execute.CopyData for named-column copies. Both tables must already exist and target columns must accept the source values. Execute.UpdateFrom maps source/target pairs. These operations retain provider limits and are outside the SQL-preview subset. A reverse data migration needs authored recovery logic; auto-reversal cannot recreate deleted or overwritten values.

', pair("Copy users into an archive table", ''' +Database.CopyDataFromTableToTable("Users", + new System.Collections.Generic.List { "Id", "Name" }, "ArchivedUsers", + new System.Collections.Generic.List { "UserId", "DisplayName" }); +''', ''' +migration.Execute.CopyData("Users", new[] { "Id", "Name" }, + "ArchivedUsers", new[] { "UserId", "DisplayName" }); +'''))) + +page("Operations", "schema", "Schema inspection", "Read the connected database before deciding what to change. Metadata is different from a model snapshot.", + section("Inspect tables and columns", '

Classic migrations read through Database. FluentMigration exposes Schema for queries and Context for the full provider API. A fluent authoring method runs before its queued operations: an inspection cannot see a table merely queued earlier in the same builder.

', pair("Add a column only when it is missing", ''' +if (!Database.ColumnExists("Users", "Email")) + Database.AddColumn("Users", new Column("Email", DbType.String, 320)); +''', ''' +if (!Schema.Table("Users").ColumnExists("Email")) + migration.Create.Column("Email", "Users").AsString(320); +''')), + section("Read ordered constraints", '

GetColumns returns inferred column attributes, not primary/unique membership flags. It is obsolete because native types and defaults cannot be mapped back to exact .NET definitions; use migration history for the original definition. Read typed table constraints to retain ordered composite keys. Unique indexes remain index metadata. MySQL/MariaDB catalogs cannot distinguish every original unique-index versus UNIQUE-clause authoring choice.

', pair("Read table constraint definitions", ''' +var constraints = Database.GetTableConstraints("Users"); +foreach (var constraint in constraints) + Console.WriteLine(constraint.Name); +''', ''' +var constraints = Schema.Table("Users").ConstraintDefinitions(); +foreach (var constraint in constraints) + Console.WriteLine(constraint.Name); +''')), + section("Create a view", '

ViewField selects columns from a base table. The alternative IViewElement overload represents explicit columns and joins. View definitions are provider-dependent and outside SQL preview and automatic reversal. Write a provider-appropriate DROP VIEW statement in the reverse method, and manage dependent views when changing their underlying tables.

', pair("A projection over Users", ''' +Database.AddView("UserNames", "Users", new ViewField("Id"), new ViewField("Name")); +''', ''' +migration.Create.View("UserNames", "Users", new ViewField("Id"), new ViewField("Name")); +''')), + section("Reads and portability", '

Dispose readers and commands obtained from the provider. Fluent Schema.Query and Select accept a reader callback and handle disposal. Use provider quoting helpers for table and column identifiers separately: quoting a table may introduce schema qualification, which is not valid for a column expression.

Metadata fidelity depends on the provider. Unsupported readers throw instead of pretending that an empty schema was found. A successful existence check is not a full schema-drift report.

'), source="src/Migrator/Framework/Fluent/FluentMigration.cs") + +page("Operations", "sql", "Execute SQL and scripts", "Use schema operations where they fit, and keep database-specific SQL explicit.", + section("Execute a statement", '

Raw SQL passes through to the selected database. It does not translate between dialects. Values from application input should be bound through a command; migration SQL is trusted application code.

', pair("A SQL data change", ''' +Database.ExecuteNonQuery("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL"); +''', ''' +migration.Execute.Sql("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL"); +''')), + section("Files and embedded resources", '

ExecuteScript reads a file; ExecuteResourceScript reads an assembly resource. Fluent equivalents capture script text as dedicated operations. Make files available at deployment and set resource names explicitly. Relative file paths are resolved against the process working directory.

', pair("Execute a SQL file", ''' +Database.ExecuteScript("Scripts/backfill.sql"); +''', ''' +migration.Execute.Script("Scripts/backfill.sql"); +'''), pair("Execute an embedded SQL resource", ''' +Database.ExecuteResourceScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql"); +''', ''' +migration.Execute.EmbeddedScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql"); +'''), '

For the second example mark backfill.sql as an EmbeddedResource in the migration project and use its actual manifest resource name. Missing resources fail before script execution.

'), + section("Client batch separators", '

The script APIs split standalone SQL Server GO lines, including optional line comments, while respecting strings, quoted identifiers and nested comments. GO repetition and SQLCMD directives fail before any batches execute. ExecuteNonQuery and Execute.Sql do not split client separators.

Other providers receive one command unless they implement IScriptBatchProvider. A database SQL file is not necessarily compatible with SQL*Plus, mysql-client or isql command syntax. Raw SQL also invalidates planned schema knowledge during SQL preview.

')) + +page("Operations", "connections", "Commands and callbacks", "Use the active provider connection when a migration needs driver-level work.", + section("Bind command parameters", '

The provider creates a command associated with its current transaction. Dispose it after use. Generate parameter names through the provider, rather than assuming every driver uses the same convention. The callback is deferred until fluent execution reaches it.

', pair("Execute a parameterized command", ''' +using var command = Database.CreateCommand(); +var name = Database.GenerateParameterName(0); +command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1"; +var value = command.CreateParameter(); +value.ParameterName = name; +value.Value = "Ada"; +command.Parameters.Add(value); +command.ExecuteNonQuery(); +''', ''' +migration.Execute.WithProvider(provider => +{ + using var command = provider.CreateCommand(); + var name = provider.GenerateParameterName(0); + command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1"; + var value = command.CreateParameter(); + value.ParameterName = name; + value.Value = "Ada"; + command.Parameters.Add(value); + command.ExecuteNonQuery(); +}); +''')), + section("Connection ownership", '

WithCommand creates and disposes a provider command around your action. WithConnection exposes the connection; WithProvider exposes the complete transformation provider. Do not close or replace a runner-owned connection, commit its transaction or switch databases while holding a native migration lock.

'), + section("Database administration", '

Database creation and other administration require a connection and identity authorized for that operation. Use a dedicated host with TransactionMode.None. Fluent administration rejects an active transaction; do not combine it with WholeSession or assume a Classic provider call can participate in transactional DDL.

', pair("Create a database on a supporting server", ''' +Database.CreateDatabases("Reporting"); +''', ''' +migration.Administration.CreateDatabase("Reporting"); +'''), '

The remaining mappings are DropDatabases / Administration.DropDatabase, SwitchDatabase / Administration.SwitchDatabase, and KillDatabaseConnections / Administration.KillConnections. These are explicit administrative actions with provider-specific support. Database switches invalidate assumptions about migration history and session locks: keep provisioning separate from ordinary schema migrations. They are outside SQL preview and automatic reversal.

'), + section("Preview and reversal", '

Callbacks can perform arbitrary C# work and cannot be translated into SQL preview. They require explicit reverse behavior. Keeping external network calls out of migration bodies makes failures easier to reason about: a database rollback cannot undo an email or an HTTP request.

')) + +page("Schema basics", "indexes", "Indexes", "An index is a separate schema object, even when it enforces uniqueness.", + section("Create and remove an index", '

Use an explicit name so the index can be inspected or removed later. Both APIs accept the same Index definition. Fully qualify this type if System.Index is also in scope. Columns retain the order in KeyColumns.

', pair("Index a user name", ''' +Database.AddIndex("Users", new DotNetProjects.Migrator.Framework.Index +{ + Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false +}); +''', ''' +migration.Create.Index("Users", new DotNetProjects.Migrator.Framework.Index +{ + Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false +}); +'''), pair("Drop an index", ''' +Database.RemoveIndex("Users", "IX_Users_Name"); +''', ''' +migration.Delete.Index("IX_Users_Name", "Users"); +''')), + section("Provider options", '

Index definitions also expose IncludeColumns, FilterItems and Clustered. These options are provider-specific. Oracle rejects included and clustered index requests; SQLite reconstruction rejects existing index SQL with explicit COLLATE clauses. Preview handles simple indexes and rejects unsupported options.

'), + section("Unique index or unique constraint?", '

Use UniqueConstraint for a table-level invariant and an Index with Unique for an index definition. Do not infer ownership from a generated name. SQLite RemoveAllIndexes preserves declared table UNIQUE constraints; remove those through the constraint APIs. Check query plans and data cardinality when choosing index keys.

'), source="src/Migrator/Framework/Index.cs") + +page("Schema basics", "constraints", "Keys and constraints", "Declare table invariants independently of column attributes.", + section("Add uniqueness and a check", '

Existing rows must satisfy a new constraint. A rebuild or ALTER operation can fail if duplicate or invalid data is present. CHECK expressions are trusted SQL and depend on the target engine. Primary keys, unique constraints, foreign keys and checks have typed definitions.

', pair("Add two named constraints", ''' +Database.AddUniqueConstraint("UQ_Users_Name", "Users", "Name"); +Database.AddCheckConstraint("CK_Users_Id", "Users", "Id > 0"); +''', ''' +migration.Create.Unique("UQ_Users_Name", "Users", "Name"); +migration.Create.Check("CK_Users_Id", "Users", "Id > 0"); +''')), + section("Remove the intended object", '

Use dedicated primary-key and foreign-key removal methods; generic RemoveConstraint is for unique/check constraints in the SQLite provider. Avoid RemoveAllConstraints unless the migration deliberately replaces every invariant.

', pair("Remove a check constraint", ''' +Database.RemoveConstraint("Users", "CK_Users_Id"); +''', ''' +migration.Delete.Constraint("CK_Users_Id", "Users"); +''')), + section("Constraint identity", '

GetTableConstraints returns ordered typed definitions. SQLite can return a null name for an unnamed legacy constraint; an autoindex name is not a substitute constraint name. PrimaryKeyExists checks the actual key name. MySQL reports the primary key name as PRIMARY.

Altering a column does not give that column ownership of a unique constraint. SQL Server implicit ownership markers are no longer used for deletion. Explicitly remove only the object your migration intends to change.

')) + +page("Schema basics", "foreign-keys", "Foreign keys", "Define ordered child/parent columns and independent actions for update and delete.", + section("Add a relationship", '

Parent key columns must identify a suitable primary/unique key. Child and parent arrays are positional: each child column corresponds to the parent column at the same index. Both tables and their compatible columns must already exist for this example. The Classic example uses IForeignKeyActions for independent update/delete actions; the older AddForeignKey overload supplies one action for both.

', pair("Orders belong to users", ''' +((IForeignKeyActions)Database).AddForeignKey( + "FK_Orders_Users", "Orders", new[] { "UserId" }, + "Users", new[] { "Id" }, ForeignKeyConstraintType.Cascade, + ForeignKeyConstraintType.NoAction); +''', ''' +migration.Create.ForeignKey("FK_Orders_Users", + "Orders", new[] { "UserId" }, "Users", new[] { "Id" }, + onDelete: ForeignKeyConstraintType.Cascade, + onUpdate: ForeignKeyConstraintType.NoAction); +''')), + section("Remove a relationship", '

Remove dependent keys before incompatible table or key changes. Restore them only after the existing data satisfies the replacement relationship.

', pair("Remove the foreign key", ''' +Database.RemoveForeignKey("Orders", "FK_Orders_Users"); +''', ''' +migration.Delete.ForeignKey("FK_Orders_Users", "Orders"); +''')), + section("Database semantics", '

Supported actions depend on the database; do not assume every engine implements CASCADE, RESTRICT, SET NULL and SET DEFAULT identically. SQLite rebuilds preserve separate update/delete actions and validate integrity before an owned transaction commits. MATCH FULL and MATCH PARTIAL requests are rejected because SQLite does not enforce those semantics.

SetNull needs nullable child columns. Test action behavior using actual data, especially composite keys and partially NULL values. Oracle supports its own subset of foreign-key actions.

')) + +page("Schema basics", "defaults-collations", "Defaults and collations", "Distinguish values from SQL expressions, and comparison intent from a provider's installed collation name.", + section("Literal and expression defaults", '

Ordinary strings are quoted literal values. RawSql.Insert marks trusted SQL to evaluate on the database. The expression below works on SQLite; provider function names and return types can differ.

', pair("A database-generated timestamp", ''' +Database.AddTable("Events", new Column("CreatedAt", DbType.DateTime) +{ + DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP"), IsNullable = false +}); +''', ''' +migration.Create.Table("Events") + .WithColumn("CreatedAt").OfType(DbType.DateTime).NotNullable() + .WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP")); +''', smoke=True)), + section("Comparison behavior", '

Collation presets request semantics. Unsupported mappings fail before DDL. SQLite AsciiIgnoreCase maps to NOCASE and folds ASCII only; it is not Unicode case folding. Named custom SQLite collations must be registered on the connection before schema or data operations use them.

', pair("ASCII-insensitive SQLite text", ''' +Database.AddTable("Labels", new Column("Name", DbType.String, 100) +{ + Collation = Collation.AsciiIgnoreCase +}); +''', ''' +migration.Create.Table("Labels") + .WithColumn("Name").AsString(100) + .WithCollation(Collation.AsciiIgnoreCase); +''', smoke=True)), + section("Presets and provider names", table(["Request", "Meaning"], [["CaseInsensitive / CaseSensitive", "Case behavior with accent sensitivity; supported SQL Server/MySQL/MariaDB mappings, or an explicit installed name on other engines."], ["Binary", "Provider binary comparison; not a promise of identical linguistic ordering."], ["AsciiIgnoreCase", "SQLite NOCASE; ASCII letters only."], ["Collation.Named(name)", "An installed or registered provider-specific collation."]]), '

Use named collations for language-specific or exact comparison behavior. PostgreSQL ICU nondeterministic collations must be created explicitly; SQL rendering does not create shared database objects. Read the mapping table for engine versions and restrictions.

')) + +page("Migration runners", "runners", "Choose a runner", "Use the same migration assembly in a dedicated host, a DI scope or the command-line tool.", + section("Execution choices", table(["Runner", "A good fit"], [["Library host", "A small deployment executable with explicit connection ownership and full provider access."], ["Microsoft DI integration", "A service collection supplying constructor dependencies, options and logging."], ["migrator CLI", "Automation that selects assemblies, providers, scopes, tags and target versions."]])), + section("A dedicated host", '

Run schema changes before application instances need the new schema. The host below works with either migration style and scans the assembly containing CreateUsers. Use explicit type selection when an assembly also contains migrations for other purposes.

', HOST), + section("Deployment responsibilities", '

Give the deployment identity the schema privileges needed by the selected migrations. Coordinate concurrent deploys through an external orchestrator or supported native lock. Configure the history table and scope consistently across invocations. Log the target and result without exposing connection strings.

Choose the CLI for a ready command surface, or DI for application services. Read transaction and lock semantics before relying on atomicity.

'), source="src/Migrator/Migrator.cs") + +page("Migration runners", "cli", "Command-line tool", "List, validate, plan, preview, apply and reverse migrations from a deployment script.", + section("Install and connect", '

Install DotNetProjects.Migrator.Tool as a .NET tool. Set MIGRATOR_CONNECTION in the deployment environment or select another variable with --connection-env. Both Classic and Fluent classes use the same commands. The tool does not print the connection-string value.

', pair("Install the CLI", 'dotnet tool install --global DotNetProjects.Migrator.Tool', kind="shell")), + section("Inspect before applying", pair("Inspect a migration assembly", ''' +migrator list --assembly MyMigrations.dll --provider SQLite +migrator status --assembly MyMigrations.dll --provider SQLite +migrator validate --assembly MyMigrations.dll --provider SQLite +migrator plan --assembly MyMigrations.dll --provider SQLite --target 10 +migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql +''', kind="shell"), '

Validate checks version planning, not arbitrary migration-body behavior. Plan lists version steps without running bodies. SQL generation renders a supported operation subset; it does not produce an idempotent history-managed bundle.

'), + section("Apply and roll back", pair("Deploy a selected scope", ''' +migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession +migrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0 +''', kind="shell"), '

Rollback requires an explicit lower target and rejects any plan containing upward steps. Target checks run after taking the configured lock and refreshing history. Tags, profiles and provider choice must match the intended deployment.

'), + section("Options and exit codes", table(["Option", "Purpose"], [["--tags a,b / --tag-match Any|All", "Filter versioned migrations."], ["--profiles a,b", "Select named profiles."], ["--schema / --scope", "Provider schema and migration history scope."], ["--timeout SECONDS", "Database command timeout."], ["--lock / --lock-timeout SECONDS", "Native migration lock on supported providers."], ["--offline", "SQL generation assuming empty history; profiles/maintenance rejected."]]), '

Exit codes: 0 success; 1 load/execution failure; 2 invalid arguments; 3 unsupported provider/operation; 4 lock timeout. SQL output can contain data authored in migrations. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird; use a custom host for other library providers.

'), source="src/Migrator.Tool/Program.cs") + +page("Migration runners", "dependency-injection", "Dependency injection and logging", "Resolve the runner and migration dependencies inside one service scope.", + section("Register the integration", '

Install DotNetProjects.Migrator.Extensions.DependencyInjection and Microsoft.Extensions.Logging alongside the core and database driver. This example uses the connection-string provider factory so provider disposal belongs to the DI scope. The providerName explicitly selects the SQLite driver.

', pair("A scoped migration host", ''' +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Providers; +using DotNetProjects.Migrator.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); +services.AddLogging(); +services.AddMigrator(_ => ProviderFactory.Create( + ProviderTypes.SQLite, "Data Source=app.db", defaultSchema: null, + providerName: "Microsoft.Data.Sqlite"), typeof(CreateUsers).Assembly, + options => options.TransactionMode = MigrationTransactionMode.PerMigration); + +using var container = services.BuildServiceProvider(); +using var scope = container.CreateScope(); +scope.ServiceProvider.GetRequiredService().MigrateToLastVersion(); +''', kind="program")), + section("Constructor dependencies", '

Migration classes are registered for activation through the service provider. Register your own constructor dependencies before resolving the runner. Options are scoped snapshots; a custom Activator can override construction. Fluent and Classic migrations use the same activation mechanism.

'), + section("Logging boundaries", '

The integration adapts runner lifecycle events to Microsoft logging. It omits SQL text and raw exception messages from these events. Configure your own logging providers through AddLogging. The core retains its lightweight logger API when you do not use DI.

Dispose the scope after migration execution. When supplying a caller-owned open connection, keep its owner alive until after the scope is disposed; the provider does not acquire ownership of an externally supplied connection.

'), source="src/Migrator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs") + +page("Migration runners", "preview", "Planning and SQL preview", "A version plan answers what runs. SQL preview shows the supported operation SQL.", + section("Read-only version planning", '

Plan and DryRun inspect applied versions through IMigrationHistory without creating/upgrading history or invoking migration bodies, callbacks, transactions or SQLite PRAGMA changes. Set the same scope, tags and assembly you intend to deploy. The fragment below assumes an initialized runner.

', pair("Inspect version steps", ''' +var plan = runner.Plan(10); +foreach (var step in plan) + Console.WriteLine($"{step.Version}: {(step.IsUp ? "up" : "down")}"); +runner.DryRun = true; +runner.MigrateTo(10); +''', kind="host")), + section("Preview connected SQL", '

PreviewSql reads connected history and schema. Classic bodies require explicit opt-in; provider calls are captured through a proxy that rejects unsupported access. Fluent authoring builds operations directly. This is trusted C# execution in both cases, not a security sandbox.

', pair("Generate operation SQL", ''' +var sql = runner.PreviewSql(10, ProviderTypes.SQLite, allowLegacyBodies: true); +Console.WriteLine(sql); +''', ''' +var sql = runner.PreviewSql(10, ProviderTypes.SQLite); +Console.WriteLine(sql); +''', kind="host")), + section("Offline generation and boundaries", '

MigrationSqlPreview.Generate can render supported operations without connecting; the CLI exposes --offline. Earlier structured create/rename operations update the planned schema. Raw SQL invalidates that knowledge, so later dependencies can fail.

Basic tables/columns, supported renames, simple indexes, inserts and raw SQL form the preview subset. Unsupported alterations, constraint changes, filters, callbacks and schema dependencies throw. InitializeOnce overrides are rejected rather than skipped silently. Post-commit callbacks do not run. Output contains operation SQL, not history guards or an idempotent deployment bundle.

'), source="src/Migrator/Migrator.cs") + +page("Migration runners", "transactions", "Transactions and locks", "Transaction rollback and cross-process coordination solve different problems.", + section("Choose the transaction boundary", table(["Mode", "Behavior"], [["PerMigration", "Default. Each successful migration commits independently."], ["None", "Provider/operation transaction behavior; no runner-managed migration transaction."], ["WholeSession", "One session transaction on SQLite, PostgreSQL or SQL Server; history initialization happens first."]]), '

Actual atomicity depends on the database and operation. Administration commands or implicit-commit DDL can violate assumptions. AfterUp/AfterDown run after commit; WholeSession defers them until the session commit. A callback failure cannot undo durable changes.

', pair("Configure a session transaction", ''' +runner.Options.TransactionMode = MigrationTransactionMode.WholeSession; +runner.MigrateToLastVersion(); +''', kind="host")), + section("Coordinate competing runners", '

DatabaseMigrationLock uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. The lease is session-owned and remains held across migration commits. This host fragment assumes a supported provider; SQLite rejects this built-in lock.

', pair("Acquire a native deployment lock", ''' +runner.Options.Lock = new DatabaseMigrationLock(); +runner.Options.LockTimeout = TimeSpan.FromSeconds(60); +runner.MigrateToLastVersion(); +''', kind="host")), + section("Scope of protection", '

Native locks are keyed by database, history table and scope. Coordinate separately if different scopes modify shared objects. Do not close/replace the connection, switch databases or manipulate the native lock inside a migration. MySQL named locks coordinate one server, not an entire distributed cluster.

Implement IMigrationLock for another lease mechanism, or serialize deployments outside the process. A transaction, history primary key or ordinary database write lock alone does not prove that the whole migration sequence is serialized.

'), source="src/Migrator/DatabaseMigrationLock.cs") + +page("Migration types", "versioning", "Versioning and scoped history", "Number changes, keep applied source immutable and give independent modules explicit histories.", + section("Choose a version scheme", '

Migration accepts a numeric version or year/month/day/hour/minute/second components. Use one monotonic scheme per migration set. The date constructor builds a numeric identifier; it does not consult a clock or resolve branch collisions for you. Missing lower-numbered versions up to the target can still be applied.

', pair("A dated migration", ''' +[Migration(2026, 9, 23, 10, 0, 0)] +public class AddUserEmail : Migration +{ + public override void Up() => Database.AddColumn("Users", new Column("Email", DbType.String, 320)); + public override void Down() => Database.RemoveColumn("Users", "Email"); +} +''', ''' +[Migration(2026, 9, 23, 10, 0, 0)] +public class AddUserEmail : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Create.Column("Email", "Users").AsString(320); + public override void BuildDown(MigrationBuilder migration) + => migration.Delete.Column("Email", "Users"); +} +''', kind="class")), + section("Scope selection", '

An explicit MigrationAttribute.Scope selects that migration only for the matching provider scope. Unscoped migrations inherit the runner scope. Discovery, duplicate validation and history reads use the effective scope. Duplicate numeric versions in distinct explicit scopes are independent; physical tables are not isolated.

Set SchemaInfoTableName before any history access if you need a different table. AppliedMigrations lists recorded versions; LastAppliedMigrationVersion is nullable when history is empty. AssemblyLastMigrationVersion describes the loaded set.

'), + section("Consolidated baselines", '

A baseline can record versions whose schema it already includes. The runner rechecks active-scope history before each planned step, skipping newly covered versions and their AfterUp callbacks. Downward runs similarly skip versions removed by an earlier Down. Recording the baseline version itself does not create a duplicate.

', pair("Mark a version included by a baseline", ''' +Database.MigrationApplied(1, "billing"); +''', ''' +migration.Execute.WithProvider(provider => provider.MigrationApplied(1, "billing")); +'''), '

Only record a version after establishing the schema it represents. History entries are not a substitute for verifying an existing database. Schema/history rollback follows the selected transaction mode. No migration-content checksum is stored.

'), source="src/Migrator/Migrator.cs") + +page("Migration types", "tags", "Tags", "Select a subset of versioned migrations using explicit ordinal names.", + section("Tag migration classes", '

Tags is in DotNetProjects.Migrator. One class can declare multiple names. Choose names for deployment intent such as core or reporting; do not use a tag to hide a dependency that a selected migration still requires.

', pair("A reporting migration", ''' +[Migration(2), Tags("reporting")] +public class CreateReportLog : Migration +{ + public override void Up() => Database.AddTable("ReportLog", new Column("Name", DbType.String, 255)); + public override void Down() => Database.RemoveTable("ReportLog"); +} +''', ''' +[Migration(2), Tags("reporting")] +public class CreateReportLog : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Create.Table("ReportLog").WithColumn("Name").AsString(255); + public override void BuildDown(MigrationBuilder migration) + => migration.Delete.Table("ReportLog"); +} +''', kind="class", smoke=True)), + section("Configure matching", pair("Select tags on the runner", ''' +runner.Options.Tags.Add("reporting"); +runner.Options.TagMatch = TagMatchMode.Any; +runner.MigrateToLastVersion(); +''', kind="host"), '

Any requires at least one selected tag; All requires every selected tag. Matching is ordinal and case-sensitive. Without a tag filter all eligible versioned migrations are selected. Profiles have their own explicit name selection.

'), + section("Downgrade behavior", '

Applied versions excluded by the active filter remain applied during downgrade. A filtered run is therefore not a promise that the whole database matches one contiguous global version range. Keep deployment filters stable and inspect the plan before reversing selected changes.

'), source="src/Migrator/MigrationLoader.cs") + +page("Migration types", "profiles", "Profiles", "Run explicitly selected work after versioned migrations without recording a version.", + section("Define a named profile", '

A profile is useful for optional seed data or environment setup. It runs every time its name is selected. Make repeated execution deliberate: use an identifying predicate or other idempotent operation where appropriate.

', pair("A development seed profile", ''' +[Profile("demo", Order = 10)] +public class DemoData : Migration +{ + public override void Up() => Database.InsertIfNotExists("Users", + new[] { "Id", "Name" }, new object[] { 1, "Ada" }, + new[] { "Id" }, new object[] { 1 }); + public override void Down() { } +} +''', ''' +[Profile("demo", Order = 10)] +public class DemoData : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Insert.IntoTable("Users") + .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" }) + .IfNotExists(new[] { "Id" }, new object[] { 1 }); + public override void BuildDown(MigrationBuilder migration) { } +} +''', kind="class")), + section("Select a profile", pair("Run the demo profile", ''' +runner.Options.Profiles.Add("demo"); +runner.MigrateToLastVersion(); +''', kind="host"), '

Profiles accept Order and Scope. Execution orders by Order and then ordinal full type name. Profile execution uses Up and does not create a migration-version entry or use Down as an undo history. An auxiliary-only run preserves existing version history.

'), + section("Execution versus repeatables", '

A selected profile runs because it was selected, not because its source checksum changed. Treat this separately from versioned migrations and checksum-based repeatable SQL. Offline CLI SQL generation rejects profiles because it cannot represent the complete lifecycle.

'), source="src/Migrator/RunnerOptions.cs") + +page("Migration types", "maintenance", "Maintenance migrations", "Place ordered work at the runner's lifecycle stages.", + section("Choose a stage", table(["Stage", "When"], [["BeforeRun", "Before versioned migration work in this run."], ["BeforeMigration", "Before each executed versioned migration."], ["AfterMigration", "After each executed versioned migration."], ["AfterRun", "After the run's migration/profile work."]]), '

Maintenance classes accept Order and Scope. They use Up and do not acquire version records. Hooks stop on failure; later stages are not finally blocks or guaranteed cleanup paths. Lock release and connection/transaction restoration are runner responsibilities.

'), + section("A scoped maintenance operation", '

The example expects an existing DeploymentLog table. Choose a table that already exists at the selected stage. Fluent callbacks execute at the corresponding operation position.

', pair("Write a deployment marker", ''' +[Maintenance(MaintenanceStage.AfterRun, Order = 10)] +public class RecordDeployment : Migration +{ + public override void Up() + => Database.Insert("DeploymentLog", new[] { "Message" }, new object[] { "Migration run finished" }); + public override void Down() { } +} +''', ''' +[Maintenance(MaintenanceStage.AfterRun, Order = 10)] +public class RecordDeployment : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Insert.IntoTable("DeploymentLog") + .Row(new[] { "Message" }, new object[] { "Migration run finished" }); + public override void BuildDown(MigrationBuilder migration) { } +} +''', kind="class")), + section("Post-commit callbacks", '

Migration.AfterUp and AfterDown run after commit, with the migration context restored. In WholeSession they wait until the entire session commits. Their failure reports an error after durable changes; it cannot reverse that commit. Do not confuse maintenance stages with a guaranteed post-commit delivery system.

'), source="src/Migrator/Migrator.cs") + +page("Migration types", "auto-reversing", "Automatic reversal", "Fluent operations can describe a supported reverse sequence; Classic migrations author it directly.", + section("Creation and its reverse", '

AutoReversingMigration derives reverse operations in reverse order and validates reversal support before its first change. The Classic equivalent makes the Down operation explicit. Both examples below create the same table and remove it on downgrade.

', pair("A reversible table creation", ''' +[Migration(3)] +public class CreateNotes : Migration +{ + public override void Up() => Database.AddTable("Notes", new Column("Text", DbType.String, 500)); + public override void Down() => Database.RemoveTable("Notes"); +} +''', ''' +[Migration(3)] +public class CreateNotes : AutoReversingMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Create.Table("Notes").WithColumn("Text").AsString(500); +} +''', kind="class", smoke=True)), + section("What needs an authored reverse", '

Destructive changes, data operations, SQL and callbacks require explicit reverse behavior. Reverse support is narrower than execution support. An operation that can run is not necessarily one that can be inverted from its definition alone.

Use FluentMigration with BuildDown when the reverse needs its own steps. MigrationBuilder.WithReverse can attach an explicit backward operation to a forward operation. Dropping a newly created table on downgrade still destroys any data inserted since creation; automatic reversal is not data recovery.

'), source="src/Migrator/Framework/Fluent/FluentMigration.cs") + +page("Database providers", "providers", "Provider overview", "One authoring contract, explicit database behavior. Choose the driver and provider together.", + section("Database families", table(["Database", "ProviderTypes", "Guide"], [["SQLite", "SQLite / MonoSQLite", 'Live-schema reconstruction'], ["SQL Server", "SqlServer / SqlServer2005", 'Constraints, batches and locks'], ["PostgreSQL", "PostgreSQL / PostgreSQL82", 'Schemas, types and locks'], ["MySQL / MariaDB", "Mysql / MariaDB", 'DDL and collation behavior'], ["Oracle", "Oracle / MsOracle", 'Identity and metadata'], ["SAP HANA", "Hana", 'Additional providers'], ["Db2 / Informix / Firebird / Ingres / Sybase", "IBM_DB2 / IBM_Informix / Firebird / Ingres / Sybase", 'Engine-specific guidance']])), + section("Bring a connection", '

Pass an open IDbConnection to ProviderFactory.Create. Both migration styles use that provider. Alternatively use the connection-string overload and configure providerName so the provider can resolve the ADO.NET factory. Use a matching driver and test the exact server version you deploy.

', pair("Provider selection · open connection supplied by the host", ''' +using var selectedProvider = ProviderFactory.Create( + ProviderTypes.PostgreSQL, connection, defaultSchema: "public", scope: "billing"); +var selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false); +selectedRunner.MigrateToLastVersion(); +''', kind="host")), + section("Qualification", '

The CI matrix includes SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase and SAP HANA. Ingres and historical provider aliases have separate qualification needs. Read testing and the live-engine matrix for exact drivers and server setup.

Database support is operation-specific. Column types, collation presets, index options, DDL transactions and metadata readers can differ. Test stored values and preserved schema, not only the generated SQL.

'), source="src/Migrator/ProviderFactory.cs") + +SQLITE_ALTER = pair("Change a column on an existing SQLite table", ''' +Database.ChangeColumn("Users", new Column("Name", DbType.String, 500) +{ + IsNullable = false, DefaultValue = "Unknown", + Collation = Collation.AsciiIgnoreCase +}); +''', ''' +migration.Alter.Column("Name", "Users") + .AsString(500).NotNullable().WithDefaultValue("Unknown") + .WithCollation(Collation.AsciiIgnoreCase); +''') + +page("Database providers", "sqlite", "SQLite", "Change existing tables from the live schema, without maintaining an ORM model.", + section("Automatic reconstruction", '

For supported changes, Migrator reads SQLiteTableInfo, changes its representation, creates a replacement table, copies mapped rows, swaps tables and recreates represented dependent objects. This provides column type/default/nullability changes and adding/removing primary, foreign, unique and check constraints.

Native rename and eligible drop-column paths are used when supported by the engine. Complex alterations use reconstruction. Existing rows must satisfy the new definition; a default does not rewrite every existing NULL during a column change.

', SQLITE_ALTER), + section("What survives a rebuild", table(["Detail", "Behavior"], [["Mapped data", "Named-column copy preserves mapped values, subject to the new definition accepting them."], ["Keys and constraints", "Named/composite keys, ordered foreign-key pairs and separate update/delete actions are retained."], ["Column collations", "Declared names are retained. Register custom collations on the connection."], ["Indexes and triggers", "Supported definitions are recreated; unsafe trigger rename/drop-column cases are rejected."], ["AUTOINCREMENT", "The sequence high-water mark survives, including previously deleted identities."], ["Hidden rowid", "Not part of the mapped data and may change."]])), + section("Boundaries are explicit", '

Reconstruction rejects generated columns, STRICT, WITHOUT ROWID and indexes with explicit COLLATE clauses. It is not an arbitrary SQL dependency rewriter. Adjust dependent views, complex expressions and triggers explicitly when required. MATCH FULL and MATCH PARTIAL are rejected because SQLite does not enforce their semantics.

Owned rebuild transactions and runner transactions validate foreign-key integrity before commit and restore the prior enforcement setting. For caller-owned active transactions configure foreign keys before beginning the transaction. A SQLite write lock is not a session-wide migration lease; coordinate deployment externally or provide IMigrationLock.

'), + section("Values and identity", '

CLR Guid defaults use blobs from Guid.ToByteArray(), matching inserted parameters. Legacy text GUID defaults remain SQL expressions during unrelated rebuilds, so storage is not silently converted. Convert mixed text/blob keys explicitly and consistently across related tables.

SQLite INTEGER is signed 64-bit. Declared text lengths and decimal precision do not impose SQL Server-like enforcement. An identity needs a single INTEGER primary key in the same definition. For adding identity to an existing table, use an atomic SQLite RecreateTable definition containing both objects.

'), + section("How this differs from other tools", '

FluentMigrator leaves general column alterations and later foreign-key changes to manual reconstruction. DbUp and Evolve run supplied scripts. EF Core also rebuilds SQLite tables using model-represented artifacts. Migrator reconstructs from live metadata without an ORM. The sourced operation comparison distinguishes native SQL, emulation and manual work.

'), source="src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs") + +page("Database providers", "sql-server", "SQL Server", "Explicit keys, provider-specific indexes, transactional DDL and application locks.", + section("Select the provider", '

Use ProviderTypes.SqlServer with an open Microsoft.Data.SqlClient connection. Pass the intended default schema, commonly dbo. Historical SqlServer2005 is a separate alias with older type mappings. WholeSession transactions and DatabaseMigrationLock are available for SQL Server.

'), + section("Name constraints explicitly", '

Column changes preserve explicit constraints and indexes. Add/remove uniqueness independently. For a nonclustered primary key on an existing compatible table use the dedicated API shown below. Review existing clustered indexes before changing key layout.

', pair("Add a nonclustered primary key", ''' +Database.AddPrimaryKeyNonClustered("PK_Users", "Users", "Id"); +''', ''' +migration.Create.NonClusteredPrimaryKey("PK_Users", "Users", "Id"); +''')), + section("Indexes and SQL batches", '

Index definitions can express included/filter/cluster options where supported. The script APIs split standalone GO lines; raw ExecuteNonQuery/Execute.Sql does not. SQLCMD directives and GO repetition are rejected before executing script batches. Prefer scripts for client batch syntax and commands for parameterized statements.

'), + section("Types and object names", '

Use TimeOnly for time values and TimeSpan for interval ticks. SqlServer2005 uses its older DATETIME precision behavior. Use separate quoting helpers for table and column names. A table rename leaves named constraints/indexes attached with their old names; assign distinct names when creating a replacement table.

'), source="src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs") + +page("Database providers", "postgresql", "PostgreSQL", "Native intervals, schema-aware metadata, transactional DDL and advisory locks.", + section("Connect with Npgsql", '

Use ProviderTypes.PostgreSQL and an open Npgsql connection, with the intended default schema. Connection search_path affects unqualified relation lookup. Metadata readers resolve the requested relation through PostgreSQL and distinguish same-named tables in different schemas.

'), + section("Use native interval values", '

PostgreSQL maps duration values to native intervals. Time without time zone maps to a time of day; use TimeOnly for that input. Parameter mappings and scalar CLR return types are separate concerns: raw ADO.NET values remain driver-specific.

', pair("Store a job duration", ''' +Database.AddColumn("Jobs", new Column("Elapsed", MigratorDbType.Interval) +{ + DefaultValue = TimeSpan.FromDays(2) +}); +''', ''' +migration.Create.Column("Elapsed", "Jobs").OfType(MigratorDbType.Interval) + .WithDefaultValue(TimeSpan.FromDays(2)); +''')), + section("Collations and schemas", '

Create any ICU nondeterministic collation explicitly, then select it with Collation.Named. Column rendering does not silently create shared collation objects. Binary maps to C; language and case semantics should use a specific installed name.

Schema-aware metadata does not establish complete qualification for every operation. Test quoted names and search-path behavior with your migration. Renaming a table retains its named constraints; avoid colliding names when recreating the old table.

'), + section("Transactions and coordination", '

WholeSession is supported for verified transactional DDL, and DatabaseMigrationLock uses a session advisory lock. Statements that require special transaction treatment need a separate deployment design. Keep the connection stable while the lease is held.

'), source="src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs") + +page("Database providers", "mysql", "MySQL and MariaDB", "Related providers with explicit engine, collation and DDL transaction differences.", + section("Choose the matching dialect", '

Select ProviderTypes.Mysql for MySQL and MariaDB for MariaDB. Use an open driver connection or configure the factory. Do not treat compatible wire protocols as proof of identical server syntax or metadata behavior. DDL can commit implicitly; the runner rejects WholeSession for these dialects.

'), + section("Select a supported collation", '

Semantic presets require utf8mb4-compatible text and the documented server versions: MySQL 8 and MariaDB 10.10+ have different mappings. Use a named installed collation if exact linguistic or trailing-space behavior matters.

', pair("Case-insensitive, accent-sensitive text", ''' +Database.AddTable("Labels", new Column("Name", DbType.String, 100) +{ + Collation = Collation.CaseInsensitive +}); +''', ''' +migration.Create.Table("Labels").WithColumn("Name").AsString(100) + .WithCollation(Collation.CaseInsensitive); +''')), + section("Constraint metadata", '

MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction.

'), + section("Locking and values", '

DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.

'), source="src/Migrator/Providers/Impl/Mysql/MysqlTransformationProvider.cs") + +page("Database providers", "oracle", "Oracle", "Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.", + section("Connection and schema", '

Use ProviderTypes.Oracle with the Oracle managed ADO.NET driver and the intended schema. MsOracle is a historical variant. Oracle DDL is not generally atomic across a migration; WholeSession is rejected. Some quoted qualified metadata lookups are explicitly rejected.

'), + section("Create an identity definition", '

Identity is a column attribute and is validated before table creation. It need not be a primary key on every engine, but the example pairs it with an explicit key. Use a server/driver combination qualified for native identity.

', pair("An identity table with an explicit key", ''' +Database.AddTable("Entries", + new Column("Id", DbType.Int32) { IsIdentity = true, IsNullable = false }, + new Column("Text", DbType.String, 255), + new PrimaryKeyConstraint("PK_Entries", "Id")); +''', ''' +migration.Create.Table("Entries") + .WithColumn("Id").AsInt32().Identity().NotNullable() + .WithColumn("Text").AsString(255) + .WithPrimaryKey("PK_Entries", "Id"); +''', smoke=True)), + section("Object cleanup", '

RemoveTable leaves unrelated sequences intact. Oracle removes table-owned triggers and native identity objects. For a legacy sequence that the migration explicitly owns, OracleTransformationProvider.RemoveTableWithOwnedSequences validates named sequences and propagates cleanup errors. It does not infer sequence ownership from naming patterns.

'), + section("Values and options", '

Oracle empty character strings become NULL. Time uses DATE with a fixed 1970-01-01 date and whole-second precision; fractional Time inputs are rejected. Intervals use native storage. Changes that require an unsupported in-place type conversion need an explicit data migration.

Included/clustered index options are rejected rather than ignored. Ordered foreign-key pairs and delete actions are preserved by structured metadata. A SQL Server clustered-index request is not translated into an Oracle index-organized table.

'), source="src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs") + +page("Database providers", "other-providers", "HANA and additional providers", "Use the live-engine matrix to qualify operations beyond the common database families.", + section("SAP HANA", '

ProviderTypes.Hana uses SAP’s native .NET driver. The CI job runs HANA Express and exercises schema/data operations, metadata, constraints, history, restart and DML rollback. DDL may autocommit. Use a custom host; the CLI driver bundle does not include an online HANA host.

HANA has its own supported type set; Guid and DateTimeOffset are outside the current matrix mappings. Review the type matrix before choosing shared column definitions.

'), + section("Db2, Informix, Firebird and Sybase", table(["Provider", "Things to check"], [["Db2 LUW", "Driver runtime dependencies, decimal/storage capacity and ordinary/unique index options."], ["Informix", "Native driver and database encoding; TEXT reads, integer NULL sentinels, whole-second Time and trailing-space trimming."], ["Firebird", "Decimal storage capacity, ordinary/unique index operations and transaction behavior."], ["Sybase ASE", "TEXTSIZE and string truncation settings, nullable BIT restrictions, trimmed strings and constraint-name limitations."]])), + section("A shared table definition", '

The same authoring API describes a portable subset. This does not imply that every native extension or type maps identically. Start with simple definitions, then qualify your actual data and schema operations on each target.

', CREATE_USERS), + section("Source inventory and evidence", '

Ingres remains a source dialect outside the eleven-engine matrix. Redshift, Snowflake and Db2 for IBM i require separate provider/infrastructure qualification; PostgreSQL tests do not qualify Redshift, and Db2 LUW tests do not qualify IBM i.

See qualification requirements and live test setup for exact coverage and reproduction commands.

'), source="src/Migrator/ProviderFactory.cs") + +page("Advanced topics", "conditional", "Conditional logic", "Choose between inspecting the live schema and declaring a provider-specific operation.", + section("Provider-specific operations", '

The Classic provider indexer selects a named provider or a no-op provider. Fluent IfDatabase wraps structured operations in a provider condition. Use the provider names understood by the dialect; this SQLite example leaves other providers unchanged.

', pair("Run a SQLite-specific statement", ''' +Database["SQLite"].ExecuteNonQuery("UPDATE Users SET Name = upper(Name)"); +''', ''' +migration.IfDatabase("SQLite", sqlite => + sqlite.Execute.Sql("UPDATE Users SET Name = upper(Name)")); +''')), + section("Schema-dependent decisions", '

Use Database.TableExists/ColumnExists or FluentMigration.Schema for connected checks. These inspect the current database. A fluent BuildUp method collects operations before they execute, so queued creation is not visible to a live metadata read in the same method.

For execution-time decisions after earlier operations, use an explicit provider callback. That callback cannot be previewed and requires an authored reverse. Avoid making a migration silently succeed with the wrong schema: an existence check alone does not validate a column’s type or constraint definition.

'), source="src/Migrator/Framework/Fluent/MigrationBuilder.cs") + +page("Advanced topics", "extensions", "Custom extensions", "Reuse schema conventions without hiding provider behavior or changing the migration contract.", + section("Share a schema convention", '

A small helper can express a repeated column policy in both styles. Keep helper behavior stable for historical migrations; changing a helper can change what an old migration does on a fresh database. The example uses static methods to keep its dependencies explicit.

', pair("Reusable audit-column helpers", ''' +public static class AuditColumns +{ + public static void Add(ITransformationProvider database, string table) + => database.AddColumn(table, new Column("CreatedAt", DbType.DateTime) + { + IsNullable = false, DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP") + }); +} +''', ''' +public static class AuditColumns +{ + public static void Add(MigrationBuilder migration, string table) + => migration.Create.Column("CreatedAt", table).OfType(DbType.DateTime) + .NotNullable().WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP")); +} +''', kind="class")), + section("Custom activation and locks", '

RunnerOptions.Activator constructs migrations when a DI container is not appropriate. IMigrationLock supplies a disposable lease for custom deployment coordination. Release must work on success and failure. Configure these at the host boundary rather than in individual migrations.

'), + section("Provider authors", '

ITransformationProvider defines execution and metadata operations. A custom provider needs accurate typed constraint definitions or an explicit unsupported error. IMigrationHistory enables read-only planning and effective-scope history selection. IScriptBatchProvider extends script processing.

Keep SQL rendering independent of a live connection. Custom MigrationOperation implementations need deliberate validation, application, SQL rendering and reversal behavior. See the API map and implementation contracts before claiming preview or reversal support.

'), source="src/Migrator/Framework/Fluent/Operations.cs") + +page("Advanced topics", "testing", "Testing and deployment", "Verify stored data, preserved schema and repeat execution on the actual target engine.", + section("Test a migration lifecycle", '

Create a disposable database, apply the migration, check the schema and rows, run to the same target again, then downgrade and verify the intended reverse. Both authoring styles use the same runner. This fragment assumes an initialized runner whose migration set creates Users.

', pair("A host-level smoke check", ''' +runner.MigrateToLastVersion(); +if (!provider.TableExists("Users")) throw new Exception("Users missing"); +runner.MigrateToLastVersion(); +runner.MigrateTo(0); +if (provider.TableExists("Users")) throw new Exception("Users was not removed"); +''', kind="host")), + section("What to assert", '

Test defaults by omitting a value, and nullability by explicitly sending NULL. Verify composite-key order, foreign-key actions and constraint names. After a SQLite rebuild check real rows, collations, supported indexes/triggers and identity high-water state. Test failure paths as well as successful SQL generation.

Use representative production-sized data to measure lock duration and backfill cost. A passing SQL-string assertion does not establish that a database accepts a command or preserves its semantics.

'), + section("Repository checks", '

Build with dotnet build Migrator.slnx, then use .github/scripts/test.ps1 -Database Unit or SQLite for local suites. The live-engine guide gives the external database setup. Homepage CI results include commit provenance and skipped/missing-suite status.

'), + section("Production rollout", '

Keep applied migrations immutable, review SQL and explicit reverse behavior, and serialize competing deploys. Validate against a restored database before making a breaking change. Plan application compatibility around expand/backfill/contract phases. Treat post-commit callback failures as durable migrations requiring follow-up handling.

'), source=".github/workflows/dotnetpull.yml") + +page("Advanced topics", "upgrading", "Upgrading existing migrations", "Update source definitions while preserving the history your databases already contain.", + section("Explicit column attributes", '

Replace old ColumnProperty flags with IsNullable, IsIdentity and IsUnsigned. Primary, unique, foreign and check constraints belong to the table. GetColumns returns column attributes; use GetTableConstraints for key membership and ordered columns.

Keep the same applied migration versions and effective scope when recompiling. Do not create a new history table merely to make an incompatible source assembly run. Verify the upgrade against a restored database and a fresh database.

'), + section("One fluent authoring surface", '

FluentMigration.BuildUp/BuildDown replaces the duplicate legacy builder. Use complete table definitions for keys, explicit operations for indexes, and independent foreign-key update/delete actions. Classic Up/Down migrations remain first-class.

', CREATE_USERS), + section("Behavior changes to review", '

Column changes preserve explicit uniqueness; old SQL Server ownership markers no longer control deletion. TimeSpan inputs mean intervals, so convert clock-time inputs to TimeOnly. SQLite GUID defaults use the same blob representation as inserted parameters; unrelated rebuilds preserve existing text defaults.

Read the complete compatibility migration guide for constructor replacements, custom-provider contracts, identity, constraint metadata and collation mappings. Version-specific details live there; these chapters describe the current API.

'), source="docs/migration-guide-12.1-to-13.md") + +page("Reference", "api-map", "Classic / Fluent API map", "A practical index of the two authoring surfaces and their shared provider contracts.", + section("Schema operations", table(["Classic", "Fluent"], [["AddTable", "Create.Table"], ["AddColumn(table, column)", "Create.Column(name, table)"], ["ChangeColumn(table, column)", "Alter.Column(name, table) / Alter.Column(table, column)"], ["RemoveTable / RemoveColumn", "Delete.Table / Delete.Column"], ["RenameTable / RenameColumn", "Rename.Table / Rename.Column"], ["AddPrimaryKey / AddUniqueConstraint / AddCheckConstraint", "Create.PrimaryKey / Create.Unique / Create.Check"], ["AddForeignKey / RemoveForeignKey", "Create.ForeignKey / Delete.ForeignKey"], ["AddIndex / RemoveIndex", "Create.Index / Delete.Index"], ["GetTableConstraints / GetColumns", "Schema.Table(name).ConstraintDefinitions() / Columns()"]])), + section("Data and execution", table(["Classic", "Fluent"], [["Insert / InsertIfNotExists", "Insert.IntoTable(...).Row(...) / IfNotExists(...)"], ["Update / Delete", "Update.Table(...).Set(...).Where(...) / Delete.FromTable(...).Where(...)"], ["ExecuteNonQuery / ExecuteScript / ExecuteResourceScript", "Execute.Sql / Execute.Script / Execute.EmbeddedScript"], ["CopyDataFromTableToTable / UpdateTargetFromSource", "Execute.CopyData / Execute.UpdateFrom"], ["TruncateTable", "Execute.Truncate"], ["CreateCommand / Connection", "Execute.WithCommand / Execute.WithConnection"], ["Database provider access", "Context or Execute.WithProvider"], ["History / transactions", "Shared runner and explicit provider context"]])), + section("Execution is not preview or reversal", '

Both APIs reach the same provider layer, but not every operation has SQL-preview or automatic-reversal support. Provider capabilities still govern execution. Read preview, reversal and the machine-checked method-family inventory for the distinction.

'), source="src/Migrator/Framework/Fluent/MigrationBuilder.cs") + +page("Reference", "contributing", "Contributing", "Make a provider change reproducible, then verify its observable behavior.", + section("A useful report", '

Include package, database and driver versions, a minimal migration, relevant schema/data, and expected versus actual behavior. Remove secrets from connection strings and logs. File reports in the issue tracker.

'), + section("A focused change", '

Add a regression that fails before the fix and checks the real result afterward. Provider-specific changes need actual-engine evidence; skipped tests and generated SQL alone do not qualify support. Keep mutable definition inputs independent from caller arrays and check failure paths.

'), + section("Documentation changes", '

Edit docs/_src/content.py for chapters and docs/_src/home.html for the homepage. Run python .github/scripts/build-docs.py to regenerate static HTML and the search index. Run python .github/scripts/verify-docs.py --compile to validate links, paired examples and compilable C# samples. Site assets live in docs/assets.

Each migration-operation example should provide Classic and Fluent versions. Shared runner and shell commands intentionally appear in both tabs. Keep provider restrictions precise and features described in the present tense. Run the homepage CI-count renderer tests when changing the site template.

'), + section("Design references", '

The chapter organization follows the learning path of FluentMigrator’s documentation, adapted to this API. Visual references include Resend’s typography and code tabs and Gel’s code walkthroughs. The site uses its own palette, layout, copy and migration illustrations.

'), source="docs/_src/content.py") diff --git a/docs/_src/home.html b/docs/_src/home.html new file mode 100644 index 00000000..f8897003 --- /dev/null +++ b/docs/_src/home.html @@ -0,0 +1,34 @@ +{{header}} +
+
+
+

DOTNETPROJECTS / DATABASE MIGRATIONS

+

Database changes,
written in C#.

+

A table today. A different table tomorrow. Keep every change explicit, versioned, and close to your application.

+

Choose Classic or Fluent migrations. Bring your ADO.NET driver. Run the same migration system alongside any ORM—or without one.

+ + +
+
ONE CHANGE. TWO WAYS TO WRITE IT.001 ↘
{{hero}}
+
+

01 / AUTHORWrite a numbered change.

02 / REVIEWPlan the next step.

03 / APPLYLeave a lasting record.

+
+

SMALL PRIMITIVES. REAL DATABASES.

Your schema.
Your decisions.

+
01

Two C# styles

Direct provider calls or a fluent builder. Tables, columns, indexes, constraints and data, with raw SQL when you need it.

Compare the APIs ↗
+
02

A deliberate deployment

Version plans, tags, profiles, maintenance stages, transaction modes and native locks. A CLI or a runner inside your own host.

Choose a runner ↗
+
03

History with boundaries

Track applied versions and give modules separate histories through scopes. Author the reverse for changes that need it.

Understand versioning ↗
+
+
+

A PARTICULAR STRENGTH / SQLITE

A small database.
Room to evolve.

Change the schema you have. Without an ORM model.

Migrator reads SQLite’s live schema and automatically reconstructs tables for supported changes to column types, defaults and nullability, and primary, foreign, unique and check constraints.

Existing rows are copied and supported schema artifacts are preserved. You describe the change; the provider handles the rebuild.

FluentMigrator requires manual reconstruction for general column alterations and later foreign-key changes. DbUp and Evolve leave it to your scripts. EF Core also rebuilds tables, using model metadata.

Read the SQLite guide and preservation limits ↗
See the sourced operation comparison →
+
TABLE / UsersΔ 002

IdINTEGER · PRIMARY KEY

Name255 500 · NOT NULL

↳ Existing rows travel with the schema.

{{sqlite}}
+
+

FROM EMPTY FOLDER TO FIRST TABLE

Start with
one change.

Install the library and your database driver. Add the migration above, wire up the runner, and apply it. SQLite makes a convenient first database.

Follow the complete quick start ↗
{{install}}
+

THE MIGRATION MANUAL

Past “hello, table.”

Detailed guides, paired examples, and provider notes.
Browse every chapter ↗

+

BRING YOUR DATABASE

One migration system.
Many dialects.

Capabilities follow the database engine. The provider guide explains aliases, drivers and operation-specific behavior.

+

BUILD AND DATABASE TESTS

Evidence from CI.

The CI matrix runs unit tests and 11 database suites, then checks for missing or duplicate test assignments. Counts represent test executions across the matrix; skipped tests are shown separately.

+

Test totals are populated during deployment from the latest completed master CI run. View CI runs and test-result artifacts ↗.

+
+ {{comparison}} +

EXPLICIT CHANGES. A LASTING RECORD.

The next version
starts with a change.

Open the manual ↗Contribute on GitHub →
+
+{{footer}} diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg index 5b4cd95b..db1ba563 100644 --- a/docs/assets/favicon.svg +++ b/docs/assets/favicon.svg @@ -1 +1 @@ - + diff --git a/docs/assets/search-index.json b/docs/assets/search-index.json new file mode 100644 index 00000000..b47070d6 --- /dev/null +++ b/docs/assets/search-index.json @@ -0,0 +1,268 @@ +[ + { + "title": "Your first migration", + "group": "Introduction", + "summary": "Create a SQLite database, apply a versioned change, and write its reverse. Choose either C# style; the runner is the same.", + "url": "guide/quick-start.html", + "text": " Start with the .NET 9 SDK and a console application. The core package supplies schema operations; your ADO.NET package connects to the database. SQLite needs no separate database server for this example. Terminal · either authoring style dotnet new console -n MigrationDemo -f net9.0\ncd MigrationDemo\ndotnet add package DotNetProjects.Migrator\ndotnet add package Microsoft.Data.Sqlite --version 9.0.7 dotnet new console -n MigrationDemo -f net9.0\ncd MigrationDemo\ndotnet add package DotNetProjects.Migrator\ndotnet add package Microsoft.Data.Sqlite --version 9.0.7 Add CreateUsers.cs . Choose one tab and copy that class. Each public migration has a numeric version; do not put both versions of the same example into one assembly. Classic migrations execute provider methods in Up and Down . Fluent migrations collect structured operations in BuildUp and BuildDown . CreateUsers.cs using System.Data;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(1)]\npublic class CreateUsers : Migration\n{\n public override void Up()\n {\n Database.AddTable(\"Users\",\n new Column(\"Id\", DbType.Int32) { IsNullable = false },\n new Column(\"Name\", DbType.String, 255),\n new PrimaryKeyConstraint(\"PK_Users\", \"Id\"));\n }\n\n public override void Down() => Database.RemoveTable(\"Users\");\n} using DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(1)]\npublic class CreateUsers : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n {\n migration.Create.Table(\"Users\")\n .WithColumn(\"Id\").AsInt32().NotNullable()\n .WithColumn(\"Name\").AsString(255)\n .WithPrimaryKey(\"PK_Users\", \"Id\");\n }\n\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Table(\"Users\");\n} Replace Program.cs with the shared host below and run dotnet run . The open connection belongs to this host and is disposed after the provider. The runner discovers public migration classes in the selected assembly. Program.cs · shared runner using DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Providers;\nusing Microsoft.Data.Sqlite;\n\nusing var connection = new SqliteConnection(\"Data Source=app.db\");\nconnection.Open();\nusing var provider = ProviderFactory.Create(\n ProviderTypes.SQLite, connection, defaultSchema: null);\n\nvar runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);\nrunner.MigrateToLastVersion(); using DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Providers;\nusing Microsoft.Data.Sqlite;\n\nusing var connection = new SqliteConnection(\"Data Source=app.db\");\nconnection.Open();\nusing var provider = ProviderFactory.Create(\n ProviderTypes.SQLite, connection, defaultSchema: null);\n\nvar runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);\nrunner.MigrateToLastVersion(); The result is an app.db file containing Users and SchemaInfo . Run again: version 1 is already recorded, so it is skipped. Add a class with [Migration(2)] for your next change. Call runner.MigrateTo(0) to execute the reverse methods for this migration set. Here that drops Users and its data. A reverse migration is a schema operation, not a restore of deleted rows. Test both directions on a disposable database before deployment. Continue with creating tables , or configure scopes, filters and transaction behavior . " + }, + { + "title": "Installation", + "group": "Introduction", + "summary": "The core library, database driver, optional DI integration and CLI each have a distinct job.", + "url": "guide/installation.html", + "text": " Package Purpose DotNetProjects.Migrator Migration classes, providers, runner and fluent operations. An ADO.NET driver Install the driver for the database your host opens. DotNetProjects.Migrator.Extensions.DependencyInjection Optional scoped runner, constructor injection and Microsoft logging. DotNetProjects.Migrator.Tool The migrator command-line tool. Terminal · either authoring style dotnet new console -n MigrationDemo -f net9.0\ncd MigrationDemo\ndotnet add package DotNetProjects.Migrator\ndotnet add package Microsoft.Data.Sqlite --version 9.0.7 dotnet new console -n MigrationDemo -f net9.0\ncd MigrationDemo\ndotnet add package DotNetProjects.Migrator\ndotnet add package Microsoft.Data.Sqlite --version 9.0.7 Common choices are Microsoft.Data.Sqlite, Microsoft.Data.SqlClient, Npgsql, MySql.Data, Oracle.ManagedDataAccess.Core and FirebirdSql.Data.FirebirdClient. The core library does not directly reference these packages. Passing an open connection makes driver selection explicit and keeps connection ownership with your application. Read the provider overview for database families, aliases and CI coverage. A provider name is not a guarantee that every native operation has the same behavior on every server. To develop against a checkout, replace the core package reference with a project reference to src/Migrator/DotNetProjects.Migrator.csproj . The solution targets .NET 9. Building the .slnx solution requires an SDK that understands that format, such as SDK 9.0.200 or later. Keep the library, CLI and optional DI integration on compatible versions. Recompile old migration assemblies when updating a breaking API; the upgrade guide explains the column and constraint changes. " + }, + { + "title": "Configuration", + "group": "Introduction", + "summary": "Select the connection, migration set and history scope first, then set runner options before executing.", + "url": "guide/configuration.html", + "text": " This host fragment assumes an open ADO.NET connection . The provider scope partitions history and selects explicitly scoped classes. An unscoped migration inherits the provider scope. Scopes do not create separate database objects: two modules can still conflict on a table name. Host configuration · both styles using var billingProvider = ProviderFactory.Create(\n ProviderTypes.SQLite, connection, defaultSchema: null, scope: \"billing\");\nbillingProvider.CommandTimeout = 60;\nvar billing = new Migrator(billingProvider, typeof(CreateUsers).Assembly, false);\nbilling.SchemaInfoTableName = \"BillingSchemaInfo\";\nbilling.Options.Tags.Add(\"core\");\nbilling.Options.TagMatch = TagMatchMode.All;\nbilling.Options.TransactionMode = MigrationTransactionMode.WholeSession;\nbilling.MigrateToLastVersion(); using var billingProvider = ProviderFactory.Create(\n ProviderTypes.SQLite, connection, defaultSchema: null, scope: \"billing\");\nbillingProvider.CommandTimeout = 60;\nvar billing = new Migrator(billingProvider, typeof(CreateUsers).Assembly, false);\nbilling.SchemaInfoTableName = \"BillingSchemaInfo\";\nbilling.Options.Tags.Add(\"core\");\nbilling.Options.TagMatch = TagMatchMode.All;\nbilling.Options.TransactionMode = MigrationTransactionMode.WholeSession;\nbilling.MigrateToLastVersion(); Option Behavior Tags / TagMatch Case-sensitive ordinal tags; match Any or All. No filter selects all versioned migrations. Profiles Explicit profile names; selected profiles run after versioned migrations. TransactionMode PerMigration, None or WholeSession. WholeSession supports SQLite, PostgreSQL and SQL Server. Activator Optional delegate for creating migration instances. Lock / LockTimeout Optional cross-process lease acquired before reading history; default timeout is 30 seconds. Set the history table before accessing history or running migrations. Its default name is SchemaInfo ; the default scope is default . Give each runner its intended assembly or explicit migration types. Duplicate versions within an effective scope fail discovery. Load connection strings from application configuration or environment variables. The CLI reads MIGRATOR_CONNECTION by default. Do not store production credentials in migration classes. " + }, + { + "title": "Frequently asked questions", + "group": "Introduction", + "summary": "Decisions to make before adopting the library or moving an existing migration project.", + "url": "guide/faq.html", + "text": " No. Migrations operate on an ADO.NET connection through the transformation provider. Use EF, Dapper, another data layer or direct SQL in the rest of your application. Migrator does not scaffold schema changes from an object model. Yes. Both implement the same migration contract, run through the same loader and share history. Keep each version unique. The tabs throughout these guides show equivalent choices, not two classes to install together. The authoring method names differ: Up/Down versus BuildUp/BuildDown . CreateUsers.cs using System.Data;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(1)]\npublic class CreateUsers : Migration\n{\n public override void Up()\n {\n Database.AddTable(\"Users\",\n new Column(\"Id\", DbType.Int32) { IsNullable = false },\n new Column(\"Name\", DbType.String, 255),\n new PrimaryKeyConstraint(\"PK_Users\", \"Id\"));\n }\n\n public override void Down() => Database.RemoveTable(\"Users\");\n} using DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(1)]\npublic class CreateUsers : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n {\n migration.Create.Table(\"Users\")\n .WithColumn(\"Id\").AsInt32().NotNullable()\n .WithColumn(\"Name\").AsString(255)\n .WithPrimaryKey(\"PK_Users\", \"Id\");\n }\n\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Table(\"Users\");\n} SQLite does not implement every ALTER TABLE operation. Migrator reads the live schema and reconstructs a supported table when a column or constraint change needs it. This is useful without an ORM model. See SQLite for preserved objects, foreign-key checks and reconstruction boundaries. A transaction can roll back a failed migration when its database operations are transactional. Downgrading a completed version executes your reverse method. Neither mechanism recovers rows already deleted by a successful migration. Use an explicit recovery design and backups for that case. The journal records versions, scopes and timestamps, not a content checksum. Editing an applied class will not make it rerun. Add a new migration for a change. Consolidated baselines are an explicit history operation; read versioning and history . Preview renders a structured subset. A provider callback, unsupported constraint alteration or schema dependency after raw SQL cannot be represented reliably and raises an error. Read planning and SQL preview rather than treating preview as a full execution simulation. " + }, + { + "title": "Creating tables", + "group": "Operations", + "summary": "Describe a complete table: columns first, with explicit named keys and constraints.", + "url": "guide/creating-tables.html", + "text": " The table definition groups related schema objects into one operation. Primary-key columns are emitted as non-nullable. In the fluent API a complete table is collected before execution, so keys can refer to columns declared in the same chain. CreateUsers.cs using System.Data;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(1)]\npublic class CreateUsers : Migration\n{\n public override void Up()\n {\n Database.AddTable(\"Users\",\n new Column(\"Id\", DbType.Int32) { IsNullable = false },\n new Column(\"Name\", DbType.String, 255),\n new PrimaryKeyConstraint(\"PK_Users\", \"Id\"));\n }\n\n public override void Down() => Database.RemoveTable(\"Users\");\n} using DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(1)]\npublic class CreateUsers : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n {\n migration.Create.Table(\"Users\")\n .WithColumn(\"Id\").AsInt32().NotNullable()\n .WithColumn(\"Name\").AsString(255)\n .WithPrimaryKey(\"PK_Users\", \"Id\");\n }\n\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Table(\"Users\");\n} Use the declared key order consistently in both primary and foreign keys. A composite unique constraint applies to the tuple; it does not make each column unique separately. The fully qualified constraint type below avoids the name collision with System.Data.UniqueConstraint. A table with an ordered composite key Database.AddTable(\"Subscriptions\",\n new Column(\"TenantId\", DbType.Int32),\n new Column(\"UserId\", DbType.Int32),\n new Column(\"Email\", DbType.String, 255),\n new PrimaryKeyConstraint(\"PK_Subscriptions\", \"TenantId\", \"UserId\"),\n new DotNetProjects.Migrator.Framework.UniqueConstraint(\n \"UQ_Subscriptions_Email\", \"TenantId\", \"Email\")); migration.Create.Table(\"Subscriptions\")\n .WithColumn(\"TenantId\").AsInt32()\n .WithColumn(\"UserId\").AsInt32()\n .WithColumn(\"Email\").AsString(255)\n .WithPrimaryKey(\"PK_Subscriptions\", \"TenantId\", \"UserId\")\n .WithUniqueConstraint(\"UQ_Subscriptions_Email\", \"TenantId\", \"Email\"); Identity generation is a column attribute, separate from primary-key membership. SQLite requires an INTEGER identity column and its single-column primary key in the same definition. Use a complete Create.Table/AddTable operation to satisfy that rule. To reverse creation use Database.RemoveTable or migration.Delete.Table ; dropping a table also removes its rows. For supported creation operations, automatic reversal can derive the reverse operation. Explicitly author reverse behavior for destructive changes. " + }, + { + "title": "Altering tables", + "group": "Operations", + "summary": "Rename objects and evolve populated tables while preserving the schema details you still need.", + "url": "guide/altering-tables.html", + "text": " Use explicit old and new names. The column rename signature is table, old name, new name in both APIs. A table rename does not rename explicit constraints or their backing indexes. Reusing the original key name for a replacement table may collide on SQL Server or PostgreSQL. Rename existing objects Database.RenameTable(\"Users\", \"Members\");\nDatabase.RenameColumn(\"Members\", \"Name\", \"DisplayName\"); migration.Rename.Table(\"Users\", \"Members\");\nmigration.Rename.Column(\"Members\", \"Name\", \"DisplayName\"); Supply the type, length, nullability, default and collation you intend to retain. ChangeColumn replaces the column definition; it does not infer that table constraints should be created or removed. Existing rows must remain valid for the new definition. Widen a required display name Database.ChangeColumn(\"Users\", new Column(\"Name\", DbType.String, 500)\n{\n IsNullable = false\n}); migration.Alter.Column(\"Name\", \"Users\")\n .AsString(500).NotNullable(); Add a nullable column, deploy code that can read both forms, backfill values, then enforce the final requirement in a later migration. Large data copies and index creation can hold locks for substantial time; test them against a representative dataset. On SQLite, a supported alteration may recreate the table and copy rows. On Oracle and some other engines, DDL may commit implicitly. Review the transaction guide and your provider page before choosing the deployment boundary. " + }, + { + "title": "Columns and data types", + "group": "Operations", + "summary": "Type, size, precision, nullability, defaults, identity and collation are explicit column attributes.", + "url": "guide/columns.html", + "text": " Column builders take column name followed by table name. Classic AddColumn takes table name first. New nullable columns accept existing rows without a backfill. A required column usually needs a compatible default or a staged data migration. Add an optional email address Database.AddColumn(\"Users\", new Column(\"Email\", DbType.String, 320)); migration.Create.Column(\"Email\", \"Users\").AsString(320).Nullable(); Remove the email column Database.RemoveColumn(\"Users\", \"Email\"); migration.Delete.Column(\"Email\", \"Users\"); For decimal values specify precision and scale. In a Column constructor an integer after the type is the size, not a numeric default. Set DefaultValue explicitly to avoid overload ambiguity. Plain strings are values; trusted SQL expressions use RawSql.Insert. An amount with four decimal places Database.AddColumn(\"Orders\", new Column(\"Amount\", DbType.Decimal)\n{\n Precision = 12, Scale = 4, IsNullable = false, DefaultValue = 0m\n}); migration.Create.Column(\"Amount\", \"Orders\").OfType(DbType.Decimal)\n .WithPrecision(12, 4).NotNullable().WithDefaultValue(0m); Use TimeOnly for time-of-day values and TimeSpan for intervals. A TimeSpan is a duration, including negative and multi-day values, so a TimeSpan default on a Time column is rejected. PostgreSQL and Oracle have native intervals; SQLite, SQL Server and MySQL/MariaDB store intervals as signed .NET ticks. Clock time and elapsed time Database.AddTable(\"Jobs\",\n new Column(\"RunAt\", DbType.Time) { DefaultValue = new TimeOnly(9, 30) },\n new Column(\"Elapsed\", MigratorDbType.Interval) { DefaultValue = TimeSpan.Zero }); migration.Create.Table(\"Jobs\")\n .WithColumn(\"RunAt\").OfType(DbType.Time).WithDefaultValue(new TimeOnly(9, 30))\n .WithColumn(\"Elapsed\").OfType(MigratorDbType.Interval).WithDefaultValue(TimeSpan.Zero); SQLite does not enforce declared string lengths or decimal precision. UInt64 values above Int64.MaxValue are rejected there. Oracle character empty strings become NULL; Informix and Sybase have their own trimming and range behavior. Consult the type support and boundary matrix for supported mappings and live-test scope. A shared DbType does not imply identical native storage. " + }, + { + "title": "Data operations", + "group": "Operations", + "summary": "Insert, update and delete using explicit column/value arrays. Keep predicates separate from changed values.", + "url": "guide/data.html", + "text": " Column and value arrays must have the same length. The provider binds values using its driver-specific parameter mappings. For multiple rows issue multiple operations; the fluent Row method describes one row, not an accumulated collection of rows. Insert a user Database.Insert(\"Users\", new[] { \"Id\", \"Name\" }, new object[] { 1, \"Ada\" }); migration.Insert.IntoTable(\"Users\")\n .Row(new[] { \"Id\", \"Name\" }, new object[] { 1, \"Ada\" }); Without a predicate, update/delete affects every row. Supply predicate columns and values deliberately. Fluent WhereSql is available for updates only; its text is trusted SQL, not an escaped user input. Update one user Database.Update(\"Users\", new[] { \"Name\" }, new object[] { \"Ada Lovelace\" },\n new[] { \"Id\" }, new object[] { 1 }); migration.Update.Table(\"Users\")\n .Set(new[] { \"Name\" }, new object[] { \"Ada Lovelace\" })\n .Where(new[] { \"Id\" }, new object[] { 1 }); Delete one user Database.Delete(\"Users\", new[] { \"Id\" }, new object[] { 1 }); migration.Delete.FromTable(\"Users\").Where(new[] { \"Id\" }, new object[] { 1 }); Use an explicit identifying predicate when a seed should exist only once. This is distinct from a migration version: a named profile can run repeatedly without a history entry. Coordinate competing writers; a check-then-insert helper is not a substitute for a database unique key. Insert a missing seed Database.InsertIfNotExists(\"Users\", new[] { \"Id\", \"Name\" },\n new object[] { 1, \"Ada\" }, new[] { \"Id\" }, new object[] { 1 }); migration.Insert.IntoTable(\"Users\")\n .Row(new[] { \"Id\", \"Name\" }, new object[] { 1, \"Ada\" })\n .IfNotExists(new[] { \"Id\" }, new object[] { 1 }); Use the provider CopyDataFromTableToTable helper or fluent Execute.CopyData for named-column copies. Both tables must already exist and target columns must accept the source values. Execute.UpdateFrom maps source/target pairs. These operations retain provider limits and are outside the SQL-preview subset. A reverse data migration needs authored recovery logic; auto-reversal cannot recreate deleted or overwritten values. Copy users into an archive table Database.CopyDataFromTableToTable(\"Users\",\n new System.Collections.Generic.List { \"Id\", \"Name\" }, \"ArchivedUsers\",\n new System.Collections.Generic.List { \"UserId\", \"DisplayName\" }); migration.Execute.CopyData(\"Users\", new[] { \"Id\", \"Name\" },\n \"ArchivedUsers\", new[] { \"UserId\", \"DisplayName\" });" + }, + { + "title": "Schema inspection", + "group": "Operations", + "summary": "Read the connected database before deciding what to change. Metadata is different from a model snapshot.", + "url": "guide/schema.html", + "text": " Classic migrations read through Database. FluentMigration exposes Schema for queries and Context for the full provider API. A fluent authoring method runs before its queued operations: an inspection cannot see a table merely queued earlier in the same builder. Add a column only when it is missing if (!Database.ColumnExists(\"Users\", \"Email\"))\n Database.AddColumn(\"Users\", new Column(\"Email\", DbType.String, 320)); if (!Schema.Table(\"Users\").ColumnExists(\"Email\"))\n migration.Create.Column(\"Email\", \"Users\").AsString(320); GetColumns returns inferred column attributes, not primary/unique membership flags. It is obsolete because native types and defaults cannot be mapped back to exact .NET definitions; use migration history for the original definition. Read typed table constraints to retain ordered composite keys. Unique indexes remain index metadata. MySQL/MariaDB catalogs cannot distinguish every original unique-index versus UNIQUE-clause authoring choice. Read table constraint definitions var constraints = Database.GetTableConstraints(\"Users\");\nforeach (var constraint in constraints)\n Console.WriteLine(constraint.Name); var constraints = Schema.Table(\"Users\").ConstraintDefinitions();\nforeach (var constraint in constraints)\n Console.WriteLine(constraint.Name); ViewField selects columns from a base table. The alternative IViewElement overload represents explicit columns and joins. View definitions are provider-dependent and outside SQL preview and automatic reversal. Write a provider-appropriate DROP VIEW statement in the reverse method, and manage dependent views when changing their underlying tables. A projection over Users Database.AddView(\"UserNames\", \"Users\", new ViewField(\"Id\"), new ViewField(\"Name\")); migration.Create.View(\"UserNames\", \"Users\", new ViewField(\"Id\"), new ViewField(\"Name\")); Dispose readers and commands obtained from the provider. Fluent Schema.Query and Select accept a reader callback and handle disposal. Use provider quoting helpers for table and column identifiers separately: quoting a table may introduce schema qualification, which is not valid for a column expression. Metadata fidelity depends on the provider. Unsupported readers throw instead of pretending that an empty schema was found. A successful existence check is not a full schema-drift report. " + }, + { + "title": "Execute SQL and scripts", + "group": "Operations", + "summary": "Use schema operations where they fit, and keep database-specific SQL explicit.", + "url": "guide/sql.html", + "text": " Raw SQL passes through to the selected database. It does not translate between dialects. Values from application input should be bound through a command; migration SQL is trusted application code. A SQL data change Database.ExecuteNonQuery(\"UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL\"); migration.Execute.Sql(\"UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL\"); ExecuteScript reads a file; ExecuteResourceScript reads an assembly resource. Fluent equivalents capture script text as dedicated operations. Make files available at deployment and set resource names explicitly. Relative file paths are resolved against the process working directory. Execute a SQL file Database.ExecuteScript(\"Scripts/backfill.sql\"); migration.Execute.Script(\"Scripts/backfill.sql\"); Execute an embedded SQL resource Database.ExecuteResourceScript(GetType().Assembly, \"MyMigrations.Scripts.backfill.sql\"); migration.Execute.EmbeddedScript(GetType().Assembly, \"MyMigrations.Scripts.backfill.sql\"); For the second example mark backfill.sql as an EmbeddedResource in the migration project and use its actual manifest resource name. Missing resources fail before script execution. The script APIs split standalone SQL Server GO lines, including optional line comments, while respecting strings, quoted identifiers and nested comments. GO repetition and SQLCMD directives fail before any batches execute. ExecuteNonQuery and Execute.Sql do not split client separators. Other providers receive one command unless they implement IScriptBatchProvider. A database SQL file is not necessarily compatible with SQL*Plus, mysql-client or isql command syntax. Raw SQL also invalidates planned schema knowledge during SQL preview. " + }, + { + "title": "Commands and callbacks", + "group": "Operations", + "summary": "Use the active provider connection when a migration needs driver-level work.", + "url": "guide/connections.html", + "text": " The provider creates a command associated with its current transaction. Dispose it after use. Generate parameter names through the provider, rather than assuming every driver uses the same convention. The callback is deferred until fluent execution reaches it. Execute a parameterized command using var command = Database.CreateCommand();\nvar name = Database.GenerateParameterName(0);\ncommand.CommandText = \"UPDATE Users SET Name = \" + name + \" WHERE Id = 1\";\nvar value = command.CreateParameter();\nvalue.ParameterName = name;\nvalue.Value = \"Ada\";\ncommand.Parameters.Add(value);\ncommand.ExecuteNonQuery(); migration.Execute.WithProvider(provider =>\n{\n using var command = provider.CreateCommand();\n var name = provider.GenerateParameterName(0);\n command.CommandText = \"UPDATE Users SET Name = \" + name + \" WHERE Id = 1\";\n var value = command.CreateParameter();\n value.ParameterName = name;\n value.Value = \"Ada\";\n command.Parameters.Add(value);\n command.ExecuteNonQuery();\n}); WithCommand creates and disposes a provider command around your action. WithConnection exposes the connection; WithProvider exposes the complete transformation provider. Do not close or replace a runner-owned connection, commit its transaction or switch databases while holding a native migration lock. Database creation and other administration require a connection and identity authorized for that operation. Use a dedicated host with TransactionMode.None. Fluent administration rejects an active transaction; do not combine it with WholeSession or assume a Classic provider call can participate in transactional DDL. Create a database on a supporting server Database.CreateDatabases(\"Reporting\"); migration.Administration.CreateDatabase(\"Reporting\"); The remaining mappings are DropDatabases / Administration.DropDatabase, SwitchDatabase / Administration.SwitchDatabase, and KillDatabaseConnections / Administration.KillConnections. These are explicit administrative actions with provider-specific support. Database switches invalidate assumptions about migration history and session locks: keep provisioning separate from ordinary schema migrations. They are outside SQL preview and automatic reversal. Callbacks can perform arbitrary C# work and cannot be translated into SQL preview. They require explicit reverse behavior. Keeping external network calls out of migration bodies makes failures easier to reason about: a database rollback cannot undo an email or an HTTP request. " + }, + { + "title": "Indexes", + "group": "Schema basics", + "summary": "An index is a separate schema object, even when it enforces uniqueness.", + "url": "guide/indexes.html", + "text": " Use an explicit name so the index can be inspected or removed later. Both APIs accept the same Index definition. Fully qualify this type if System.Index is also in scope. Columns retain the order in KeyColumns. Index a user name Database.AddIndex(\"Users\", new DotNetProjects.Migrator.Framework.Index\n{\n Name = \"IX_Users_Name\", KeyColumns = new[] { \"Name\" }, Unique = false\n}); migration.Create.Index(\"Users\", new DotNetProjects.Migrator.Framework.Index\n{\n Name = \"IX_Users_Name\", KeyColumns = new[] { \"Name\" }, Unique = false\n}); Drop an index Database.RemoveIndex(\"Users\", \"IX_Users_Name\"); migration.Delete.Index(\"IX_Users_Name\", \"Users\"); Index definitions also expose IncludeColumns, FilterItems and Clustered. These options are provider-specific. Oracle rejects included and clustered index requests; SQLite reconstruction rejects existing index SQL with explicit COLLATE clauses. Preview handles simple indexes and rejects unsupported options. Use UniqueConstraint for a table-level invariant and an Index with Unique for an index definition. Do not infer ownership from a generated name. SQLite RemoveAllIndexes preserves declared table UNIQUE constraints; remove those through the constraint APIs. Check query plans and data cardinality when choosing index keys. " + }, + { + "title": "Keys and constraints", + "group": "Schema basics", + "summary": "Declare table invariants independently of column attributes.", + "url": "guide/constraints.html", + "text": " Existing rows must satisfy a new constraint. A rebuild or ALTER operation can fail if duplicate or invalid data is present. CHECK expressions are trusted SQL and depend on the target engine. Primary keys, unique constraints, foreign keys and checks have typed definitions. Add two named constraints Database.AddUniqueConstraint(\"UQ_Users_Name\", \"Users\", \"Name\");\nDatabase.AddCheckConstraint(\"CK_Users_Id\", \"Users\", \"Id > 0\"); migration.Create.Unique(\"UQ_Users_Name\", \"Users\", \"Name\");\nmigration.Create.Check(\"CK_Users_Id\", \"Users\", \"Id > 0\"); Use dedicated primary-key and foreign-key removal methods; generic RemoveConstraint is for unique/check constraints in the SQLite provider. Avoid RemoveAllConstraints unless the migration deliberately replaces every invariant. Remove a check constraint Database.RemoveConstraint(\"Users\", \"CK_Users_Id\"); migration.Delete.Constraint(\"CK_Users_Id\", \"Users\"); GetTableConstraints returns ordered typed definitions. SQLite can return a null name for an unnamed legacy constraint; an autoindex name is not a substitute constraint name. PrimaryKeyExists checks the actual key name. MySQL reports the primary key name as PRIMARY. Altering a column does not give that column ownership of a unique constraint. SQL Server implicit ownership markers are no longer used for deletion. Explicitly remove only the object your migration intends to change. " + }, + { + "title": "Foreign keys", + "group": "Schema basics", + "summary": "Define ordered child/parent columns and independent actions for update and delete.", + "url": "guide/foreign-keys.html", + "text": " Parent key columns must identify a suitable primary/unique key. Child and parent arrays are positional: each child column corresponds to the parent column at the same index. Both tables and their compatible columns must already exist for this example. The Classic example uses IForeignKeyActions for independent update/delete actions; the older AddForeignKey overload supplies one action for both. Orders belong to users ((IForeignKeyActions)Database).AddForeignKey(\n \"FK_Orders_Users\", \"Orders\", new[] { \"UserId\" },\n \"Users\", new[] { \"Id\" }, ForeignKeyConstraintType.Cascade,\n ForeignKeyConstraintType.NoAction); migration.Create.ForeignKey(\"FK_Orders_Users\",\n \"Orders\", new[] { \"UserId\" }, \"Users\", new[] { \"Id\" },\n onDelete: ForeignKeyConstraintType.Cascade,\n onUpdate: ForeignKeyConstraintType.NoAction); Remove dependent keys before incompatible table or key changes. Restore them only after the existing data satisfies the replacement relationship. Remove the foreign key Database.RemoveForeignKey(\"Orders\", \"FK_Orders_Users\"); migration.Delete.ForeignKey(\"FK_Orders_Users\", \"Orders\"); Supported actions depend on the database; do not assume every engine implements CASCADE, RESTRICT, SET NULL and SET DEFAULT identically. SQLite rebuilds preserve separate update/delete actions and validate integrity before an owned transaction commits. MATCH FULL and MATCH PARTIAL requests are rejected because SQLite does not enforce those semantics. SetNull needs nullable child columns. Test action behavior using actual data, especially composite keys and partially NULL values. Oracle supports its own subset of foreign-key actions. " + }, + { + "title": "Defaults and collations", + "group": "Schema basics", + "summary": "Distinguish values from SQL expressions, and comparison intent from a provider's installed collation name.", + "url": "guide/defaults-collations.html", + "text": " Ordinary strings are quoted literal values. RawSql.Insert marks trusted SQL to evaluate on the database. The expression below works on SQLite; provider function names and return types can differ. A database-generated timestamp Database.AddTable(\"Events\", new Column(\"CreatedAt\", DbType.DateTime)\n{\n DefaultValue = RawSql.Insert(\"CURRENT_TIMESTAMP\"), IsNullable = false\n}); migration.Create.Table(\"Events\")\n .WithColumn(\"CreatedAt\").OfType(DbType.DateTime).NotNullable()\n .WithDefaultValue(RawSql.Insert(\"CURRENT_TIMESTAMP\")); Collation presets request semantics. Unsupported mappings fail before DDL. SQLite AsciiIgnoreCase maps to NOCASE and folds ASCII only; it is not Unicode case folding. Named custom SQLite collations must be registered on the connection before schema or data operations use them. ASCII-insensitive SQLite text Database.AddTable(\"Labels\", new Column(\"Name\", DbType.String, 100)\n{\n Collation = Collation.AsciiIgnoreCase\n}); migration.Create.Table(\"Labels\")\n .WithColumn(\"Name\").AsString(100)\n .WithCollation(Collation.AsciiIgnoreCase); Request Meaning CaseInsensitive / CaseSensitive Case behavior with accent sensitivity; supported SQL Server/MySQL/MariaDB mappings, or an explicit installed name on other engines. Binary Provider binary comparison; not a promise of identical linguistic ordering. AsciiIgnoreCase SQLite NOCASE; ASCII letters only. Collation.Named(name) An installed or registered provider-specific collation. Use named collations for language-specific or exact comparison behavior. PostgreSQL ICU nondeterministic collations must be created explicitly; SQL rendering does not create shared database objects. Read the mapping table for engine versions and restrictions. " + }, + { + "title": "Choose a runner", + "group": "Migration runners", + "summary": "Use the same migration assembly in a dedicated host, a DI scope or the command-line tool.", + "url": "guide/runners.html", + "text": " Runner A good fit Library host A small deployment executable with explicit connection ownership and full provider access. Microsoft DI integration A service collection supplying constructor dependencies, options and logging. migrator CLI Automation that selects assemblies, providers, scopes, tags and target versions. Run schema changes before application instances need the new schema. The host below works with either migration style and scans the assembly containing CreateUsers. Use explicit type selection when an assembly also contains migrations for other purposes. Program.cs · shared runner using DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Providers;\nusing Microsoft.Data.Sqlite;\n\nusing var connection = new SqliteConnection(\"Data Source=app.db\");\nconnection.Open();\nusing var provider = ProviderFactory.Create(\n ProviderTypes.SQLite, connection, defaultSchema: null);\n\nvar runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);\nrunner.MigrateToLastVersion(); using DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Providers;\nusing Microsoft.Data.Sqlite;\n\nusing var connection = new SqliteConnection(\"Data Source=app.db\");\nconnection.Open();\nusing var provider = ProviderFactory.Create(\n ProviderTypes.SQLite, connection, defaultSchema: null);\n\nvar runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);\nrunner.MigrateToLastVersion(); Give the deployment identity the schema privileges needed by the selected migrations. Coordinate concurrent deploys through an external orchestrator or supported native lock. Configure the history table and scope consistently across invocations. Log the target and result without exposing connection strings. Choose the CLI for a ready command surface, or DI for application services. Read transaction and lock semantics before relying on atomicity. " + }, + { + "title": "Command-line tool", + "group": "Migration runners", + "summary": "List, validate, plan, preview, apply and reverse migrations from a deployment script.", + "url": "guide/cli.html", + "text": " Install DotNetProjects.Migrator.Tool as a .NET tool. Set MIGRATOR_CONNECTION in the deployment environment or select another variable with --connection-env. Both Classic and Fluent classes use the same commands. The tool does not print the connection-string value. Install the CLI dotnet tool install --global DotNetProjects.Migrator.Tool dotnet tool install --global DotNetProjects.Migrator.Tool Inspect a migration assembly migrator list --assembly MyMigrations.dll --provider SQLite\nmigrator status --assembly MyMigrations.dll --provider SQLite\nmigrator validate --assembly MyMigrations.dll --provider SQLite\nmigrator plan --assembly MyMigrations.dll --provider SQLite --target 10\nmigrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql migrator list --assembly MyMigrations.dll --provider SQLite\nmigrator status --assembly MyMigrations.dll --provider SQLite\nmigrator validate --assembly MyMigrations.dll --provider SQLite\nmigrator plan --assembly MyMigrations.dll --provider SQLite --target 10\nmigrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql Validate checks version planning, not arbitrary migration-body behavior. Plan lists version steps without running bodies. SQL generation renders a supported operation subset; it does not produce an idempotent history-managed bundle. Deploy a selected scope migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession\nmigrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0 migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession\nmigrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0 Rollback requires an explicit lower target and rejects any plan containing upward steps. Target checks run after taking the configured lock and refreshing history. Tags, profiles and provider choice must match the intended deployment. Option Purpose --tags a,b / --tag-match Any|All Filter versioned migrations. --profiles a,b Select named profiles. --schema / --scope Provider schema and migration history scope. --timeout SECONDS Database command timeout. --lock / --lock-timeout SECONDS Native migration lock on supported providers. --offline SQL generation assuming empty history; profiles/maintenance rejected. Exit codes: 0 success; 1 load/execution failure; 2 invalid arguments; 3 unsupported provider/operation; 4 lock timeout. SQL output can contain data authored in migrations. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird; use a custom host for other library providers. " + }, + { + "title": "Dependency injection and logging", + "group": "Migration runners", + "summary": "Resolve the runner and migration dependencies inside one service scope.", + "url": "guide/dependency-injection.html", + "text": " Install DotNetProjects.Migrator.Extensions.DependencyInjection and Microsoft.Extensions.Logging alongside the core and database driver. This example uses the connection-string provider factory so provider disposal belongs to the DI scope. The providerName explicitly selects the SQLite driver. A scoped migration host using DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Providers;\nusing DotNetProjects.Migrator.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection;\n\nvar services = new ServiceCollection();\nservices.AddLogging();\nservices.AddMigrator(_ => ProviderFactory.Create(\n ProviderTypes.SQLite, \"Data Source=app.db\", defaultSchema: null,\n providerName: \"Microsoft.Data.Sqlite\"), typeof(CreateUsers).Assembly,\n options => options.TransactionMode = MigrationTransactionMode.PerMigration);\n\nusing var container = services.BuildServiceProvider();\nusing var scope = container.CreateScope();\nscope.ServiceProvider.GetRequiredService ().MigrateToLastVersion(); using DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Providers;\nusing DotNetProjects.Migrator.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection;\n\nvar services = new ServiceCollection();\nservices.AddLogging();\nservices.AddMigrator(_ => ProviderFactory.Create(\n ProviderTypes.SQLite, \"Data Source=app.db\", defaultSchema: null,\n providerName: \"Microsoft.Data.Sqlite\"), typeof(CreateUsers).Assembly,\n options => options.TransactionMode = MigrationTransactionMode.PerMigration);\n\nusing var container = services.BuildServiceProvider();\nusing var scope = container.CreateScope();\nscope.ServiceProvider.GetRequiredService ().MigrateToLastVersion(); Migration classes are registered for activation through the service provider. Register your own constructor dependencies before resolving the runner. Options are scoped snapshots; a custom Activator can override construction. Fluent and Classic migrations use the same activation mechanism. The integration adapts runner lifecycle events to Microsoft logging. It omits SQL text and raw exception messages from these events. Configure your own logging providers through AddLogging. The core retains its lightweight logger API when you do not use DI. Dispose the scope after migration execution. When supplying a caller-owned open connection, keep its owner alive until after the scope is disposed; the provider does not acquire ownership of an externally supplied connection. " + }, + { + "title": "Planning and SQL preview", + "group": "Migration runners", + "summary": "A version plan answers what runs. SQL preview shows the supported operation SQL.", + "url": "guide/preview.html", + "text": " Plan and DryRun inspect applied versions through IMigrationHistory without creating/upgrading history or invoking migration bodies, callbacks, transactions or SQLite PRAGMA changes. Set the same scope, tags and assembly you intend to deploy. The fragment below assumes an initialized runner. Inspect version steps var plan = runner.Plan(10);\nforeach (var step in plan)\n Console.WriteLine($\"{step.Version}: {(step.IsUp ? \"up\" : \"down\")}\");\nrunner.DryRun = true;\nrunner.MigrateTo(10); var plan = runner.Plan(10);\nforeach (var step in plan)\n Console.WriteLine($\"{step.Version}: {(step.IsUp ? \"up\" : \"down\")}\");\nrunner.DryRun = true;\nrunner.MigrateTo(10); PreviewSql reads connected history and schema. Classic bodies require explicit opt-in; provider calls are captured through a proxy that rejects unsupported access. Fluent authoring builds operations directly. This is trusted C# execution in both cases, not a security sandbox. Generate operation SQL var sql = runner.PreviewSql(10, ProviderTypes.SQLite, allowLegacyBodies: true);\nConsole.WriteLine(sql); var sql = runner.PreviewSql(10, ProviderTypes.SQLite);\nConsole.WriteLine(sql); MigrationSqlPreview.Generate can render supported operations without connecting; the CLI exposes --offline. Earlier structured create/rename operations update the planned schema. Raw SQL invalidates that knowledge, so later dependencies can fail. Basic tables/columns, supported renames, simple indexes, inserts and raw SQL form the preview subset. Unsupported alterations, constraint changes, filters, callbacks and schema dependencies throw. InitializeOnce overrides are rejected rather than skipped silently. Post-commit callbacks do not run. Output contains operation SQL, not history guards or an idempotent deployment bundle. " + }, + { + "title": "Transactions and locks", + "group": "Migration runners", + "summary": "Transaction rollback and cross-process coordination solve different problems.", + "url": "guide/transactions.html", + "text": " Mode Behavior PerMigration Default. Each successful migration commits independently. None Provider/operation transaction behavior; no runner-managed migration transaction. WholeSession One session transaction on SQLite, PostgreSQL or SQL Server; history initialization happens first. Actual atomicity depends on the database and operation. Administration commands or implicit-commit DDL can violate assumptions. AfterUp/AfterDown run after commit; WholeSession defers them until the session commit. A callback failure cannot undo durable changes. Configure a session transaction runner.Options.TransactionMode = MigrationTransactionMode.WholeSession;\nrunner.MigrateToLastVersion(); runner.Options.TransactionMode = MigrationTransactionMode.WholeSession;\nrunner.MigrateToLastVersion(); DatabaseMigrationLock uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. The lease is session-owned and remains held across migration commits. This host fragment assumes a supported provider; SQLite rejects this built-in lock. Acquire a native deployment lock runner.Options.Lock = new DatabaseMigrationLock();\nrunner.Options.LockTimeout = TimeSpan.FromSeconds(60);\nrunner.MigrateToLastVersion(); runner.Options.Lock = new DatabaseMigrationLock();\nrunner.Options.LockTimeout = TimeSpan.FromSeconds(60);\nrunner.MigrateToLastVersion(); Native locks are keyed by database, history table and scope. Coordinate separately if different scopes modify shared objects. Do not close/replace the connection, switch databases or manipulate the native lock inside a migration. MySQL named locks coordinate one server, not an entire distributed cluster. Implement IMigrationLock for another lease mechanism, or serialize deployments outside the process. A transaction, history primary key or ordinary database write lock alone does not prove that the whole migration sequence is serialized. " + }, + { + "title": "Versioning and scoped history", + "group": "Migration types", + "summary": "Number changes, keep applied source immutable and give independent modules explicit histories.", + "url": "guide/versioning.html", + "text": " Migration accepts a numeric version or year/month/day/hour/minute/second components. Use one monotonic scheme per migration set. The date constructor builds a numeric identifier; it does not consult a clock or resolve branch collisions for you. Missing lower-numbered versions up to the target can still be applied. A dated migration using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(2026, 9, 23, 10, 0, 0)]\npublic class AddUserEmail : Migration\n{\n public override void Up() => Database.AddColumn(\"Users\", new Column(\"Email\", DbType.String, 320));\n public override void Down() => Database.RemoveColumn(\"Users\", \"Email\");\n} using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(2026, 9, 23, 10, 0, 0)]\npublic class AddUserEmail : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n => migration.Create.Column(\"Email\", \"Users\").AsString(320);\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Column(\"Email\", \"Users\");\n} An explicit MigrationAttribute.Scope selects that migration only for the matching provider scope. Unscoped migrations inherit the runner scope. Discovery, duplicate validation and history reads use the effective scope. Duplicate numeric versions in distinct explicit scopes are independent; physical tables are not isolated. Set SchemaInfoTableName before any history access if you need a different table. AppliedMigrations lists recorded versions; LastAppliedMigrationVersion is nullable when history is empty. AssemblyLastMigrationVersion describes the loaded set. A baseline can record versions whose schema it already includes. The runner rechecks active-scope history before each planned step, skipping newly covered versions and their AfterUp callbacks. Downward runs similarly skip versions removed by an earlier Down. Recording the baseline version itself does not create a duplicate. Mark a version included by a baseline Database.MigrationApplied(1, \"billing\"); migration.Execute.WithProvider(provider => provider.MigrationApplied(1, \"billing\")); Only record a version after establishing the schema it represents. History entries are not a substitute for verifying an existing database. Schema/history rollback follows the selected transaction mode. No migration-content checksum is stored. " + }, + { + "title": "Tags", + "group": "Migration types", + "summary": "Select a subset of versioned migrations using explicit ordinal names.", + "url": "guide/tags.html", + "text": " Tags is in DotNetProjects.Migrator. One class can declare multiple names. Choose names for deployment intent such as core or reporting; do not use a tag to hide a dependency that a selected migration still requires. A reporting migration using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(2), Tags(\"reporting\")]\npublic class CreateReportLog : Migration\n{\n public override void Up() => Database.AddTable(\"ReportLog\", new Column(\"Name\", DbType.String, 255));\n public override void Down() => Database.RemoveTable(\"ReportLog\");\n} using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(2), Tags(\"reporting\")]\npublic class CreateReportLog : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n => migration.Create.Table(\"ReportLog\").WithColumn(\"Name\").AsString(255);\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Table(\"ReportLog\");\n} Select tags on the runner runner.Options.Tags.Add(\"reporting\");\nrunner.Options.TagMatch = TagMatchMode.Any;\nrunner.MigrateToLastVersion(); runner.Options.Tags.Add(\"reporting\");\nrunner.Options.TagMatch = TagMatchMode.Any;\nrunner.MigrateToLastVersion(); Any requires at least one selected tag; All requires every selected tag. Matching is ordinal and case-sensitive. Without a tag filter all eligible versioned migrations are selected. Profiles have their own explicit name selection. Applied versions excluded by the active filter remain applied during downgrade. A filtered run is therefore not a promise that the whole database matches one contiguous global version range. Keep deployment filters stable and inspect the plan before reversing selected changes. " + }, + { + "title": "Profiles", + "group": "Migration types", + "summary": "Run explicitly selected work after versioned migrations without recording a version.", + "url": "guide/profiles.html", + "text": " A profile is useful for optional seed data or environment setup. It runs every time its name is selected. Make repeated execution deliberate: use an identifying predicate or other idempotent operation where appropriate. A development seed profile using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\n\n[Profile(\"demo\", Order = 10)]\npublic class DemoData : Migration\n{\n public override void Up() => Database.InsertIfNotExists(\"Users\",\n new[] { \"Id\", \"Name\" }, new object[] { 1, \"Ada\" },\n new[] { \"Id\" }, new object[] { 1 });\n public override void Down() { }\n} using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Profile(\"demo\", Order = 10)]\npublic class DemoData : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n => migration.Insert.IntoTable(\"Users\")\n .Row(new[] { \"Id\", \"Name\" }, new object[] { 1, \"Ada\" })\n .IfNotExists(new[] { \"Id\" }, new object[] { 1 });\n public override void BuildDown(MigrationBuilder migration) { }\n} Run the demo profile runner.Options.Profiles.Add(\"demo\");\nrunner.MigrateToLastVersion(); runner.Options.Profiles.Add(\"demo\");\nrunner.MigrateToLastVersion(); Profiles accept Order and Scope. Execution orders by Order and then ordinal full type name. Profile execution uses Up and does not create a migration-version entry or use Down as an undo history. An auxiliary-only run preserves existing version history. A selected profile runs because it was selected, not because its source checksum changed. Treat this separately from versioned migrations and checksum-based repeatable SQL. Offline CLI SQL generation rejects profiles because it cannot represent the complete lifecycle. " + }, + { + "title": "Maintenance migrations", + "group": "Migration types", + "summary": "Place ordered work at the runner's lifecycle stages.", + "url": "guide/maintenance.html", + "text": " Stage When BeforeRun Before versioned migration work in this run. BeforeMigration Before each executed versioned migration. AfterMigration After each executed versioned migration. AfterRun After the run's migration/profile work. Maintenance classes accept Order and Scope. They use Up and do not acquire version records. Hooks stop on failure; later stages are not finally blocks or guaranteed cleanup paths. Lock release and connection/transaction restoration are runner responsibilities. The example expects an existing DeploymentLog table. Choose a table that already exists at the selected stage. Fluent callbacks execute at the corresponding operation position. Write a deployment marker using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\n\n[Maintenance(MaintenanceStage.AfterRun, Order = 10)]\npublic class RecordDeployment : Migration\n{\n public override void Up()\n => Database.Insert(\"DeploymentLog\", new[] { \"Message\" }, new object[] { \"Migration run finished\" });\n public override void Down() { }\n} using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Maintenance(MaintenanceStage.AfterRun, Order = 10)]\npublic class RecordDeployment : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n => migration.Insert.IntoTable(\"DeploymentLog\")\n .Row(new[] { \"Message\" }, new object[] { \"Migration run finished\" });\n public override void BuildDown(MigrationBuilder migration) { }\n} Migration.AfterUp and AfterDown run after commit, with the migration context restored. In WholeSession they wait until the entire session commits. Their failure reports an error after durable changes; it cannot reverse that commit. Do not confuse maintenance stages with a guaranteed post-commit delivery system. " + }, + { + "title": "Automatic reversal", + "group": "Migration types", + "summary": "Fluent operations can describe a supported reverse sequence; Classic migrations author it directly.", + "url": "guide/auto-reversing.html", + "text": " AutoReversingMigration derives reverse operations in reverse order and validates reversal support before its first change. The Classic equivalent makes the Down operation explicit. Both examples below create the same table and remove it on downgrade. A reversible table creation using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(3)]\npublic class CreateNotes : Migration\n{\n public override void Up() => Database.AddTable(\"Notes\", new Column(\"Text\", DbType.String, 500));\n public override void Down() => Database.RemoveTable(\"Notes\");\n} using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(3)]\npublic class CreateNotes : AutoReversingMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n => migration.Create.Table(\"Notes\").WithColumn(\"Text\").AsString(500);\n} Destructive changes, data operations, SQL and callbacks require explicit reverse behavior. Reverse support is narrower than execution support. An operation that can run is not necessarily one that can be inverted from its definition alone. Use FluentMigration with BuildDown when the reverse needs its own steps. MigrationBuilder.WithReverse can attach an explicit backward operation to a forward operation. Dropping a newly created table on downgrade still destroys any data inserted since creation; automatic reversal is not data recovery. " + }, + { + "title": "Provider overview", + "group": "Database providers", + "summary": "One authoring contract, explicit database behavior. Choose the driver and provider together.", + "url": "guide/providers.html", + "text": " Database ProviderTypes Guide SQLite SQLite / MonoSQLite Live-schema reconstruction SQL Server SqlServer / SqlServer2005 Constraints, batches and locks PostgreSQL PostgreSQL / PostgreSQL82 Schemas, types and locks MySQL / MariaDB Mysql / MariaDB DDL and collation behavior Oracle Oracle / MsOracle Identity and metadata SAP HANA Hana Additional providers Db2 / Informix / Firebird / Ingres / Sybase IBM_DB2 / IBM_Informix / Firebird / Ingres / Sybase Engine-specific guidance Pass an open IDbConnection to ProviderFactory.Create. Both migration styles use that provider. Alternatively use the connection-string overload and configure providerName so the provider can resolve the ADO.NET factory. Use a matching driver and test the exact server version you deploy. Provider selection · open connection supplied by the host using var selectedProvider = ProviderFactory.Create(\n ProviderTypes.PostgreSQL, connection, defaultSchema: \"public\", scope: \"billing\");\nvar selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false);\nselectedRunner.MigrateToLastVersion(); using var selectedProvider = ProviderFactory.Create(\n ProviderTypes.PostgreSQL, connection, defaultSchema: \"public\", scope: \"billing\");\nvar selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false);\nselectedRunner.MigrateToLastVersion(); The CI matrix includes SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase and SAP HANA. Ingres and historical provider aliases have separate qualification needs. Read testing and the live-engine matrix for exact drivers and server setup. Database support is operation-specific. Column types, collation presets, index options, DDL transactions and metadata readers can differ. Test stored values and preserved schema, not only the generated SQL. " + }, + { + "title": "SQLite", + "group": "Database providers", + "summary": "Change existing tables from the live schema, without maintaining an ORM model.", + "url": "guide/sqlite.html", + "text": " For supported changes, Migrator reads SQLiteTableInfo, changes its representation, creates a replacement table, copies mapped rows, swaps tables and recreates represented dependent objects. This provides column type/default/nullability changes and adding/removing primary, foreign, unique and check constraints. Native rename and eligible drop-column paths are used when supported by the engine. Complex alterations use reconstruction. Existing rows must satisfy the new definition; a default does not rewrite every existing NULL during a column change. Change a column on an existing SQLite table Database.ChangeColumn(\"Users\", new Column(\"Name\", DbType.String, 500)\n{\n IsNullable = false, DefaultValue = \"Unknown\",\n Collation = Collation.AsciiIgnoreCase\n}); migration.Alter.Column(\"Name\", \"Users\")\n .AsString(500).NotNullable().WithDefaultValue(\"Unknown\")\n .WithCollation(Collation.AsciiIgnoreCase); Detail Behavior Mapped data Named-column copy preserves mapped values, subject to the new definition accepting them. Keys and constraints Named/composite keys, ordered foreign-key pairs and separate update/delete actions are retained. Column collations Declared names are retained. Register custom collations on the connection. Indexes and triggers Supported definitions are recreated; unsafe trigger rename/drop-column cases are rejected. AUTOINCREMENT The sequence high-water mark survives, including previously deleted identities. Hidden rowid Not part of the mapped data and may change. Reconstruction rejects generated columns, STRICT, WITHOUT ROWID and indexes with explicit COLLATE clauses. It is not an arbitrary SQL dependency rewriter. Adjust dependent views, complex expressions and triggers explicitly when required. MATCH FULL and MATCH PARTIAL are rejected because SQLite does not enforce their semantics. Owned rebuild transactions and runner transactions validate foreign-key integrity before commit and restore the prior enforcement setting. For caller-owned active transactions configure foreign keys before beginning the transaction. A SQLite write lock is not a session-wide migration lease; coordinate deployment externally or provide IMigrationLock. CLR Guid defaults use blobs from Guid.ToByteArray(), matching inserted parameters. Legacy text GUID defaults remain SQL expressions during unrelated rebuilds, so storage is not silently converted. Convert mixed text/blob keys explicitly and consistently across related tables. SQLite INTEGER is signed 64-bit. Declared text lengths and decimal precision do not impose SQL Server-like enforcement. An identity needs a single INTEGER primary key in the same definition. For adding identity to an existing table, use an atomic SQLite RecreateTable definition containing both objects. FluentMigrator leaves general column alterations and later foreign-key changes to manual reconstruction. DbUp and Evolve run supplied scripts. EF Core also rebuilds SQLite tables using model-represented artifacts. Migrator reconstructs from live metadata without an ORM. The sourced operation comparison distinguishes native SQL, emulation and manual work. " + }, + { + "title": "SQL Server", + "group": "Database providers", + "summary": "Explicit keys, provider-specific indexes, transactional DDL and application locks.", + "url": "guide/sql-server.html", + "text": " Use ProviderTypes.SqlServer with an open Microsoft.Data.SqlClient connection. Pass the intended default schema, commonly dbo. Historical SqlServer2005 is a separate alias with older type mappings. WholeSession transactions and DatabaseMigrationLock are available for SQL Server. Column changes preserve explicit constraints and indexes. Add/remove uniqueness independently. For a nonclustered primary key on an existing compatible table use the dedicated API shown below. Review existing clustered indexes before changing key layout. Add a nonclustered primary key Database.AddPrimaryKeyNonClustered(\"PK_Users\", \"Users\", \"Id\"); migration.Create.NonClusteredPrimaryKey(\"PK_Users\", \"Users\", \"Id\"); Index definitions can express included/filter/cluster options where supported. The script APIs split standalone GO lines; raw ExecuteNonQuery/Execute.Sql does not. SQLCMD directives and GO repetition are rejected before executing script batches. Prefer scripts for client batch syntax and commands for parameterized statements. Use TimeOnly for time values and TimeSpan for interval ticks. SqlServer2005 uses its older DATETIME precision behavior. Use separate quoting helpers for table and column names. A table rename leaves named constraints/indexes attached with their old names; assign distinct names when creating a replacement table. " + }, + { + "title": "PostgreSQL", + "group": "Database providers", + "summary": "Native intervals, schema-aware metadata, transactional DDL and advisory locks.", + "url": "guide/postgresql.html", + "text": " Use ProviderTypes.PostgreSQL and an open Npgsql connection, with the intended default schema. Connection search_path affects unqualified relation lookup. Metadata readers resolve the requested relation through PostgreSQL and distinguish same-named tables in different schemas. PostgreSQL maps duration values to native intervals. Time without time zone maps to a time of day; use TimeOnly for that input. Parameter mappings and scalar CLR return types are separate concerns: raw ADO.NET values remain driver-specific. Store a job duration Database.AddColumn(\"Jobs\", new Column(\"Elapsed\", MigratorDbType.Interval)\n{\n DefaultValue = TimeSpan.FromDays(2)\n}); migration.Create.Column(\"Elapsed\", \"Jobs\").OfType(MigratorDbType.Interval)\n .WithDefaultValue(TimeSpan.FromDays(2)); Create any ICU nondeterministic collation explicitly, then select it with Collation.Named. Column rendering does not silently create shared collation objects. Binary maps to C; language and case semantics should use a specific installed name. Schema-aware metadata does not establish complete qualification for every operation. Test quoted names and search-path behavior with your migration. Renaming a table retains its named constraints; avoid colliding names when recreating the old table. WholeSession is supported for verified transactional DDL, and DatabaseMigrationLock uses a session advisory lock. Statements that require special transaction treatment need a separate deployment design. Keep the connection stable while the lease is held. " + }, + { + "title": "MySQL and MariaDB", + "group": "Database providers", + "summary": "Related providers with explicit engine, collation and DDL transaction differences.", + "url": "guide/mysql.html", + "text": " Select ProviderTypes.Mysql for MySQL and MariaDB for MariaDB. Use an open driver connection or configure the factory. Do not treat compatible wire protocols as proof of identical server syntax or metadata behavior. DDL can commit implicitly; the runner rejects WholeSession for these dialects. Semantic presets require utf8mb4-compatible text and the documented server versions: MySQL 8 and MariaDB 10.10+ have different mappings. Use a named installed collation if exact linguistic or trailing-space behavior matters. Case-insensitive, accent-sensitive text Database.AddTable(\"Labels\", new Column(\"Name\", DbType.String, 100)\n{\n Collation = Collation.CaseInsensitive\n}); migration.Create.Table(\"Labels\").WithColumn(\"Name\").AsString(100)\n .WithCollation(Collation.CaseInsensitive); MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction. DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors. " + }, + { + "title": "Oracle", + "group": "Database providers", + "summary": "Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.", + "url": "guide/oracle.html", + "text": " Use ProviderTypes.Oracle with the Oracle managed ADO.NET driver and the intended schema. MsOracle is a historical variant. Oracle DDL is not generally atomic across a migration; WholeSession is rejected. Some quoted qualified metadata lookups are explicitly rejected. Identity is a column attribute and is validated before table creation. It need not be a primary key on every engine, but the example pairs it with an explicit key. Use a server/driver combination qualified for native identity. An identity table with an explicit key Database.AddTable(\"Entries\",\n new Column(\"Id\", DbType.Int32) { IsIdentity = true, IsNullable = false },\n new Column(\"Text\", DbType.String, 255),\n new PrimaryKeyConstraint(\"PK_Entries\", \"Id\")); migration.Create.Table(\"Entries\")\n .WithColumn(\"Id\").AsInt32().Identity().NotNullable()\n .WithColumn(\"Text\").AsString(255)\n .WithPrimaryKey(\"PK_Entries\", \"Id\"); RemoveTable leaves unrelated sequences intact. Oracle removes table-owned triggers and native identity objects. For a legacy sequence that the migration explicitly owns, OracleTransformationProvider.RemoveTableWithOwnedSequences validates named sequences and propagates cleanup errors. It does not infer sequence ownership from naming patterns. Oracle empty character strings become NULL. Time uses DATE with a fixed 1970-01-01 date and whole-second precision; fractional Time inputs are rejected. Intervals use native storage. Changes that require an unsupported in-place type conversion need an explicit data migration. Included/clustered index options are rejected rather than ignored. Ordered foreign-key pairs and delete actions are preserved by structured metadata. A SQL Server clustered-index request is not translated into an Oracle index-organized table. " + }, + { + "title": "HANA and additional providers", + "group": "Database providers", + "summary": "Use the live-engine matrix to qualify operations beyond the common database families.", + "url": "guide/other-providers.html", + "text": " ProviderTypes.Hana uses SAP’s native .NET driver. The CI job runs HANA Express and exercises schema/data operations, metadata, constraints, history, restart and DML rollback. DDL may autocommit. Use a custom host; the CLI driver bundle does not include an online HANA host. HANA has its own supported type set; Guid and DateTimeOffset are outside the current matrix mappings. Review the type matrix before choosing shared column definitions. Provider Things to check Db2 LUW Driver runtime dependencies, decimal/storage capacity and ordinary/unique index options. Informix Native driver and database encoding; TEXT reads, integer NULL sentinels, whole-second Time and trailing-space trimming. Firebird Decimal storage capacity, ordinary/unique index operations and transaction behavior. Sybase ASE TEXTSIZE and string truncation settings, nullable BIT restrictions, trimmed strings and constraint-name limitations. The same authoring API describes a portable subset. This does not imply that every native extension or type maps identically. Start with simple definitions, then qualify your actual data and schema operations on each target. CreateUsers.cs using System.Data;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(1)]\npublic class CreateUsers : Migration\n{\n public override void Up()\n {\n Database.AddTable(\"Users\",\n new Column(\"Id\", DbType.Int32) { IsNullable = false },\n new Column(\"Name\", DbType.String, 255),\n new PrimaryKeyConstraint(\"PK_Users\", \"Id\"));\n }\n\n public override void Down() => Database.RemoveTable(\"Users\");\n} using DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(1)]\npublic class CreateUsers : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n {\n migration.Create.Table(\"Users\")\n .WithColumn(\"Id\").AsInt32().NotNullable()\n .WithColumn(\"Name\").AsString(255)\n .WithPrimaryKey(\"PK_Users\", \"Id\");\n }\n\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Table(\"Users\");\n} Ingres remains a source dialect outside the eleven-engine matrix. Redshift, Snowflake and Db2 for IBM i require separate provider/infrastructure qualification; PostgreSQL tests do not qualify Redshift, and Db2 LUW tests do not qualify IBM i. See qualification requirements and live test setup for exact coverage and reproduction commands. " + }, + { + "title": "Conditional logic", + "group": "Advanced topics", + "summary": "Choose between inspecting the live schema and declaring a provider-specific operation.", + "url": "guide/conditional.html", + "text": " The Classic provider indexer selects a named provider or a no-op provider. Fluent IfDatabase wraps structured operations in a provider condition. Use the provider names understood by the dialect; this SQLite example leaves other providers unchanged. Run a SQLite-specific statement Database[\"SQLite\"].ExecuteNonQuery(\"UPDATE Users SET Name = upper(Name)\"); migration.IfDatabase(\"SQLite\", sqlite =>\n sqlite.Execute.Sql(\"UPDATE Users SET Name = upper(Name)\")); Use Database.TableExists/ColumnExists or FluentMigration.Schema for connected checks. These inspect the current database. A fluent BuildUp method collects operations before they execute, so queued creation is not visible to a live metadata read in the same method. For execution-time decisions after earlier operations, use an explicit provider callback. That callback cannot be previewed and requires an authored reverse. Avoid making a migration silently succeed with the wrong schema: an existence check alone does not validate a column’s type or constraint definition. " + }, + { + "title": "Custom extensions", + "group": "Advanced topics", + "summary": "Reuse schema conventions without hiding provider behavior or changing the migration contract.", + "url": "guide/extensions.html", + "text": " A small helper can express a repeated column policy in both styles. Keep helper behavior stable for historical migrations; changing a helper can change what an old migration does on a fresh database. The example uses static methods to keep its dependencies explicit. Reusable audit-column helpers using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\n\npublic static class AuditColumns\n{\n public static void Add(ITransformationProvider database, string table)\n => database.AddColumn(table, new Column(\"CreatedAt\", DbType.DateTime)\n {\n IsNullable = false, DefaultValue = RawSql.Insert(\"CURRENT_TIMESTAMP\")\n });\n} using System;\nusing System.Data;\nusing DotNetProjects.Migrator;\nusing DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\npublic static class AuditColumns\n{\n public static void Add(MigrationBuilder migration, string table)\n => migration.Create.Column(\"CreatedAt\", table).OfType(DbType.DateTime)\n .NotNullable().WithDefaultValue(RawSql.Insert(\"CURRENT_TIMESTAMP\"));\n} RunnerOptions.Activator constructs migrations when a DI container is not appropriate. IMigrationLock supplies a disposable lease for custom deployment coordination. Release must work on success and failure. Configure these at the host boundary rather than in individual migrations. ITransformationProvider defines execution and metadata operations. A custom provider needs accurate typed constraint definitions or an explicit unsupported error. IMigrationHistory enables read-only planning and effective-scope history selection. IScriptBatchProvider extends script processing. Keep SQL rendering independent of a live connection. Custom MigrationOperation implementations need deliberate validation, application, SQL rendering and reversal behavior. See the API map and implementation contracts before claiming preview or reversal support. " + }, + { + "title": "Testing and deployment", + "group": "Advanced topics", + "summary": "Verify stored data, preserved schema and repeat execution on the actual target engine.", + "url": "guide/testing.html", + "text": " Create a disposable database, apply the migration, check the schema and rows, run to the same target again, then downgrade and verify the intended reverse. Both authoring styles use the same runner. This fragment assumes an initialized runner whose migration set creates Users. A host-level smoke check runner.MigrateToLastVersion();\nif (!provider.TableExists(\"Users\")) throw new Exception(\"Users missing\");\nrunner.MigrateToLastVersion();\nrunner.MigrateTo(0);\nif (provider.TableExists(\"Users\")) throw new Exception(\"Users was not removed\"); runner.MigrateToLastVersion();\nif (!provider.TableExists(\"Users\")) throw new Exception(\"Users missing\");\nrunner.MigrateToLastVersion();\nrunner.MigrateTo(0);\nif (provider.TableExists(\"Users\")) throw new Exception(\"Users was not removed\"); Test defaults by omitting a value, and nullability by explicitly sending NULL. Verify composite-key order, foreign-key actions and constraint names. After a SQLite rebuild check real rows, collations, supported indexes/triggers and identity high-water state. Test failure paths as well as successful SQL generation. Use representative production-sized data to measure lock duration and backfill cost. A passing SQL-string assertion does not establish that a database accepts a command or preserves its semantics. Build with dotnet build Migrator.slnx, then use .github/scripts/test.ps1 -Database Unit or SQLite for local suites. The live-engine guide gives the external database setup. Homepage CI results include commit provenance and skipped/missing-suite status. Keep applied migrations immutable, review SQL and explicit reverse behavior, and serialize competing deploys. Validate against a restored database before making a breaking change. Plan application compatibility around expand/backfill/contract phases. Treat post-commit callback failures as durable migrations requiring follow-up handling. " + }, + { + "title": "Upgrading existing migrations", + "group": "Advanced topics", + "summary": "Update source definitions while preserving the history your databases already contain.", + "url": "guide/upgrading.html", + "text": " Replace old ColumnProperty flags with IsNullable, IsIdentity and IsUnsigned. Primary, unique, foreign and check constraints belong to the table. GetColumns returns column attributes; use GetTableConstraints for key membership and ordered columns. Keep the same applied migration versions and effective scope when recompiling. Do not create a new history table merely to make an incompatible source assembly run. Verify the upgrade against a restored database and a fresh database. FluentMigration.BuildUp/BuildDown replaces the duplicate legacy builder. Use complete table definitions for keys, explicit operations for indexes, and independent foreign-key update/delete actions. Classic Up/Down migrations remain first-class. CreateUsers.cs using System.Data;\nusing DotNetProjects.Migrator.Framework;\n\n[Migration(1)]\npublic class CreateUsers : Migration\n{\n public override void Up()\n {\n Database.AddTable(\"Users\",\n new Column(\"Id\", DbType.Int32) { IsNullable = false },\n new Column(\"Name\", DbType.String, 255),\n new PrimaryKeyConstraint(\"PK_Users\", \"Id\"));\n }\n\n public override void Down() => Database.RemoveTable(\"Users\");\n} using DotNetProjects.Migrator.Framework;\nusing DotNetProjects.Migrator.Framework.Fluent;\n\n[Migration(1)]\npublic class CreateUsers : FluentMigration\n{\n public override void BuildUp(MigrationBuilder migration)\n {\n migration.Create.Table(\"Users\")\n .WithColumn(\"Id\").AsInt32().NotNullable()\n .WithColumn(\"Name\").AsString(255)\n .WithPrimaryKey(\"PK_Users\", \"Id\");\n }\n\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Table(\"Users\");\n} Column changes preserve explicit uniqueness; old SQL Server ownership markers no longer control deletion. TimeSpan inputs mean intervals, so convert clock-time inputs to TimeOnly. SQLite GUID defaults use the same blob representation as inserted parameters; unrelated rebuilds preserve existing text defaults. Read the complete compatibility migration guide for constructor replacements, custom-provider contracts, identity, constraint metadata and collation mappings. Version-specific details live there; these chapters describe the current API. " + }, + { + "title": "Classic / Fluent API map", + "group": "Reference", + "summary": "A practical index of the two authoring surfaces and their shared provider contracts.", + "url": "guide/api-map.html", + "text": " Classic Fluent AddTable Create.Table AddColumn(table, column) Create.Column(name, table) ChangeColumn(table, column) Alter.Column(name, table) / Alter.Column(table, column) RemoveTable / RemoveColumn Delete.Table / Delete.Column RenameTable / RenameColumn Rename.Table / Rename.Column AddPrimaryKey / AddUniqueConstraint / AddCheckConstraint Create.PrimaryKey / Create.Unique / Create.Check AddForeignKey / RemoveForeignKey Create.ForeignKey / Delete.ForeignKey AddIndex / RemoveIndex Create.Index / Delete.Index GetTableConstraints / GetColumns Schema.Table(name).ConstraintDefinitions() / Columns() Classic Fluent Insert / InsertIfNotExists Insert.IntoTable(...).Row(...) / IfNotExists(...) Update / Delete Update.Table(...).Set(...).Where(...) / Delete.FromTable(...).Where(...) ExecuteNonQuery / ExecuteScript / ExecuteResourceScript Execute.Sql / Execute.Script / Execute.EmbeddedScript CopyDataFromTableToTable / UpdateTargetFromSource Execute.CopyData / Execute.UpdateFrom TruncateTable Execute.Truncate CreateCommand / Connection Execute.WithCommand / Execute.WithConnection Database provider access Context or Execute.WithProvider History / transactions Shared runner and explicit provider context Both APIs reach the same provider layer, but not every operation has SQL-preview or automatic-reversal support. Provider capabilities still govern execution. Read preview , reversal and the machine-checked method-family inventory for the distinction. " + }, + { + "title": "Contributing", + "group": "Reference", + "summary": "Make a provider change reproducible, then verify its observable behavior.", + "url": "guide/contributing.html", + "text": " Include package, database and driver versions, a minimal migration, relevant schema/data, and expected versus actual behavior. Remove secrets from connection strings and logs. File reports in the issue tracker . Add a regression that fails before the fix and checks the real result afterward. Provider-specific changes need actual-engine evidence; skipped tests and generated SQL alone do not qualify support. Keep mutable definition inputs independent from caller arrays and check failure paths. Edit docs/_src/content.py for chapters and docs/_src/home.html for the homepage. Run python .github/scripts/build-docs.py to regenerate static HTML and the search index. Run python .github/scripts/verify-docs.py --compile to validate links, paired examples and compilable C# samples. Site assets live in docs/assets. Each migration-operation example should provide Classic and Fluent versions. Shared runner and shell commands intentionally appear in both tabs. Keep provider restrictions precise and features described in the present tense. Run the homepage CI-count renderer tests when changing the site template. The chapter organization follows the learning path of FluentMigrator’s documentation , adapted to this API. Visual references include Resend’s typography and code tabs and Gel’s code walkthroughs . The site uses its own palette, layout, copy and migration illustrations. " + } +] diff --git a/docs/assets/site.css b/docs/assets/site.css index 82be1fde..7d708b18 100644 --- a/docs/assets/site.css +++ b/docs/assets/site.css @@ -1,808 +1,1512 @@ -:root { - color-scheme: light; - --ink: #152238; - --muted: #536176; - --blue: #1552cb; - --line: #dce3ed; - --paper: #fff; - --navy: #101a2d; - --mono: Consolas, "SFMono-Regular", monospace; - font-family: Inter, "Segoe UI", Arial, sans-serif; - color: var(--ink); - background: var(--paper); - font-size: 16px; - line-height: 1.65; -} -* { - box-sizing: border-box; -} -html { - scroll-behavior: smooth; - scroll-padding-top: 100px; -} -body { - margin: 0; -} -a { - color: var(--blue); - text-underline-offset: 4px; -} -a:hover { - text-decoration-thickness: 2px; -} -button, -a { - -webkit-tap-highlight-color: transparent; -} -a:focus-visible, -button:focus-visible, -summary:focus-visible, -[tabindex]:focus-visible { - outline: 3px solid #e38b00; - outline-offset: 5px; -} -button { - font: inherit; - cursor: pointer; -} -[hidden] { - display: none !important; -} -.container { - width: min(1240px, calc(100% - 96px)); - margin-inline: auto; -} -.site-header { - border-bottom: 1px solid var(--line); - background: #fffffff5; - position: sticky; - top: 0; - z-index: 10; -} -.header-inner { - min-height: 84px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; -} -.brand { - display: inline-flex; - align-items: center; - font-weight: 750; - font-size: 23px; - letter-spacing: -0.7px; - text-decoration: none; - color: var(--ink); - white-space: nowrap; -} -.brand-suffix { - font-weight: 400; - color: var(--muted); -} -.brand-mark { - margin-right: 12px; - display: inline-flex; - align-items: center; - justify-content: center; - background: var(--blue); - color: white; - width: 38px; - height: 38px; - border-radius: 8px; - font-size: 22px; - letter-spacing: -4px; - padding-right: 4px; -} -.brand-mark span { - font-size: 17px; - align-self: flex-start; -} -nav { - display: flex; - gap: 30px; - align-items: center; -} -nav a { - font-weight: 600; - font-size: 14px; - color: var(--ink); - text-decoration: none; -} -nav a:hover { - color: var(--blue); -} -.hero { - padding: 84px 0 78px; -} -.hero-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 54px; - align-items: center; -} -.eyebrow { - font-size: 12px; - font-weight: 750; - letter-spacing: 1.8px; - color: var(--blue); - margin: 0 0 20px; -} -h1, -h2, -h3, -p { - margin-top: 0; -} -h1 { - font-size: clamp(38px, 4.1vw, 58px); - line-height: 1.13; - letter-spacing: -2.4px; - margin-bottom: 24px; -} -h1 span { - color: var(--blue); -} -.intro { - font-size: 19px; - color: var(--muted); - max-width: 480px; - line-height: 1.65; - margin-bottom: 30px; -} -.actions { - display: flex; - flex-wrap: wrap; - gap: 12px; -} -.button { - display: inline-flex; - align-items: center; - gap: 20px; - justify-content: center; - padding: 12px 18px; - border-radius: 6px; - font-size: 14px; - font-weight: 650; - text-decoration: none; - border: 1px solid var(--line); -} -.primary { - color: #fff; - background: var(--blue); - border-color: var(--blue); -} -.primary:hover { - background: #1043a7; -} -.secondary { - color: var(--ink); -} -.secondary:hover { - background: #f1f5fc; -} -.hero-meta { - font-size: 12px; - color: var(--muted); - margin: 22px 0 0; -} -.code-window { - background: var(--navy); - color: #e8edf5; - border-radius: 10px; - box-shadow: 0 18px 50px #15223820; - min-width: 0; - border: 1px solid #24334b; -} -.code-heading, -.snippet-heading { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - padding: 14px 20px; - border-bottom: 1px solid #2d3a50; - font-size: 12px; -} -.code-heading > span:last-child { - color: #a4b3c9; - font-size: 12px; - letter-spacing: 1px; -} -.file-icon { - font-weight: 700; - color: #93b8ff; - margin-right: 8px; -} -pre { - margin: 0; - overflow: auto; - tab-size: 4; - font-family: var(--mono); - font-size: 14px; - line-height: 1.7; - padding: 22px; -} -code { - font-family: var(--mono); - font-size: 0.9em; - overflow-wrap: anywhere; -} -pre code { - font-size: inherit; - overflow-wrap: normal; -} -.code-window pre { - font-size: 13px; -} -.syntax-attribute { - color: #d7c087; -} -.syntax-keyword { - color: #acb9ff; -} -.syntax-string { - color: #91d9c2; -} -.code-footer { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 8px; - padding: 13px 20px; - border-top: 1px solid #2d3a50; - font-size: 12px; - color: #a4b3c9; -} -.code-footer a { - color: #b9d0ff; - text-decoration: none; -} -.database-strip { - background: #f6f8fc; - border-block: 1px solid var(--line); -} -.database-strip .container { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 16px; - padding-block: 22px; - font-weight: 600; - font-size: 14px; -} -.strip-label { - font-size: 11px; - letter-spacing: 1.2px; - color: var(--muted); -} -.database-strip a { - font-size: 13px; - text-decoration: none; -} -.section { - padding-block: 82px; -} -h2 { - font-size: 38px; - line-height: 1.2; - letter-spacing: -1.2px; - margin-bottom: 24px; -} -h3 { - font-size: 19px; - line-height: 1.4; - letter-spacing: -0.25px; - margin-bottom: 12px; -} -.features-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 40px; - margin-top: 42px; -} -.features-grid article { - border-top: 2px solid var(--line); - padding-top: 23px; -} -.feature-number { - display: block; - font-size: 12px; - letter-spacing: 1px; - color: var(--blue); - margin-bottom: 22px; - font-weight: 650; -} -.features-grid p, -.fit-grid p, -.step-description p, -.project-section p { - color: var(--muted); -} -.quick-section { - background: #f6f8fc; - padding-block: 76px; -} -.section-heading { - display: flex; - justify-content: space-between; - align-items: flex-end; - gap: 36px; - margin-bottom: 32px; -} -.section-heading h2 { - margin-bottom: 0; -} -.section-heading > p { - font-size: 14px; - color: var(--muted); - margin-bottom: 3px; - max-width: 380px; -} -.step { - display: grid; - grid-template-columns: 1fr 1.65fr; - gap: 64px; - padding-block: 32px; - align-items: start; -} -.step-index { - display: flex; - width: 32px; - height: 32px; - align-items: center; - justify-content: center; - border: 1px solid #b7c9e9; - color: var(--blue); - border-radius: 50%; - font-weight: 650; - margin-bottom: 16px; -} -.step-description a { - font-size: 14px; -} -.snippet { - background: #fff; - border: 1px solid var(--line); - border-radius: 8px; - overflow: hidden; - min-width: 0; -} -.snippet-heading { - border-color: var(--line); - font-family: var(--mono); - color: var(--muted); - min-height: 50px; - padding-block: 9px; -} -.snippet-heading button { - font-family: inherit; - font-size: 12px; - color: var(--blue); - padding: 4px 10px; - background: #f3f6fc; - border: 1px solid #d3ddef; - border-radius: 4px; -} -.snippet-heading button:hover { - background: #e6efff; -} -.note { - border-left: 3px solid var(--blue); - background: #eaf0fc; - padding: 20px 24px; - font-size: 14px; - margin-top: 22px; -} -.note strong { - display: block; - margin-bottom: 5px; -} -.provider-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 50px; - padding-block: 20px; -} -.provider-list { - list-style: none; - display: flex; - gap: 10px; - flex-wrap: wrap; - padding: 0; - margin: 16px 0; -} -.provider-list li { - border: 1px solid var(--line); - padding: 7px 13px; - border-radius: 4px; - font-size: 14px; -} -.muted { - color: var(--muted); - font-size: 14px; - max-width: 980px; -} -.comparison-section { - background: var(--navy); - color: #eef3ff; - padding-block: 76px; -} -.comparison-section .eyebrow { - color: #97bbff; -} -.comparison-section a { - color: #b0cbff; -} -.comparison-section .section-heading > p, -.comparison-intro { - color: #b7c4d9; -} -.comparison-intro { - max-width: 810px; -} -.table-hint { - color: #aab9d0; - font-size: 12px; - margin-top: 32px; -} -.table-scroll { - overflow-x: auto; - border: 1px solid #3c4b64; - border-radius: 8px; - max-width: 100%; -} -table { - border-collapse: collapse; - min-width: 1100px; - width: 100%; - font-size: 14px; - line-height: 1.55; -} -caption { - text-align: left; - padding: 16px; - background: #18243a; - color: #b7c4d9; - font-size: 12px; - caption-side: bottom; -} -th, -td { - text-align: left; - vertical-align: top; - padding: 19px 16px; - border-bottom: 1px solid #344158; - border-right: 1px solid #344158; - width: 16.66%; -} -th:last-child, -td:last-child { - border-right: 0; -} -thead th { - background: #1d2a41; - font-size: 16px; - padding-block: 23px; -} -thead th span, -thead th a { - display: block; - font-size: 12px; - font-weight: 400; -} -thead th a { - margin-top: 7px; -} -tbody th { - font-weight: 550; - color: #d3dded; -} -tbody td { - color: #c2cee0; -} -.ours { - background: #1c3356; - color: #fff; -} -thead .ours { - background: #224477; -} -tbody tr:hover td, -tbody tr:hover th { - background: #243851; -} -tbody tr:last-child > * { - border-bottom: 0; -} -.comparison-notes { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 36px; - font-size: 14px; - color: #b7c4d9; - margin-top: 26px; -} -.comparison-notes strong { - color: #eef3ff; -} -.fit-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 36px; - padding-top: 26px; - margin-top: 20px; - border-top: 1px solid #344158; -} -.fit-grid h3 { - font-size: 18px; -} -.fit-grid p { - font-size: 14px; - color: #b7c4d9; -} -.fit-grid strong { - color: #e5edfb; -} -.sources { - border-top: 1px solid #344158; - margin-top: 28px; - padding-top: 22px; - font-size: 14px; - color: #b7c4d9; -} -.sources summary { - cursor: pointer; - color: #eef3ff; - font-weight: 600; - padding-block: 8px; -} -.sources > p { - margin-top: 22px; -} -.sources li { - padding: 8px 0; -} -.sources li:target { - background: #243851; - outline: 2px solid #97bbff; - outline-offset: 5px; -} -.project-section { - display: grid; - grid-template-columns: 1.1fr 1fr; - gap: 100px; - padding-block: 80px; - align-items: center; -} -.project-section p { - max-width: 510px; -} -.project-links a { - display: flex; - justify-content: space-between; - padding-block: 22px; - border-bottom: 1px solid var(--line); - font-size: 18px; - font-weight: 600; - text-decoration: none; -} -.project-links span { - font-weight: 400; -} -footer { - border-top: 1px solid var(--line); - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - padding-block: 30px; -} -footer p, -footer > a:last-child { - font-size: 12px; - color: var(--muted); - margin: 0; -} -footer .brand { - font-size: 18px; -} -.skip { - position: fixed; - left: 20px; - top: -100px; - z-index: 20; - background: white; - padding: 12px; -} -.skip:focus { - top: 15px; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; -} -@media (min-width: 1450px) { - .hero { - padding-block: 100px; - } - .hero-grid { - gap: 70px; - } -} -@media (max-width: 1050px) { - .container { - width: calc(100% - 48px); - } - .hero-grid { - gap: 28px; - } - .hero { - padding-block: 60px; - } - h1 { - font-size: 42px; - } - .hero-meta { - max-width: 280px; - } - .step { - gap: 30px; - } - .code-window pre { - font-size: 12px; - padding: 18px; - } - .features-grid { - gap: 26px; - } - .project-section { - gap: 45px; - } - nav { - gap: 20px; - } -} -@media (max-width: 760px) { - html { - scroll-padding-top: 130px; - } - .container { - width: calc(100% - 36px); - } - .header-inner { - flex-wrap: wrap; - gap: 12px; - justify-content: center; - padding-block: 14px; - min-height: 0; - } - .brand { - font-size: 21px; - } - nav { - justify-content: center; - gap: 22px; - flex-wrap: wrap; - } - nav a { - font-size: 12px; - } - .hero { - padding: 48px 0; - } - .hero-grid, - .step, - .provider-grid, - .project-section { - grid-template-columns: 1fr; - gap: 28px; - } - .hero-copy { - max-width: 540px; - } - h1 { - font-size: 44px; - letter-spacing: -1.8px; - } - .intro { - font-size: 18px; - } - .hero-meta { - max-width: none; - } - .code-window pre { - font-size: 13px; - } - .database-strip .container { - justify-content: flex-start; - gap: 13px 22px; - } - .strip-label { - width: 100%; - } - .section { - padding-block: 54px; - } - h2 { - font-size: 31px; - } - .features-grid, - .fit-grid, - .comparison-notes { - grid-template-columns: 1fr; - gap: 20px; - } - .features-grid { - margin-top: 32px; - } - .features-grid p { - margin-bottom: 0; - } - .feature-number { - margin-bottom: 14px; - } - .quick-section, - .comparison-section { - padding-block: 50px; - } - .section-heading { - display: block; - } - .section-heading > p { - margin-top: 20px; - } - .step { - padding-block: 25px; - gap: 12px; - } - .step-description p:last-child { - margin-bottom: 5px; - } - .snippet pre { - font-size: 13px; - padding: 18px; - } - .provider-grid { - padding-block: 4px; - } - .provider-grid > div + div { - margin-top: 10px; - } - .comparison-notes { - gap: 0; - } - .fit-grid { - gap: 16px; - } - .project-section { - padding-block: 54px; - } - footer { - flex-wrap: wrap; - gap: 15px; - } - footer p { - order: 3; - width: 100%; - } -} -@media (prefers-reduced-motion: reduce) { - html { - scroll-behavior: auto; - } -} - -#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; } +:root { + --paper:#f4f1e9; + --ink:#242b25; + --muted:#656b60; + --rust:#a63820; + --line:#cecfc1; + --soft:#e9e8dc; + --code:#202923; + --serif:Georgia,'Times New Roman',serif; + --mono:Consolas,'Liberation Mono',monospace; +} +* { + box-sizing:border-box; +} +html { + scroll-behavior:smooth; + scroll-padding-top:100px; +} +body { + margin:0; + background:var(--paper); + color:var(--ink); + font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; + font-size:16px; + line-height:1.65; +} +a { + color:inherit; + text-underline-offset:4px; + text-decoration-thickness:1px; +} +a:hover { + color:var(--rust); +} +button,input { + font:inherit; +} +button { + cursor:pointer; +} +button:disabled { + cursor:default; +} +[hidden] { + display:none!important; +} +::selection { + background:#d6d5ab; + color:#1d251f; +} +:focus-visible { + outline:3px solid #bd573b; + outline-offset:4px; +} +h1,h2,h3,p { + margin:0; +} +h1,h2 { + font-family:var(--serif); + font-weight:400; + line-height:1.08; + letter-spacing:-.045em; +} +h1 { + font-size:clamp(3.25rem,5.5vw,5.7rem); +} +h2 { + font-size:clamp(2.6rem,4vw,4.1rem); +} +h3 { + font-size:1.12rem; + line-height:1.4; + font-weight:600; + letter-spacing:-.015em; +} +h1 em,h2 em { + color:var(--rust); + font-weight:400; +} +p+p { + margin-top:1.1rem; +} +p { + max-width:74ch; +} +code { + font-family:var(--mono); + font-size:.86em; + overflow-wrap:anywhere; +} +p code,td code,li code { + background:#e5e5d8; + padding:.08em .25em; +} +pre { + margin:0; + overflow:auto; + line-height:1.6; + tab-size:4; +} +pre code { + font-size:12.5px; + white-space:pre; + overflow-wrap:normal; +} +ul,ol { + padding-left:1.3rem; +} +img { + max-width:100%; +} +.container { + max-width:1392px; + width:calc(100% - 112px); + margin-inline:auto; +} +.section { + padding-block:100px; + border-bottom:1px solid var(--line); +} +.eyebrow,.file-caption,.item-number,.hero-meta,.history-strip span,.chapter-preview>a>span,.schema-sketch,.breadcrumb,.chapter-group h2,.page-toc>p,.manual-group h2>span { + font-family:var(--mono); + font-size:11px; + letter-spacing:.08em; +} +.eyebrow { + color:var(--rust); + margin-bottom:26px; + line-height:1.5; +} +.lead { + font-size:20px; + line-height:1.55; + letter-spacing:-.018em; +} +.button { + display:inline-flex; + align-items:center; + justify-content:center; + padding:14px 20px; + text-decoration:none; + font-size:14px; + font-weight:600; + line-height:1.5; +} +.primary { + background:var(--ink); + color:#fff; +} +.primary:hover { + background:var(--rust); + color:#fff; +} +.text-link { + font-size:14px; + font-weight:600; +} +.subtle-link { + display:inline-block; + font-size:13px; + color:var(--muted); + margin-top:16px; +} +.sr-only { + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + white-space:nowrap; + border:0; +} +.skip { + position:fixed; + left:20px; + top:-100px; + z-index:100; + background:var(--paper); + padding:10px; +} +.skip:focus { + top:10px; +} +.site-header { + border-bottom:1px solid var(--line); + position:relative; + z-index:10; + background:var(--paper); +} +.header-inner { + max-width:1536px; + margin:auto; + padding:22px 56px; + display:flex; + align-items:center; + gap:35px; +} +.brand { + display:inline-flex; + align-items:center; + text-decoration:none; + font-size:20px; + font-weight:650; + letter-spacing:-.05em; + white-space:nowrap; +} +.brand-suffix { + font-weight:400; +} +.brand-mark { + font-family:var(--serif); + font-weight:700; + font-size:35px; + line-height:1; + position:relative; + margin-right:20px; + color:var(--rust); +} +.brand-mark>span { + position:absolute; + font-family:var(--mono); + font-size:16px; + right:-9px; + top:-3px; +} +.site-header nav { + display:flex; + gap:28px; + margin-left:auto; + align-items:center; +} +.site-header nav a { + font-size:13px; + text-decoration:none; + white-space:nowrap; +} +.site-header nav a[aria-current] { + color:var(--rust); + text-decoration:underline; +} +.search-wrap { + width:225px; + position:relative; + flex-shrink:0; +} +.search-wrap form { + display:flex; + border:1px solid var(--line); + background:transparent; +} +.search-wrap input { + min-width:0; + width:100%; + border:0; + background:transparent; + padding:9px 10px; + font-size:12px; + color:var(--ink); +} +.search-wrap form button { + background:none; + border:0; + padding:0 10px; + color:var(--muted); +} +.search-popover { + position:absolute; + right:0; + top:calc(100% + 8px); + background:var(--paper); + border:1px solid var(--line); + width:420px; + max-width:calc(100vw - 32px); + box-shadow:0 15px 40px #242b2520; + padding:16px; + max-height:65vh; + overflow:auto; +} +.search-popover p { + font-size:12px; + color:var(--muted); +} +.search-popover ul { + list-style:none; + margin:6px 0 0; + padding:0; +} +.search-popover li+li { + border-top:1px solid var(--line); +} +.search-popover a { + display:block; + padding:12px 3px; + text-decoration:none; +} +.search-popover a strong { + display:block; + font-size:14px; +} +.search-popover a span { + display:block; + color:var(--muted); + font-size:12px; + line-height:1.5; +} +.hero { + display:grid; + grid-template-columns:1fr 1fr; + gap:64px; + align-items:start; + padding-block:74px 70px; +} +.hero-copy { + padding-top:12px; +} +.hero h1 { + font-size:clamp(3.4rem,5.2vw,5.4rem); + margin-bottom:28px; +} +.hero .lead { + max-width:33ch; + margin-bottom:20px; +} +.hero-copy>p:not(.eyebrow):not(.lead) { + max-width:47ch; + color:var(--muted); + font-size:15px; +} +.hero-actions { + margin-top:34px; + display:flex; + align-items:center; + gap:25px; + flex-wrap:wrap; +} +.hero-meta { + display:flex; + gap:20px; + margin-top:34px; + color:var(--muted); + font-size:10px; + letter-spacing:0; +} +.hero-meta a { + text-decoration:none; +} +.file-caption { + display:flex; + justify-content:space-between; + margin-bottom:12px; + color:var(--muted); + font-size:10px; +} +.hero-code { + min-width:0; +} +.history-strip { + border-block:1px solid var(--line); + background:var(--soft); +} +.history-strip>.container { + display:flex; + justify-content:space-between; + align-items:center; + gap:20px; + padding-block:22px; +} +.history-strip p { + margin:0; + font-size:15px; +} +.history-strip p span { + display:block; + color:var(--muted); + font-size:10px; + margin-bottom:5px; +} +.history-arrow { + font-size:24px!important; + color:var(--rust); +} +.promise-grid { + display:grid; + grid-template-columns:1.25fr 1fr 1fr 1fr; + gap:42px; +} +.promise-grid h2 { + font-size:3rem; +} +.promise .item-number { + color:var(--rust); + display:block; + margin-bottom:30px; +} +.promise p { + font-size:14px; + color:var(--muted); + margin-top:13px; +} +.promise>a { + font-size:13px; + display:inline-block; + margin-top:20px; +} +.sqlite-section { + background:#e6e7da; + border-bottom:1px solid #c6c8b7; + padding-block:95px; +} +.sqlite-grid { + display:grid; + grid-template-columns:1fr 1fr; + gap:90px; + align-items:center; +} +.sqlite-grid h2 { + margin-bottom:27px; +} +.sqlite-grid p:not(.eyebrow) { + font-size:15px; +} +.sqlite-grid p.lead { + font-size:21px; + margin-bottom:20px; +} +.comparison-note { + color:#565e50; + font-size:13px!important; + border-left:2px solid var(--rust); + padding-left:18px; + margin-block:24px!important; +} +.sqlite-workbench { + min-width:0; +} +.schema-sketch { + background:var(--paper); + border:1px solid #bfc2ae; + margin-bottom:24px; + box-shadow:6px 6px 0 #cfd1be; + letter-spacing:0; +} +.schema-sketch>div { + display:flex; + justify-content:space-between; + padding:16px 20px; + border-bottom:1px solid var(--line); + font-size:12px; +} +.schema-sketch p { + display:flex; + justify-content:space-between; + padding:12px 20px; + margin:0; + font-size:12px!important; +} +.schema-sketch p+p { + border-top:1px dashed var(--line); +} +.schema-sketch p span:first-child { + min-width:65px; +} +.schema-sketch strong { + color:var(--rust); + font-size:16px; +} +.schema-sketch s { + color:var(--muted); +} +.schema-sketch .schema-retained { + font-size:10px!important; + color:var(--muted); +} +.start-grid { + display:grid; + grid-template-columns:1fr 1.15fr; + gap:100px; + align-items:center; +} +.start-grid h2 { + margin-bottom:24px; +} +.start-grid p:not(.eyebrow) { + font-size:15px; + color:var(--muted); + margin-bottom:28px; + max-width:42ch; +} +.section-heading { + display:flex; + justify-content:space-between; + gap:40px; + align-items:end; + margin-bottom:36px; +} +.section-heading>p { + font-size:13px; + color:var(--muted); +} +.chapter-preview { + display:grid; + grid-template-columns:repeat(4,1fr); + border-block:1px solid var(--line); +} +.chapter-preview>a { + text-decoration:none; + padding:30px 24px 30px 0; +} +.chapter-preview>a+a { + border-left:1px solid var(--line); + padding-left:24px; +} +.chapter-preview>a>span { + font-size:10px; + color:var(--rust); +} +.chapter-preview h3 { + margin-top:28px; +} +.chapter-preview p { + font-size:13px; + color:var(--muted); + margin-top:12px; +} +.provider-list { + padding:0; + margin:38px 0 25px; + list-style:none; + display:grid; + grid-template-columns:repeat(6,1fr); + border-top:1px solid var(--line); +} +.provider-list li { + border-bottom:1px solid var(--line); + padding:20px 8px 20px 0; +} +.provider-list a { + text-decoration:none; + font-size:17px; + font-weight:500; +} +.provider-section>p:last-child { + font-size:14px; + color:var(--muted); +} +.ci-section h2 { + font-size:3rem; + margin-bottom:24px; +} +.ci-section p:not(.eyebrow) { + font-size:14px; + color:var(--muted); +} +.test-counts { + display:flex; + list-style:none; + gap:35px; + padding:20px 0; + flex-wrap:wrap; +} +.test-counts li { + font-size:12px; +} +.test-counts strong { + display:block; + font-size:30px; + font-weight:500; + color:var(--ink); +} +.comparison-section { + padding:95px 0; + background:#eeece2; + border-bottom:1px solid var(--line); +} +.comparison-section .section-heading h2 { + font-size:3.5rem; +} +.comparison-section>div>p { + font-size:14px; + margin-bottom:16px; +} +.comparison-section .table-hint { + font-size:12px; + color:var(--muted); + margin-top:25px; +} +.table-scroll { + overflow:auto; + border:1px solid var(--line); + margin:22px 0; +} +table { + border-collapse:collapse; + font-size:13px; + width:100%; + line-height:1.5; + text-align:left; +} +th,td { + border-bottom:1px solid var(--line); + padding:15px 16px; + vertical-align:top; +} +th { + font-weight:600; +} +thead { + background:var(--soft); +} +caption { + text-align:left; + padding:14px 16px; + font-size:12px; + background:#e3e3d5; + color:var(--muted); +} +.comparison-section table { + min-width:1130px; + font-size:12px; +} +.comparison-section th,.comparison-section td { + min-width:170px; + border-right:1px solid var(--line); +} +.comparison-section th:first-child { + min-width:200px; +} +.comparison-section thead th { + font-size:13px; +} +.comparison-section thead th a,.comparison-section thead th span { + display:block; + font-size:10px; + font-weight:400; + margin-top:5px; +} +.comparison-section .ours { + background:#e0e4ce; +} +.sources { + border-block:1px solid var(--line); + padding:18px 0; + margin-top:32px; + font-size:13px; +} +.sources summary { + font-weight:600; + cursor:pointer; +} +.sources ol { + margin-top:24px; +} +.sources li { + margin-bottom:20px; +} +.sources li:target { + background:#dedfc9; +} +.closing-section { + padding-block:110px; +} +.closing-section h2 { + font-size:clamp(3.2rem,5.5vw,5.3rem); + margin-bottom:40px; +} +.closing-section>a+a { + margin-left:28px; +} +.site-footer { + border-top:1px solid var(--line); + padding:35px 56px; + display:flex; + gap:50px; + align-items:center; + max-width:1536px; + margin:auto; +} +.site-footer p { + font-size:12px; + color:var(--muted); + margin:0; +} +.site-footer>a:last-child { + margin-left:auto; + font-size:13px; +} +.code-example { + background:var(--code); + color:#eff1e6; + min-width:0; + margin:22px 0; +} +.hero-code>.code-example { + margin:0; +} +.code-toolbar { + display:flex; + align-items:center; + justify-content:space-between; + border-bottom:1px solid #465147; + min-height:48px; + padding-left:18px; + gap:10px; +} +.code-toolbar>span { + font-family:var(--mono); + font-size:10px; + color:#b6c2b3; + white-space:normal; + line-height:1.4; +} +.code-tabs { + display:flex; + align-self:stretch; + flex-shrink:0; +} +.code-tabs button { + font-size:11px; + color:#b6c2b3; + background:transparent; + border:0; + border-left:1px solid #465147; + padding:12px 16px; + position:relative; +} +.code-tabs button[aria-selected=true] { + color:#fff; + background:#364437; +} +.code-tabs button[aria-selected=true]:after { + content:''; + position:absolute; + left:0; + right:0; + bottom:0; + height:2px; + background:#d1b27e; +} +.code-panel { + margin:0!important; +} +.code-label { + display:flex; + align-items:center; + justify-content:space-between; + padding:10px 18px 0; +} +.code-label h3 { + font-family:var(--mono); + font-size:10px; + color:#b6c2b3; + font-weight:400; +} +.code-label button { + border:1px solid #667563; + background:transparent; + color:#d5ddcd; + font-family:var(--mono); + font-size:10px; + padding:3px 8px; +} +.code-label button:hover { + background:#364437; +} +.code-panel pre { + padding:12px 20px 20px; +} +.code-caption { + max-width:none; + font-family:var(--mono); + font-size:9px!important; + letter-spacing:.01em; + color:#b6c2b3!important; + padding:11px 18px; + border-top:1px solid #465147; + margin:0!important; +} +.syntax-keyword { + color:#f0b28c; +} +.syntax-string { + color:#c5d6a2; +} +.syntax-number { + color:#edcb89; +} +.syntax-comment { + color:#a5b49e; +} +.docs-layout { + display:grid; + grid-template-columns:252px minmax(0,820px) 190px; + max-width:1480px; + margin:auto; + gap:48px; + padding:42px 32px 70px; +} +.docs-sidebar { + font-size:13px; + min-width:0; +} +.chapter-menu summary { + display:none; +} +.manual-index { + font-family:var(--serif); + font-size:21px; + text-decoration:none; + display:block; + margin-bottom:30px; + letter-spacing:-.03em; +} +.chapter-group { + margin-bottom:24px; +} +.chapter-group h2 { + font-weight:500; + font-size:10px; + letter-spacing:0; + color:var(--muted); + margin-bottom:10px; + line-height:1.5; +} +.chapter-group h2 span { + color:var(--rust); + margin-right:6px; +} +.chapter-group ul { + list-style:none; + padding:0; + margin:0; +} +.chapter-group li a { + display:block; + font-size:12px; + text-decoration:none; + padding:5px 9px; + border-left:1px solid var(--line); +} +.chapter-group li a[aria-current=page] { + color:var(--rust); + background:#e9e7da; + border-left:2px solid var(--rust); + font-weight:600; +} +.docs-article { + min-width:0; +} +.breadcrumb { + font-size:10px; + color:var(--muted); + display:flex; + gap:10px; + margin-bottom:26px; + letter-spacing:0; +} +.breadcrumb a { + text-decoration:none; +} +.docs-article h1 { + font-size:clamp(2.8rem,4vw,4rem); + margin-bottom:24px; +} +.docs-article>.lead { + font-size:19px; + max-width:60ch; +} +.sample-note { + font-size:11px; + color:var(--muted); + padding-bottom:25px; + border-bottom:1px solid var(--line); + margin-top:22px; +} +.docs-article>section { + margin-top:48px; +} +.docs-article>section>h2 { + font-size:30px; + letter-spacing:-.03em; + margin-bottom:20px; + line-height:1.2; +} +.docs-article section>p,.docs-article section>ul { + font-size:14px; + line-height:1.8; +} +.docs-article section a { + color:var(--rust); +} +.docs-article .table-scroll { + margin:24px 0; +} +.docs-article th,.docs-article td { + padding:13px; + font-size:12px; + min-width:125px; +} +.docs-article p+ .table-scroll { + margin-top:25px; +} +.page-toc { + align-self:start; + position:sticky; + top:28px; + padding-top:6px; +} +.page-toc>p { + font-size:10px; + color:var(--muted); + margin-bottom:14px; + letter-spacing:0; +} +.page-toc a { + display:block; + text-decoration:none; + font-size:11px; + line-height:1.55; + padding:6px 0; +} +.source-link { + display:flex; + gap:25px; + margin-top:55px; + font-size:11px; + color:var(--muted); +} +.page-turn { + margin-top:25px; + border-top:1px solid var(--line); + padding-top:22px; + display:flex; + justify-content:space-between; + gap:24px; + font-size:14px; +} +.page-turn a { + text-decoration:none; +} +.page-turn span { + display:block; + font-size:10px; + color:var(--muted); + margin-bottom:4px; +} +.manual-home { + max-width:1260px; + width:calc(100% - 112px); + margin:auto; + padding:80px 0; +} +.manual-home>.lead { + max-width:60ch; + margin:30px 0; +} +.manual-directory { + margin-top:75px; + display:grid; + grid-template-columns:1fr 1fr; + gap:54px 70px; +} +.manual-group { + border-top:1px solid var(--line); + padding-top:25px; +} +.manual-group h2 { + font-size:30px; + display:flex; + align-items:baseline; + gap:20px; +} +.manual-group h2>span { + color:var(--rust); + font-size:11px; +} +.manual-group ul { + list-style:none; + padding:0; + margin:26px 0 0; +} +.manual-group li { + margin-top:20px; +} +.manual-group li a { + font-size:16px; + font-weight:600; + text-decoration:none; + display:flex; + justify-content:space-between; +} +.manual-group li p { + font-size:12px; + color:var(--muted); + margin-top:5px; + max-width:58ch; +} +@media(min-width:1600px) { + .hero { + gap:95px; + } + .hero h1 { + font-size:86px; + } + .hero-copy { + padding-top:25px; + } +} +@media(max-width:1200px) { + .header-inner { + gap:20px; + padding-inline:32px; + } + .site-header nav { + gap:20px; + } + .search-wrap { + width:195px; + } + .container { + width:calc(100% - 64px); + } + .hero { + gap:35px; + } + .hero h1 { + font-size:58px; + } + .hero .lead { + font-size:18px; + } + .hero-actions { + gap:18px; + } + .button { + font-size:12px; + padding:13px 17px; + } + .promise-grid { + gap:28px; + } + .promise-grid h2 { + font-size:2.5rem; + } + .sqlite-grid { + gap:45px; + } + .start-grid { + gap:60px; + } + .docs-layout { + grid-template-columns:205px minmax(0,1fr); + gap:35px; + } + .page-toc { + display:none; + } + .manual-home { + width:calc(100% - 64px); + } + .site-footer { + padding-inline:32px; + } +} +@media(max-width:900px) { + .header-inner { + flex-wrap:wrap; + gap:15px; + } + .site-header nav { + gap:18px; + } + .search-wrap { + width:100%; + } + .search-popover { + left:0; + right:auto; + width:100%; + max-width:none; + } + .hero { + grid-template-columns:1fr 1fr; + gap:25px; + padding-top:45px; + } + .hero h1 { + font-size:47px; + } + .hero-copy { + padding-top:0; + } + .hero .lead { + font-size:17px; + } + .hero-meta { + gap:12px; + flex-wrap:wrap; + } + .hero pre code { + font-size:11px; + } + .hero .code-panel pre { + padding-inline:13px; + } + .hero .code-toolbar { + padding-left:13px; + } + .code-tabs button { + padding-inline:12px; + } + .promise-grid { + grid-template-columns:1fr 1fr; + gap:40px; + } + .promise .item-number { + margin-bottom:18px; + } + .sqlite-grid { + gap:28px; + } + .sqlite-grid h2 { + font-size:42px; + } + .section { + padding-block:65px; + } + .start-grid { + gap:40px; + } + .chapter-preview { + grid-template-columns:1fr 1fr; + } + .chapter-preview>a:nth-child(3) { + border-left:0; + padding-left:0; + border-top:1px solid var(--line); + } + .chapter-preview>a:nth-child(4) { + border-top:1px solid var(--line); + } + .provider-list { + grid-template-columns:repeat(4,1fr); + } + .section-heading { + align-items:start; + flex-direction:column; + gap:25px; + } + .site-footer { + gap:25px; + } +} +@media(max-width:680px) { + html { + scroll-padding-top:20px; + } + .container,.manual-home { + width:calc(100% - 36px); + } + .header-inner { + padding:18px; + gap:18px; + } + .brand { + font-size:18px; + } + .brand-mark { + font-size:30px; + } + .site-header nav { + gap:16px; + margin-left:0; + width:100%; + justify-content:space-between; + order:3; + } + .site-header nav a { + font-size:11px; + } + .search-wrap { + width:165px; + margin-left:auto; + } + .search-wrap input { + font-size:11px; + padding:8px; + } + .search-popover { + position:fixed; + left:18px; + right:18px; + top:116px; + width:auto; + max-width:none; + } + .hero { + display:block; + padding-block:42px; + } + .hero h1 { + font-size:57px; + max-width:9ch; + } + .hero .lead { + font-size:19px; + } + .hero-actions { + margin-top:28px; + } + .hero-meta { + margin-top:24px; + margin-bottom:40px; + } + .hero pre code { + font-size:12px; + } + .hero-code .file-caption { + font-size:9px; + } + .history-strip>.container { + gap:15px; + padding-block:18px; + } + .history-strip p { + font-size:11px; + line-height:1.5; + } + .history-strip p span { + font-size:8px; + } + .history-arrow { + display:none; + } + .section { + padding-block:55px; + } + .eyebrow { + font-size:9px; + margin-bottom:22px; + } + .promise-grid { + gap:35px 20px; + } + .promise-grid h2 { + font-size:34px; + } + .promise h3 { + font-size:16px; + } + .promise p { + font-size:12px; + } + .promise>a { + font-size:11px; + } + .sqlite-section { + padding-block:50px; + } + .sqlite-grid { + display:block; + } + .sqlite-grid h2 { + font-size:43px; + } + .sqlite-workbench { + margin-top:35px; + } + .start-grid { + display:block; + } + .start-grid h2 { + font-size:43px; + } + .start-grid .code-example { + margin-top:32px; + } + .code-panel pre { + padding-inline:14px; + } + .code-toolbar { + padding-left:14px; + } + .code-toolbar>span { + font-size:9px; + } + .code-tabs button { + font-size:10px; + padding:12px; + } + .code-caption { + font-size:8px!important; + padding-inline:14px; + } + .chapter-preview h3 { + font-size:16px; + margin-top:22px; + } + .chapter-preview p { + font-size:12px; + } + .chapter-preview>a { + padding-right:15px; + } + .chapter-preview>a+a { + padding-left:15px; + } + .provider-section h2 { + font-size:43px; + } + .provider-list { + grid-template-columns:repeat(3,1fr); + } + .provider-list a { + font-size:13px; + } + .ci-section h2 { + font-size:36px; + } + .test-counts { + gap:24px; + } + .test-counts strong { + font-size:24px; + } + .comparison-section { + padding-block:50px; + } + .comparison-section .section-heading h2 { + font-size:43px; + } + .closing-section { + padding-block:65px; + } + .closing-section h2 { + font-size:45px; + } + .closing-section>a+a { + display:block; + margin:22px 0 0; + } + .site-footer { + padding:28px 18px; + flex-wrap:wrap; + gap:20px; + } + .site-footer>a:last-child { + margin-left:0; + width:100%; + } + .docs-layout { + display:block; + padding:24px 18px 55px; + } + .docs-sidebar { + margin-bottom:35px; + } + .chapter-menu { + border:1px solid var(--line); + } + .chapter-menu summary { + display:block; + padding:12px 14px; + cursor:pointer; + font-size:12px; + font-weight:600; + } + .chapter-menu summary:after { + content:' +'; + float:right; + } + .chapter-menu[open] summary:after { + content:' −'; + } + .chapter-menu nav { + padding:16px; + } + .chapter-menu[open] nav { + max-height:65vh; + overflow:auto; + } + .docs-article h1 { + font-size:43px; + } + .docs-article>.lead { + font-size:17px; + } + .docs-article>section { + margin-top:38px; + } + .docs-article>section>h2 { + font-size:28px; + } + .docs-article section>p { + font-size:14px; + } + .docs-article pre code { + font-size:11px; + } + .source-link { + flex-wrap:wrap; + gap:12px; + } + .manual-home { + padding-block:45px; + } + .manual-home h1 { + font-size:48px; + } + .manual-home>.lead { + font-size:17px; + } + .manual-directory { + display:block; + margin-top:50px; + } + .manual-group { + margin-top:35px; + } + .manual-group h2 { + font-size:28px; + } +} +@media(prefers-reduced-motion:reduce) { + html { + scroll-behavior:auto; + } +} +@media print { + body { + background:white; + color:black; + } + .site-header,.docs-sidebar,.page-toc,.site-footer,.code-tabs,.code-label button,.search-wrap,.page-turn,.skip { + display:none!important; + } + .container,.manual-home { + width:100%; + } + .docs-layout { + display:block; + padding:0; + } + .code-panel[hidden] { + display:block!important; + } + .code-example,.code-toolbar,.code-caption { + background:white; + color:black; + border:1px solid #aaa; + } + .code-label h3,.syntax-keyword,.syntax-string,.syntax-number,.syntax-comment,.code-toolbar>span { + color:black; + } + .code-caption { + color:black!important; + } + pre { + white-space:pre-wrap; + } + pre code { + white-space:pre-wrap; + } + .docs-article h1 { + font-size:30pt; + } + .docs-article>section>h2 { + font-size:20pt; + } + .hero,.sqlite-grid,.start-grid { + display:block; + } + .table-scroll { + overflow:visible; + } + .comparison-section table { + min-width:0; + font-size:8pt; + } +} +.comparison-notes { + display:grid; + grid-template-columns:1fr 1fr; + gap:28px; + margin:30px 0; + font-size:12px; + color:var(--muted); +} +.comparison-notes p { + margin:0; +} +.fit-grid { + display:grid; + grid-template-columns:repeat(3,1fr); + gap:28px; + padding-top:30px; + border-top:1px solid var(--line); +} +.fit-grid h3 { + font-size:16px; + margin-bottom:14px; +} +.fit-grid p { + font-size:13px; + color:var(--muted); +} +@media(max-width:680px) { + .comparison-notes,.fit-grid { + grid-template-columns:1fr; + } +} diff --git a/docs/assets/site.js b/docs/assets/site.js index ff873e83..6f412360 100644 --- a/docs/assets/site.js +++ b/docs/assets/site.js @@ -1,33 +1,120 @@ -// The documentation remains readable and navigable without JavaScript. -const status = document.getElementById("copy-status"); -if (navigator.clipboard && window.isSecureContext) { - document.querySelectorAll("[data-copy]").forEach((button) => { - button.hidden = false; - button.addEventListener("click", async () => { - try { - await navigator.clipboard.writeText( - document.getElementById(button.dataset.copy).textContent, - ); - button.textContent = "Copied"; - status.textContent = "Code copied to clipboard."; - setTimeout(() => { - button.textContent = "Copy"; - }, 2000); - } catch { - status.textContent = - "Unable to copy. Select the code and copy it manually."; - } +// Progressive enhancement: both examples and every chapter work without JavaScript. +(() => { + const key = 'migrator-code-style'; + let preferred = 'classic'; + try { if (localStorage.getItem(key) === 'fluent') preferred = 'fluent'; } catch { /* Storage may be disabled. */ } + const examples = [...document.querySelectorAll('.code-example')]; + function choose(style) { + preferred = style; + examples.forEach(example => { + example.querySelectorAll('[data-style]').forEach(button => { + const active = button.dataset.style === style; + button.setAttribute('aria-selected', String(active)); + button.tabIndex = active ? 0 : -1; + }); + example.querySelectorAll('[data-code-style]').forEach(panel => { panel.hidden = panel.dataset.codeStyle !== style; }); + }); + try { localStorage.setItem(key, style); } catch { /* Preference is optional. */ } + } + examples.forEach(example => { + const tabs = example.querySelector('.code-tabs'); + tabs.hidden = false; + tabs.setAttribute('role', 'tablist'); + tabs.querySelectorAll('button').forEach(button => { + button.setAttribute('role', 'tab'); + const panel = document.getElementById(button.getAttribute('aria-controls')); + panel.setAttribute('role', 'tabpanel'); + panel.setAttribute('aria-labelledby', button.id); + button.addEventListener('click', () => choose(button.dataset.style)); + button.addEventListener('keydown', event => { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + event.preventDefault(); + const style = event.key === 'Home' ? 'classic' : event.key === 'End' ? 'fluent' : preferred === 'classic' ? 'fluent' : 'classic'; + choose(style); + tabs.querySelector(`[data-style="${style}"]`).focus(); + }); }); }); -} - -// Open the source notes when following a direct citation or shared fragment. -function revealSource() { - const id = window.location.hash.slice(1); - if (id === "sources" || id.startsWith("source-")) { - document.getElementById("sources").open = true; - document.getElementById(id)?.scrollIntoView(); + choose(preferred); + const status = document.getElementById('copy-status'); + if (navigator.clipboard && window.isSecureContext) { + document.querySelectorAll('[data-copy]').forEach(button => { + button.hidden = false; + button.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(document.getElementById(button.dataset.copy).textContent); + button.textContent = 'Copied'; + status.textContent = 'Code copied to clipboard.'; + setTimeout(() => { button.textContent = 'Copy'; }, 2000); + } catch { status.textContent = 'Unable to copy. Select the code and copy it manually.'; } + }); + }); + } + const chapters = document.querySelector('.chapter-menu'); + if (chapters && matchMedia('(max-width: 680px)').matches) chapters.open = false; + function revealSource() { + const id = location.hash.slice(1); + const sources = document.getElementById('sources'); + if (sources && (id === 'sources' || id.startsWith('source-'))) { + sources.open = true; + document.getElementById(id)?.scrollIntoView(); + } } -} -window.addEventListener("hashchange", revealSource); -revealSource(); + window.addEventListener('hashchange', revealSource); + revealSource(); + const search = document.querySelector('.search-wrap'); + if (!search) return; + search.hidden = false; + const input = search.querySelector('input'); + const popover = search.querySelector('.search-popover'); + const results = search.querySelector('ul'); + const searchStatus = search.querySelector('[role=status]'); + const indexUrl = new URL(search.dataset.searchIndex, document.baseURI); + let indexPromise; + let request = 0; + async function runSearch() { + const current = ++request; + const query = input.value.trim().toLowerCase(); + results.replaceChildren(); + if (!query) { popover.hidden = true; return; } + popover.hidden = false; + searchStatus.textContent = 'Searching…'; + try { + indexPromise ??= fetch(indexUrl).then(response => { + if (!response.ok) throw new Error('Search index unavailable'); + return response.json(); + }); + const index = await indexPromise; + if (current !== request) return; + const words = query.split(/\s+/); + const ranked = index.map(entry => { + const heading = `${entry.title} ${entry.group}`.toLowerCase(); + const text = `${heading} ${entry.summary} ${entry.text}`.toLowerCase(); + return { entry, score: words.every(word => text.includes(word)) ? words.reduce((sum, word) => sum + (heading.includes(word) ? 10 : 1), 0) : 0 }; + }).filter(hit => hit.score > 0).sort((a, b) => b.score - a.score).slice(0, 8); + searchStatus.textContent = ranked.length ? `${ranked.length} matching chapters` : 'No chapters found. Try a database name or an operation.'; + ranked.forEach(({ entry }) => { + const li = document.createElement('li'); + const link = document.createElement('a'); + link.href = new URL('../' + entry.url, indexUrl).href; + const title = document.createElement('strong'); title.textContent = entry.title; + const description = document.createElement('span'); description.textContent = `${entry.group} · ${entry.summary}`; + link.append(title, description); li.append(link); results.append(li); + }); + } catch { + if (current !== request) return; + indexPromise = undefined; + searchStatus.textContent = 'Search is unavailable. Use the documentation chapter navigation.'; + } + } + input.addEventListener('input', runSearch); + input.addEventListener('focus', () => { if (input.value.trim()) runSearch(); }); + search.querySelector('form').addEventListener('submit', event => { event.preventDefault(); runSearch(); }); + input.addEventListener('keydown', event => { + if (event.key === 'ArrowDown') { const first = results.querySelector('a'); if (first) { event.preventDefault(); first.focus(); } } + }); + search.addEventListener('keydown', event => { + if (event.key === 'Escape') { ++request; popover.hidden = true; input.focus(); popover.hidden = true; } + }); + document.addEventListener('click', event => { if (!search.contains(event.target)) { ++request; popover.hidden = true; } }); +})(); diff --git a/docs/guide/altering-tables.html b/docs/guide/altering-tables.html new file mode 100644 index 00000000..0a3121a9 --- /dev/null +++ b/docs/guide/altering-tables.html @@ -0,0 +1,22 @@ + + +Altering tables · Migrator.NET +

Altering tables

Rename objects and evolve populated tables while preserving the schema details you still need.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Rename a table and column

Use explicit old and new names. The column rename signature is table, old name, new name in both APIs. A table rename does not rename explicit constraints or their backing indexes. Reusing the original key name for a replacement table may collide on SQL Server or PostgreSQL.

Rename existing objects
+

Classic

+
Database.RenameTable("Users", "Members");
+Database.RenameColumn("Members", "Name", "DisplayName");
+

Fluent

+
migration.Rename.Table("Users", "Members");
+migration.Rename.Column("Members", "Name", "DisplayName");

Inside Up() / BuildUp(MigrationBuilder migration)

Change a complete column definition

Supply the type, length, nullability, default and collation you intend to retain. ChangeColumn replaces the column definition; it does not infer that table constraints should be created or removed. Existing rows must remain valid for the new definition.

Widen a required display name
+

Classic

+
Database.ChangeColumn("Users", new Column("Name", DbType.String, 500)
+{
+    IsNullable = false
+});
+

Fluent

+
migration.Alter.Column("Name", "Users")
+    .AsString(500).NotNullable();

Inside Up() / BuildUp(MigrationBuilder migration)

A deployment sequence for populated data

Add a nullable column, deploy code that can read both forms, backfill values, then enforce the final requirement in a later migration. Large data copies and index creation can hold locks for substantial time; test them against a representative dataset.

On SQLite, a supported alteration may recreate the table and copy rows. On Oracle and some other engines, DDL may commit implicitly. Review the transaction guide and your provider page before choosing the deployment boundary.

diff --git a/docs/guide/api-map.html b/docs/guide/api-map.html new file mode 100644 index 00000000..e528c382 --- /dev/null +++ b/docs/guide/api-map.html @@ -0,0 +1,8 @@ + + +Classic / Fluent API map · Migrator.NET +

Classic / Fluent API map

A practical index of the two authoring surfaces and their shared provider contracts.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Schema operations

ClassicFluent
AddTableCreate.Table
AddColumn(table, column)Create.Column(name, table)
ChangeColumn(table, column)Alter.Column(name, table) / Alter.Column(table, column)
RemoveTable / RemoveColumnDelete.Table / Delete.Column
RenameTable / RenameColumnRename.Table / Rename.Column
AddPrimaryKey / AddUniqueConstraint / AddCheckConstraintCreate.PrimaryKey / Create.Unique / Create.Check
AddForeignKey / RemoveForeignKeyCreate.ForeignKey / Delete.ForeignKey
AddIndex / RemoveIndexCreate.Index / Delete.Index
GetTableConstraints / GetColumnsSchema.Table(name).ConstraintDefinitions() / Columns()

Data and execution

ClassicFluent
Insert / InsertIfNotExistsInsert.IntoTable(...).Row(...) / IfNotExists(...)
Update / DeleteUpdate.Table(...).Set(...).Where(...) / Delete.FromTable(...).Where(...)
ExecuteNonQuery / ExecuteScript / ExecuteResourceScriptExecute.Sql / Execute.Script / Execute.EmbeddedScript
CopyDataFromTableToTable / UpdateTargetFromSourceExecute.CopyData / Execute.UpdateFrom
TruncateTableExecute.Truncate
CreateCommand / ConnectionExecute.WithCommand / Execute.WithConnection
Database provider accessContext or Execute.WithProvider
History / transactionsShared runner and explicit provider context

Execution is not preview or reversal

Both APIs reach the same provider layer, but not every operation has SQL-preview or automatic-reversal support. Provider capabilities still govern execution. Read preview, reversal and the machine-checked method-family inventory for the distinction.

diff --git a/docs/guide/auto-reversing.html b/docs/guide/auto-reversing.html new file mode 100644 index 00000000..2fb9913e --- /dev/null +++ b/docs/guide/auto-reversing.html @@ -0,0 +1,33 @@ + + +Automatic reversal · Migrator.NET +

Automatic reversal

Fluent operations can describe a supported reverse sequence; Classic migrations author it directly.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Creation and its reverse

AutoReversingMigration derives reverse operations in reverse order and validates reversal support before its first change. The Classic equivalent makes the Down operation explicit. Both examples below create the same table and remove it on downgrade.

A reversible table creation
+

Classic

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(3)]
+public class CreateNotes : Migration
+{
+    public override void Up() => Database.AddTable("Notes", new Column("Text", DbType.String, 500));
+    public override void Down() => Database.RemoveTable("Notes");
+}
+

Fluent

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(3)]
+public class CreateNotes : AutoReversingMigration
+{
+    public override void BuildUp(MigrationBuilder migration)
+        => migration.Create.Table("Notes").WithColumn("Text").AsString(500);
+}

Choose one authoring style

What needs an authored reverse

Destructive changes, data operations, SQL and callbacks require explicit reverse behavior. Reverse support is narrower than execution support. An operation that can run is not necessarily one that can be inverted from its definition alone.

Use FluentMigration with BuildDown when the reverse needs its own steps. MigrationBuilder.WithReverse can attach an explicit backward operation to a forward operation. Dropping a newly created table on downgrade still destroys any data inserted since creation; automatic reversal is not data recovery.

diff --git a/docs/guide/cli.html b/docs/guide/cli.html new file mode 100644 index 00000000..6c5a989a --- /dev/null +++ b/docs/guide/cli.html @@ -0,0 +1,30 @@ + + +Command-line tool · Migrator.NET +

Command-line tool

List, validate, plan, preview, apply and reverse migrations from a deployment script.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Install and connect

Install DotNetProjects.Migrator.Tool as a .NET tool. Set MIGRATOR_CONNECTION in the deployment environment or select another variable with --connection-env. Both Classic and Fluent classes use the same commands. The tool does not print the connection-string value.

Install the CLI
+

Classic

+
dotnet tool install --global DotNetProjects.Migrator.Tool
+

Fluent

+
dotnet tool install --global DotNetProjects.Migrator.Tool

Shared commands · both styles

Inspect before applying

Inspect a migration assembly
+

Classic

+
migrator list --assembly MyMigrations.dll --provider SQLite
+migrator status --assembly MyMigrations.dll --provider SQLite
+migrator validate --assembly MyMigrations.dll --provider SQLite
+migrator plan --assembly MyMigrations.dll --provider SQLite --target 10
+migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql
+

Fluent

+
migrator list --assembly MyMigrations.dll --provider SQLite
+migrator status --assembly MyMigrations.dll --provider SQLite
+migrator validate --assembly MyMigrations.dll --provider SQLite
+migrator plan --assembly MyMigrations.dll --provider SQLite --target 10
+migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql

Shared commands · both styles

Validate checks version planning, not arbitrary migration-body behavior. Plan lists version steps without running bodies. SQL generation renders a supported operation subset; it does not produce an idempotent history-managed bundle.

Apply and roll back

Deploy a selected scope
+

Classic

+
migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession
+migrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0
+

Fluent

+
migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession
+migrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0

Shared commands · both styles

Rollback requires an explicit lower target and rejects any plan containing upward steps. Target checks run after taking the configured lock and refreshing history. Tags, profiles and provider choice must match the intended deployment.

Options and exit codes

OptionPurpose
--tags a,b / --tag-match Any|AllFilter versioned migrations.
--profiles a,bSelect named profiles.
--schema / --scopeProvider schema and migration history scope.
--timeout SECONDSDatabase command timeout.
--lock / --lock-timeout SECONDSNative migration lock on supported providers.
--offlineSQL generation assuming empty history; profiles/maintenance rejected.

Exit codes: 0 success; 1 load/execution failure; 2 invalid arguments; 3 unsupported provider/operation; 4 lock timeout. SQL output can contain data authored in migrations. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird; use a custom host for other library providers.

diff --git a/docs/guide/columns.html b/docs/guide/columns.html new file mode 100644 index 00000000..a1d22c48 --- /dev/null +++ b/docs/guide/columns.html @@ -0,0 +1,32 @@ + + +Columns and data types · Migrator.NET +

Columns and data types

Type, size, precision, nullability, defaults, identity and collation are explicit column attributes.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Add and remove a column

Column builders take column name followed by table name. Classic AddColumn takes table name first. New nullable columns accept existing rows without a backfill. A required column usually needs a compatible default or a staged data migration.

Add an optional email address
+

Classic

+
Database.AddColumn("Users", new Column("Email", DbType.String, 320));
+

Fluent

+
migration.Create.Column("Email", "Users").AsString(320).Nullable();

Inside Up() / BuildUp(MigrationBuilder migration)

Remove the email column
+

Classic

+
Database.RemoveColumn("Users", "Email");
+

Fluent

+
migration.Delete.Column("Email", "Users");

Inside Up() / BuildUp(MigrationBuilder migration)

Precision and defaults

For decimal values specify precision and scale. In a Column constructor an integer after the type is the size, not a numeric default. Set DefaultValue explicitly to avoid overload ambiguity. Plain strings are values; trusted SQL expressions use RawSql.Insert.

An amount with four decimal places
+

Classic

+
Database.AddColumn("Orders", new Column("Amount", DbType.Decimal)
+{
+    Precision = 12, Scale = 4, IsNullable = false, DefaultValue = 0m
+});
+

Fluent

+
migration.Create.Column("Amount", "Orders").OfType(DbType.Decimal)
+    .WithPrecision(12, 4).NotNullable().WithDefaultValue(0m);

Inside Up() / BuildUp(MigrationBuilder migration)

Time of day and durations

Use TimeOnly for time-of-day values and TimeSpan for intervals. A TimeSpan is a duration, including negative and multi-day values, so a TimeSpan default on a Time column is rejected. PostgreSQL and Oracle have native intervals; SQLite, SQL Server and MySQL/MariaDB store intervals as signed .NET ticks.

Clock time and elapsed time
+

Classic

+
Database.AddTable("Jobs",
+    new Column("RunAt", DbType.Time) { DefaultValue = new TimeOnly(9, 30) },
+    new Column("Elapsed", MigratorDbType.Interval) { DefaultValue = TimeSpan.Zero });
+

Fluent

+
migration.Create.Table("Jobs")
+    .WithColumn("RunAt").OfType(DbType.Time).WithDefaultValue(new TimeOnly(9, 30))
+    .WithColumn("Elapsed").OfType(MigratorDbType.Interval).WithDefaultValue(TimeSpan.Zero);

Inside Up() / BuildUp(MigrationBuilder migration)

Database storage differs

SQLite does not enforce declared string lengths or decimal precision. UInt64 values above Int64.MaxValue are rejected there. Oracle character empty strings become NULL; Informix and Sybase have their own trimming and range behavior. Consult the type support and boundary matrix for supported mappings and live-test scope. A shared DbType does not imply identical native storage.

diff --git a/docs/guide/conditional.html b/docs/guide/conditional.html new file mode 100644 index 00000000..faef06f8 --- /dev/null +++ b/docs/guide/conditional.html @@ -0,0 +1,13 @@ + + +Conditional logic · Migrator.NET +

Conditional logic

Choose between inspecting the live schema and declaring a provider-specific operation.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Provider-specific operations

The Classic provider indexer selects a named provider or a no-op provider. Fluent IfDatabase wraps structured operations in a provider condition. Use the provider names understood by the dialect; this SQLite example leaves other providers unchanged.

Run a SQLite-specific statement
+

Classic

+
Database["SQLite"].ExecuteNonQuery("UPDATE Users SET Name = upper(Name)");
+

Fluent

+
migration.IfDatabase("SQLite", sqlite =>
+    sqlite.Execute.Sql("UPDATE Users SET Name = upper(Name)"));

Inside Up() / BuildUp(MigrationBuilder migration)

Schema-dependent decisions

Use Database.TableExists/ColumnExists or FluentMigration.Schema for connected checks. These inspect the current database. A fluent BuildUp method collects operations before they execute, so queued creation is not visible to a live metadata read in the same method.

For execution-time decisions after earlier operations, use an explicit provider callback. That callback cannot be previewed and requires an authored reverse. Avoid making a migration silently succeed with the wrong schema: an existence check alone does not validate a column’s type or constraint definition.

diff --git a/docs/guide/configuration.html b/docs/guide/configuration.html new file mode 100644 index 00000000..2cb5b409 --- /dev/null +++ b/docs/guide/configuration.html @@ -0,0 +1,28 @@ + + +Configuration · Migrator.NET +

Configuration

Select the connection, migration set and history scope first, then set runner options before executing.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Connect and select a scope

This host fragment assumes an open ADO.NET connection. The provider scope partitions history and selects explicitly scoped classes. An unscoped migration inherits the provider scope. Scopes do not create separate database objects: two modules can still conflict on a table name.

Host configuration · both styles
+

Classic

+
using var billingProvider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null, scope: "billing");
+billingProvider.CommandTimeout = 60;
+var billing = new Migrator(billingProvider, typeof(CreateUsers).Assembly, false);
+billing.SchemaInfoTableName = "BillingSchemaInfo";
+billing.Options.Tags.Add("core");
+billing.Options.TagMatch = TagMatchMode.All;
+billing.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+billing.MigrateToLastVersion();
+

Fluent

+
using var billingProvider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null, scope: "billing");
+billingProvider.CommandTimeout = 60;
+var billing = new Migrator(billingProvider, typeof(CreateUsers).Assembly, false);
+billing.SchemaInfoTableName = "BillingSchemaInfo";
+billing.Options.Tags.Add("core");
+billing.Options.TagMatch = TagMatchMode.All;
+billing.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+billing.MigrateToLastVersion();

Shared host · both styles

Runner options

OptionBehavior
Tags / TagMatchCase-sensitive ordinal tags; match Any or All. No filter selects all versioned migrations.
ProfilesExplicit profile names; selected profiles run after versioned migrations.
TransactionModePerMigration, None or WholeSession. WholeSession supports SQLite, PostgreSQL and SQL Server.
ActivatorOptional delegate for creating migration instances.
Lock / LockTimeoutOptional cross-process lease acquired before reading history; default timeout is 30 seconds.

History and ownership

Set the history table before accessing history or running migrations. Its default name is SchemaInfo; the default scope is default. Give each runner its intended assembly or explicit migration types. Duplicate versions within an effective scope fail discovery.

Load connection strings from application configuration or environment variables. The CLI reads MIGRATOR_CONNECTION by default. Do not store production credentials in migration classes.

diff --git a/docs/guide/connections.html b/docs/guide/connections.html new file mode 100644 index 00000000..051d4ecb --- /dev/null +++ b/docs/guide/connections.html @@ -0,0 +1,33 @@ + + +Commands and callbacks · Migrator.NET +

Commands and callbacks

Use the active provider connection when a migration needs driver-level work.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Bind command parameters

The provider creates a command associated with its current transaction. Dispose it after use. Generate parameter names through the provider, rather than assuming every driver uses the same convention. The callback is deferred until fluent execution reaches it.

Execute a parameterized command
+

Classic

+
using var command = Database.CreateCommand();
+var name = Database.GenerateParameterName(0);
+command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1";
+var value = command.CreateParameter();
+value.ParameterName = name;
+value.Value = "Ada";
+command.Parameters.Add(value);
+command.ExecuteNonQuery();
+

Fluent

+
migration.Execute.WithProvider(provider =>
+{
+    using var command = provider.CreateCommand();
+    var name = provider.GenerateParameterName(0);
+    command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1";
+    var value = command.CreateParameter();
+    value.ParameterName = name;
+    value.Value = "Ada";
+    command.Parameters.Add(value);
+    command.ExecuteNonQuery();
+});

Inside Up() / BuildUp(MigrationBuilder migration)

Connection ownership

WithCommand creates and disposes a provider command around your action. WithConnection exposes the connection; WithProvider exposes the complete transformation provider. Do not close or replace a runner-owned connection, commit its transaction or switch databases while holding a native migration lock.

Database administration

Database creation and other administration require a connection and identity authorized for that operation. Use a dedicated host with TransactionMode.None. Fluent administration rejects an active transaction; do not combine it with WholeSession or assume a Classic provider call can participate in transactional DDL.

Create a database on a supporting server
+

Classic

+
Database.CreateDatabases("Reporting");
+

Fluent

+
migration.Administration.CreateDatabase("Reporting");

Inside Up() / BuildUp(MigrationBuilder migration)

The remaining mappings are DropDatabases / Administration.DropDatabase, SwitchDatabase / Administration.SwitchDatabase, and KillDatabaseConnections / Administration.KillConnections. These are explicit administrative actions with provider-specific support. Database switches invalidate assumptions about migration history and session locks: keep provisioning separate from ordinary schema migrations. They are outside SQL preview and automatic reversal.

Preview and reversal

Callbacks can perform arbitrary C# work and cannot be translated into SQL preview. They require explicit reverse behavior. Keeping external network calls out of migration bodies makes failures easier to reason about: a database rollback cannot undo an email or an HTTP request.

diff --git a/docs/guide/constraints.html b/docs/guide/constraints.html new file mode 100644 index 00000000..b73378d3 --- /dev/null +++ b/docs/guide/constraints.html @@ -0,0 +1,18 @@ + + +Keys and constraints · Migrator.NET +

Keys and constraints

Declare table invariants independently of column attributes.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Add uniqueness and a check

Existing rows must satisfy a new constraint. A rebuild or ALTER operation can fail if duplicate or invalid data is present. CHECK expressions are trusted SQL and depend on the target engine. Primary keys, unique constraints, foreign keys and checks have typed definitions.

Add two named constraints
+

Classic

+
Database.AddUniqueConstraint("UQ_Users_Name", "Users", "Name");
+Database.AddCheckConstraint("CK_Users_Id", "Users", "Id > 0");
+

Fluent

+
migration.Create.Unique("UQ_Users_Name", "Users", "Name");
+migration.Create.Check("CK_Users_Id", "Users", "Id > 0");

Inside Up() / BuildUp(MigrationBuilder migration)

Remove the intended object

Use dedicated primary-key and foreign-key removal methods; generic RemoveConstraint is for unique/check constraints in the SQLite provider. Avoid RemoveAllConstraints unless the migration deliberately replaces every invariant.

Remove a check constraint
+

Classic

+
Database.RemoveConstraint("Users", "CK_Users_Id");
+

Fluent

+
migration.Delete.Constraint("CK_Users_Id", "Users");

Inside Up() / BuildUp(MigrationBuilder migration)

Constraint identity

GetTableConstraints returns ordered typed definitions. SQLite can return a null name for an unnamed legacy constraint; an autoindex name is not a substitute constraint name. PrimaryKeyExists checks the actual key name. MySQL reports the primary key name as PRIMARY.

Altering a column does not give that column ownership of a unique constraint. SQL Server implicit ownership markers are no longer used for deletion. Explicitly remove only the object your migration intends to change.

diff --git a/docs/guide/contributing.html b/docs/guide/contributing.html new file mode 100644 index 00000000..0e89a2e0 --- /dev/null +++ b/docs/guide/contributing.html @@ -0,0 +1,8 @@ + + +Contributing · Migrator.NET +

Contributing

Make a provider change reproducible, then verify its observable behavior.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

A useful report

Include package, database and driver versions, a minimal migration, relevant schema/data, and expected versus actual behavior. Remove secrets from connection strings and logs. File reports in the issue tracker.

A focused change

Add a regression that fails before the fix and checks the real result afterward. Provider-specific changes need actual-engine evidence; skipped tests and generated SQL alone do not qualify support. Keep mutable definition inputs independent from caller arrays and check failure paths.

Documentation changes

Edit docs/_src/content.py for chapters and docs/_src/home.html for the homepage. Run python .github/scripts/build-docs.py to regenerate static HTML and the search index. Run python .github/scripts/verify-docs.py --compile to validate links, paired examples and compilable C# samples. Site assets live in docs/assets.

Each migration-operation example should provide Classic and Fluent versions. Shared runner and shell commands intentionally appear in both tabs. Keep provider restrictions precise and features described in the present tense. Run the homepage CI-count renderer tests when changing the site template.

Design references

The chapter organization follows the learning path of FluentMigrator’s documentation, adapted to this API. Visual references include Resend’s typography and code tabs and Gel’s code walkthroughs. The site uses its own palette, layout, copy and migration illustrations.

diff --git a/docs/guide/creating-tables.html b/docs/guide/creating-tables.html new file mode 100644 index 00000000..cb1d124a --- /dev/null +++ b/docs/guide/creating-tables.html @@ -0,0 +1,58 @@ + + +Creating tables · Migrator.NET +

Creating tables

Describe a complete table: columns first, with explicit named keys and constraints.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Create a table with a key

The table definition groups related schema objects into one operation. Primary-key columns are emitted as non-nullable. In the fluent API a complete table is collected before execution, so keys can refer to columns declared in the same chain.

CreateUsers.cs
+

Classic

+
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32) { IsNullable = false },
+            new Column("Name", DbType.String, 255),
+            new PrimaryKeyConstraint("PK_Users", "Id"));
+    }
+
+    public override void Down() => Database.RemoveTable("Users");
+}
+

Fluent

+
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()
+            .WithColumn("Name").AsString(255)
+            .WithPrimaryKey("PK_Users", "Id");
+    }
+
+    public override void BuildDown(MigrationBuilder migration)
+        => migration.Delete.Table("Users");
+}

Choose one authoring style

Composite keys and uniqueness

Use the declared key order consistently in both primary and foreign keys. A composite unique constraint applies to the tuple; it does not make each column unique separately. The fully qualified constraint type below avoids the name collision with System.Data.UniqueConstraint.

A table with an ordered composite key
+

Classic

+
Database.AddTable("Subscriptions",
+    new Column("TenantId", DbType.Int32),
+    new Column("UserId", DbType.Int32),
+    new Column("Email", DbType.String, 255),
+    new PrimaryKeyConstraint("PK_Subscriptions", "TenantId", "UserId"),
+    new DotNetProjects.Migrator.Framework.UniqueConstraint(
+        "UQ_Subscriptions_Email", "TenantId", "Email"));
+

Fluent

+
migration.Create.Table("Subscriptions")
+    .WithColumn("TenantId").AsInt32()
+    .WithColumn("UserId").AsInt32()
+    .WithColumn("Email").AsString(255)
+    .WithPrimaryKey("PK_Subscriptions", "TenantId", "UserId")
+    .WithUniqueConstraint("UQ_Subscriptions_Email", "TenantId", "Email");

Inside Up() / BuildUp(MigrationBuilder migration)

Identity and removal

Identity generation is a column attribute, separate from primary-key membership. SQLite requires an INTEGER identity column and its single-column primary key in the same definition. Use a complete Create.Table/AddTable operation to satisfy that rule. To reverse creation use Database.RemoveTable or migration.Delete.Table; dropping a table also removes its rows.

For supported creation operations, automatic reversal can derive the reverse operation. Explicitly author reverse behavior for destructive changes.

diff --git a/docs/guide/data.html b/docs/guide/data.html new file mode 100644 index 00000000..1c0bae90 --- /dev/null +++ b/docs/guide/data.html @@ -0,0 +1,38 @@ + + +Data operations · Migrator.NET +

Data operations

Insert, update and delete using explicit column/value arrays. Keep predicates separate from changed values.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Insert rows

Column and value arrays must have the same length. The provider binds values using its driver-specific parameter mappings. For multiple rows issue multiple operations; the fluent Row method describes one row, not an accumulated collection of rows.

Insert a user
+

Classic

+
Database.Insert("Users", new[] { "Id", "Name" }, new object[] { 1, "Ada" });
+

Fluent

+
migration.Insert.IntoTable("Users")
+    .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" });

Inside Up() / BuildUp(MigrationBuilder migration)

Update and delete with predicates

Without a predicate, update/delete affects every row. Supply predicate columns and values deliberately. Fluent WhereSql is available for updates only; its text is trusted SQL, not an escaped user input.

Update one user
+

Classic

+
Database.Update("Users", new[] { "Name" }, new object[] { "Ada Lovelace" },
+    new[] { "Id" }, new object[] { 1 });
+

Fluent

+
migration.Update.Table("Users")
+    .Set(new[] { "Name" }, new object[] { "Ada Lovelace" })
+    .Where(new[] { "Id" }, new object[] { 1 });

Inside Up() / BuildUp(MigrationBuilder migration)

Delete one user
+

Classic

+
Database.Delete("Users", new[] { "Id" }, new object[] { 1 });
+

Fluent

+
migration.Delete.FromTable("Users").Where(new[] { "Id" }, new object[] { 1 });

Inside Up() / BuildUp(MigrationBuilder migration)

Conditional seed data

Use an explicit identifying predicate when a seed should exist only once. This is distinct from a migration version: a named profile can run repeatedly without a history entry. Coordinate competing writers; a check-then-insert helper is not a substitute for a database unique key.

Insert a missing seed
+

Classic

+
Database.InsertIfNotExists("Users", new[] { "Id", "Name" },
+    new object[] { 1, "Ada" }, new[] { "Id" }, new object[] { 1 });
+

Fluent

+
migration.Insert.IntoTable("Users")
+    .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" })
+    .IfNotExists(new[] { "Id" }, new object[] { 1 });

Inside Up() / BuildUp(MigrationBuilder migration)

Copying and reversal

Use the provider CopyDataFromTableToTable helper or fluent Execute.CopyData for named-column copies. Both tables must already exist and target columns must accept the source values. Execute.UpdateFrom maps source/target pairs. These operations retain provider limits and are outside the SQL-preview subset. A reverse data migration needs authored recovery logic; auto-reversal cannot recreate deleted or overwritten values.

Copy users into an archive table
+

Classic

+
Database.CopyDataFromTableToTable("Users",
+    new System.Collections.Generic.List<string> { "Id", "Name" }, "ArchivedUsers",
+    new System.Collections.Generic.List<string> { "UserId", "DisplayName" });
+

Fluent

+
migration.Execute.CopyData("Users", new[] { "Id", "Name" },
+    "ArchivedUsers", new[] { "UserId", "DisplayName" });

Inside Up() / BuildUp(MigrationBuilder migration)

diff --git a/docs/guide/defaults-collations.html b/docs/guide/defaults-collations.html new file mode 100644 index 00000000..b87739e4 --- /dev/null +++ b/docs/guide/defaults-collations.html @@ -0,0 +1,26 @@ + + +Defaults and collations · Migrator.NET +

Defaults and collations

Distinguish values from SQL expressions, and comparison intent from a provider's installed collation name.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Literal and expression defaults

Ordinary strings are quoted literal values. RawSql.Insert marks trusted SQL to evaluate on the database. The expression below works on SQLite; provider function names and return types can differ.

A database-generated timestamp
+

Classic

+
Database.AddTable("Events", new Column("CreatedAt", DbType.DateTime)
+{
+    DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP"), IsNullable = false
+});
+

Fluent

+
migration.Create.Table("Events")
+    .WithColumn("CreatedAt").OfType(DbType.DateTime).NotNullable()
+    .WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP"));

Inside Up() / BuildUp(MigrationBuilder migration)

Comparison behavior

Collation presets request semantics. Unsupported mappings fail before DDL. SQLite AsciiIgnoreCase maps to NOCASE and folds ASCII only; it is not Unicode case folding. Named custom SQLite collations must be registered on the connection before schema or data operations use them.

ASCII-insensitive SQLite text
+

Classic

+
Database.AddTable("Labels", new Column("Name", DbType.String, 100)
+{
+    Collation = Collation.AsciiIgnoreCase
+});
+

Fluent

+
migration.Create.Table("Labels")
+    .WithColumn("Name").AsString(100)
+    .WithCollation(Collation.AsciiIgnoreCase);

Inside Up() / BuildUp(MigrationBuilder migration)

Presets and provider names

RequestMeaning
CaseInsensitive / CaseSensitiveCase behavior with accent sensitivity; supported SQL Server/MySQL/MariaDB mappings, or an explicit installed name on other engines.
BinaryProvider binary comparison; not a promise of identical linguistic ordering.
AsciiIgnoreCaseSQLite NOCASE; ASCII letters only.
Collation.Named(name)An installed or registered provider-specific collation.

Use named collations for language-specific or exact comparison behavior. PostgreSQL ICU nondeterministic collations must be created explicitly; SQL rendering does not create shared database objects. Read the mapping table for engine versions and restrictions.

diff --git a/docs/guide/dependency-injection.html b/docs/guide/dependency-injection.html new file mode 100644 index 00000000..abcf44a9 --- /dev/null +++ b/docs/guide/dependency-injection.html @@ -0,0 +1,40 @@ + + +Dependency injection and logging · Migrator.NET +

Dependency injection and logging

Resolve the runner and migration dependencies inside one service scope.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Register the integration

Install DotNetProjects.Migrator.Extensions.DependencyInjection and Microsoft.Extensions.Logging alongside the core and database driver. This example uses the connection-string provider factory so provider disposal belongs to the DI scope. The providerName explicitly selects the SQLite driver.

A scoped migration host
+

Classic

+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using DotNetProjects.Migrator.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+services.AddLogging();
+services.AddMigrator(_ => ProviderFactory.Create(
+    ProviderTypes.SQLite, "Data Source=app.db", defaultSchema: null,
+    providerName: "Microsoft.Data.Sqlite"), typeof(CreateUsers).Assembly,
+    options => options.TransactionMode = MigrationTransactionMode.PerMigration);
+
+using var container = services.BuildServiceProvider();
+using var scope = container.CreateScope();
+scope.ServiceProvider.GetRequiredService<Migrator>().MigrateToLastVersion();
+

Fluent

+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using DotNetProjects.Migrator.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+services.AddLogging();
+services.AddMigrator(_ => ProviderFactory.Create(
+    ProviderTypes.SQLite, "Data Source=app.db", defaultSchema: null,
+    providerName: "Microsoft.Data.Sqlite"), typeof(CreateUsers).Assembly,
+    options => options.TransactionMode = MigrationTransactionMode.PerMigration);
+
+using var container = services.BuildServiceProvider();
+using var scope = container.CreateScope();
+scope.ServiceProvider.GetRequiredService<Migrator>().MigrateToLastVersion();

Shared host · both styles

Constructor dependencies

Migration classes are registered for activation through the service provider. Register your own constructor dependencies before resolving the runner. Options are scoped snapshots; a custom Activator can override construction. Fluent and Classic migrations use the same activation mechanism.

Logging boundaries

The integration adapts runner lifecycle events to Microsoft logging. It omits SQL text and raw exception messages from these events. Configure your own logging providers through AddLogging. The core retains its lightweight logger API when you do not use DI.

Dispose the scope after migration execution. When supplying a caller-owned open connection, keep its owner alive until after the scope is disposed; the provider does not acquire ownership of an externally supplied connection.

diff --git a/docs/guide/extensions.html b/docs/guide/extensions.html new file mode 100644 index 00000000..b11e31f8 --- /dev/null +++ b/docs/guide/extensions.html @@ -0,0 +1,35 @@ + + +Custom extensions · Migrator.NET +

Custom extensions

Reuse schema conventions without hiding provider behavior or changing the migration contract.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Share a schema convention

A small helper can express a repeated column policy in both styles. Keep helper behavior stable for historical migrations; changing a helper can change what an old migration does on a fresh database. The example uses static methods to keep its dependencies explicit.

Reusable audit-column helpers
+

Classic

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+public static class AuditColumns
+{
+    public static void Add(ITransformationProvider database, string table)
+        => database.AddColumn(table, new Column("CreatedAt", DbType.DateTime)
+        {
+            IsNullable = false, DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP")
+        });
+}
+

Fluent

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+public static class AuditColumns
+{
+    public static void Add(MigrationBuilder migration, string table)
+        => migration.Create.Column("CreatedAt", table).OfType(DbType.DateTime)
+            .NotNullable().WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP"));
+}

Choose one authoring style

Custom activation and locks

RunnerOptions.Activator constructs migrations when a DI container is not appropriate. IMigrationLock supplies a disposable lease for custom deployment coordination. Release must work on success and failure. Configure these at the host boundary rather than in individual migrations.

Provider authors

ITransformationProvider defines execution and metadata operations. A custom provider needs accurate typed constraint definitions or an explicit unsupported error. IMigrationHistory enables read-only planning and effective-scope history selection. IScriptBatchProvider extends script processing.

Keep SQL rendering independent of a live connection. Custom MigrationOperation implementations need deliberate validation, application, SQL rendering and reversal behavior. See the API map and implementation contracts before claiming preview or reversal support.

diff --git a/docs/guide/faq.html b/docs/guide/faq.html new file mode 100644 index 00000000..be04b250 --- /dev/null +++ b/docs/guide/faq.html @@ -0,0 +1,43 @@ + + +Frequently asked questions · Migrator.NET +

Frequently asked questions

Decisions to make before adopting the library or moving an existing migration project.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Do I need an ORM?

No. Migrations operate on an ADO.NET connection through the transformation provider. Use EF, Dapper, another data layer or direct SQL in the rest of your application. Migrator does not scaffold schema changes from an object model.

Can I mix Classic and Fluent?

Yes. Both implement the same migration contract, run through the same loader and share history. Keep each version unique. The tabs throughout these guides show equivalent choices, not two classes to install together. The authoring method names differ: Up/Down versus BuildUp/BuildDown.

CreateUsers.cs
+

Classic

+
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32) { IsNullable = false },
+            new Column("Name", DbType.String, 255),
+            new PrimaryKeyConstraint("PK_Users", "Id"));
+    }
+
+    public override void Down() => Database.RemoveTable("Users");
+}
+

Fluent

+
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()
+            .WithColumn("Name").AsString(255)
+            .WithPrimaryKey("PK_Users", "Id");
+    }
+
+    public override void BuildDown(MigrationBuilder migration)
+        => migration.Delete.Table("Users");
+}

Choose one authoring style

Why does SQLite rebuilding matter?

SQLite does not implement every ALTER TABLE operation. Migrator reads the live schema and reconstructs a supported table when a column or constraint change needs it. This is useful without an ORM model. See SQLite for preserved objects, foreign-key checks and reconstruction boundaries.

Does rollback recover data?

A transaction can roll back a failed migration when its database operations are transactional. Downgrading a completed version executes your reverse method. Neither mechanism recovers rows already deleted by a successful migration. Use an explicit recovery design and backups for that case.

Can I edit an applied migration?

The journal records versions, scopes and timestamps, not a content checksum. Editing an applied class will not make it rerun. Add a new migration for a change. Consolidated baselines are an explicit history operation; read versioning and history.

Why does SQL preview reject my migration?

Preview renders a structured subset. A provider callback, unsupported constraint alteration or schema dependency after raw SQL cannot be represented reliably and raises an error. Read planning and SQL preview rather than treating preview as a full execution simulation.

diff --git a/docs/guide/foreign-keys.html b/docs/guide/foreign-keys.html new file mode 100644 index 00000000..3075f854 --- /dev/null +++ b/docs/guide/foreign-keys.html @@ -0,0 +1,22 @@ + + +Foreign keys · Migrator.NET +

Foreign keys

Define ordered child/parent columns and independent actions for update and delete.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Add a relationship

Parent key columns must identify a suitable primary/unique key. Child and parent arrays are positional: each child column corresponds to the parent column at the same index. Both tables and their compatible columns must already exist for this example. The Classic example uses IForeignKeyActions for independent update/delete actions; the older AddForeignKey overload supplies one action for both.

Orders belong to users
+

Classic

+
((IForeignKeyActions)Database).AddForeignKey(
+    "FK_Orders_Users", "Orders", new[] { "UserId" },
+    "Users", new[] { "Id" }, ForeignKeyConstraintType.Cascade,
+    ForeignKeyConstraintType.NoAction);
+

Fluent

+
migration.Create.ForeignKey("FK_Orders_Users",
+    "Orders", new[] { "UserId" }, "Users", new[] { "Id" },
+    onDelete: ForeignKeyConstraintType.Cascade,
+    onUpdate: ForeignKeyConstraintType.NoAction);

Inside Up() / BuildUp(MigrationBuilder migration)

Remove a relationship

Remove dependent keys before incompatible table or key changes. Restore them only after the existing data satisfies the replacement relationship.

Remove the foreign key
+

Classic

+
Database.RemoveForeignKey("Orders", "FK_Orders_Users");
+

Fluent

+
migration.Delete.ForeignKey("FK_Orders_Users", "Orders");

Inside Up() / BuildUp(MigrationBuilder migration)

Database semantics

Supported actions depend on the database; do not assume every engine implements CASCADE, RESTRICT, SET NULL and SET DEFAULT identically. SQLite rebuilds preserve separate update/delete actions and validate integrity before an owned transaction commits. MATCH FULL and MATCH PARTIAL requests are rejected because SQLite does not enforce those semantics.

SetNull needs nullable child columns. Test action behavior using actual data, especially composite keys and partially NULL values. Oracle supports its own subset of foreign-key actions.

diff --git a/docs/guide/index.html b/docs/guide/index.html new file mode 100644 index 00000000..88527ec8 --- /dev/null +++ b/docs/guide/index.html @@ -0,0 +1,8 @@ + + +The migration manual · Migrator.NET +

DOTNETPROJECTS / THE MIGRATION MANUAL

Know what changes.
Know how it runs.

From your first table to deployment locks and SQLite reconstruction. Practical guides with a Classic and Fluent example for every authoring task.

Start with a working example ↗

01Introduction

  • Your first migration ↗

    Create a SQLite database, apply a versioned change, and write its reverse. Choose either C# style; the runner is the same.

  • Installation ↗

    The core library, database driver, optional DI integration and CLI each have a distinct job.

  • Configuration ↗

    Select the connection, migration set and history scope first, then set runner options before executing.

  • Frequently asked questions ↗

    Decisions to make before adopting the library or moving an existing migration project.

02Operations

  • Creating tables ↗

    Describe a complete table: columns first, with explicit named keys and constraints.

  • Altering tables ↗

    Rename objects and evolve populated tables while preserving the schema details you still need.

  • Columns and data types ↗

    Type, size, precision, nullability, defaults, identity and collation are explicit column attributes.

  • Data operations ↗

    Insert, update and delete using explicit column/value arrays. Keep predicates separate from changed values.

  • Schema inspection ↗

    Read the connected database before deciding what to change. Metadata is different from a model snapshot.

  • Execute SQL and scripts ↗

    Use schema operations where they fit, and keep database-specific SQL explicit.

  • Commands and callbacks ↗

    Use the active provider connection when a migration needs driver-level work.

03Schema basics

  • Indexes ↗

    An index is a separate schema object, even when it enforces uniqueness.

  • Keys and constraints ↗

    Declare table invariants independently of column attributes.

  • Foreign keys ↗

    Define ordered child/parent columns and independent actions for update and delete.

  • Defaults and collations ↗

    Distinguish values from SQL expressions, and comparison intent from a provider's installed collation name.

04Migration runners

05Migration types

06Database providers

  • Provider overview ↗

    One authoring contract, explicit database behavior. Choose the driver and provider together.

  • SQLite ↗

    Change existing tables from the live schema, without maintaining an ORM model.

  • SQL Server ↗

    Explicit keys, provider-specific indexes, transactional DDL and application locks.

  • PostgreSQL ↗

    Native intervals, schema-aware metadata, transactional DDL and advisory locks.

  • MySQL and MariaDB ↗

    Related providers with explicit engine, collation and DDL transaction differences.

  • Oracle ↗

    Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.

  • HANA and additional providers ↗

    Use the live-engine matrix to qualify operations beyond the common database families.

07Advanced topics

08Reference

diff --git a/docs/guide/indexes.html b/docs/guide/indexes.html new file mode 100644 index 00000000..1618946e --- /dev/null +++ b/docs/guide/indexes.html @@ -0,0 +1,22 @@ + + +Indexes · Migrator.NET +

Indexes

An index is a separate schema object, even when it enforces uniqueness.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Create and remove an index

Use an explicit name so the index can be inspected or removed later. Both APIs accept the same Index definition. Fully qualify this type if System.Index is also in scope. Columns retain the order in KeyColumns.

Index a user name
+

Classic

+
Database.AddIndex("Users", new DotNetProjects.Migrator.Framework.Index
+{
+    Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false
+});
+

Fluent

+
migration.Create.Index("Users", new DotNetProjects.Migrator.Framework.Index
+{
+    Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false
+});

Inside Up() / BuildUp(MigrationBuilder migration)

Drop an index
+

Classic

+
Database.RemoveIndex("Users", "IX_Users_Name");
+

Fluent

+
migration.Delete.Index("IX_Users_Name", "Users");

Inside Up() / BuildUp(MigrationBuilder migration)

Provider options

Index definitions also expose IncludeColumns, FilterItems and Clustered. These options are provider-specific. Oracle rejects included and clustered index requests; SQLite reconstruction rejects existing index SQL with explicit COLLATE clauses. Preview handles simple indexes and rejects unsupported options.

Unique index or unique constraint?

Use UniqueConstraint for a table-level invariant and an Index with Unique for an index definition. Do not infer ownership from a generated name. SQLite RemoveAllIndexes preserves declared table UNIQUE constraints; remove those through the constraint APIs. Check query plans and data cardinality when choosing index keys.

diff --git a/docs/guide/installation.html b/docs/guide/installation.html new file mode 100644 index 00000000..0c54c888 --- /dev/null +++ b/docs/guide/installation.html @@ -0,0 +1,18 @@ + + +Installation · Migrator.NET +

Installation

The core library, database driver, optional DI integration and CLI each have a distinct job.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Choose your packages

PackagePurpose
DotNetProjects.MigratorMigration classes, providers, runner and fluent operations.
An ADO.NET driverInstall the driver for the database your host opens.
DotNetProjects.Migrator.Extensions.DependencyInjectionOptional scoped runner, constructor injection and Microsoft logging.
DotNetProjects.Migrator.ToolThe migrator command-line tool.
Terminal · either authoring style
+

Classic

+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7
+

Fluent

+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7

Shared commands · both styles

Choose a driver

Common choices are Microsoft.Data.Sqlite, Microsoft.Data.SqlClient, Npgsql, MySql.Data, Oracle.ManagedDataAccess.Core and FirebirdSql.Data.FirebirdClient. The core library does not directly reference these packages. Passing an open connection makes driver selection explicit and keeps connection ownership with your application.

Read the provider overview for database families, aliases and CI coverage. A provider name is not a guarantee that every native operation has the same behavior on every server.

Use the repository

To develop against a checkout, replace the core package reference with a project reference to src/Migrator/DotNetProjects.Migrator.csproj. The solution targets .NET 9. Building the .slnx solution requires an SDK that understands that format, such as SDK 9.0.200 or later.

Keep the library, CLI and optional DI integration on compatible versions. Recompile old migration assemblies when updating a breaking API; the upgrade guide explains the column and constraint changes.

diff --git a/docs/guide/maintenance.html b/docs/guide/maintenance.html new file mode 100644 index 00000000..d2ba7e1a --- /dev/null +++ b/docs/guide/maintenance.html @@ -0,0 +1,36 @@ + + +Maintenance migrations · Migrator.NET +

Maintenance migrations

Place ordered work at the runner's lifecycle stages.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Choose a stage

StageWhen
BeforeRunBefore versioned migration work in this run.
BeforeMigrationBefore each executed versioned migration.
AfterMigrationAfter each executed versioned migration.
AfterRunAfter the run's migration/profile work.

Maintenance classes accept Order and Scope. They use Up and do not acquire version records. Hooks stop on failure; later stages are not finally blocks or guaranteed cleanup paths. Lock release and connection/transaction restoration are runner responsibilities.

A scoped maintenance operation

The example expects an existing DeploymentLog table. Choose a table that already exists at the selected stage. Fluent callbacks execute at the corresponding operation position.

Write a deployment marker
+

Classic

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Maintenance(MaintenanceStage.AfterRun, Order = 10)]
+public class RecordDeployment : Migration
+{
+    public override void Up()
+        => Database.Insert("DeploymentLog", new[] { "Message" }, new object[] { "Migration run finished" });
+    public override void Down() { }
+}
+

Fluent

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Maintenance(MaintenanceStage.AfterRun, Order = 10)]
+public class RecordDeployment : FluentMigration
+{
+    public override void BuildUp(MigrationBuilder migration)
+        => migration.Insert.IntoTable("DeploymentLog")
+            .Row(new[] { "Message" }, new object[] { "Migration run finished" });
+    public override void BuildDown(MigrationBuilder migration) { }
+}

Choose one authoring style

Post-commit callbacks

Migration.AfterUp and AfterDown run after commit, with the migration context restored. In WholeSession they wait until the entire session commits. Their failure reports an error after durable changes; it cannot reverse that commit. Do not confuse maintenance stages with a guaranteed post-commit delivery system.

diff --git a/docs/guide/mysql.html b/docs/guide/mysql.html new file mode 100644 index 00000000..5d86c5a6 --- /dev/null +++ b/docs/guide/mysql.html @@ -0,0 +1,16 @@ + + +MySQL and MariaDB · Migrator.NET +

MySQL and MariaDB

Related providers with explicit engine, collation and DDL transaction differences.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Choose the matching dialect

Select ProviderTypes.Mysql for MySQL and MariaDB for MariaDB. Use an open driver connection or configure the factory. Do not treat compatible wire protocols as proof of identical server syntax or metadata behavior. DDL can commit implicitly; the runner rejects WholeSession for these dialects.

Select a supported collation

Semantic presets require utf8mb4-compatible text and the documented server versions: MySQL 8 and MariaDB 10.10+ have different mappings. Use a named installed collation if exact linguistic or trailing-space behavior matters.

Case-insensitive, accent-sensitive text
+

Classic

+
Database.AddTable("Labels", new Column("Name", DbType.String, 100)
+{
+    Collation = Collation.CaseInsensitive
+});
+

Fluent

+
migration.Create.Table("Labels").WithColumn("Name").AsString(100)
+    .WithCollation(Collation.CaseInsensitive);

Inside Up() / BuildUp(MigrationBuilder migration)

Constraint metadata

MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction.

Locking and values

DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.

diff --git a/docs/guide/oracle.html b/docs/guide/oracle.html new file mode 100644 index 00000000..71e7368d --- /dev/null +++ b/docs/guide/oracle.html @@ -0,0 +1,18 @@ + + +Oracle · Migrator.NET +

Oracle

Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Connection and schema

Use ProviderTypes.Oracle with the Oracle managed ADO.NET driver and the intended schema. MsOracle is a historical variant. Oracle DDL is not generally atomic across a migration; WholeSession is rejected. Some quoted qualified metadata lookups are explicitly rejected.

Create an identity definition

Identity is a column attribute and is validated before table creation. It need not be a primary key on every engine, but the example pairs it with an explicit key. Use a server/driver combination qualified for native identity.

An identity table with an explicit key
+

Classic

+
Database.AddTable("Entries",
+    new Column("Id", DbType.Int32) { IsIdentity = true, IsNullable = false },
+    new Column("Text", DbType.String, 255),
+    new PrimaryKeyConstraint("PK_Entries", "Id"));
+

Fluent

+
migration.Create.Table("Entries")
+    .WithColumn("Id").AsInt32().Identity().NotNullable()
+    .WithColumn("Text").AsString(255)
+    .WithPrimaryKey("PK_Entries", "Id");

Inside Up() / BuildUp(MigrationBuilder migration)

Object cleanup

RemoveTable leaves unrelated sequences intact. Oracle removes table-owned triggers and native identity objects. For a legacy sequence that the migration explicitly owns, OracleTransformationProvider.RemoveTableWithOwnedSequences validates named sequences and propagates cleanup errors. It does not infer sequence ownership from naming patterns.

Values and options

Oracle empty character strings become NULL. Time uses DATE with a fixed 1970-01-01 date and whole-second precision; fractional Time inputs are rejected. Intervals use native storage. Changes that require an unsupported in-place type conversion need an explicit data migration.

Included/clustered index options are rejected rather than ignored. Ordered foreign-key pairs and delete actions are preserved by structured metadata. A SQL Server clustered-index request is not translated into an Oracle index-organized table.

diff --git a/docs/guide/other-providers.html b/docs/guide/other-providers.html new file mode 100644 index 00000000..82490556 --- /dev/null +++ b/docs/guide/other-providers.html @@ -0,0 +1,43 @@ + + +HANA and additional providers · Migrator.NET +

HANA and additional providers

Use the live-engine matrix to qualify operations beyond the common database families.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

SAP HANA

ProviderTypes.Hana uses SAP’s native .NET driver. The CI job runs HANA Express and exercises schema/data operations, metadata, constraints, history, restart and DML rollback. DDL may autocommit. Use a custom host; the CLI driver bundle does not include an online HANA host.

HANA has its own supported type set; Guid and DateTimeOffset are outside the current matrix mappings. Review the type matrix before choosing shared column definitions.

Db2, Informix, Firebird and Sybase

ProviderThings to check
Db2 LUWDriver runtime dependencies, decimal/storage capacity and ordinary/unique index options.
InformixNative driver and database encoding; TEXT reads, integer NULL sentinels, whole-second Time and trailing-space trimming.
FirebirdDecimal storage capacity, ordinary/unique index operations and transaction behavior.
Sybase ASETEXTSIZE and string truncation settings, nullable BIT restrictions, trimmed strings and constraint-name limitations.

A shared table definition

The same authoring API describes a portable subset. This does not imply that every native extension or type maps identically. Start with simple definitions, then qualify your actual data and schema operations on each target.

CreateUsers.cs
+

Classic

+
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32) { IsNullable = false },
+            new Column("Name", DbType.String, 255),
+            new PrimaryKeyConstraint("PK_Users", "Id"));
+    }
+
+    public override void Down() => Database.RemoveTable("Users");
+}
+

Fluent

+
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()
+            .WithColumn("Name").AsString(255)
+            .WithPrimaryKey("PK_Users", "Id");
+    }
+
+    public override void BuildDown(MigrationBuilder migration)
+        => migration.Delete.Table("Users");
+}

Choose one authoring style

Source inventory and evidence

Ingres remains a source dialect outside the eleven-engine matrix. Redshift, Snowflake and Db2 for IBM i require separate provider/infrastructure qualification; PostgreSQL tests do not qualify Redshift, and Db2 LUW tests do not qualify IBM i.

See qualification requirements and live test setup for exact coverage and reproduction commands.

diff --git a/docs/guide/postgresql.html b/docs/guide/postgresql.html new file mode 100644 index 00000000..f43f636c --- /dev/null +++ b/docs/guide/postgresql.html @@ -0,0 +1,16 @@ + + +PostgreSQL · Migrator.NET +

PostgreSQL

Native intervals, schema-aware metadata, transactional DDL and advisory locks.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Connect with Npgsql

Use ProviderTypes.PostgreSQL and an open Npgsql connection, with the intended default schema. Connection search_path affects unqualified relation lookup. Metadata readers resolve the requested relation through PostgreSQL and distinguish same-named tables in different schemas.

Use native interval values

PostgreSQL maps duration values to native intervals. Time without time zone maps to a time of day; use TimeOnly for that input. Parameter mappings and scalar CLR return types are separate concerns: raw ADO.NET values remain driver-specific.

Store a job duration
+

Classic

+
Database.AddColumn("Jobs", new Column("Elapsed", MigratorDbType.Interval)
+{
+    DefaultValue = TimeSpan.FromDays(2)
+});
+

Fluent

+
migration.Create.Column("Elapsed", "Jobs").OfType(MigratorDbType.Interval)
+    .WithDefaultValue(TimeSpan.FromDays(2));

Inside Up() / BuildUp(MigrationBuilder migration)

Collations and schemas

Create any ICU nondeterministic collation explicitly, then select it with Collation.Named. Column rendering does not silently create shared collation objects. Binary maps to C; language and case semantics should use a specific installed name.

Schema-aware metadata does not establish complete qualification for every operation. Test quoted names and search-path behavior with your migration. Renaming a table retains its named constraints; avoid colliding names when recreating the old table.

Transactions and coordination

WholeSession is supported for verified transactional DDL, and DatabaseMigrationLock uses a session advisory lock. Statements that require special transaction treatment need a separate deployment design. Keep the connection stable while the lease is held.

diff --git a/docs/guide/preview.html b/docs/guide/preview.html new file mode 100644 index 00000000..789fa53e --- /dev/null +++ b/docs/guide/preview.html @@ -0,0 +1,26 @@ + + +Planning and SQL preview · Migrator.NET +

Planning and SQL preview

A version plan answers what runs. SQL preview shows the supported operation SQL.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Read-only version planning

Plan and DryRun inspect applied versions through IMigrationHistory without creating/upgrading history or invoking migration bodies, callbacks, transactions or SQLite PRAGMA changes. Set the same scope, tags and assembly you intend to deploy. The fragment below assumes an initialized runner.

Inspect version steps
+

Classic

+
var plan = runner.Plan(10);
+foreach (var step in plan)
+    Console.WriteLine($"{step.Version}: {(step.IsUp ? "up" : "down")}");
+runner.DryRun = true;
+runner.MigrateTo(10);
+

Fluent

+
var plan = runner.Plan(10);
+foreach (var step in plan)
+    Console.WriteLine($"{step.Version}: {(step.IsUp ? "up" : "down")}");
+runner.DryRun = true;
+runner.MigrateTo(10);

Shared host · both styles

Preview connected SQL

PreviewSql reads connected history and schema. Classic bodies require explicit opt-in; provider calls are captured through a proxy that rejects unsupported access. Fluent authoring builds operations directly. This is trusted C# execution in both cases, not a security sandbox.

Generate operation SQL
+

Classic

+
var sql = runner.PreviewSql(10, ProviderTypes.SQLite, allowLegacyBodies: true);
+Console.WriteLine(sql);
+

Fluent

+
var sql = runner.PreviewSql(10, ProviderTypes.SQLite);
+Console.WriteLine(sql);

Choose one authoring style

Offline generation and boundaries

MigrationSqlPreview.Generate can render supported operations without connecting; the CLI exposes --offline. Earlier structured create/rename operations update the planned schema. Raw SQL invalidates that knowledge, so later dependencies can fail.

Basic tables/columns, supported renames, simple indexes, inserts and raw SQL form the preview subset. Unsupported alterations, constraint changes, filters, callbacks and schema dependencies throw. InitializeOnce overrides are rejected rather than skipped silently. Post-commit callbacks do not run. Output contains operation SQL, not history guards or an idempotent deployment bundle.

diff --git a/docs/guide/profiles.html b/docs/guide/profiles.html new file mode 100644 index 00000000..06168c0d --- /dev/null +++ b/docs/guide/profiles.html @@ -0,0 +1,44 @@ + + +Profiles · Migrator.NET +

Profiles

Run explicitly selected work after versioned migrations without recording a version.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Define a named profile

A profile is useful for optional seed data or environment setup. It runs every time its name is selected. Make repeated execution deliberate: use an identifying predicate or other idempotent operation where appropriate.

A development seed profile
+

Classic

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Profile("demo", Order = 10)]
+public class DemoData : Migration
+{
+    public override void Up() => Database.InsertIfNotExists("Users",
+        new[] { "Id", "Name" }, new object[] { 1, "Ada" },
+        new[] { "Id" }, new object[] { 1 });
+    public override void Down() { }
+}
+

Fluent

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Profile("demo", Order = 10)]
+public class DemoData : FluentMigration
+{
+    public override void BuildUp(MigrationBuilder migration)
+        => migration.Insert.IntoTable("Users")
+            .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" })
+            .IfNotExists(new[] { "Id" }, new object[] { 1 });
+    public override void BuildDown(MigrationBuilder migration) { }
+}

Choose one authoring style

Select a profile

Run the demo profile
+

Classic

+
runner.Options.Profiles.Add("demo");
+runner.MigrateToLastVersion();
+

Fluent

+
runner.Options.Profiles.Add("demo");
+runner.MigrateToLastVersion();

Shared host · both styles

Profiles accept Order and Scope. Execution orders by Order and then ordinal full type name. Profile execution uses Up and does not create a migration-version entry or use Down as an undo history. An auxiliary-only run preserves existing version history.

Execution versus repeatables

A selected profile runs because it was selected, not because its source checksum changed. Treat this separately from versioned migrations and checksum-based repeatable SQL. Offline CLI SQL generation rejects profiles because it cannot represent the complete lifecycle.

diff --git a/docs/guide/providers.html b/docs/guide/providers.html new file mode 100644 index 00000000..de6be442 --- /dev/null +++ b/docs/guide/providers.html @@ -0,0 +1,18 @@ + + +Provider overview · Migrator.NET +

Provider overview

One authoring contract, explicit database behavior. Choose the driver and provider together.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Database families

DatabaseProviderTypesGuide
SQLiteSQLite / MonoSQLiteLive-schema reconstruction
SQL ServerSqlServer / SqlServer2005Constraints, batches and locks
PostgreSQLPostgreSQL / PostgreSQL82Schemas, types and locks
MySQL / MariaDBMysql / MariaDBDDL and collation behavior
OracleOracle / MsOracleIdentity and metadata
SAP HANAHanaAdditional providers
Db2 / Informix / Firebird / Ingres / SybaseIBM_DB2 / IBM_Informix / Firebird / Ingres / SybaseEngine-specific guidance

Bring a connection

Pass an open IDbConnection to ProviderFactory.Create. Both migration styles use that provider. Alternatively use the connection-string overload and configure providerName so the provider can resolve the ADO.NET factory. Use a matching driver and test the exact server version you deploy.

Provider selection · open connection supplied by the host
+

Classic

+
using var selectedProvider = ProviderFactory.Create(
+    ProviderTypes.PostgreSQL, connection, defaultSchema: "public", scope: "billing");
+var selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false);
+selectedRunner.MigrateToLastVersion();
+

Fluent

+
using var selectedProvider = ProviderFactory.Create(
+    ProviderTypes.PostgreSQL, connection, defaultSchema: "public", scope: "billing");
+var selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false);
+selectedRunner.MigrateToLastVersion();

Shared host · both styles

Qualification

The CI matrix includes SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase and SAP HANA. Ingres and historical provider aliases have separate qualification needs. Read testing and the live-engine matrix for exact drivers and server setup.

Database support is operation-specific. Column types, collation presets, index options, DDL transactions and metadata readers can differ. Test stored values and preserved schema, not only the generated SQL.

diff --git a/docs/guide/quick-start.html b/docs/guide/quick-start.html new file mode 100644 index 00000000..cbc8885b --- /dev/null +++ b/docs/guide/quick-start.html @@ -0,0 +1,77 @@ + + +Your first migration · Migrator.NET +

Your first migration

Create a SQLite database, apply a versioned change, and write its reverse. Choose either C# style; the runner is the same.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Install the library

Start with the .NET 9 SDK and a console application. The core package supplies schema operations; your ADO.NET package connects to the database. SQLite needs no separate database server for this example.

Terminal · either authoring style
+

Classic

+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7
+

Fluent

+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7

Shared commands · both styles

Write the change

Add CreateUsers.cs. Choose one tab and copy that class. Each public migration has a numeric version; do not put both versions of the same example into one assembly. Classic migrations execute provider methods in Up and Down. Fluent migrations collect structured operations in BuildUp and BuildDown.

CreateUsers.cs
+

Classic

+
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32) { IsNullable = false },
+            new Column("Name", DbType.String, 255),
+            new PrimaryKeyConstraint("PK_Users", "Id"));
+    }
+
+    public override void Down() => Database.RemoveTable("Users");
+}
+

Fluent

+
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()
+            .WithColumn("Name").AsString(255)
+            .WithPrimaryKey("PK_Users", "Id");
+    }
+
+    public override void BuildDown(MigrationBuilder migration)
+        => migration.Delete.Table("Users");
+}

Choose one authoring style

Run it

Replace Program.cs with the shared host below and run dotnet run. The open connection belongs to this host and is disposed after the provider. The runner discovers public migration classes in the selected assembly.

Program.cs · shared runner
+

Classic

+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();
+

Fluent

+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();

Shared host · both styles

The result is an app.db file containing Users and SchemaInfo. Run again: version 1 is already recorded, so it is skipped. Add a class with [Migration(2)] for your next change.

Reverse a change

Call runner.MigrateTo(0) to execute the reverse methods for this migration set. Here that drops Users and its data. A reverse migration is a schema operation, not a restore of deleted rows. Test both directions on a disposable database before deployment.

Continue with creating tables, or configure scopes, filters and transaction behavior.

diff --git a/docs/guide/runners.html b/docs/guide/runners.html new file mode 100644 index 00000000..9b24761c --- /dev/null +++ b/docs/guide/runners.html @@ -0,0 +1,32 @@ + + +Choose a runner · Migrator.NET +

Choose a runner

Use the same migration assembly in a dedicated host, a DI scope or the command-line tool.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Execution choices

RunnerA good fit
Library hostA small deployment executable with explicit connection ownership and full provider access.
Microsoft DI integrationA service collection supplying constructor dependencies, options and logging.
migrator CLIAutomation that selects assemblies, providers, scopes, tags and target versions.

A dedicated host

Run schema changes before application instances need the new schema. The host below works with either migration style and scans the assembly containing CreateUsers. Use explicit type selection when an assembly also contains migrations for other purposes.

Program.cs · shared runner
+

Classic

+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();
+

Fluent

+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();

Shared host · both styles

Deployment responsibilities

Give the deployment identity the schema privileges needed by the selected migrations. Coordinate concurrent deploys through an external orchestrator or supported native lock. Configure the history table and scope consistently across invocations. Log the target and result without exposing connection strings.

Choose the CLI for a ready command surface, or DI for application services. Read transaction and lock semantics before relying on atomicity.

diff --git a/docs/guide/schema.html b/docs/guide/schema.html new file mode 100644 index 00000000..3fce2273 --- /dev/null +++ b/docs/guide/schema.html @@ -0,0 +1,26 @@ + + +Schema inspection · Migrator.NET +

Schema inspection

Read the connected database before deciding what to change. Metadata is different from a model snapshot.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Inspect tables and columns

Classic migrations read through Database. FluentMigration exposes Schema for queries and Context for the full provider API. A fluent authoring method runs before its queued operations: an inspection cannot see a table merely queued earlier in the same builder.

Add a column only when it is missing
+

Classic

+
if (!Database.ColumnExists("Users", "Email"))
+    Database.AddColumn("Users", new Column("Email", DbType.String, 320));
+

Fluent

+
if (!Schema.Table("Users").ColumnExists("Email"))
+    migration.Create.Column("Email", "Users").AsString(320);

Inside Up() / BuildUp(MigrationBuilder migration)

Read ordered constraints

GetColumns returns inferred column attributes, not primary/unique membership flags. It is obsolete because native types and defaults cannot be mapped back to exact .NET definitions; use migration history for the original definition. Read typed table constraints to retain ordered composite keys. Unique indexes remain index metadata. MySQL/MariaDB catalogs cannot distinguish every original unique-index versus UNIQUE-clause authoring choice.

Read table constraint definitions
+

Classic

+
var constraints = Database.GetTableConstraints("Users");
+foreach (var constraint in constraints)
+    Console.WriteLine(constraint.Name);
+

Fluent

+
var constraints = Schema.Table("Users").ConstraintDefinitions();
+foreach (var constraint in constraints)
+    Console.WriteLine(constraint.Name);

Inside Up() / BuildUp(MigrationBuilder migration)

Create a view

ViewField selects columns from a base table. The alternative IViewElement overload represents explicit columns and joins. View definitions are provider-dependent and outside SQL preview and automatic reversal. Write a provider-appropriate DROP VIEW statement in the reverse method, and manage dependent views when changing their underlying tables.

A projection over Users
+

Classic

+
Database.AddView("UserNames", "Users", new ViewField("Id"), new ViewField("Name"));
+

Fluent

+
migration.Create.View("UserNames", "Users", new ViewField("Id"), new ViewField("Name"));

Inside Up() / BuildUp(MigrationBuilder migration)

Reads and portability

Dispose readers and commands obtained from the provider. Fluent Schema.Query and Select accept a reader callback and handle disposal. Use provider quoting helpers for table and column identifiers separately: quoting a table may introduce schema qualification, which is not valid for a column expression.

Metadata fidelity depends on the provider. Unsupported readers throw instead of pretending that an empty schema was found. A successful existence check is not a full schema-drift report.

diff --git a/docs/guide/sql-server.html b/docs/guide/sql-server.html new file mode 100644 index 00000000..cec02f87 --- /dev/null +++ b/docs/guide/sql-server.html @@ -0,0 +1,12 @@ + + +SQL Server · Migrator.NET +

SQL Server

Explicit keys, provider-specific indexes, transactional DDL and application locks.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Select the provider

Use ProviderTypes.SqlServer with an open Microsoft.Data.SqlClient connection. Pass the intended default schema, commonly dbo. Historical SqlServer2005 is a separate alias with older type mappings. WholeSession transactions and DatabaseMigrationLock are available for SQL Server.

Name constraints explicitly

Column changes preserve explicit constraints and indexes. Add/remove uniqueness independently. For a nonclustered primary key on an existing compatible table use the dedicated API shown below. Review existing clustered indexes before changing key layout.

Add a nonclustered primary key
+

Classic

+
Database.AddPrimaryKeyNonClustered("PK_Users", "Users", "Id");
+

Fluent

+
migration.Create.NonClusteredPrimaryKey("PK_Users", "Users", "Id");

Inside Up() / BuildUp(MigrationBuilder migration)

Indexes and SQL batches

Index definitions can express included/filter/cluster options where supported. The script APIs split standalone GO lines; raw ExecuteNonQuery/Execute.Sql does not. SQLCMD directives and GO repetition are rejected before executing script batches. Prefer scripts for client batch syntax and commands for parameterized statements.

Types and object names

Use TimeOnly for time values and TimeSpan for interval ticks. SqlServer2005 uses its older DATETIME precision behavior. Use separate quoting helpers for table and column names. A table rename leaves named constraints/indexes attached with their old names; assign distinct names when creating a replacement table.

diff --git a/docs/guide/sql.html b/docs/guide/sql.html new file mode 100644 index 00000000..2ef5085b --- /dev/null +++ b/docs/guide/sql.html @@ -0,0 +1,20 @@ + + +Execute SQL and scripts · Migrator.NET +

Execute SQL and scripts

Use schema operations where they fit, and keep database-specific SQL explicit.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Execute a statement

Raw SQL passes through to the selected database. It does not translate between dialects. Values from application input should be bound through a command; migration SQL is trusted application code.

A SQL data change
+

Classic

+
Database.ExecuteNonQuery("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL");
+

Fluent

+
migration.Execute.Sql("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL");

Inside Up() / BuildUp(MigrationBuilder migration)

Files and embedded resources

ExecuteScript reads a file; ExecuteResourceScript reads an assembly resource. Fluent equivalents capture script text as dedicated operations. Make files available at deployment and set resource names explicitly. Relative file paths are resolved against the process working directory.

Execute a SQL file
+

Classic

+
Database.ExecuteScript("Scripts/backfill.sql");
+

Fluent

+
migration.Execute.Script("Scripts/backfill.sql");

Inside Up() / BuildUp(MigrationBuilder migration)

Execute an embedded SQL resource
+

Classic

+
Database.ExecuteResourceScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql");
+

Fluent

+
migration.Execute.EmbeddedScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql");

Inside Up() / BuildUp(MigrationBuilder migration)

For the second example mark backfill.sql as an EmbeddedResource in the migration project and use its actual manifest resource name. Missing resources fail before script execution.

Client batch separators

The script APIs split standalone SQL Server GO lines, including optional line comments, while respecting strings, quoted identifiers and nested comments. GO repetition and SQLCMD directives fail before any batches execute. ExecuteNonQuery and Execute.Sql do not split client separators.

Other providers receive one command unless they implement IScriptBatchProvider. A database SQL file is not necessarily compatible with SQL*Plus, mysql-client or isql command syntax. Raw SQL also invalidates planned schema knowledge during SQL preview.

diff --git a/docs/guide/sqlite.html b/docs/guide/sqlite.html new file mode 100644 index 00000000..4b6922b4 --- /dev/null +++ b/docs/guide/sqlite.html @@ -0,0 +1,18 @@ + + +SQLite · Migrator.NET +

SQLite

Change existing tables from the live schema, without maintaining an ORM model.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Automatic reconstruction

For supported changes, Migrator reads SQLiteTableInfo, changes its representation, creates a replacement table, copies mapped rows, swaps tables and recreates represented dependent objects. This provides column type/default/nullability changes and adding/removing primary, foreign, unique and check constraints.

Native rename and eligible drop-column paths are used when supported by the engine. Complex alterations use reconstruction. Existing rows must satisfy the new definition; a default does not rewrite every existing NULL during a column change.

Change a column on an existing SQLite table
+

Classic

+
Database.ChangeColumn("Users", new Column("Name", DbType.String, 500)
+{
+    IsNullable = false, DefaultValue = "Unknown",
+    Collation = Collation.AsciiIgnoreCase
+});
+

Fluent

+
migration.Alter.Column("Name", "Users")
+    .AsString(500).NotNullable().WithDefaultValue("Unknown")
+    .WithCollation(Collation.AsciiIgnoreCase);

Inside Up() / BuildUp(MigrationBuilder migration)

What survives a rebuild

DetailBehavior
Mapped dataNamed-column copy preserves mapped values, subject to the new definition accepting them.
Keys and constraintsNamed/composite keys, ordered foreign-key pairs and separate update/delete actions are retained.
Column collationsDeclared names are retained. Register custom collations on the connection.
Indexes and triggersSupported definitions are recreated; unsafe trigger rename/drop-column cases are rejected.
AUTOINCREMENTThe sequence high-water mark survives, including previously deleted identities.
Hidden rowidNot part of the mapped data and may change.

Boundaries are explicit

Reconstruction rejects generated columns, STRICT, WITHOUT ROWID and indexes with explicit COLLATE clauses. It is not an arbitrary SQL dependency rewriter. Adjust dependent views, complex expressions and triggers explicitly when required. MATCH FULL and MATCH PARTIAL are rejected because SQLite does not enforce their semantics.

Owned rebuild transactions and runner transactions validate foreign-key integrity before commit and restore the prior enforcement setting. For caller-owned active transactions configure foreign keys before beginning the transaction. A SQLite write lock is not a session-wide migration lease; coordinate deployment externally or provide IMigrationLock.

Values and identity

CLR Guid defaults use blobs from Guid.ToByteArray(), matching inserted parameters. Legacy text GUID defaults remain SQL expressions during unrelated rebuilds, so storage is not silently converted. Convert mixed text/blob keys explicitly and consistently across related tables.

SQLite INTEGER is signed 64-bit. Declared text lengths and decimal precision do not impose SQL Server-like enforcement. An identity needs a single INTEGER primary key in the same definition. For adding identity to an existing table, use an atomic SQLite RecreateTable definition containing both objects.

How this differs from other tools

FluentMigrator leaves general column alterations and later foreign-key changes to manual reconstruction. DbUp and Evolve run supplied scripts. EF Core also rebuilds SQLite tables using model-represented artifacts. Migrator reconstructs from live metadata without an ORM. The sourced operation comparison distinguishes native SQL, emulation and manual work.

diff --git a/docs/guide/tags.html b/docs/guide/tags.html new file mode 100644 index 00000000..d2046ae7 --- /dev/null +++ b/docs/guide/tags.html @@ -0,0 +1,43 @@ + + +Tags · Migrator.NET +

Tags

Select a subset of versioned migrations using explicit ordinal names.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Tag migration classes

Tags is in DotNetProjects.Migrator. One class can declare multiple names. Choose names for deployment intent such as core or reporting; do not use a tag to hide a dependency that a selected migration still requires.

A reporting migration
+

Classic

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(2), Tags("reporting")]
+public class CreateReportLog : Migration
+{
+    public override void Up() => Database.AddTable("ReportLog", new Column("Name", DbType.String, 255));
+    public override void Down() => Database.RemoveTable("ReportLog");
+}
+

Fluent

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(2), Tags("reporting")]
+public class CreateReportLog : FluentMigration
+{
+    public override void BuildUp(MigrationBuilder migration)
+        => migration.Create.Table("ReportLog").WithColumn("Name").AsString(255);
+    public override void BuildDown(MigrationBuilder migration)
+        => migration.Delete.Table("ReportLog");
+}

Choose one authoring style

Configure matching

Select tags on the runner
+

Classic

+
runner.Options.Tags.Add("reporting");
+runner.Options.TagMatch = TagMatchMode.Any;
+runner.MigrateToLastVersion();
+

Fluent

+
runner.Options.Tags.Add("reporting");
+runner.Options.TagMatch = TagMatchMode.Any;
+runner.MigrateToLastVersion();

Shared host · both styles

Any requires at least one selected tag; All requires every selected tag. Matching is ordinal and case-sensitive. Without a tag filter all eligible versioned migrations are selected. Profiles have their own explicit name selection.

Downgrade behavior

Applied versions excluded by the active filter remain applied during downgrade. A filtered run is therefore not a promise that the whole database matches one contiguous global version range. Keep deployment filters stable and inspect the plan before reversing selected changes.

diff --git a/docs/guide/testing.html b/docs/guide/testing.html new file mode 100644 index 00000000..8997e61a --- /dev/null +++ b/docs/guide/testing.html @@ -0,0 +1,20 @@ + + +Testing and deployment · Migrator.NET +

Testing and deployment

Verify stored data, preserved schema and repeat execution on the actual target engine.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Test a migration lifecycle

Create a disposable database, apply the migration, check the schema and rows, run to the same target again, then downgrade and verify the intended reverse. Both authoring styles use the same runner. This fragment assumes an initialized runner whose migration set creates Users.

A host-level smoke check
+

Classic

+
runner.MigrateToLastVersion();
+if (!provider.TableExists("Users")) throw new Exception("Users missing");
+runner.MigrateToLastVersion();
+runner.MigrateTo(0);
+if (provider.TableExists("Users")) throw new Exception("Users was not removed");
+

Fluent

+
runner.MigrateToLastVersion();
+if (!provider.TableExists("Users")) throw new Exception("Users missing");
+runner.MigrateToLastVersion();
+runner.MigrateTo(0);
+if (provider.TableExists("Users")) throw new Exception("Users was not removed");

Shared host · both styles

What to assert

Test defaults by omitting a value, and nullability by explicitly sending NULL. Verify composite-key order, foreign-key actions and constraint names. After a SQLite rebuild check real rows, collations, supported indexes/triggers and identity high-water state. Test failure paths as well as successful SQL generation.

Use representative production-sized data to measure lock duration and backfill cost. A passing SQL-string assertion does not establish that a database accepts a command or preserves its semantics.

Repository checks

Build with dotnet build Migrator.slnx, then use .github/scripts/test.ps1 -Database Unit or SQLite for local suites. The live-engine guide gives the external database setup. Homepage CI results include commit provenance and skipped/missing-suite status.

Production rollout

Keep applied migrations immutable, review SQL and explicit reverse behavior, and serialize competing deploys. Validate against a restored database before making a breaking change. Plan application compatibility around expand/backfill/contract phases. Treat post-commit callback failures as durable migrations requiring follow-up handling.

diff --git a/docs/guide/transactions.html b/docs/guide/transactions.html new file mode 100644 index 00000000..8d5a9b5a --- /dev/null +++ b/docs/guide/transactions.html @@ -0,0 +1,22 @@ + + +Transactions and locks · Migrator.NET +

Transactions and locks

Transaction rollback and cross-process coordination solve different problems.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Choose the transaction boundary

ModeBehavior
PerMigrationDefault. Each successful migration commits independently.
NoneProvider/operation transaction behavior; no runner-managed migration transaction.
WholeSessionOne session transaction on SQLite, PostgreSQL or SQL Server; history initialization happens first.

Actual atomicity depends on the database and operation. Administration commands or implicit-commit DDL can violate assumptions. AfterUp/AfterDown run after commit; WholeSession defers them until the session commit. A callback failure cannot undo durable changes.

Configure a session transaction
+

Classic

+
runner.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+runner.MigrateToLastVersion();
+

Fluent

+
runner.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+runner.MigrateToLastVersion();

Shared host · both styles

Coordinate competing runners

DatabaseMigrationLock uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. The lease is session-owned and remains held across migration commits. This host fragment assumes a supported provider; SQLite rejects this built-in lock.

Acquire a native deployment lock
+

Classic

+
runner.Options.Lock = new DatabaseMigrationLock();
+runner.Options.LockTimeout = TimeSpan.FromSeconds(60);
+runner.MigrateToLastVersion();
+

Fluent

+
runner.Options.Lock = new DatabaseMigrationLock();
+runner.Options.LockTimeout = TimeSpan.FromSeconds(60);
+runner.MigrateToLastVersion();

Shared host · both styles

Scope of protection

Native locks are keyed by database, history table and scope. Coordinate separately if different scopes modify shared objects. Do not close/replace the connection, switch databases or manipulate the native lock inside a migration. MySQL named locks coordinate one server, not an entire distributed cluster.

Implement IMigrationLock for another lease mechanism, or serialize deployments outside the process. A transaction, history primary key or ordinary database write lock alone does not prove that the whole migration sequence is serialized.

diff --git a/docs/guide/upgrading.html b/docs/guide/upgrading.html new file mode 100644 index 00000000..924d58af --- /dev/null +++ b/docs/guide/upgrading.html @@ -0,0 +1,43 @@ + + +Upgrading existing migrations · Migrator.NET +

Upgrading existing migrations

Update source definitions while preserving the history your databases already contain.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Explicit column attributes

Replace old ColumnProperty flags with IsNullable, IsIdentity and IsUnsigned. Primary, unique, foreign and check constraints belong to the table. GetColumns returns column attributes; use GetTableConstraints for key membership and ordered columns.

Keep the same applied migration versions and effective scope when recompiling. Do not create a new history table merely to make an incompatible source assembly run. Verify the upgrade against a restored database and a fresh database.

One fluent authoring surface

FluentMigration.BuildUp/BuildDown replaces the duplicate legacy builder. Use complete table definitions for keys, explicit operations for indexes, and independent foreign-key update/delete actions. Classic Up/Down migrations remain first-class.

CreateUsers.cs
+

Classic

+
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32) { IsNullable = false },
+            new Column("Name", DbType.String, 255),
+            new PrimaryKeyConstraint("PK_Users", "Id"));
+    }
+
+    public override void Down() => Database.RemoveTable("Users");
+}
+

Fluent

+
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()
+            .WithColumn("Name").AsString(255)
+            .WithPrimaryKey("PK_Users", "Id");
+    }
+
+    public override void BuildDown(MigrationBuilder migration)
+        => migration.Delete.Table("Users");
+}

Choose one authoring style

Behavior changes to review

Column changes preserve explicit uniqueness; old SQL Server ownership markers no longer control deletion. TimeSpan inputs mean intervals, so convert clock-time inputs to TimeOnly. SQLite GUID defaults use the same blob representation as inserted parameters; unrelated rebuilds preserve existing text defaults.

Read the complete compatibility migration guide for constructor replacements, custom-provider contracts, identity, constraint metadata and collation mappings. Version-specific details live there; these chapters describe the current API.

diff --git a/docs/guide/versioning.html b/docs/guide/versioning.html new file mode 100644 index 00000000..0502fda3 --- /dev/null +++ b/docs/guide/versioning.html @@ -0,0 +1,39 @@ + + +Versioning and scoped history · Migrator.NET +

Versioning and scoped history

Number changes, keep applied source immutable and give independent modules explicit histories.

Examples use Classic and Fluent tabs. Your choice follows you through the manual.

Choose a version scheme

Migration accepts a numeric version or year/month/day/hour/minute/second components. Use one monotonic scheme per migration set. The date constructor builds a numeric identifier; it does not consult a clock or resolve branch collisions for you. Missing lower-numbered versions up to the target can still be applied.

A dated migration
+

Classic

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(2026, 9, 23, 10, 0, 0)]
+public class AddUserEmail : Migration
+{
+    public override void Up() => Database.AddColumn("Users", new Column("Email", DbType.String, 320));
+    public override void Down() => Database.RemoveColumn("Users", "Email");
+}
+

Fluent

+
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(2026, 9, 23, 10, 0, 0)]
+public class AddUserEmail : FluentMigration
+{
+    public override void BuildUp(MigrationBuilder migration)
+        => migration.Create.Column("Email", "Users").AsString(320);
+    public override void BuildDown(MigrationBuilder migration)
+        => migration.Delete.Column("Email", "Users");
+}

Choose one authoring style

Scope selection

An explicit MigrationAttribute.Scope selects that migration only for the matching provider scope. Unscoped migrations inherit the runner scope. Discovery, duplicate validation and history reads use the effective scope. Duplicate numeric versions in distinct explicit scopes are independent; physical tables are not isolated.

Set SchemaInfoTableName before any history access if you need a different table. AppliedMigrations lists recorded versions; LastAppliedMigrationVersion is nullable when history is empty. AssemblyLastMigrationVersion describes the loaded set.

Consolidated baselines

A baseline can record versions whose schema it already includes. The runner rechecks active-scope history before each planned step, skipping newly covered versions and their AfterUp callbacks. Downward runs similarly skip versions removed by an earlier Down. Recording the baseline version itself does not create a duplicate.

Mark a version included by a baseline
+

Classic

+
Database.MigrationApplied(1, "billing");
+

Fluent

+
migration.Execute.WithProvider(provider => provider.MigrationApplied(1, "billing"));

Inside Up() / BuildUp(MigrationBuilder migration)

Only record a version after establishing the schema it represents. History entries are not a substitute for verifying an existing database. Schema/history rollback follows the selected transaction mode. No migration-content checksum is stored.

diff --git a/docs/index.html b/docs/index.html index 02731859..c6dfd9cf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,454 +1,96 @@ - - - - - - - Migrator.NET — Database changes, in your code. - - - - - - - -
-
-
-
-

DOTNETPROJECTS / MIGRATOR.NET

-

Database changes.
Part of your code.

-

- Write schema changes in C# with imperative or fluent APIs. Version them with your application. - Run them with the database provider and ORM you choose. Rebuild SQLite tables automatically from their live schema, without an ORM model. -

- -
- Build and database tests on master - Latest stable NuGet version - License: MPL-1.1 -
-

- Open source · MPL-1.1 · Current source targets .NET 9 -

-
-
-
- - 001_CreateUsers.csUP / DOWN -
-
[Migration(1)]
-public class CreateUsers : Migration
-{
-    public override void Up()
-    {
-        Database.AddTable("Users",
-            new Column("Id", DbType.Int32) { IsNullable = false },
-            new Column("Name", DbType.String, 255),
-            new PrimaryKeyConstraint("PK_Users", "Id"));
-    }
-
-    public override void Down()
-    {
-        Database.RemoveTable("Users");
-    }
-}
- -
-
-
-
-
- PROVIDER DIALECTSSQL ServerPostgreSQLSQLiteMySQL / MariaDBOracleSee all → -
-
-
-

SMALL API. EXPLICIT CONTROL.

-

- Your schema has a history.
Keep it in the repository. -

-
-
- 01 / AUTHOR -

C# without an ORM dependency

-

- Define tables, columns, indexes and constraints through a - transformation API or structured fluent builders. Use raw SQL when a change needs - database-specific behavior. -

-
-
- 02 / VERSION -

Move forward. Step back.

-

- Number your migrations, implement Up() and - Down(), and migrate to a chosen version. Applied - migrations are recorded in the database. -

-
-
- 03 / ORGANIZE -

Separate histories by scope

-

- Keep module version histories in one database using named scopes. - Select each module’s migration assembly or types when you create - its runner. -

-
-
-
-
-

SQLITE SCHEMA MIGRATIONS

-

Change existing tables. Keep your data.

-

Migrator reads the live SQLite schema and automatically rebuilds tables for - supported column type, nullability and default changes, plus primary, foreign, - unique and check constraint changes. No ORM model is required.

-
-
-

Automatic reconstruction

-

FluentMigrator leaves general column alterations and later foreign-key changes - to manual reconstruction. DbUp and Evolve execute your scripts. EF Core also - rebuilds tables, using artifacts represented in its model.

-
-
-

Preserve supported schema details

-

Rebuilds retain mapped rows, named and composite keys, column collations, - supported indexes and triggers, and AUTOINCREMENT high-water state. - Foreign keys retain independent update and delete actions.

-
-
-

Explicit preservation limits

-

Reconstruction rejects generated columns, STRICT and WITHOUT ROWID tables, - and indexes with explicit collations. Hidden rowid values can change; - arbitrary dependent SQL requires a migration plan.

-
-
-

Compare SQLite operations, sources and preservation limits →

-
-
-
-
-

SCHEMA DEFINITIONS

Explicit definitions, shared authoring.

-

Named constraints, explicit defaults and provider-aware collations.

-
-

Define primary, unique, foreign-key and check constraints as table objects. - The imperative and fluent APIs share column definitions, trusted SQL defaults and typed collation requests.

-
new Column("Id", DbType.String, 27)
-    { DefaultValue = RawSql.Insert("ksuid_new()") };
-
-builder.Create.Table("Events")
-    .WithColumn("Id").AsString(27)
-    .WithDefaultValue(RawSql.Insert("ksuid_new()"))
-    .WithColumn("Name").AsString(100)
-    .WithCollation(Collation.AsciiIgnoreCase);
-

The target database must supply the SQL function. Collation mappings have explicit provider limits: - The SQLite example uses ASCII-only NOCASE; it does not satisfy a Unicode case-insensitive request. - Read the migration guide ↗.

-

Use TimeOnly for time-of-day values and TimeSpan for intervals. - Review type support, precision and data limits by provider.

-

Runner options support tags, named profiles, ordered maintenance and consolidated history. - Integrate migration constructors and lifecycle logging with the optional Microsoft DI package. - Explore runner and deployment options.

-

Additional databases require passing real-engine CI. - SAP HANA provider qualification ↗; - Redshift, Snowflake and Db2 for IBM i remain unsupported.

-
-
-
-
-
-
-

QUICK START

-

From code to schema.

-
-

- A minimal SQLite example using the current repository API.
- Use .NET 9 and a checkout of this repository. -

-
-
-
- 1 -

Reference the library

-

- Run these commands from your Migrator.NET checkout. This example references - the source project and passes an open SQLite connection to the provider. -

- View package versions on NuGet ↗ -
-
-
- Terminal -
-
dotnet new console -n MigrationDemo -f net9.0
-cd MigrationDemo
-dotnet add reference ../src/Migrator/DotNetProjects.Migrator.csproj
-dotnet add package Microsoft.Data.Sqlite --version 9.0.7
-
-
-
-
- 2 -

Describe the change

-

- Add a public migration class. Each version must be unique within - the migration set loaded by a runner. -

-

- Down() is your explicit reverse operation; dropping - a table also removes its data. -

-
-
-
- CreateUsers.cs -
-
using System.Data;
-using DotNetProjects.Migrator.Framework;
+
+Database changes, written in C# · Migrator.NET
+
+
+
+
+

DOTNETPROJECTS / DATABASE MIGRATIONS

+

Database changes,
written in C#.

+

A table today. A different table tomorrow. Keep every change explicit, versioned, and close to your application.

+

Choose Classic or Fluent migrations. Bring your ADO.NET driver. Run the same migration system alongside any ORM—or without one.

+ + +
+
ONE CHANGE. TWO WAYS TO WRITE IT.001 ↘
CreateUsers.cs
+

Classic

+
using System.Data;
+using DotNetProjects.Migrator.Framework;
 
-[Migration(1)]
-public class CreateUsers : Migration
+[Migration(1)]
+public class CreateUsers : Migration
 {
-    public override void Up()
+    public override void Up()
     {
-        Database.AddTable("Users",
-            new Column("Id", DbType.Int32) { IsNullable = false },
-            new Column("Name", DbType.String, 255),
-            new PrimaryKeyConstraint("PK_Users", "Id"));
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32) { IsNullable = false },
+            new Column("Name", DbType.String, 255),
+            new PrimaryKeyConstraint("PK_Users", "Id"));
     }
 
-    public override void Down()
-    {
-        Database.RemoveTable("Users");
-    }
-}
-
-
-
-
- 3 -

Run pending migrations

-

- Replace Program.cs with this code, then run - dotnet run. The runner discovers the migration in - your assembly and records it under the default scope. -

-

- Subsequent runs skip applied versions. Use - MigrateTo(version) to target an earlier or later - version. -

-
-
-
- Program.cs -
-
using DotNetProjects.Migrator;
-using DotNetProjects.Migrator.Providers;
-using Microsoft.Data.Sqlite;
-
-using var connection = new SqliteConnection("Data Source=app.db");
-connection.Open();
-
-using var provider = ProviderFactory.Create(
-    ProviderTypes.SQLite, connection, defaultSchema: null);
+    public override void Down() => Database.RemoveTable("Users");
+}
+

Fluent

+
using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
 
-var migrator = new Migrator(
-    provider, typeof(CreateUsers).Assembly, trace: false);
-
-if (migrator.LastAppliedMigrationVersion is long applied
-    && applied > migrator.AssemblyLastMigrationVersion)
+[Migration(1)]
+public class CreateUsers : FluentMigration
 {
-    throw new InvalidOperationException(
-        "Database version is newer than this application.");
-}
-
-migrator.MigrateToLastVersion();
-
-
- -
-
-
-
-
-

FLUENT API

-

Chain operations. Keep control.

-
-

Use fluent and imperative migrations in the same assembly and runner.

-
-
-
-

The same quick start, written fluently

-

Replace CreateUsers.cs from step 2 with this class. - Keep the source reference and Program.cs from the quick start.

-

BuildUp collects operations before execution; - BuildDown describes the reverse change.

-

Builders cover tables, columns, keys, indexes, data and SQL. - Database support still depends on the provider.

-
-
-
- CreateUsers.cs · fluent - -
-
using DotNetProjects.Migrator.Framework;
-using DotNetProjects.Migrator.Framework.Fluent;
-
-[Migration(1)]
-public class CreateUsers : FluentMigration
-{
-    public override void BuildUp(MigrationBuilder migration)
+    public override void BuildUp(MigrationBuilder migration)
     {
-        migration.Create.Table("Users")
-            .WithColumn("Id").AsInt32().NotNullable()
-            .WithPrimaryKey("PK_Users", "Id")
-            .WithColumn("Name").AsString(255);
+        migration.Create.Table("Users")
+            .WithColumn("Id").AsInt32().NotNullable()
+            .WithColumn("Name").AsString(255)
+            .WithPrimaryKey("PK_Users", "Id");
     }
 
-    public override void BuildDown(MigrationBuilder migration)
-    {
-        migration.Delete.Table("Users");
-    }
-}
-
-
- -
-
-
-
-

DATABASE PROVIDERS

-

One API. Multiple dialects.

-
-

- Supply your ADO.NET driver.
Migrator supplies the schema - operations. -

-
-
-
-

Common database families

-
    -
  • SQL Server
  • -
  • PostgreSQL
  • -
  • SQLite
  • -
  • MySQL
  • -
  • MariaDB
  • -
  • Oracle
  • -
-
-
-

Additional dialects in source

-
    -
  • IBM Db2
  • -
  • IBM Informix
  • -
  • Firebird
  • -
  • Ingres
  • -
  • Sybase
  • -
  • SAP HANA
  • -
-
-
-

- This is an implementation inventory, not a certification of every - server or driver version. Schema operations and transactional DDL vary - by provider. Check the - provider factory - and - provider tests - for your database. -

-
-
-

BUILD AND DATABASE TESTS

-

Evidence from CI.

-

The CI matrix runs unit tests and 11 database suites, then checks for missing - or duplicate test assignments. Results count test executions across the matrix; - skipped tests are shown separately.

- -

Test totals are populated during deployment from the latest completed master CI run. - View CI runs and test-result artifacts ↗.

- -
-
public override void BuildDown(MigrationBuilder migration) + => migration.Delete.Table("Users"); +}

Choose one authoring style

+ +

01 / AUTHORWrite a numbered change.

02 / REVIEWPlan the next step.

03 / APPLYLeave a lasting record.

+
+

SMALL PRIMITIVES. REAL DATABASES.

Your schema.
Your decisions.

+
01

Two C# styles

Direct provider calls or a fluent builder. Tables, columns, indexes, constraints and data, with raw SQL when you need it.

Compare the APIs ↗
+
02

A deliberate deployment

Version plans, tags, profiles, maintenance stages, transaction modes and native locks. A CLI or a runner inside your own host.

Choose a runner ↗
+
03

History with boundaries

Track applied versions and give modules separate histories through scopes. Author the reverse for changes that need it.

Understand versioning ↗
+
+
+

A PARTICULAR STRENGTH / SQLITE

A small database.
Room to evolve.

Change the schema you have. Without an ORM model.

Migrator reads SQLite’s live schema and automatically reconstructs tables for supported changes to column types, defaults and nullability, and primary, foreign, unique and check constraints.

Existing rows are copied and supported schema artifacts are preserved. You describe the change; the provider handles the rebuild.

FluentMigrator requires manual reconstruction for general column alterations and later foreign-key changes. DbUp and Evolve leave it to your scripts. EF Core also rebuilds tables, using model metadata.

Read the SQLite guide and preservation limits ↗
See the sourced operation comparison →
+
TABLE / UsersΔ 002

IdINTEGER · PRIMARY KEY

Name255 500 · NOT NULL

↳ Existing rows travel with the schema.

Change a column on an existing SQLite table
+

Classic

+
Database.ChangeColumn("Users", new Column("Name", DbType.String, 500)
+{
+    IsNullable = false, DefaultValue = "Unknown",
+    Collation = Collation.AsciiIgnoreCase
+});
+

Fluent

+
migration.Alter.Column("Name", "Users")
+    .AsString(500).NotNullable().WithDefaultValue("Unknown")
+    .WithCollation(Collation.AsciiIgnoreCase);

Inside Up() / BuildUp(MigrationBuilder migration)

+
+

FROM EMPTY FOLDER TO FIRST TABLE

Start with
one change.

Install the library and your database driver. Add the migration above, wire up the runner, and apply it. SQLite makes a convenient first database.

Follow the complete quick start ↗
Terminal · either authoring style
+

Classic

+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7
+

Fluent

+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7

Shared commands · both styles

+

THE MIGRATION MANUAL

Past “hello, table.”

Detailed guides, paired examples, and provider notes.
Browse every chapter ↗

+

BRING YOUR DATABASE

One migration system.
Many dialects.

Capabilities follow the database engine. The provider guide explains aliases, drivers and operation-specific behavior.

+

BUILD AND DATABASE TESTS

Evidence from CI.

The CI matrix runs unit tests and 11 database suites, then checks for missing or duplicate test assignments. Counts represent test executions across the matrix; skipped tests are shown separately.

+

Test totals are populated during deployment from the latest completed master CI run. View CI runs and test-result artifacts ↗.

+
+
Choose by how you work.

Use fluent operations, version planning, a SQL-preview subset, runner options, native locks and the CLI. - Read the runner guide and provider limits. + Read the runner guide and provider limits.

Migrator fits applications that want explicit C# migrations and @@ -817,39 +459,8 @@

Keep SQL as the source

-
-
-

CONTINUING MIGRATOR.NET

-

A familiar idea.
A maintained fork.

-

- DotNetProjects.Migrator continues the original Migrator.NET project, - bringing together fork contributions with work on SQLite schema - handling, provider independence and migration scopes. -

-
- -
-
- - - - + +

EXPLICIT CHANGES. A LASTING RECORD.

The next version
starts with a change.

Open the manual ↗Contribute on GitHub →
+ + + diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index 9512bd38..c342a633 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -1,6 +1,6 @@ # Migrating from 12.1 to 13 -Version 13 is a breaking release. This guide is maintained alongside the implementation; items explicitly marked planned are not available yet. Do not run a changed migration history against production without validating the upgrade on a restored database. +Version 13 is a breaking release. This guide covers compatibility changes when updating existing migrations. Validate the upgrade on a restored database before running a changed migration history against production. For current API usage, see the [migration manual](https://dotnetprojects.github.io/Migrator.NET/guide/). ## Schema model @@ -128,9 +128,9 @@ constraint name and the sequence high-water mark. ## Provider authors and dialects (design) -Keep SQL generation independent of a live connection. A dialect defines identifier quoting, type/literal rendering and SQL capabilities. Metadata readers inspect existing schema; execution manages commands, transactions and history. Neither preview nor a SQL generator may query or mutate the database. +Keep SQL rendering independent of a live connection. A dialect defines identifier quoting, type/literal rendering and SQL capabilities. Metadata readers inspect existing schema; execution manages commands, transactions and history. Connected preview may read history and schema before rendering, but it must not mutate the database. Offline rendering uses an explicitly supplied schema context. -The current provider surface mixes these concerns. The v13 implementation is staged to preserve testable provider behavior while replacing authoring APIs. Unsupported combinations must fail explicitly before DDL, not disappear from generated SQL. +The provider surface combines these concerns through execution and metadata contracts. SQL rendering uses a separate context. Unsupported combinations must fail explicitly before DDL, not disappear from generated SQL. ## Design references @@ -140,9 +140,9 @@ Reviewed 2026-09-22: - [FluentMigrator ColumnDefinition](https://github.com/fluentmigrator/fluentmigrator/blob/main/src/FluentMigrator.Abstractions/Model/ColumnDefinition.cs) still carries constraint flags; its expression/generator separation is useful, but its column model is not the target here. - [Alembic operations](https://alembic.sqlalchemy.org/en/latest/ops.html) distinguish named table constraints from column alteration and use explicit batch reconstruction for SQLite. -## Additional v13 candidates +## Schema API changes -Typed constraints with ordered metadata, the SQLite constraint tokenizer, explicit SQL defaults, semantic collations and removal of the duplicate authoring API are implemented in this source stack. Typed schema-qualified identifiers, deterministic naming conventions and a broader provider-capability model remain candidates; they are not implemented features. +Typed constraints with ordered metadata, the SQLite constraint tokenizer, explicit SQL defaults, semantic collations and the consolidated authoring API work together. Use explicit object names and check provider-specific operation behavior when upgrading a custom dialect. ## Explicit SQL defaults and semantic collations @@ -273,4 +273,4 @@ matching primary/unique definitions, so later caller-array edits cannot change t ## Build and package identity -Source builds now identify the core, optional DI package and CLI as 13.0.0-preview.1. The core assembly and file versions are 13.0.0.0; generated assembly metadata is enabled while preserving its existing title and description. Recompile consumers of the breaking API and update assembly/version binding assumptions. These metadata changes do not publish a package. +The core assembly and file versions are 13.0.0.0; generated assembly metadata preserves the existing title and description. Recompile consumers of the breaking API and update assembly/version binding assumptions. Keep the core, optional DI integration and CLI on compatible package versions. diff --git a/docs/runner-guide.md b/docs/runner-guide.md index cdd0de8e..31ad675b 100644 --- a/docs/runner-guide.md +++ b/docs/runner-guide.md @@ -1,5 +1,7 @@ # Runner and fluent API +For step-by-step chapters and paired Classic/Fluent examples, use the [migration manual](https://dotnetprojects.github.io/Migrator.NET/guide/). This page is the compact runner reference. + Use imperative or fluent migrations with scope and tag filtering, profiles, ordered maintenance, transaction modes, planning, SQL preview and deployment locks. This guide describes the current repository API; check package compatibility when using an older release. ## Fluent quick start From de356a992dae5adebc372d1ac9d274a00c5db2aa Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 23 Sep 2026 10:37:58 +0200 Subject: [PATCH 2/2] Fix MySQL documentation link casing and validate paths cross-platform --- .github/scripts/test-docs.py | 32 ++++++++++++++++++++++++++++++++ .github/scripts/verify-docs.py | 27 +++++++++++++++++++++------ .github/workflows/docs.yml | 4 +++- docs/_src/content.py | 2 +- docs/guide/mysql.html | 2 +- 5 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 .github/scripts/test-docs.py diff --git a/.github/scripts/test-docs.py b/.github/scripts/test-docs.py new file mode 100644 index 00000000..7952dafc --- /dev/null +++ b/.github/scripts/test-docs.py @@ -0,0 +1,32 @@ +"""Regression checks for documentation paths on both Windows and Linux.""" +import importlib.util +from pathlib import Path +import tempfile +import unittest + +spec = importlib.util.spec_from_file_location("verify_docs", Path(__file__).with_name("verify-docs.py")) +verifier = importlib.util.module_from_spec(spec) +spec.loader.exec_module(verifier) + + +class ExactPathTests(unittest.TestCase): + def test_checks_file_and_directory_case_and_relative_links(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "Mysql").mkdir() + (root / "Mysql/MySqlTransformationProvider.cs").write_text("", encoding="utf-8") + (root / "index.html").write_text("", encoding="utf-8") + for relative, expected in ( + ("Mysql/MySqlTransformationProvider.cs", True), + ("Mysql/MysqlTransformationProvider.cs", False), + ("mysql/MySqlTransformationProvider.cs", False), + ("Mysql/Missing.cs", False), + ("Mysql/../index.html", True), + ("Mysql/../Index.html", False), + ): + with self.subTest(path=relative): + self.assertEqual(verifier.exact_path_exists(root, relative), expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/verify-docs.py b/.github/scripts/verify-docs.py index ec7447c2..fbbc978b 100644 --- a/.github/scripts/verify-docs.py +++ b/.github/scripts/verify-docs.py @@ -38,7 +38,20 @@ def handle_starttag(self, tag, attrs): self.panels.append(attrs["data-code-style"]) -def check_site(): +def exact_path_exists(root, relative): + """Check URL spelling even on case-insensitive filesystems such as Windows.""" + target = root + for part in Path(relative).parts: + if part == "..": + target = target.parent + continue + if not target.is_dir() or part not in {entry.name for entry in target.iterdir()}: + return False + target /= part + return target.exists() + + +def check_site(): pages = {path.resolve(): Page(path) for path in [DOCS / "index.html", *sorted((DOCS / "guide").glob("*.html"))]} for path, page in pages.items(): assert page.panels == [style for _ in range(len(page.panels) // 2) for style in ("classic", "fluent")], f"Unpaired samples: {path}" @@ -48,16 +61,18 @@ def check_site(): parsed = urlsplit(link) if parsed.scheme or parsed.netloc: continue - target = (path.parent / unquote(parsed.path)).resolve() if parsed.path else path - assert target.exists(), f"Broken link: {path}: {link}" + relative = unquote(parsed.path) if parsed.path else path.name + assert exact_path_exists(path.parent, relative), f"Missing or incorrectly cased link: {path}: {link}" + target = (path.parent / relative).resolve() if parsed.fragment and target in pages: assert unquote(parsed.fragment) in pages[target].ids, f"Broken anchor: {path}: {link}" for page in content.PAGES: - assert (ROOT / page["source"]).exists(), f"Missing implementation reference: {page['source']}" + assert exact_path_exists(ROOT, page["source"]), f"Missing or incorrectly cased implementation reference: {page['source']}" entries = json.loads((DOCS / "assets/search-index.json").read_text(encoding="utf-8")) assert len(entries) == len(content.PAGES) - for entry in entries: - assert (DOCS / entry["url"]).resolve() in pages + for entry in entries: + assert exact_path_exists(DOCS, entry["url"]), f"Missing or incorrectly cased search link: {entry['url']}" + assert (DOCS / entry["url"]).resolve() in pages print(f"Checked {len(pages)} HTML pages: local links, anchors, control references, search entries and paired samples.") diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a74932a2..e8ebc9a8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,6 +26,8 @@ jobs: - name: Check generated pages run: python -B .github/scripts/build-docs.py --check - name: Validate links and compile all C# examples - run: python -B .github/scripts/verify-docs.py --compile + run: | + python -B .github/scripts/test-docs.py + python -B .github/scripts/verify-docs.py --compile - name: Verify CI-count rendering run: python -B .github/scripts/test-homepage-tests.py diff --git a/docs/_src/content.py b/docs/_src/content.py index 4111e9fb..dd38d0d8 100644 --- a/docs/_src/content.py +++ b/docs/_src/content.py @@ -630,7 +630,7 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ .WithCollation(Collation.CaseInsensitive); ''')), section("Constraint metadata", '

MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction.

'), - section("Locking and values", '

DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.

'), source="src/Migrator/Providers/Impl/Mysql/MysqlTransformationProvider.cs") + section("Locking and values", '

DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.

'), source="src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs") page("Database providers", "oracle", "Oracle", "Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.", section("Connection and schema", '

Use ProviderTypes.Oracle with the Oracle managed ADO.NET driver and the intended schema. MsOracle is a historical variant. Oracle DDL is not generally atomic across a migration; WholeSession is rejected. Some quoted qualified metadata lookups are explicitly rejected.

'), diff --git a/docs/guide/mysql.html b/docs/guide/mysql.html index 5d86c5a6..ae16ef51 100644 --- a/docs/guide/mysql.html +++ b/docs/guide/mysql.html @@ -13,4 +13,4 @@ });

Fluent

migration.Create.Table("Labels").WithColumn("Name").AsString(100)
-    .WithCollation(Collation.CaseInsensitive);

Inside Up() / BuildUp(MigrationBuilder migration)

Constraint metadata

MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction.

Locking and values

DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.

+ .WithCollation(Collation.CaseInsensitive);

Inside Up() / BuildUp(MigrationBuilder migration)

Constraint metadata

MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction.

Locking and values

DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.