diff --git a/README.md b/README.md index 321a55b2..32141e26 100644 --- a/README.md +++ b/README.md @@ -262,15 +262,34 @@ public override void Down() ```csharp public override void BuildUp(MigrationBuilder migration) { - migration.Create.Column("Email", "Users").AsString(320); + migration.Create.Column("Email").OnTable("Users").AsString(320); } public override void BuildDown(MigrationBuilder migration) { - migration.Delete.Column("Email", "Users"); + migration.Delete.Column("Email").FromTable("Users"); } ``` +Fluent expressions name the table separately: `Create.Column(name).OnTable(table)`, +`Alter.Column(name).OnTable(table)` and `Delete.Column(name).FromTable(table)`. +Renames read `Rename.Column(oldName).OnTable(table).To(newName)`. Indexes and +constraints follow the same pattern: + +```csharp +migration.Create.Index("IX_Users_Email").OnTable("Users").WithColumns("Email").Unique(); +migration.Create.ForeignKey("FK_Orders_Users") + .FromTable("Orders").WithColumns("UserId") + .ToTable("Users").WithColumns("Id") + .OnDelete(ForeignKeyConstraintType.Cascade); +``` + +Table, create-column and alter-column builders share type and option methods; +each column requires `As...` or `OfType(...)`. Update and delete require a +predicate or explicit `AllRows()`. Unfinished expressions fail before execution. +See the [API map](https://dotnetprojects.github.io/Migrator.NET/guide/api-map.html) +for the complete syntax. + 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 diff --git a/docs/_src/content.py b/docs/_src/content.py index dd38d0d8..58d5562c 100644 --- a/docs/_src/content.py +++ b/docs/_src/content.py @@ -126,7 +126,7 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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("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. WithColumn returns a builder bound to that specific column. Table-level methods such as WithPrimaryKey return the table builder; call WithColumn again before supplying more column options.
', 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), @@ -150,8 +150,8 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ Database.RenameTable("Users", "Members"); Database.RenameColumn("Members", "Name", "DisplayName"); ''', ''' -migration.Rename.Table("Users", "Members"); -migration.Rename.Column("Members", "Name", "DisplayName"); +migration.Rename.Table("Users").To("Members"); +migration.Rename.Column("Name").OnTable("Members").To("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) @@ -159,20 +159,21 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ IsNullable = false }); ''', ''' -migration.Alter.Column("Name", "Users") +migration.Alter.Column("Name").OnTable("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("Explicit table and column steps", 'Create.Column(name).OnTable(table) and Alter.Column(name).OnTable(table) select the table before exposing type and column options. Delete.Column(name).FromTable(table) completes a removal. For a complete Column model use Create.Column(definition).OnTable(table) or Alter.Column(definition).OnTable(table); the definition is copied. Every named fluent column requires As... or OfType(...) before execution.
Table columns, added columns and altered columns share the same options, including AsGuid, AsBoolean, AsDecimal(precision, scale), AsDate, AsDateTime and AsDateTime2. AsDateTime maps to DbType.DateTime; AsDateTime2 maps to DbType.DateTime2. OfType(DbType) and OfType(MigratorDbType) remain available for other types. Nullability defaults to nullable.
'), 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(); +migration.Create.Column("Email").OnTable("Users").AsString(320).Nullable(); '''), pair("Remove the email column", ''' Database.RemoveColumn("Users", "Email"); ''', ''' -migration.Delete.Column("Email", "Users"); +migration.Delete.Column("Email").FromTable("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) @@ -180,7 +181,7 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ Precision = 12, Scale = 4, IsNullable = false, DefaultValue = 0m }); ''', ''' -migration.Create.Column("Amount", "Orders").OfType(DbType.Decimal) +migration.Create.Column("Amount").OnTable("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", ''' @@ -195,13 +196,13 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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", ''' + 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 Insert.IntoTable(...).Row(...) expressions. Each Row completes one insert; its returned builder offers only IfNotExists, so a second Row cannot silently replace the first. Insert, update and delete each expose only their supported steps.
', 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", ''' + section("Update and delete with predicates", 'Classic update/delete without a predicate affects every row. Fluent update/delete requires Where(...) or an explicit AllRows() to complete the operation. Empty predicate arrays are rejected; an unfinished chain fails during Build, Apply or Preview before any queued operation executes. 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 }); ''', ''' @@ -221,13 +222,13 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ .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", ''' + section("Copying and reversal", 'Use the provider CopyDataFromTableToTable helper or fluent Execute.CopyDataFromTable(...).ToTable(...).WithColumns(...) for named-column copies. Both tables must already exist and target columns must accept the source values. Execute.UpdateTable(target).FromTable(source).Set(copyPairs).Match(keyPairs) maps source/target pairs. CopyDataFromTable also supports OrderBy after WithColumns. 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.ListGetColumns 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"); @@ -250,9 +251,10 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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")); +migration.Create.View("UserNames").FromTable("Users") + .WithFields(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") + section("Reads and portability", 'Dispose readers and commands obtained from the provider. Fluent Schema.Query and Schema.Table(name).Select accept a reader callback and handle disposal. Use Schema.Table(name).SelectScalar(columns, where) for a scalar selection. 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", ''' @@ -303,20 +305,17 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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", ''' + section("Create and remove an index", 'Use an explicit name so the index can be inspected or removed later. Fluent Create.Index(name).OnTable(table).WithColumns(...) names each part explicitly and preserves column order. Append Unique(), Clustered(), IncludeColumns(...) or WithFilter(...). For an existing Index definition, use Create.Index(definition).OnTable(table); fully qualify the model type if System.Index is also in scope.
', 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 -}); +migration.Create.Index("IX_Users_Name").OnTable("Users").WithColumns("Name"); '''), pair("Drop an index", ''' Database.RemoveIndex("Users", "IX_Users_Name"); ''', ''' -migration.Delete.Index("IX_Users_Name", "Users"); +migration.Delete.Index("IX_Users_Name").FromTable("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") @@ -326,13 +325,13 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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"); +migration.Create.UniqueConstraint("UQ_Users_Name").OnTable("Users").WithColumns("Name"); +migration.Create.CheckConstraint("CK_Users_Id").OnTable("Users").WithExpression("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"); +migration.Delete.Constraint("CK_Users_Id").FromTable("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.
')) @@ -343,15 +342,16 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ "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); +migration.Create.ForeignKey("FK_Orders_Users") + .FromTable("Orders").WithColumns("UserId") + .ToTable("Users").WithColumns("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"); +migration.Delete.ForeignKey("FK_Orders_Users").FromTable("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.
')) @@ -461,9 +461,9 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ public class AddUserEmail : FluentMigration { public override void BuildUp(MigrationBuilder migration) - => migration.Create.Column("Email", "Users").AsString(320); + => migration.Create.Column("Email").OnTable("Users").AsString(320); public override void BuildDown(MigrationBuilder migration) - => migration.Delete.Column("Email", "Users"); + => migration.Delete.Column("Email").FromTable("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.
'), @@ -582,7 +582,7 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ Collation = Collation.AsciiIgnoreCase }); ''', ''' -migration.Alter.Column("Name", "Users") +migration.Alter.Column("Name").OnTable("Users") .AsString(500).NotNullable().WithDefaultValue("Unknown") .WithCollation(Collation.AsciiIgnoreCase); ''') @@ -599,7 +599,7 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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"); +migration.Create.NonClusteredPrimaryKey("PK_Users").OnTable("Users").WithColumns("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") @@ -612,7 +612,7 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ DefaultValue = TimeSpan.FromDays(2) }); ''', ''' -migration.Create.Column("Elapsed", "Jobs").OfType(MigratorDbType.Interval) +migration.Create.Column("Elapsed").OnTable("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.
'), @@ -655,10 +655,10 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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", ''' + section("Provider-specific operations", 'The Classic provider indexer selects a named provider or a no-op provider. Fluent IfProvider 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 => +migration.IfProvider("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") @@ -677,7 +677,7 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ public static class AuditColumns { public static void Add(MigrationBuilder migration, string table) - => migration.Create.Column("CreatedAt", table).OfType(DbType.DateTime) + => migration.Create.Column("CreatedAt").OnTable(table).OfType(DbType.DateTime) .NotNullable().WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP")); } ''', kind="class")), @@ -702,8 +702,8 @@ def page(group, slug, title, summary, *sections, source="src/Migrator/Framework/ 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("Schema operations", table(["Classic", "Fluent"], [["AddTable", "Create.Table"], ["AddColumn(table, column)", "Create.Column(name).OnTable(table)"], ["ChangeColumn(table, column)", "Alter.Column(name).OnTable(table) / Alter.Column(definition).OnTable(table)"], ["RemoveTable / RemoveColumn", "Delete.Table(table) / Delete.Column(name).FromTable(table)"], ["RenameTable / RenameColumn", "Rename.Table(old).To(new) / Rename.Column(old).OnTable(table).To(new)"], ["AddPrimaryKey / AddUniqueConstraint / AddCheckConstraint", "Create.PrimaryKey(name).OnTable(table).WithColumns(...) / Create.UniqueConstraint / Create.CheckConstraint"], ["AddForeignKey / RemoveForeignKey", "Create.ForeignKey(name).FromTable(child).WithColumns(...).ToTable(parent).WithColumns(...) / Delete.ForeignKey(name).FromTable(table)"], ["AddIndex / RemoveIndex", "Create.Index(name).OnTable(table).WithColumns(...) / Delete.Index(name).FromTable(table)"], ["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.CopyDataFromTable(source).ToTable(target).WithColumns(...) / Execute.UpdateTable(target).FromTable(source).Set(...).Match(...)"], ["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.", diff --git a/docs/assets/search-index.json b/docs/assets/search-index.json index b47070d6..b936899e 100644 --- a/docs/assets/search-index.json +++ b/docs/assets/search-index.json @@ -32,35 +32,35 @@ "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. " + "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. WithColumn returns a builder bound to that specific column. Table-level methods such as WithPrimaryKey return the table builder; call WithColumn again before supplying more column options. 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. " + "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\").To(\"Members\");\nmigration.Rename.Column(\"Name\").OnTable(\"Members\").To(\"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\").OnTable(\"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. " + "text": " Create.Column(name).OnTable(table) and Alter.Column(name).OnTable(table) select the table before exposing type and column options. Delete.Column(name).FromTable(table) completes a removal. For a complete Column model use Create.Column(definition).OnTable(table) or Alter.Column(definition).OnTable(table); the definition is copied. Every named fluent column requires As... or OfType(...) before execution. Table columns, added columns and altered columns share the same options, including AsGuid, AsBoolean, AsDecimal(precision, scale), AsDate, AsDateTime and AsDateTime2. AsDateTime maps to DbType.DateTime; AsDateTime2 maps to DbType.DateTime2. OfType(DbType) and OfType(MigratorDbType) remain available for other types. Nullability defaults to nullable. 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\").OnTable(\"Users\").AsString(320).Nullable(); Remove the email column Database.RemoveColumn(\"Users\", \"Email\"); migration.Delete.Column(\"Email\").FromTable(\"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\").OnTable(\"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\" });" + "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 Insert.IntoTable(...).Row(...) expressions. Each Row completes one insert; its returned builder offers only IfNotExists, so a second Row cannot silently replace the first. Insert, update and delete each expose only their supported steps. 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\" }); Classic update/delete without a predicate affects every row. Fluent update/delete requires Where(...) or an explicit AllRows() to complete the operation. Empty predicate arrays are rejected; an unfinished chain fails during Build, Apply or Preview before any queued operation executes. 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.CopyDataFromTable(...).ToTable(...).WithColumns(...) for named-column copies. Both tables must already exist and target columns must accept the source values. Execute.UpdateTable(target).FromTable(source).Set(copyPairs).Match(keyPairs) maps source/target pairs. CopyDataFromTable also supports OrderBy after WithColumns. 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.CopyDataFromTable(\"Users\").ToTable(\"ArchivedUsers\")\n .WithColumns(new[] { \"Id\", \"Name\" }, 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. " + "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\").OnTable(\"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\").FromTable(\"Users\")\n .WithFields(new ViewField(\"Id\"), new ViewField(\"Name\")); Dispose readers and commands obtained from the provider. Fluent Schema.Query and Schema.Table(name).Select accept a reader callback and handle disposal. Use Schema.Table(name).SelectScalar(columns, where) for a scalar selection. 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", @@ -81,21 +81,21 @@ "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. " + "text": " Use an explicit name so the index can be inspected or removed later. Fluent Create.Index(name).OnTable(table).WithColumns(...) names each part explicitly and preserves column order. Append Unique(), Clustered(), IncludeColumns(...) or WithFilter(...). For an existing Index definition, use Create.Index(definition).OnTable(table); fully qualify the model type if System.Index is also in scope. 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(\"IX_Users_Name\").OnTable(\"Users\").WithColumns(\"Name\"); Drop an index Database.RemoveIndex(\"Users\", \"IX_Users_Name\"); migration.Delete.Index(\"IX_Users_Name\").FromTable(\"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. " + "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.UniqueConstraint(\"UQ_Users_Name\").OnTable(\"Users\").WithColumns(\"Name\");\nmigration.Create.CheckConstraint(\"CK_Users_Id\").OnTable(\"Users\").WithExpression(\"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\").FromTable(\"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. " + "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 .FromTable(\"Orders\").WithColumns(\"UserId\")\n .ToTable(\"Users\").WithColumns(\"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\").FromTable(\"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", @@ -144,7 +144,7 @@ "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. " + "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\").OnTable(\"Users\").AsString(320);\n public override void BuildDown(MigrationBuilder migration)\n => migration.Delete.Column(\"Email\").FromTable(\"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", @@ -186,21 +186,21 @@ "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. " + "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\").OnTable(\"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. " + "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\").OnTable(\"Users\").WithColumns(\"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. " + "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\").OnTable(\"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", @@ -228,14 +228,14 @@ "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. " + "text": " The Classic provider indexer selects a named provider or a no-op provider. Fluent IfProvider 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.IfProvider(\"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. " + "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\").OnTable(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", @@ -256,7 +256,7 @@ "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. " + "text": " Classic Fluent AddTable Create.Table AddColumn(table, column) Create.Column(name).OnTable(table) ChangeColumn(table, column) Alter.Column(name).OnTable(table) / Alter.Column(definition).OnTable(table) RemoveTable / RemoveColumn Delete.Table(table) / Delete.Column(name).FromTable(table) RenameTable / RenameColumn Rename.Table(old).To(new) / Rename.Column(old).OnTable(table).To(new) AddPrimaryKey / AddUniqueConstraint / AddCheckConstraint Create.PrimaryKey(name).OnTable(table).WithColumns(...) / Create.UniqueConstraint / Create.CheckConstraint AddForeignKey / RemoveForeignKey Create.ForeignKey(name).FromTable(child).WithColumns(...).ToTable(parent).WithColumns(...) / Delete.ForeignKey(name).FromTable(table) AddIndex / RemoveIndex Create.Index(name).OnTable(table).WithColumns(...) / Delete.Index(name).FromTable(table) 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.CopyDataFromTable(source).ToTable(target).WithColumns(...) / Execute.UpdateTable(target).FromTable(source).Set(...).Match(...) 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", diff --git a/docs/fluent-operation-coverage.json b/docs/fluent-operation-coverage.json index af329b52..d510cf1b 100644 --- a/docs/fluent-operation-coverage.json +++ b/docs/fluent-operation-coverage.json @@ -1,41 +1,41 @@ { "AddTable": "Create.Table(...).WithFields(...) / WithEngine(...)", - "AddColumn": "Create.Column(...).OfType(...).WithPrecision(...)", - "ChangeColumn": "Alter.Column(...).OfType(...).WithPrecision(...)", - "AddPrimaryKey": "Create.PrimaryKey(...) / Create.NonClusteredPrimaryKey(...)", - "AddPrimaryKeyNonClustered": "Create.PrimaryKey(...) / Create.NonClusteredPrimaryKey(...)", - "AddForeignKey": "Create.ForeignKey(...); supply a generated name for GenerateForeignKey convenience overloads", - "GenerateForeignKey": "Create.ForeignKey(...); supply a generated name for GenerateForeignKey convenience overloads", - "AddUniqueConstraint": "Create.Unique(...) / Create.Check(...)", - "AddCheckConstraint": "Create.Unique(...) / Create.Check(...)", - "AddIndex": "Create.Index(table, Index)", - "AddView": "Create.View(...), both view-definition forms", - "RemoveTable": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveColumn": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveForeignKey": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveConstraint": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemovePrimaryKey": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveColumnDefaultValue": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveIndex": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveAllIndexes": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveAllConstraints": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RemoveAllForeignKeys": "Delete.Table/Column/ForeignKey/Constraint/PrimaryKey/Default/Index/AllIndexes/AllConstraints/ForeignKeysForColumn", - "RenameTable": "Rename.Table(...) / Rename.Column(...)", - "RenameColumn": "Rename.Table(...) / Rename.Column(...)", + "AddColumn": "Create.Column(name).OnTable(table).OfType(...) / Create.Column(definition).OnTable(table)", + "ChangeColumn": "Alter.Column(name).OnTable(table).OfType(...) / Alter.Column(definition).OnTable(table)", + "AddPrimaryKey": "Create.PrimaryKey(name).OnTable(table).WithColumns(...)", + "AddPrimaryKeyNonClustered": "Create.NonClusteredPrimaryKey(name).OnTable(table).WithColumns(...)", + "AddForeignKey": "Create.ForeignKey(name).FromTable(child).WithColumns(...).ToTable(parent).WithColumns(...).OnDelete(...).OnUpdate(...)", + "GenerateForeignKey": "Create.ForeignKey(name).FromTable(child).WithColumns(...).ToTable(parent).WithColumns(...); supply the generated name explicitly", + "AddUniqueConstraint": "Create.UniqueConstraint(name).OnTable(table).WithColumns(...)", + "AddCheckConstraint": "Create.CheckConstraint(name).OnTable(table).WithExpression(sql)", + "AddIndex": "Create.Index(name).OnTable(table).WithColumns(...) / Create.Index(definition).OnTable(table)", + "AddView": "Create.View(name).FromTable(table).WithFields(...) / WithElements(...)", + "RemoveTable": "Delete.Table(table)", + "RemoveColumn": "Delete.Column(name).FromTable(table)", + "RemoveForeignKey": "Delete.ForeignKey(name).FromTable(table)", + "RemoveConstraint": "Delete.Constraint(name).FromTable(table)", + "RemovePrimaryKey": "Delete.PrimaryKey().FromTable(table)", + "RemoveColumnDefaultValue": "Delete.DefaultValue(column).FromTable(table)", + "RemoveIndex": "Delete.Index(name).FromTable(table)", + "RemoveAllIndexes": "Delete.AllIndexes().FromTable(table)", + "RemoveAllConstraints": "Delete.AllConstraints().FromTable(table)", + "RemoveAllForeignKeys": "Delete.ForeignKeysForColumn(column).FromTable(table)", + "RenameTable": "Rename.Table(oldName).To(newName)", + "RenameColumn": "Rename.Column(oldName).OnTable(table).To(newName)", "Insert": "Insert.IntoTable(...).Row(...)[.IfNotExists(...)]", "InsertIfNotExists": "Insert.IntoTable(...).Row(...)[.IfNotExists(...)]", - "Update": "Update.Table(...).Set(...).Where(...) / WhereSql(...)", - "Delete": "Delete.FromTable(...).Where(...)", + "Update": "Update.Table(table).Set(...).Where(...) / WhereSql(...) / AllRows()", + "Delete": "Delete.FromTable(table).Where(...) / AllRows()", "TruncateTable": "Execute.Truncate(...)", - "CopyDataFromTableToTable": "Execute.CopyData(...) / Execute.UpdateFrom(...)", - "UpdateTargetFromSource": "Execute.CopyData(...) / Execute.UpdateFrom(...)", + "CopyDataFromTableToTable": "Execute.CopyDataFromTable(source).ToTable(target).WithColumns(sourceColumns, targetColumns).OrderBy(...)", + "UpdateTargetFromSource": "Execute.UpdateTable(target).FromTable(source).Set(copyPairs).Match(keyPairs)", "ExecuteNonQuery": "Execute.Sql(...) / Script(...) / EmbeddedScript(...)", "ExecuteQuery": "Schema.Query(...) / Strings(...) / Scalar(...) / NullableScalarDatabase.RenameTable("Users", "Members");
Database.RenameColumn("Members", "Name", "DisplayName");migration.Rename.Table("Users", "Members");
-migration.Rename.Column("Members", "Name", "DisplayName");Inside Up() / BuildUp(MigrationBuilder migration)
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.
migration.Rename.Table("Users").To("Members");
+migration.Rename.Column("Name").OnTable("Members").To("DisplayName");Inside Up() / BuildUp(MigrationBuilder migration)
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.
Database.ChangeColumn("Users", new Column("Name", DbType.String, 500)
{
IsNullable = false
});migration.Alter.Column("Name", "Users")
+migration.Alter.Column("Name").OnTable("Users")
.AsString(500).NotNullable();
Inside Up() / BuildUp(MigrationBuilder migration)
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.