From 6f051821f351b6c272fe04c428fce943bb504de5 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Thu, 24 Sep 2026 11:00:29 +0200 Subject: [PATCH 1/3] Add portable column-with-primary-key and unique-constraint removal APIs Consumers should not manipulate SQLite table-rebuild metadata to introduce an identity column or remove a legacy unnamed unique constraint. Add an explicit column-plus-primary-key overload, using one SQLite rebuild and one MySQL/MariaDB ALTER statement, with the normal DDL semantics elsewhere. Add metadata-based unique removal that requires an exact unambiguous match and preserves unrelated constraints. Regression coverage exercises both SQLite drivers, populated data, caller-owned definitions, downgrade, composite key order, failed backfill rollback, existing-key rejection, named and unnamed uniqueness, and SQL Server/MySQL SQL dispatch. Four cases failed before the SQLite implementation; all 400 Unit/SQLite tests now pass. Document the new APIs and provider transaction limitations. --- docs/migration-guide-12.1-to-13.md | 617 ++- .../ColumnWithPrimaryKeySqlTests.cs | 53 + .../ColumnWithPrimaryKeyTests.cs | 104 + .../Framework/ITransformationProvider.cs | 1468 +++--- .../Impl/Mysql/MySqlTransformationProvider.cs | 689 +-- .../SQLite/SQLiteTransformationProvider.cs | 2986 ++++++------ .../Providers/NoOpTransformationProvider.cs | 1147 ++--- .../Providers/TransformationProvider.cs | 4022 +++++++++-------- 8 files changed, 5663 insertions(+), 5423 deletions(-) create mode 100644 src/Migrator.Tests/ColumnWithPrimaryKeySqlTests.cs create mode 100644 src/Migrator.Tests/ColumnWithPrimaryKeyTests.cs diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index 4cf37c92..43eaa56e 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -1,320 +1,319 @@ -# Migrating from 12.1 to 13 - +# Migrating from 12.1 to 13 + Version 13 is a breaking release. This guide covers compatibility changes when updating existing migrations. Validate the upgrade on a restored database before running a changed migration history against production. For current API usage, see the [migration manual](https://dotnetprojects.github.io/Migrator.NET/guide/). - -## Schema model - -Columns describe data type, length, precision/scale, nullability, identity generation and defaults. Primary keys, unique constraints, foreign keys and checks belong to the table. Indexes are separate schema objects: a unique index is not automatically a unique constraint. - -The v13 API accepts complete named constraint definitions when creating a table, and returns the same kinds of definitions from metadata inspection. Key column order is significant. A composite UNIQUE constraint must never mark each member column as individually unique. Altering a column must not infer that its table constraints should be removed. - -The old column flags and duplicate fluent builder are removed. These are source-breaking changes: update historical migration source before recompiling for v13; the runner preserves the existing history table format. - -## Implemented breaking changes - -### Column flags, constructors and inspection - -`ColumnProperty`, its extensions, `Column.ColumnProperty`, -`IColumn.ColumnProperty`, `IsPrimaryKey` and `IsPrimaryKeyNonClustered` are -removed. Constructors and `AddColumn` overloads taking flags are removed. - -| 12.1 | 13 | -| --- | --- | -| `ColumnProperty.Null` / `None` | `IsNullable = true` (the default) | -| `ColumnProperty.NotNull` | `IsNullable = false` | -| `ColumnProperty.Identity` | `IsIdentity = true` | -| `ColumnProperty.Unsigned` | `IsUnsigned = true` | -| `ColumnProperty.PrimaryKey` | `new PrimaryKeyConstraint(name, columns)` in the table definition | -| `PrimaryKeyWithIdentity` | Identity on the column plus a separate primary key | -| `PrimaryKeyNonClustered` | `new PrimaryKeyConstraint(name, columns) { NonClustered = true }` | -| `ColumnProperty.Unique` | `new UniqueConstraint(name, columns)` | -| `ColumnProperty.Indexed` | An explicit `Index` definition | -| `ColumnProperty.CaseSensitive` | `Collation = "provider_collation_name"` | - -For example, replace a flagged `AddColumn` call with: - -```csharp -Database.AddColumn("Users", new Column("Email", DbType.String, 200) -{ - IsNullable = false, - DefaultValue = "unknown" -}); -Database.AddUniqueConstraint("UQ_Users_Email", "Users", "Email"); -``` - -Use `DefaultValue = 10` for numeric defaults: a positional integer after the -type is the column **size**, not its default. Unsupported unsigned/collation -combinations produce diagnostics. Collation names are explicit; the SQL Server -provider no longer queries or guesses a case-sensitive database collation. -SQLite's former `CaseSensitive` flag emitted `NOCASE`; choose `BINARY` or -`NOCASE` explicitly for the desired behavior. - -Read primary/unique membership through `GetTableConstraints(table)`, retaining -the constraint's member order. `GetColumns` returns column attributes only. -SQLite column metadata now follows physical column order, not primary-key order. -A plain SQLite INTEGER primary key remains a rowid alias; `IsIdentity` indicates -explicit `AUTOINCREMENT`, which is preserved on rebuild. - -### One fluent authoring API - -The obsolete `Framework.SchemaBuilder` namespace and `ExecuteSchemaBuilder` -method are removed. Use `Framework.Fluent.MigrationBuilder` and -`builder.Apply(provider)`, or derive from `FluentMigration`. -Replace `AddTable/AddColumn` chains with `Create.Table(...).WithColumn(...)`. -Replace `WithProperty`, unnamed `PrimaryKey()` and `Unique()` with -`NotNullable()`, `Identity()`, `Unsigned()`, `WithCollation(name)`, -`WithPrimaryKey(name, columns)` and `WithUniqueConstraint(name, columns)`. -Use `Create.ForeignKey` with separate delete/update actions. - -Fluent expressions select their table in a separate step: - -```csharp -migration.Create.Column("Email").OnTable("Users").AsString(320).Nullable(); -migration.Alter.Column("Name").OnTable("Users").AsString(200).NotNullable(); -migration.Rename.Column("Name").OnTable("Users").To("DisplayName"); -migration.Delete.Column("Email").FromTable("Users"); -migration.Create.Index("IX_Users_Name").OnTable("Users").WithColumns("DisplayName"); -migration.Create.ForeignKey("FK_Orders_Users") - .FromTable("Orders").WithColumns("UserId") - .ToTable("Users").WithColumns("Id") - .OnDelete(ForeignKeyConstraintType.Cascade); -``` - -The unreleased positional fluent overloads are removed. Use -`Create.UniqueConstraint(name).OnTable(table).WithColumns(...)` and -`Create.CheckConstraint(name).OnTable(table).WithExpression(sql)` for named -constraints. Renames end with `.To(newName)`. All object removal expressions -except `Delete.Table(table)` end with `.FromTable(table)`; for example, -`Delete.PrimaryKey().FromTable(table)` and -`Delete.DefaultValue(column).FromTable(table)`. - -Every named fluent column must specify a type. Table creation, column creation -and alteration share the same type/options methods. `AsDateTime()` now means -`DbType.DateTime`; use `AsDateTime2()` to preserve the earlier helper's mapping. -Table-level methods return the table builder: configure column attributes before -adding a constraint, or retain the specific column builder in a variable. - -Insert exposes only `IntoTable(...).Row(...)[.IfNotExists(...)]`; issue another -insert expression for each row. Update exposes `Table(...).Set(...)` followed by -`Where(...)`, `WhereSql(...)` or `AllRows()`. Delete exposes `FromTable(...)` -followed by `Where(...)` or `AllRows()`. Incomplete expressions throw during -`Build`, `Apply` and `Preview`, before any operation executes. - -Provider conditions use `IfProvider(name, configure)`. Table reads use -`Schema.Table(table).Select(...)` and `.SelectScalar(columns, where)`. -Data transfer uses `Execute.CopyDataFromTable(source).ToTable(target) -.WithColumns(sourceColumns, targetColumns)[.OrderBy(...)]`; joined updates use -`Execute.UpdateTable(target).FromTable(source).Set(copyPairs).Match(keyPairs)`. - -### Column changes do not own constraints - -`ChangeColumn` changes attributes without inferring creation or removal of -unique constraints. SQL Server's `AdoptColumnUniqueConstraint` and the implicit -ownership marker mechanism are removed. Use explicit `AddUniqueConstraint` and -`RemoveConstraint` calls. Existing extended-property markers are harmless; -v13 does not use them to delete constraints. - -Oracle changes columns in place so native constraints remain attached; a type -conversion that Oracle cannot perform must be expressed as an explicit data -migration. Identity validation runs before table creation, and identity no longer -requires primary-key membership. - -SQLite `RemoveAllIndexes` now preserves table UNIQUE constraints. To remove -constraints too, call `RemoveAllConstraints` explicitly. SQLite -`PrimaryKeyExists(table, name)` checks the actual name (use null for an unnamed -legacy key), rather than returning true for any primary key. - -### `Unique` is renamed to `UniqueConstraint` - -Replace `new Unique { Name = "UQ_Users_Email", KeyColumns = ["Email"] }` with `new UniqueConstraint("UQ_Users_Email", "Email")`. When importing both `System.Data` and `DotNetProjects.Migrator.Framework`, use an alias for the latter's `UniqueConstraint` (ADO.NET also defines that name). - -### Explicit table keys and complete constraint definitions - -```csharp -Database.AddTable("Users", - new Column("TenantId", DbType.Int32), - new Column("Id", DbType.Int32), - new Column("Email", DbType.String, 200), - new PrimaryKeyConstraint("PK_Users", "TenantId", "Id"), - new UniqueConstraint("UQ_Users_Email", "TenantId", "Email"), - new CheckConstraint("CK_Users_Id", "Id > 0")); -``` - -The supplied key order is preserved. Explicit primary-key definitions make their columns non-nullable without mutating the caller's column objects. This also rejects NULL in a composite SQLite primary key; old flag-based composite SQLite keys allowed NULL. SQLite identity requires a single INTEGER primary key and rejects incompatible combinations instead of silently removing identity. - -Fluent equivalent: append `.WithPrimaryKey("PK_Users", "TenantId", "Id")`, `.WithUniqueConstraint("UQ_Users_Email", "TenantId", "Email")`, or `.WithCheckConstraint("CK_Users_Id", "Id > 0")` to the table builder. Each is part of the complete table definition. - -### Structured constraint inspection - -Use `Database.GetTableConstraints("Users")`, or `Schema.Table("Users").ConstraintDefinitions()`, then select `PrimaryKeyConstraint`, `UniqueConstraint`, `ForeignKeyConstraint` or `CheckConstraint`. Key column order belongs to the constraint. A unique index stays in index metadata. SQLite returns `Name == null` for unnamed legacy constraints; a backing autoindex name is not an invented constraint name. - -Structured readers cover SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Db2, Firebird, Informix and Sybase. Each reader is exercised in its live CI job; unsupported engines throw `NotSupportedException`. MySQL identifies the primary key as `PRIMARY` regardless of a supplied symbolic name. Quoted qualified Oracle/MySQL lookups are currently rejected explicitly. These limitations must not be interpreted as empty metadata. - -### Custom provider and dialect implementations - -`ITransformationProvider` now requires `GetTableConstraints(string)`. Return accurate typed definitions, including ordered key members, or throw `NotSupportedException`; do not return an empty array for an unsupported reader. `IDialect` replaces `RegisterProperty/SqlForProperty` with `RegisterColumnAttribute/SqlForColumnAttribute`, using the non-flag `ColumnAttribute` enum (Null, NotNull, Identity, Unsigned). Custom column mappers use explicit column attributes; removed helpers include `IndexSql`, `PropertySelected`, `AddPrimaryKey`, `AddUnique`, and `AddForeignKey`. `GetPrimaryKeys(IEnumerable)` and column/index joining helpers are removed: inspect table constraints and create indexes explicitly. `GetCollationSql` generates a supported collation clause or throws before DDL. `IDialect` also adds `QuoteIdentifier(string)` for one identifier atom and `GetTableConstraintSql(TableConstraint)` for pure SQL rendering. Implementations derived from `Dialect` inherit defaults. Constraint names containing quote delimiters are escaped; a dot within a constraint name is not a schema separator. - -### SQLite alterations preserve named primary keys - -Rebuilding a table now retains an explicitly named primary key and its declared -column order. Changing a column definition does not implicitly remove that key. -To drop a column belonging to a named primary key, first call -`RemovePrimaryKey(table)`, then remove the column, and explicitly create any -replacement key. A failed attempt leaves the original table intact. - -Rebuilds also retain physical column order for tables with named primary keys, -including when a column's type or size changes. Identity rebuilds retain the -constraint name and the sequence high-water mark. - -## Provider authors and dialects (design) - + +## Schema model + +Columns describe data type, length, precision/scale, nullability, identity generation and defaults. Primary keys, unique constraints, foreign keys and checks belong to the table. Indexes are separate schema objects: a unique index is not automatically a unique constraint. + +The v13 API accepts complete named constraint definitions when creating a table, and returns the same kinds of definitions from metadata inspection. Key column order is significant. A composite UNIQUE constraint must never mark each member column as individually unique. Altering a column must not infer that its table constraints should be removed. + +The old column flags and duplicate fluent builder are removed. These are source-breaking changes: update historical migration source before recompiling for v13; the runner preserves the existing history table format. + +## Implemented breaking changes + +### Column flags, constructors and inspection + +`ColumnProperty`, its extensions, `Column.ColumnProperty`, +`IColumn.ColumnProperty`, `IsPrimaryKey` and `IsPrimaryKeyNonClustered` are +removed. Constructors and `AddColumn` overloads taking flags are removed. + +| 12.1 | 13 | +| --- | --- | +| `ColumnProperty.Null` / `None` | `IsNullable = true` (the default) | +| `ColumnProperty.NotNull` | `IsNullable = false` | +| `ColumnProperty.Identity` | `IsIdentity = true` | +| `ColumnProperty.Unsigned` | `IsUnsigned = true` | +| `ColumnProperty.PrimaryKey` | `new PrimaryKeyConstraint(name, columns)` in the table definition | +| `PrimaryKeyWithIdentity` | Identity on the column plus a separate primary key | +| `PrimaryKeyNonClustered` | `new PrimaryKeyConstraint(name, columns) { NonClustered = true }` | +| `ColumnProperty.Unique` | `new UniqueConstraint(name, columns)` | +| `ColumnProperty.Indexed` | An explicit `Index` definition | +| `ColumnProperty.CaseSensitive` | `Collation = "provider_collation_name"` | + +For example, replace a flagged `AddColumn` call with: + +```csharp +Database.AddColumn("Users", new Column("Email", DbType.String, 200) +{ + IsNullable = false, + DefaultValue = "unknown" +}); +Database.AddUniqueConstraint("UQ_Users_Email", "Users", "Email"); +``` + +Use `DefaultValue = 10` for numeric defaults: a positional integer after the +type is the column **size**, not its default. Unsupported unsigned/collation +combinations produce diagnostics. Collation names are explicit; the SQL Server +provider no longer queries or guesses a case-sensitive database collation. +SQLite's former `CaseSensitive` flag emitted `NOCASE`; choose `BINARY` or +`NOCASE` explicitly for the desired behavior. + +Read primary/unique membership through `GetTableConstraints(table)`, retaining +the constraint's member order. `GetColumns` returns column attributes only. +SQLite column metadata now follows physical column order, not primary-key order. +A plain SQLite INTEGER primary key remains a rowid alias; `IsIdentity` indicates +explicit `AUTOINCREMENT`, which is preserved on rebuild. + +### One fluent authoring API + +The obsolete `Framework.SchemaBuilder` namespace and `ExecuteSchemaBuilder` +method are removed. Use `Framework.Fluent.MigrationBuilder` and +`builder.Apply(provider)`, or derive from `FluentMigration`. +Replace `AddTable/AddColumn` chains with `Create.Table(...).WithColumn(...)`. +Replace `WithProperty`, unnamed `PrimaryKey()` and `Unique()` with +`NotNullable()`, `Identity()`, `Unsigned()`, `WithCollation(name)`, +`WithPrimaryKey(name, columns)` and `WithUniqueConstraint(name, columns)`. +Use `Create.ForeignKey` with separate delete/update actions. + +Fluent expressions select their table in a separate step: + +```csharp +migration.Create.Column("Email").OnTable("Users").AsString(320).Nullable(); +migration.Alter.Column("Name").OnTable("Users").AsString(200).NotNullable(); +migration.Rename.Column("Name").OnTable("Users").To("DisplayName"); +migration.Delete.Column("Email").FromTable("Users"); +migration.Create.Index("IX_Users_Name").OnTable("Users").WithColumns("DisplayName"); +migration.Create.ForeignKey("FK_Orders_Users") + .FromTable("Orders").WithColumns("UserId") + .ToTable("Users").WithColumns("Id") + .OnDelete(ForeignKeyConstraintType.Cascade); +``` + +The unreleased positional fluent overloads are removed. Use +`Create.UniqueConstraint(name).OnTable(table).WithColumns(...)` and +`Create.CheckConstraint(name).OnTable(table).WithExpression(sql)` for named +constraints. Renames end with `.To(newName)`. All object removal expressions +except `Delete.Table(table)` end with `.FromTable(table)`; for example, +`Delete.PrimaryKey().FromTable(table)` and +`Delete.DefaultValue(column).FromTable(table)`. + +Every named fluent column must specify a type. Table creation, column creation +and alteration share the same type/options methods. `AsDateTime()` now means +`DbType.DateTime`; use `AsDateTime2()` to preserve the earlier helper's mapping. +Table-level methods return the table builder: configure column attributes before +adding a constraint, or retain the specific column builder in a variable. + +Insert exposes only `IntoTable(...).Row(...)[.IfNotExists(...)]`; issue another +insert expression for each row. Update exposes `Table(...).Set(...)` followed by +`Where(...)`, `WhereSql(...)` or `AllRows()`. Delete exposes `FromTable(...)` +followed by `Where(...)` or `AllRows()`. Incomplete expressions throw during +`Build`, `Apply` and `Preview`, before any operation executes. + +Provider conditions use `IfProvider(name, configure)`. Table reads use +`Schema.Table(table).Select(...)` and `.SelectScalar(columns, where)`. +Data transfer uses `Execute.CopyDataFromTable(source).ToTable(target) +.WithColumns(sourceColumns, targetColumns)[.OrderBy(...)]`; joined updates use +`Execute.UpdateTable(target).FromTable(source).Set(copyPairs).Match(keyPairs)`. + +### Column changes do not own constraints + +`ChangeColumn` changes attributes without inferring creation or removal of +unique constraints. SQL Server's `AdoptColumnUniqueConstraint` and the implicit +ownership marker mechanism are removed. Use explicit `AddUniqueConstraint` and +`RemoveConstraint` calls. Existing extended-property markers are harmless; +v13 does not use them to delete constraints. + +Oracle changes columns in place so native constraints remain attached; a type +conversion that Oracle cannot perform must be expressed as an explicit data +migration. Identity validation runs before table creation, and identity no longer +requires primary-key membership. + +SQLite `RemoveAllIndexes` now preserves table UNIQUE constraints. To remove +constraints too, call `RemoveAllConstraints` explicitly. SQLite +`PrimaryKeyExists(table, name)` checks the actual name (use null for an unnamed +legacy key), rather than returning true for any primary key. + +### `Unique` is renamed to `UniqueConstraint` + +Replace `new Unique { Name = "UQ_Users_Email", KeyColumns = ["Email"] }` with `new UniqueConstraint("UQ_Users_Email", "Email")`. When importing both `System.Data` and `DotNetProjects.Migrator.Framework`, use an alias for the latter's `UniqueConstraint` (ADO.NET also defines that name). + +### Explicit table keys and complete constraint definitions + +```csharp +Database.AddTable("Users", + new Column("TenantId", DbType.Int32), + new Column("Id", DbType.Int32), + new Column("Email", DbType.String, 200), + new PrimaryKeyConstraint("PK_Users", "TenantId", "Id"), + new UniqueConstraint("UQ_Users_Email", "TenantId", "Email"), + new CheckConstraint("CK_Users_Id", "Id > 0")); +``` + +The supplied key order is preserved. Explicit primary-key definitions make their columns non-nullable without mutating the caller's column objects. This also rejects NULL in a composite SQLite primary key; old flag-based composite SQLite keys allowed NULL. SQLite identity requires a single INTEGER primary key and rejects incompatible combinations instead of silently removing identity. + +Fluent equivalent: append `.WithPrimaryKey("PK_Users", "TenantId", "Id")`, `.WithUniqueConstraint("UQ_Users_Email", "TenantId", "Email")`, or `.WithCheckConstraint("CK_Users_Id", "Id > 0")` to the table builder. Each is part of the complete table definition. + +### Structured constraint inspection + +Use `Database.GetTableConstraints("Users")`, or `Schema.Table("Users").ConstraintDefinitions()`, then select `PrimaryKeyConstraint`, `UniqueConstraint`, `ForeignKeyConstraint` or `CheckConstraint`. Key column order belongs to the constraint. A unique index stays in index metadata. SQLite returns `Name == null` for unnamed legacy constraints; a backing autoindex name is not an invented constraint name. + +Structured readers cover SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Db2, Firebird, Informix and Sybase. Each reader is exercised in its live CI job; unsupported engines throw `NotSupportedException`. MySQL identifies the primary key as `PRIMARY` regardless of a supplied symbolic name. Quoted qualified Oracle/MySQL lookups are currently rejected explicitly. These limitations must not be interpreted as empty metadata. + +### Custom provider and dialect implementations + +`ITransformationProvider` now requires `GetTableConstraints(string)`. Return accurate typed definitions, including ordered key members, or throw `NotSupportedException`; do not return an empty array for an unsupported reader. `IDialect` replaces `RegisterProperty/SqlForProperty` with `RegisterColumnAttribute/SqlForColumnAttribute`, using the non-flag `ColumnAttribute` enum (Null, NotNull, Identity, Unsigned). Custom column mappers use explicit column attributes; removed helpers include `IndexSql`, `PropertySelected`, `AddPrimaryKey`, `AddUnique`, and `AddForeignKey`. `GetPrimaryKeys(IEnumerable)` and column/index joining helpers are removed: inspect table constraints and create indexes explicitly. `GetCollationSql` generates a supported collation clause or throws before DDL. `IDialect` also adds `QuoteIdentifier(string)` for one identifier atom and `GetTableConstraintSql(TableConstraint)` for pure SQL rendering. Implementations derived from `Dialect` inherit defaults. Constraint names containing quote delimiters are escaped; a dot within a constraint name is not a schema separator. + +### SQLite alterations preserve named primary keys + +Rebuilding a table now retains an explicitly named primary key and its declared +column order. Changing a column definition does not implicitly remove that key. +To drop a column belonging to a named primary key, first call +`RemovePrimaryKey(table)`, then remove the column, and explicitly create any +replacement key. A failed attempt leaves the original table intact. + +Rebuilds also retain physical column order for tables with named primary keys, +including when a column's type or size changes. Identity rebuilds retain the +constraint name and the sequence high-water mark. + +## Provider authors and dialects (design) + Keep SQL rendering independent of a live connection. A dialect defines identifier quoting, type/literal rendering and SQL capabilities. Metadata readers inspect existing schema; execution manages commands, transactions and history. Connected preview may read history and schema before rendering, but it must not mutate the database. Offline rendering uses an explicitly supplied schema context. - + The provider surface combines these concerns through execution and metadata contracts. SQL rendering uses a separate context. Unsupported combinations must fail explicitly before DDL, not disappear from generated SQL. - -## Design references - -Reviewed 2026-09-22: - -- [EF Core CreateTableOperation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.migrations.operations.createtableoperation?view=efcore-10.0) separates columns, primary key, unique constraints, checks and foreign keys. -- [FluentMigrator ColumnDefinition](https://github.com/fluentmigrator/fluentmigrator/blob/main/src/FluentMigrator.Abstractions/Model/ColumnDefinition.cs) still carries constraint flags; its expression/generator separation is useful, but its column model is not the target here. -- [Alembic operations](https://alembic.sqlalchemy.org/en/latest/ops.html) distinguish named table constraints from column alteration and use explicit batch reconstruction for SQLite. - + +## Design references + +Reviewed 2026-09-22: + +- [EF Core CreateTableOperation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.migrations.operations.createtableoperation?view=efcore-10.0) separates columns, primary key, unique constraints, checks and foreign keys. +- [FluentMigrator ColumnDefinition](https://github.com/fluentmigrator/fluentmigrator/blob/main/src/FluentMigrator.Abstractions/Model/ColumnDefinition.cs) still carries constraint flags; its expression/generator separation is useful, but its column model is not the target here. +- [Alembic operations](https://alembic.sqlalchemy.org/en/latest/ops.html) distinguish named table constraints from column alteration and use explicit batch reconstruction for SQLite. + ## Schema API changes - + Typed constraints with ordered metadata, the SQLite constraint tokenizer, explicit SQL defaults, semantic collations and the consolidated authoring API work together. Use explicit object names and check provider-specific operation behavior when upgrading a custom dialect. - -## Explicit SQL defaults and semantic collations - -`RawSql.Insert("ksuid_new()")` marks trusted SQL as an expression in either API: - -```csharp -new Column("Id", DbType.String, 27) { DefaultValue = RawSql.Insert("ksuid_new()") }; -// Fluent: -builder.Create.Table("Events").WithColumn("Id").AsString(27) - .WithDefaultValue(RawSql.Insert("ksuid_new()")); -``` - -The database must provide that function. Strings remain quoted values, so -`WithDefaultValue("ksuid_new()")` stores that text instead of calling a function. -SQLite wraps expressions in parentheses as required for expression defaults. -Metadata exposes unparsed SQL defaults as `RawSql`, replacing the previous private -expression object; inspect `RawSql.Sql` instead of assuming every default is a string. -Expression text is trusted migration code, not a parameter or a cross-database function abstraction. - -`Column.Collation` is now a typed `Collation` value. String assignment still selects -a provider name through an implicit conversion; `Collation.Named("name")` is explicit. -Fluent `.WithCollation(...)` takes the same type. - -```csharp -new Column("Name", DbType.String, 100) { Collation = Collation.CaseInsensitive }; -builder.Create.Table("Names").WithColumn("Name").AsString(100) - .WithCollation(Collation.CaseInsensitive); -``` - -| Preset | SQL Server | MySQL 8 | MariaDB 10.10+ | PostgreSQL | SQLite | -| --- | --- | --- | --- | --- | --- | -| `CaseInsensitive` (accent-sensitive) | Latin1 General 100 CI AS SC | utf8mb4 0900 as ci | utf8mb4 UCA1400 nopad as ci | Explicit installed name required | Unsupported | -| `CaseSensitive` (accent-sensitive) | Latin1 General 100 CS AS SC | utf8mb4 0900 as cs | utf8mb4 UCA1400 nopad as cs | Explicit installed name required | Explicit installed name required | -| `Binary` | Latin1 General 100 BIN2 | utf8mb4 0900 bin | utf8mb4 nopad bin | C | BINARY | -| `AsciiIgnoreCase` | Unsupported | Unsupported | Unsupported | Unsupported | NOCASE | - -These presets describe comparison intent, not identical sorting, normalization, -language tailoring, or trailing-space behavior across engines. Use a named collation -for a specific language or exact provider semantics. MySQL/MariaDB presets require -utf8mb4-compatible text columns and the listed engine versions. Other dialects reject -unmapped presets; custom dialects can override `ResolveCollation(CollationKind)`. -Unsupported requests fail during SQL generation, before executing the table operation. -SQLite never downgrades Unicode case-insensitivity to its ASCII-only NOCASE behavior. -SQLite rebuilds preserve declared column collations, including named custom -collations registered on the connection. `GetColumns` reports these names. -Changing a collation explicitly rebuilds the table; a resulting uniqueness -violation rolls back the change and preserves the original data. Index-level -`COLLATE` clauses remain unsupported for rebuilds and fail before replacing the table. - -## Consolidated migration history - -A consolidated baseline can use `Database.MigrationApplied(version, scope)` to -record versions whose schema it already includes. The runner rechecks the active -scope's history before each planned migration and skips versions now applied, -including their `AfterUp` callbacks. The same rule applies to downgrades when an -earlier `Down` removes another version from history. Recording the baseline's own -version does not insert it twice. History for another scope does not skip a step -in the current scope. Transaction rollback still applies to baseline schema and -history changes according to the selected transaction mode. - -## SQLite defaults and identity - -### SQLite GUID defaults - -SQLite now renders a CLR `Guid` default as a blob using `Guid.ToByteArray()`, -matching GUID parameters inserted by the provider. Previously a GUID default -was text, so a defaulted foreign-key value did not match an explicitly inserted -parent GUID even when both represented the same identifier. - -This fixes new table/column definitions, including backfilling a new column. -Existing text GUID defaults and data are preserved during unrelated rebuilds; -column inspection retains their SQL as `RawSql` so storage classes are not -silently converted. Databases already containing mixed text/blob GUIDs require -an explicit data migration that converts related keys consistently. A string -default remains text; use a CLR `Guid` when authoring a GUID default. - -### SQLite identity columns - -SQLite requires the identity column and its single-column primary key in the -same table definition. Separate `AddColumn` and `AddPrimaryKey` calls create an -invalid intermediate definition. Use the SQLite provider's atomic rebuild API: - -```csharp -var sqlite = (SQLiteTransformationProvider)Database; -var definition = sqlite.GetSQLiteTableInfo("Settings"); -definition.Columns.Add(new Column("Id", DbType.Int32) { IsIdentity = true }); -definition.ColumnMappings.Add(new MappingInfo { OldName = null, NewName = "Id" }); -definition.PrimaryKey = new PrimaryKeyConstraint("PK_Settings", "Id"); -sqlite.RecreateTable(definition); -``` - -`SQLiteTransformationProvider` is in `DotNetProjects.Migrator.Providers.Impl.SQLite`; -`MappingInfo` is in its `Models` namespace. Existing rows receive generated IDs. -This example assumes the table has no existing primary key or dependent foreign -keys requiring a separate migration plan. - -## Identifier quoting and renamed tables - -Use `QuoteColumnNameIfRequired` for columns in authored SQL and -`QuoteTableNameIfRequired` for tables. A table name may acquire a schema prefix; -using that API for a column can produce an invalid reference such as `dbo.Color`. - -Renaming a table does not rename its explicitly named constraints or backing -indexes. On SQL Server and PostgreSQL, recreating the old table with the old -primary-key name can therefore collide with the renamed table's key. Give the -replacement table a distinct key name (for example `PK_Client_New`), or explicitly -rename the retained key using provider-specific SQL before reusing its name. - -For PostgreSQL, create an ICU nondeterministic collation explicitly (for example -`CREATE COLLATION app_ci (provider=icu, locale='und-u-ks-level2', deterministic=false)`) -and use `Collation.Named("app_ci")`. The framework does not silently create shared -database objects while rendering a column or preview. - -MySQL/MariaDB expose unique indexes as unique constraints in their catalogs, so -metadata cannot recover whether the original author used CREATE UNIQUE INDEX or -a UNIQUE table clause. No ownership decision may be inferred from that syntax. - -## Oracle index options and constraint metadata - -Oracle now rejects nonempty `Index.IncludeColumns` and `Index.Clustered = true` before -DDL. Version 12.1 silently ignored them. Remove these options for an ordinary Oracle -index or author an explicit Oracle-specific design; a SQL Server clustered-index + +## Explicit SQL defaults and semantic collations + +`RawSql.Insert("ksuid_new()")` marks trusted SQL as an expression in either API: + +```csharp +new Column("Id", DbType.String, 27) { DefaultValue = RawSql.Insert("ksuid_new()") }; +// Fluent: +builder.Create.Table("Events").WithColumn("Id").AsString(27) + .WithDefaultValue(RawSql.Insert("ksuid_new()")); +``` + +The database must provide that function. Strings remain quoted values, so +`WithDefaultValue("ksuid_new()")` stores that text instead of calling a function. +SQLite wraps expressions in parentheses as required for expression defaults. +Metadata exposes unparsed SQL defaults as `RawSql`, replacing the previous private +expression object; inspect `RawSql.Sql` instead of assuming every default is a string. +Expression text is trusted migration code, not a parameter or a cross-database function abstraction. + +`Column.Collation` is now a typed `Collation` value. String assignment still selects +a provider name through an implicit conversion; `Collation.Named("name")` is explicit. +Fluent `.WithCollation(...)` takes the same type. + +```csharp +new Column("Name", DbType.String, 100) { Collation = Collation.CaseInsensitive }; +builder.Create.Table("Names").WithColumn("Name").AsString(100) + .WithCollation(Collation.CaseInsensitive); +``` + +| Preset | SQL Server | MySQL 8 | MariaDB 10.10+ | PostgreSQL | SQLite | +| --- | --- | --- | --- | --- | --- | +| `CaseInsensitive` (accent-sensitive) | Latin1 General 100 CI AS SC | utf8mb4 0900 as ci | utf8mb4 UCA1400 nopad as ci | Explicit installed name required | Unsupported | +| `CaseSensitive` (accent-sensitive) | Latin1 General 100 CS AS SC | utf8mb4 0900 as cs | utf8mb4 UCA1400 nopad as cs | Explicit installed name required | Explicit installed name required | +| `Binary` | Latin1 General 100 BIN2 | utf8mb4 0900 bin | utf8mb4 nopad bin | C | BINARY | +| `AsciiIgnoreCase` | Unsupported | Unsupported | Unsupported | Unsupported | NOCASE | + +These presets describe comparison intent, not identical sorting, normalization, +language tailoring, or trailing-space behavior across engines. Use a named collation +for a specific language or exact provider semantics. MySQL/MariaDB presets require +utf8mb4-compatible text columns and the listed engine versions. Other dialects reject +unmapped presets; custom dialects can override `ResolveCollation(CollationKind)`. +Unsupported requests fail during SQL generation, before executing the table operation. +SQLite never downgrades Unicode case-insensitivity to its ASCII-only NOCASE behavior. +SQLite rebuilds preserve declared column collations, including named custom +collations registered on the connection. `GetColumns` reports these names. +Changing a collation explicitly rebuilds the table; a resulting uniqueness +violation rolls back the change and preserves the original data. Index-level +`COLLATE` clauses remain unsupported for rebuilds and fail before replacing the table. + +## Consolidated migration history + +A consolidated baseline can use `Database.MigrationApplied(version, scope)` to +record versions whose schema it already includes. The runner rechecks the active +scope's history before each planned migration and skips versions now applied, +including their `AfterUp` callbacks. The same rule applies to downgrades when an +earlier `Down` removes another version from history. Recording the baseline's own +version does not insert it twice. History for another scope does not skip a step +in the current scope. Transaction rollback still applies to baseline schema and +history changes according to the selected transaction mode. + +## SQLite defaults and identity + +### SQLite GUID defaults + +SQLite now renders a CLR `Guid` default as a blob using `Guid.ToByteArray()`, +matching GUID parameters inserted by the provider. Previously a GUID default +was text, so a defaulted foreign-key value did not match an explicitly inserted +parent GUID even when both represented the same identifier. + +This fixes new table/column definitions, including backfilling a new column. +Existing text GUID defaults and data are preserved during unrelated rebuilds; +column inspection retains their SQL as `RawSql` so storage classes are not +silently converted. Databases already containing mixed text/blob GUIDs require +an explicit data migration that converts related keys consistently. A string +default remains text; use a CLR `Guid` when authoring a GUID default. + +### SQLite identity columns + +Use the provider-independent overload to add a column with an explicit primary key: + +```csharp +Database.AddColumn("Settings", + new Column("Id", DbType.Int32) { IsIdentity = true }, + new PrimaryKeyConstraint("PK_Settings", "Id")); +``` + +The key must have a nonempty name and valid, ordered column members. Existing primary keys are rejected rather than replaced, and caller-owned definitions are not mutated. SQLite adds both definitions in a single transactional table rebuild, preserving existing rows and generating IDs. MySQL/MariaDB add both in one ALTER statement because AUTO_INCREMENT must be indexed immediately. Other providers use their normal AddColumn/AddPrimaryKey operations and DDL transaction semantics; the overload does not promise cross-provider rollback if a later DDL statement fails. Existing columns in a composite key must already meet the provider's requirements. + +`IsIdentity` alone still does not imply a primary key. Separate AddColumn/AddPrimaryKey calls cannot introduce an SQLite identity column. SQLite downgrade can use RemovePrimaryKey followed by RemoveColumn; the migration runner manages SQLite's foreign-key state. + +### Removing legacy unnamed unique constraints + +Use `Database.RemoveUniqueConstraint(table, constraint)` with a `UniqueConstraint` returned by `GetTableConstraints`. The operation matches both the declared name and ordered columns, and requires exactly one match. SQLite rebuilds internally to remove unnamed legacy constraints without discarding other unique/check constraints. Other providers remove the verified named constraint using their existing DDL implementation. Unknown or ambiguous selections fail before mutation. + +Custom ITransformationProvider implementations must implement these two new methods; implementations derived from TransformationProvider inherit the portable defaults. NoOpTransformationProvider supports both as no-ops. These are direct provider APIs; fluent callers can use Database for these combined operations. +## Identifier quoting and renamed tables + +Use `QuoteColumnNameIfRequired` for columns in authored SQL and +`QuoteTableNameIfRequired` for tables. A table name may acquire a schema prefix; +using that API for a column can produce an invalid reference such as `dbo.Color`. + +Renaming a table does not rename its explicitly named constraints or backing +indexes. On SQL Server and PostgreSQL, recreating the old table with the old +primary-key name can therefore collide with the renamed table's key. Give the +replacement table a distinct key name (for example `PK_Client_New`), or explicitly +rename the retained key using provider-specific SQL before reusing its name. + +For PostgreSQL, create an ICU nondeterministic collation explicitly (for example +`CREATE COLLATION app_ci (provider=icu, locale='und-u-ks-level2', deterministic=false)`) +and use `Collation.Named("app_ci")`. The framework does not silently create shared +database objects while rendering a column or preview. + +MySQL/MariaDB expose unique indexes as unique constraints in their catalogs, so +metadata cannot recover whether the original author used CREATE UNIQUE INDEX or +a UNIQUE table clause. No ownership decision may be inferred from that syntax. + +## Oracle index options and constraint metadata + +Oracle now rejects nonempty `Index.IncludeColumns` and `Index.Clustered = true` before +DDL. Version 12.1 silently ignored them. Remove these options for an ordinary Oracle +index or author an explicit Oracle-specific design; a SQL Server clustered-index request is not translated to an Oracle index-organized table. Oracle column changes and default removal use `MODIFY (...)`. This disambiguates valid column names such as `Element`, which Oracle can interpret as syntax in the unparenthesized form, rejecting the statement with ORA-00903 or ORA-01735. - -Structured metadata preserves SQL Server nonclustered primary keys and Oracle -ordered foreign-key pairs/delete actions. Foreign-key constructor arrays are copied, -matching primary/unique definitions, so later caller-array edits cannot change the key. - -## Build and package identity - + +Structured metadata preserves SQL Server nonclustered primary keys and Oracle +ordered foreign-key pairs/delete actions. Foreign-key constructor arrays are copied, +matching primary/unique definitions, so later caller-array edits cannot change the key. + +## Build and package identity + The core assembly and file versions are 13.0.0.0; generated assembly metadata preserves the existing title and description. Recompile consumers of the breaking API and update assembly/version binding assumptions. Keep the core, optional DI integration and CLI on compatible package versions. diff --git a/src/Migrator.Tests/ColumnWithPrimaryKeySqlTests.cs b/src/Migrator.Tests/ColumnWithPrimaryKeySqlTests.cs new file mode 100644 index 00000000..3c075f2b --- /dev/null +++ b/src/Migrator.Tests/ColumnWithPrimaryKeySqlTests.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Data; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers; +using DotNetProjects.Migrator.Providers.Impl.Mysql; +using DotNetProjects.Migrator.Providers.Impl.SqlServer; +using NUnit.Framework; + +namespace Migrator.Tests; + +[Category("Unit")] +public class ColumnWithPrimaryKeySqlTests +{ + private sealed class RecordingMySqlProvider() : MySqlTransformationProvider(new MysqlDialect(), (IDbConnection)null, null, null) + { + public List Statements { get; } = []; + public override bool TableExists(string table) => true; + public override Column[] GetColumns(string table) => [new Column("Value", DbType.String)]; + public override TableConstraint[] GetTableConstraints(string table) => []; + public override int ExecuteNonQuery(string sql) { Statements.Add(sql); return 0; } + } + + private sealed class RecordingSqlServerProvider() : SqlServerTransformationProvider(new SqlServerDialect(), (IDbConnection)null, null, null, null) + { + public List Statements { get; } = []; + public override bool TableExists(string table) => true; + public override Column[] GetColumns(string table) => [new Column("Value", DbType.String)]; + public override TableConstraint[] GetTableConstraints(string table) => []; + public override int ExecuteNonQuery(string sql) { Statements.Add(sql); return 0; } + } + + [Test] + public void MySqlAddsAutoIncrementAndKeyInOneStatement() + { + using var provider = new RecordingMySqlProvider(); + provider.AddColumn("Settings", new Column("Id", DbType.Int32) { IsIdentity = true }, new PrimaryKeyConstraint("PK_Settings", "Id")); + Assert.That(provider.Statements, Has.Count.EqualTo(1)); + Assert.That(provider.Statements[0], Does.Contain("AUTO_INCREMENT").IgnoreCase.And.Contain("PRIMARY KEY").IgnoreCase); + Assert.That(provider.Statements[0], Does.Contain(", ADD CONSTRAINT")); + } + + [Test] + public void SqlServerUsesItsNonclusteredPrimaryKeyImplementation() + { + using var provider = new RecordingSqlServerProvider(); + var column = new Column("Id", DbType.Int32) { IsIdentity = true }; + provider.AddColumn("Settings", column, new PrimaryKeyConstraint("PK_Settings", "Id") { NonClustered = true }); + Assert.That(provider.Statements, Has.Count.EqualTo(2)); + Assert.That(provider.Statements[0], Does.Contain("IDENTITY").IgnoreCase); + Assert.That(provider.Statements[1], Does.Contain("PRIMARY KEY NONCLUSTERED").IgnoreCase); + Assert.That(column.IsNullable, Is.True); + } +} diff --git a/src/Migrator.Tests/ColumnWithPrimaryKeyTests.cs b/src/Migrator.Tests/ColumnWithPrimaryKeyTests.cs new file mode 100644 index 00000000..2e3ca62a --- /dev/null +++ b/src/Migrator.Tests/ColumnWithPrimaryKeyTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers; +using NUnit.Framework; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; + +namespace Migrator.Tests; + +[Category("SQLite")] +[TestFixture(false)] +[TestFixture(true)] +public class ColumnWithPrimaryKeyTests(bool systemData) +{ + private IDbConnection connection; + private ITransformationProvider provider; + + [SetUp] + public void SetUp() + { + connection = systemData + ? new System.Data.SQLite.SQLiteConnection("Data Source=:memory:") + : new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:;Foreign Keys=False"); + connection.Open(); + provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); + provider.ExecuteNonQuery("CREATE TABLE Settings (Value TEXT, CONSTRAINT UQ_Value UNIQUE(Value), CHECK(length(Value)>0)); CREATE INDEX IX_Value ON Settings(Value); INSERT INTO Settings VALUES ('first'), ('second');"); + } + + [TearDown] + public void TearDown() { provider.Dispose(); connection.Dispose(); } + + [Test] + public void AddsIdentityAndKeyTogetherAndSupportsPortableDowngrade() + { + var column = new Column("Id", DbType.Int32) { IsIdentity = true }; + provider.AddColumn("Settings", column, new PrimaryKeyConstraint("PK_Settings", "Id")); + Assert.That(column.IsNullable, Is.True, "The caller's definition must not be mutated."); + Assert.That(provider.ExecuteScalar("SELECT COUNT(DISTINCT Id) FROM Settings"), Is.EqualTo(2)); + Assert.That(provider.GetTableConstraints("Settings").OfType().Single().Name, Is.EqualTo("PK_Settings")); + provider.Insert("Settings", ["Value"], ["third"]); + Assert.That(provider.ExecuteScalar("SELECT Id FROM Settings WHERE Value='third'"), Is.EqualTo(3)); + provider.RemovePrimaryKey("Settings"); + provider.RemoveColumn("Settings", "Id"); + Assert.That(provider.GetColumns("Settings").Select(c => c.Name), Is.EqualTo(new[] { "Value" })); + Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Settings"), Is.EqualTo(3)); + Assert.That(provider.GetTableConstraints("Settings").OfType().Single().Name, Is.EqualTo("UQ_Value")); + Assert.That(provider.GetTableConstraints("Settings").OfType().Count(), Is.EqualTo(1)); + Assert.That(provider.GetIndexes("Settings").Any(index => index.Name == "IX_Value"), Is.True); + } + + [TestCase("Missing")] + [TestCase("Value")] + public void InvalidIdentityKeyLeavesOriginalSchemaAndData(string keyColumn) + { + Assert.Catch(() => provider.AddColumn("Settings", new Column("Id", DbType.Int32) { IsIdentity = true }, new PrimaryKeyConstraint("PK_Settings", keyColumn))); + Assert.That(provider.ColumnExists("Settings", "Id"), Is.False); + Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Settings"), Is.EqualTo(2)); + Assert.That(provider.GetTables(), Is.EqualTo(new[] { "Settings" })); + } + + [Test] + public void ExistingPrimaryKeyIsNotReplaced() + { + provider.AddPrimaryKey("PK_Old", "Settings", "Value"); + Assert.Catch(() => provider.AddColumn("Settings", new Column("Id", DbType.Int32) { IsIdentity = true }, new PrimaryKeyConstraint("PK_New", "Id"))); + Assert.That(provider.ColumnExists("Settings", "Id"), Is.False); + Assert.That(provider.GetTableConstraints("Settings").OfType().Single().Name, Is.EqualTo("PK_Old")); + } + + [TestCase(false)] + [TestCase(true)] + public void RemovesOnlyTheSelectedUniqueDefinition(bool named) + { + provider.ExecuteNonQuery($"CREATE TABLE Legacy (A TEXT, B TEXT, {(named ? "CONSTRAINT UQ_A " : "")}UNIQUE(A), UNIQUE(B), CHECK(length(A)>0)); INSERT INTO Legacy VALUES ('a','b');"); + var constraint = provider.GetTableConstraints("Legacy").OfType().Single(key => key.KeyColumns.SequenceEqual(new[] { "A" })); + provider.RemoveUniqueConstraint("Legacy", constraint); + Assert.That(provider.GetTableConstraints("Legacy").OfType().Single().KeyColumns, Is.EqualTo(new[] { "B" })); + Assert.That(provider.GetTableConstraints("Legacy").OfType().Count(), Is.EqualTo(1)); + provider.ExecuteNonQuery("INSERT INTO Legacy VALUES ('a','c')"); + Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Legacy"), Is.EqualTo(2)); + } + + [Test] + public void CompositeKeyKeepsOrderAndFailedBackfillRollsBack() + { + provider.AddColumn("Settings", new Column("Part", DbType.Int32) { DefaultValue = 1 }, new PrimaryKeyConstraint("PK_Settings", "Part", "Value")); + Assert.That(provider.GetTableConstraints("Settings").OfType().Single().KeyColumns, Is.EqualTo(new[] { "Part", "Value" })); + provider.RemovePrimaryKey("Settings"); + Assert.Catch(() => provider.AddColumn("Settings", new Column("Duplicate", DbType.String, 20) { DefaultValue = "same" }, new PrimaryKeyConstraint("PK_Duplicate", "Duplicate"))); + Assert.That(provider.ColumnExists("Settings", "Duplicate"), Is.False); + Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Settings"), Is.EqualTo(2)); + Assert.That(provider.GetTables(), Is.EqualTo(new[] { "Settings" })); + } + + [Test] + public void UnknownUniqueDefinitionDoesNotRemoveOtherConstraints() + { + Assert.Throws(() => provider.RemoveUniqueConstraint("Settings", new UniqueConstraint("UQ_Value", "Missing"))); + Assert.That(provider.GetTableConstraints("Settings").OfType().Single().Name, Is.EqualTo("UQ_Value")); + Assert.That(provider.ExecuteScalar("SELECT COUNT(*) FROM Settings"), Is.EqualTo(2)); + } +} diff --git a/src/Migrator/Framework/ITransformationProvider.cs b/src/Migrator/Framework/ITransformationProvider.cs index a1a43116..abcff336 100644 --- a/src/Migrator/Framework/ITransformationProvider.cs +++ b/src/Migrator/Framework/ITransformationProvider.cs @@ -1,731 +1,737 @@ -using System; -using System.Collections.Generic; -using System.Data; -using DotNetProjects.Migrator.Framework.Models; - -namespace DotNetProjects.Migrator.Framework; - -/// -/// The main interface to use in Migrations to make changes on a database schema. -/// -public interface ITransformationProvider : IDisposable -{ - /// Read named table constraints with ordered key columns; indexes are separate objects. - TableConstraint[] GetTableConstraints(string table); - - /// - /// Get this provider or a NoOp provider if you are not running in the context of 'provider'. - /// - ITransformationProvider this[string provider] { get; } - - string SchemaInfoTable { get; set; } - - int? CommandTimeout { get; set; } - - IDialect Dialect { get; } - - /// - /// The list of Migrations currently applied to the database. - /// - List AppliedMigrations { get; } - - bool IsMigrationApplied(long version, string scope); - - /// - /// Connection string to the database - /// - string ConnectionString { get; } - - /// - /// Logger used to log details of operations performed during migration - /// - ILogger Logger { get; set; } - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - void AddColumn(string table, string column, DbType type); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - void AddColumn(string table, string column, MigratorDbType type); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - void AddColumn(string table, string column, DbType type, int size); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - void AddColumn(string table, string column, MigratorDbType type, int size); - - /// - /// Add a column to an existing table with the default column size. - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The default value of the column if no value is given in a query - void AddColumn(string table, string column, DbType type, object defaultValue); - - /// - /// Add a column to an existing table with the default column size. - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The default value of the column if no value is given in a query - void AddColumn(string table, string column, MigratorDbType type, object defaultValue); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// An instance of a Column with the specified properties - void AddColumn(string table, Column column); - - /// - /// Add a foreign key constraint - /// - /// The name of the foreign key. e.g. FK_TABLE_REF - /// The table that the foreign key will be created in (e.g. Child) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary keys (e.g. Parent) - /// The columns that are the primary keys in the parent table (e.g. Id) - void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns); - - /// - /// Add a foreign key constraint - /// - /// The name of the foreign key. e.g. FK_TABLE_REF - /// The table that the foreign key will be created in (e.g. Child) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary keys (e.g. Parent) - /// The columns that are the primary keys in the parent table(e.g. Id) - /// Constraint parameters - void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint - /// - /// - /// The name of the foreign key. e.g. FK_TABLE_REF - /// The table that the foreign key will be created in (e.g. Child) - /// The column that is the foreign key (e.g. ParentId) - /// The table that holds the primary keys (e.g. Parent) - /// The column that is the primary key int the parent table (e.g. Id) - void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn); - - /// - /// Add a foreign key constraint - /// - /// The name of the foreign key. e.g. FK_CHILD_PARENT - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The column that is the foreign key (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table(e.g. Id) - /// Constraint parameters - void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The column that is the foreign key (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table(e.g. Id) - void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table (e.g. Id) - void GenerateForeignKey(string foreignTable, string[] foreignColumns, string primaryTable, string[] primaryColumns); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The columns that are the primary keys in the parent table (e.g. Id) - /// Constraint parameters - void GenerateForeignKey(string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table (e.g. Id) - /// Constraint parameters - void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The current expectations are that there is a column named the same as the foreignTable present in - /// the table. This is subject to change because I think it's not a good convention. - /// - /// The table that the foreign key will be created in (eg. ChildTable.ParentId) - /// The table that holds the primary key (eg. Table.PK_id) - void GenerateForeignKey(string childTable, string parentTable); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The current expectations are that there is a column named the same as the foreignTable present in - /// the table. This is subject to change because I think it's not a good convention. - /// - /// The table that the foreign key will be created in (eg. ChildTable.ParentId) - /// The table that holds the primary key (eg. Table.PK_id) - /// - void GenerateForeignKey(string foreignTable, string primaryTable, ForeignKeyConstraintType constraint); - - /// - /// Add a primary key to a table - /// - /// The name of the primary key to add. - /// The name of the table that will get the primary key. - /// The name of the column or columns that are in the primary key. - void AddPrimaryKey(string name, string table, params string[] columns); - - void AddPrimaryKeyNonClustered(string name, string table, params string[] columns); - /// - /// Add a constraint to a table - /// - /// The name of the constraint to add. - /// The name of the table that will get the constraint - /// The name of the column or columns that will get the constraint. - void AddUniqueConstraint(string name, string table, params string[] columns); - - /// - /// Add a constraint to a table - /// - /// The name of the constraint to add. - /// The name of the table that will get the constraint - /// The check constraint definition. - void AddCheckConstraint(string name, string table, string checkSql); - - void AddView(string name, string tableName, params IViewElement[] viewElements); - - void AddView(string name, string tableName, params IViewField[] fields); - - /// - /// Add a table - /// - /// The name of the table to add. - /// The columns that are part of the table. - void AddTable(string name, params IDbField[] columns); - - /// - /// Add a table - /// - /// The name of the table to add. - /// The name of the database engine to use. (MySQL) - /// The columns that are part of the table. - void AddTable(string name, string engine, params IDbField[] columns); - - /// - /// Start a transction - /// - void BeginTransaction(); - - /// - /// Change the definition of an existing column. - /// - /// The name of the table that will get the new column - /// An instance of a Column with the specified properties and the name of an existing column - void ChangeColumn(string table, Column column); - - void RemoveColumnDefaultValue(string table, string column); - - /// - /// Check to see if a column exists - /// - /// - /// - /// - bool ColumnExists(string table, string column); - - /// - /// Commit the running transction - /// - void Commit(); - - /// - /// Check to see if a constraint exists - /// - /// The name of the constraint - /// The table that the constraint lives on. - /// - bool ConstraintExists(string table, string name); - - /// - /// Copies data from source table to target table using INSERT INTO...SELECT..FROM - /// Be aware that the order of and matters. - /// - /// - /// - /// - /// - /// Sort source by these columns. must contain the . - void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null); - - /// - /// Check to see if a primary key constraint exists on the table - /// - /// The name of the primary key - /// The table that the constraint lives on. - /// - bool PrimaryKeyExists(string table, string name); - - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// timeout - /// Array of parameters of type object - /// - int ExecuteNonQuery(string sql, int timeout, object[] args); - - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// timeout - /// - int ExecuteNonQuery(string sql, int timeout); - - int ExecuteNonQuery(string sql); - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// - IDataReader ExecuteQuery(IDbCommand cmd, string sql); - - /// - /// Creates a DbCommand - /// - /// - IDbCommand CreateCommand(); - - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// A single value that is returned. - object ExecuteScalar(string sql); - - List ExecuteStringQuery(string sql, params object[] args); - - /// - /// Oracle: The retrieval of filter items is not supported in this migrator. If functional expressions are used: they seem to be stored as separate columns (with generated names). - /// - /// - /// - Index[] GetIndexes(string table); - - /// - /// Get the information about the columns in a table. - /// and can in some cases only be guessed. Do not rely on them. Same for - /// - /// The table name that you want the columns for. - /// - [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] - Column[] GetColumns(string table); - - /// - /// Reads the MaxLength of the Data in the Column - /// - /// - /// - /// - int GetColumnContentSize(string table, string columnName); - - /// - /// Gets information about a single column in a table. - /// and can in some cases only be guessed. Do not rely on them. Same for - /// - /// The table name that you want the columns for. - /// The column name for which you want information. - /// - [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] - Column GetColumnByName(string table, string column); - - /// - /// Get the names of all of the tables - /// - /// The names of all the tables. - string[] GetTables(); - - /// - /// Get all foreign keys by the given table name. - /// ATTENTION: For Postgre SQL the result will be lower case if the names were not quoted on table creation of on FK creation! For Oracle they are uppercase! - /// - /// - /// - ForeignKeyConstraint[] GetForeignKeyConstraints(string table); - - /// - /// Insert data into a table - /// - /// The table that will get the new data - /// The names of the columns - /// The values in the same order as the columns - /// - int Insert(string table, string[] columns, object[] values); - - /// - /// Insert data into a table (if it not exists) - /// - /// The table that will get the new data - /// The names of the columns - /// The values in the same order as the columns - /// - int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); - - /// - /// Delete data from a table - /// - /// The table that will have the data deleted - /// The names of the columns used in a where clause - /// The values in the same order as the columns - /// - int Delete(string table, string[] whereColumns = null, object[] whereValues = null); - - /// - /// Delete data from a table - /// - /// The table that will have the data deleted - /// The name of the column used in a where clause - /// The value for the where clause - /// - int Delete(string table, string whereColumn, string whereValue); - - /// - /// Truncate data from a table - /// - /// The table that will have the data deleted - /// - int TruncateTable(string table); - - /// - /// Marks a Migration version number as having been applied - /// - /// The version number of the migration that was applied - void MigrationApplied(long version, string scope); - - /// - /// Marks a Migration version number as having been rolled back from the database - /// - /// The version number of the migration that was removed - void MigrationUnApplied(long version, string scope); - - /// - /// Remove an existing column from a table - /// - /// The name of the table to remove the column from - /// The column to remove - void RemoveColumn(string table, string column); - - /// - /// Remove an existing foreign key constraint. - /// - /// The table that contains the foreign key. - /// The name of the foreign key to remove - void RemoveForeignKey(string table, string name); - - /// - /// Remove an existing constraint. - /// - /// The table that contains the foreign key. - /// The name of the constraint to remove - void RemoveConstraint(string table, string name); - - /// - /// Removes PK, FKs, Unique and CHECK constraints. - /// - /// - [Obsolete("Drop all constraints separately.")] - void RemoveAllConstraints(string table); - - /// - /// Remove an existing primary key. - /// - /// The table that contains the primary key. - void RemovePrimaryKey(string table); - - /// - /// Drops an existing table. - /// - /// The name of the table - void RemoveTable(string tableName); - - /// - /// Rename an existing table - /// - /// The old name of the table - /// The new name of the table - void RenameTable(string oldName, string newName); - - /// - /// Rename an existing table - /// - /// The name of the table - /// The old name of the column - /// The new name of the column - void RenameColumn(string tableName, string oldColumnName, string newColumnName); - - /// - /// Rollback the currently running transaction. - /// - void Rollback(); - - /// - /// Get values from a table - /// - /// The columns to select - /// The table to select from - /// The where clause to limit the selection - /// - IDataReader Select(IDbCommand cmd, string what, string from, string where); - - /// - /// Get values from a table - /// - /// - /// - /// - /// - /// - IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null); - - /// - /// Get values from a table - /// - /// - /// - /// - /// - /// - /// - /// - IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, - object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null); - - /// - /// Get values from a table - /// - /// The columns to select - /// The table to select from - /// - IDataReader Select(IDbCommand cmd, string what, string from); - - /// - /// Get a single value from a table - /// - /// The columns to select - /// The table to select from - /// - /// - object SelectScalar(string what, string from, string where); - - /// - /// Get a single value from a table - /// - /// The columns to select - /// The table to select from - /// - object SelectScalar(string what, string from); - - /// - /// Check if a table already exists - /// - /// The name of the table that you want to check on. - /// - bool TableExists(string tableName); - - /// - /// Check if a view already exists - /// - /// The name of the view that you want to check on. - /// - bool ViewExists(string viewName); - - /// - /// Update the values in a table - /// - /// The name of the table to update - /// The names of the columns. - /// The values for the columns in the same order as the names. - /// - int Update(string table, string[] columns, object[] values); - - /// - /// Update the values in a table - /// - /// The name of the table to update - /// The names of the columns. - /// The values for the columns in the same order as the names. - /// A where clause to limit the update - /// - int Update(string table, string[] columns, object[] values, string where); - - int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); - - /// - /// Updates the target table with data from the source table. Make sure to use primary key or unique columns in - /// - /// Source table name (unquoted). - /// Target table name (unquoted). - /// Pairs of columns that are used to copy data from column in source table to column in target table. - /// Pairs of columns that are used to match rows in source and target table. - void UpdateTargetFromSource(string tableNameSource, string tableNameTarget, ColumnPair[] copyColumnPairs, ColumnPair[] matchColumnPairs); - - /// - /// Get a command instance - /// - /// - IDbCommand GetCommand(); - - - void RemoveAllForeignKeys(string tableName, string columnName); - - bool IsThisProvider(string provider); - - /// - /// Quote a multiple column names, if required - /// - /// - /// - string[] QuoteColumnNamesIfRequired(params string[] columnNames); - - /// - /// Quaote column if required - /// - /// - /// - string QuoteColumnNameIfRequired(string name); - - /// - /// Quote table name if required - /// - /// - /// - string QuoteTableNameIfRequired(string name); - - /// - /// Encodes a guid value as a string, suitable for inclusion in sql statement - /// - /// - /// - string Encode(Guid guid); - - /// - /// Change the target database - /// - /// Name of the new target database - void SwitchDatabase(string databaseName); - - - /// - /// Get a list of databases available on the server - /// - List GetDatabases(); - - /// - /// Checks to see if a database with specific name exists on the server - /// - bool DatabaseExists(string name); - - /// - /// Create a new database on the server - /// - /// Name of the new database - void CreateDatabases(string databaseName); - - /// - /// Close all Connections to the Database. Sometimes needed for DropDatabase or redefine PrimaryKey. - /// - /// Name of the database to close all Connections - void KillDatabaseConnections(string databaseName); - - /// - /// Delete a database from the server - /// - /// Name of the database to delete - void DropDatabases(string databaseName); - - string AddIndex(string table, Index index); - - /// - /// Add a multi-column index to a table - /// - /// The name of the index to add. - /// The name of the table that will get the index. - /// The name of the column or columns that are in the index. - string AddIndex(string name, string table, params string[] columns); - - /// - /// Check to see if an index exists - /// - /// The name of the index - /// The table that the index lives on. - /// - bool IndexExists(string table, string name); - - /// - /// Remove an existing index - /// - /// The table that contains the index. - /// The name of the index to remove - void RemoveIndex(string table, string name); - - /// - /// Generate parameter name based on an index number - /// - /// The index number of the parameter - string GenerateParameterName(int index); - - /// - /// Remove all indexes of a table - /// - /// The table name - void RemoveAllIndexes(string table); - - string Concatenate(params string[] strings); - - IDbConnection Connection { get; } - - IEnumerable GetTables(string schema); - - IEnumerable GetColumns(string schema, string table); -} +using System; +using System.Collections.Generic; +using System.Data; +using DotNetProjects.Migrator.Framework.Models; + +namespace DotNetProjects.Migrator.Framework; + +/// +/// The main interface to use in Migrations to make changes on a database schema. +/// +public interface ITransformationProvider : IDisposable +{ + /// Read named table constraints with ordered key columns; indexes are separate objects. + TableConstraint[] GetTableConstraints(string table); + + /// + /// Get this provider or a NoOp provider if you are not running in the context of 'provider'. + /// + ITransformationProvider this[string provider] { get; } + + string SchemaInfoTable { get; set; } + + int? CommandTimeout { get; set; } + + IDialect Dialect { get; } + + /// + /// The list of Migrations currently applied to the database. + /// + List AppliedMigrations { get; } + + bool IsMigrationApplied(long version, string scope); + + /// + /// Connection string to the database + /// + string ConnectionString { get; } + + /// + /// Logger used to log details of operations performed during migration + /// + ILogger Logger { get; set; } + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + void AddColumn(string table, string column, DbType type); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + void AddColumn(string table, string column, MigratorDbType type); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The precision or size of the column + void AddColumn(string table, string column, DbType type, int size); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The precision or size of the column + void AddColumn(string table, string column, MigratorDbType type, int size); + + /// + /// Add a column to an existing table with the default column size. + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The default value of the column if no value is given in a query + void AddColumn(string table, string column, DbType type, object defaultValue); + + /// + /// Add a column to an existing table with the default column size. + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The default value of the column if no value is given in a query + void AddColumn(string table, string column, MigratorDbType type, object defaultValue); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// An instance of a Column with the specified properties + void AddColumn(string table, Column column); + + /// Add a column and an explicit primary key as one schema operation. SQLite rebuilds once; other providers use their normal DDL transaction semantics. + void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey); + + /// Remove the exact unique constraint returned by metadata, including unnamed SQLite constraints. + void RemoveUniqueConstraint(string table, UniqueConstraint constraint); + + /// + /// Add a foreign key constraint + /// + /// The name of the foreign key. e.g. FK_TABLE_REF + /// The table that the foreign key will be created in (e.g. Child) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary keys (e.g. Parent) + /// The columns that are the primary keys in the parent table (e.g. Id) + void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns); + + /// + /// Add a foreign key constraint + /// + /// The name of the foreign key. e.g. FK_TABLE_REF + /// The table that the foreign key will be created in (e.g. Child) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary keys (e.g. Parent) + /// The columns that are the primary keys in the parent table(e.g. Id) + /// Constraint parameters + void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint + /// + /// + /// The name of the foreign key. e.g. FK_TABLE_REF + /// The table that the foreign key will be created in (e.g. Child) + /// The column that is the foreign key (e.g. ParentId) + /// The table that holds the primary keys (e.g. Parent) + /// The column that is the primary key int the parent table (e.g. Id) + void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn); + + /// + /// Add a foreign key constraint + /// + /// The name of the foreign key. e.g. FK_CHILD_PARENT + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The column that is the foreign key (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table(e.g. Id) + /// Constraint parameters + void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The column that is the foreign key (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table(e.g. Id) + void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table (e.g. Id) + void GenerateForeignKey(string foreignTable, string[] foreignColumns, string primaryTable, string[] primaryColumns); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The columns that are the primary keys in the parent table (e.g. Id) + /// Constraint parameters + void GenerateForeignKey(string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table (e.g. Id) + /// Constraint parameters + void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The current expectations are that there is a column named the same as the foreignTable present in + /// the table. This is subject to change because I think it's not a good convention. + /// + /// The table that the foreign key will be created in (eg. ChildTable.ParentId) + /// The table that holds the primary key (eg. Table.PK_id) + void GenerateForeignKey(string childTable, string parentTable); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The current expectations are that there is a column named the same as the foreignTable present in + /// the table. This is subject to change because I think it's not a good convention. + /// + /// The table that the foreign key will be created in (eg. ChildTable.ParentId) + /// The table that holds the primary key (eg. Table.PK_id) + /// + void GenerateForeignKey(string foreignTable, string primaryTable, ForeignKeyConstraintType constraint); + + /// + /// Add a primary key to a table + /// + /// The name of the primary key to add. + /// The name of the table that will get the primary key. + /// The name of the column or columns that are in the primary key. + void AddPrimaryKey(string name, string table, params string[] columns); + + void AddPrimaryKeyNonClustered(string name, string table, params string[] columns); + /// + /// Add a constraint to a table + /// + /// The name of the constraint to add. + /// The name of the table that will get the constraint + /// The name of the column or columns that will get the constraint. + void AddUniqueConstraint(string name, string table, params string[] columns); + + /// + /// Add a constraint to a table + /// + /// The name of the constraint to add. + /// The name of the table that will get the constraint + /// The check constraint definition. + void AddCheckConstraint(string name, string table, string checkSql); + + void AddView(string name, string tableName, params IViewElement[] viewElements); + + void AddView(string name, string tableName, params IViewField[] fields); + + /// + /// Add a table + /// + /// The name of the table to add. + /// The columns that are part of the table. + void AddTable(string name, params IDbField[] columns); + + /// + /// Add a table + /// + /// The name of the table to add. + /// The name of the database engine to use. (MySQL) + /// The columns that are part of the table. + void AddTable(string name, string engine, params IDbField[] columns); + + /// + /// Start a transction + /// + void BeginTransaction(); + + /// + /// Change the definition of an existing column. + /// + /// The name of the table that will get the new column + /// An instance of a Column with the specified properties and the name of an existing column + void ChangeColumn(string table, Column column); + + void RemoveColumnDefaultValue(string table, string column); + + /// + /// Check to see if a column exists + /// + /// + /// + /// + bool ColumnExists(string table, string column); + + /// + /// Commit the running transction + /// + void Commit(); + + /// + /// Check to see if a constraint exists + /// + /// The name of the constraint + /// The table that the constraint lives on. + /// + bool ConstraintExists(string table, string name); + + /// + /// Copies data from source table to target table using INSERT INTO...SELECT..FROM + /// Be aware that the order of and matters. + /// + /// + /// + /// + /// + /// Sort source by these columns. must contain the . + void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null); + + /// + /// Check to see if a primary key constraint exists on the table + /// + /// The name of the primary key + /// The table that the constraint lives on. + /// + bool PrimaryKeyExists(string table, string name); + + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// timeout + /// Array of parameters of type object + /// + int ExecuteNonQuery(string sql, int timeout, object[] args); + + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// timeout + /// + int ExecuteNonQuery(string sql, int timeout); + + int ExecuteNonQuery(string sql); + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// + IDataReader ExecuteQuery(IDbCommand cmd, string sql); + + /// + /// Creates a DbCommand + /// + /// + IDbCommand CreateCommand(); + + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// A single value that is returned. + object ExecuteScalar(string sql); + + List ExecuteStringQuery(string sql, params object[] args); + + /// + /// Oracle: The retrieval of filter items is not supported in this migrator. If functional expressions are used: they seem to be stored as separate columns (with generated names). + /// + /// + /// + Index[] GetIndexes(string table); + + /// + /// Get the information about the columns in a table. + /// and can in some cases only be guessed. Do not rely on them. Same for + /// + /// The table name that you want the columns for. + /// + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] + Column[] GetColumns(string table); + + /// + /// Reads the MaxLength of the Data in the Column + /// + /// + /// + /// + int GetColumnContentSize(string table, string columnName); + + /// + /// Gets information about a single column in a table. + /// and can in some cases only be guessed. Do not rely on them. Same for + /// + /// The table name that you want the columns for. + /// The column name for which you want information. + /// + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] + Column GetColumnByName(string table, string column); + + /// + /// Get the names of all of the tables + /// + /// The names of all the tables. + string[] GetTables(); + + /// + /// Get all foreign keys by the given table name. + /// ATTENTION: For Postgre SQL the result will be lower case if the names were not quoted on table creation of on FK creation! For Oracle they are uppercase! + /// + /// + /// + ForeignKeyConstraint[] GetForeignKeyConstraints(string table); + + /// + /// Insert data into a table + /// + /// The table that will get the new data + /// The names of the columns + /// The values in the same order as the columns + /// + int Insert(string table, string[] columns, object[] values); + + /// + /// Insert data into a table (if it not exists) + /// + /// The table that will get the new data + /// The names of the columns + /// The values in the same order as the columns + /// + int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); + + /// + /// Delete data from a table + /// + /// The table that will have the data deleted + /// The names of the columns used in a where clause + /// The values in the same order as the columns + /// + int Delete(string table, string[] whereColumns = null, object[] whereValues = null); + + /// + /// Delete data from a table + /// + /// The table that will have the data deleted + /// The name of the column used in a where clause + /// The value for the where clause + /// + int Delete(string table, string whereColumn, string whereValue); + + /// + /// Truncate data from a table + /// + /// The table that will have the data deleted + /// + int TruncateTable(string table); + + /// + /// Marks a Migration version number as having been applied + /// + /// The version number of the migration that was applied + void MigrationApplied(long version, string scope); + + /// + /// Marks a Migration version number as having been rolled back from the database + /// + /// The version number of the migration that was removed + void MigrationUnApplied(long version, string scope); + + /// + /// Remove an existing column from a table + /// + /// The name of the table to remove the column from + /// The column to remove + void RemoveColumn(string table, string column); + + /// + /// Remove an existing foreign key constraint. + /// + /// The table that contains the foreign key. + /// The name of the foreign key to remove + void RemoveForeignKey(string table, string name); + + /// + /// Remove an existing constraint. + /// + /// The table that contains the foreign key. + /// The name of the constraint to remove + void RemoveConstraint(string table, string name); + + /// + /// Removes PK, FKs, Unique and CHECK constraints. + /// + /// + [Obsolete("Drop all constraints separately.")] + void RemoveAllConstraints(string table); + + /// + /// Remove an existing primary key. + /// + /// The table that contains the primary key. + void RemovePrimaryKey(string table); + + /// + /// Drops an existing table. + /// + /// The name of the table + void RemoveTable(string tableName); + + /// + /// Rename an existing table + /// + /// The old name of the table + /// The new name of the table + void RenameTable(string oldName, string newName); + + /// + /// Rename an existing table + /// + /// The name of the table + /// The old name of the column + /// The new name of the column + void RenameColumn(string tableName, string oldColumnName, string newColumnName); + + /// + /// Rollback the currently running transaction. + /// + void Rollback(); + + /// + /// Get values from a table + /// + /// The columns to select + /// The table to select from + /// The where clause to limit the selection + /// + IDataReader Select(IDbCommand cmd, string what, string from, string where); + + /// + /// Get values from a table + /// + /// + /// + /// + /// + /// + IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null); + + /// + /// Get values from a table + /// + /// + /// + /// + /// + /// + /// + /// + IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, + object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null); + + /// + /// Get values from a table + /// + /// The columns to select + /// The table to select from + /// + IDataReader Select(IDbCommand cmd, string what, string from); + + /// + /// Get a single value from a table + /// + /// The columns to select + /// The table to select from + /// + /// + object SelectScalar(string what, string from, string where); + + /// + /// Get a single value from a table + /// + /// The columns to select + /// The table to select from + /// + object SelectScalar(string what, string from); + + /// + /// Check if a table already exists + /// + /// The name of the table that you want to check on. + /// + bool TableExists(string tableName); + + /// + /// Check if a view already exists + /// + /// The name of the view that you want to check on. + /// + bool ViewExists(string viewName); + + /// + /// Update the values in a table + /// + /// The name of the table to update + /// The names of the columns. + /// The values for the columns in the same order as the names. + /// + int Update(string table, string[] columns, object[] values); + + /// + /// Update the values in a table + /// + /// The name of the table to update + /// The names of the columns. + /// The values for the columns in the same order as the names. + /// A where clause to limit the update + /// + int Update(string table, string[] columns, object[] values, string where); + + int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); + + /// + /// Updates the target table with data from the source table. Make sure to use primary key or unique columns in + /// + /// Source table name (unquoted). + /// Target table name (unquoted). + /// Pairs of columns that are used to copy data from column in source table to column in target table. + /// Pairs of columns that are used to match rows in source and target table. + void UpdateTargetFromSource(string tableNameSource, string tableNameTarget, ColumnPair[] copyColumnPairs, ColumnPair[] matchColumnPairs); + + /// + /// Get a command instance + /// + /// + IDbCommand GetCommand(); + + + void RemoveAllForeignKeys(string tableName, string columnName); + + bool IsThisProvider(string provider); + + /// + /// Quote a multiple column names, if required + /// + /// + /// + string[] QuoteColumnNamesIfRequired(params string[] columnNames); + + /// + /// Quaote column if required + /// + /// + /// + string QuoteColumnNameIfRequired(string name); + + /// + /// Quote table name if required + /// + /// + /// + string QuoteTableNameIfRequired(string name); + + /// + /// Encodes a guid value as a string, suitable for inclusion in sql statement + /// + /// + /// + string Encode(Guid guid); + + /// + /// Change the target database + /// + /// Name of the new target database + void SwitchDatabase(string databaseName); + + + /// + /// Get a list of databases available on the server + /// + List GetDatabases(); + + /// + /// Checks to see if a database with specific name exists on the server + /// + bool DatabaseExists(string name); + + /// + /// Create a new database on the server + /// + /// Name of the new database + void CreateDatabases(string databaseName); + + /// + /// Close all Connections to the Database. Sometimes needed for DropDatabase or redefine PrimaryKey. + /// + /// Name of the database to close all Connections + void KillDatabaseConnections(string databaseName); + + /// + /// Delete a database from the server + /// + /// Name of the database to delete + void DropDatabases(string databaseName); + + string AddIndex(string table, Index index); + + /// + /// Add a multi-column index to a table + /// + /// The name of the index to add. + /// The name of the table that will get the index. + /// The name of the column or columns that are in the index. + string AddIndex(string name, string table, params string[] columns); + + /// + /// Check to see if an index exists + /// + /// The name of the index + /// The table that the index lives on. + /// + bool IndexExists(string table, string name); + + /// + /// Remove an existing index + /// + /// The table that contains the index. + /// The name of the index to remove + void RemoveIndex(string table, string name); + + /// + /// Generate parameter name based on an index number + /// + /// The index number of the parameter + string GenerateParameterName(int index); + + /// + /// Remove all indexes of a table + /// + /// The table name + void RemoveAllIndexes(string table); + + string Concatenate(params string[] strings); + + IDbConnection Connection { get; } + + IEnumerable GetTables(string schema); + + IEnumerable GetColumns(string schema, string table); +} diff --git a/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs b/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs index 09cd7eed..5c3a6f3a 100644 --- a/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs @@ -1,341 +1,348 @@ -using DotNetProjects.Migrator.Framework; -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.Linq; -using Index = DotNetProjects.Migrator.Framework.Index; - -namespace DotNetProjects.Migrator.Providers.Impl.Mysql; - -/// -/// MySql transformation provider -/// -public class MySqlTransformationProvider : TransformationProvider -{ - public MySqlTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) - : base(dialect, connectionString, null, scope) // we ignore schemas for MySql (schema == database for MySql) - { - if (string.IsNullOrEmpty(providerName)) - { - providerName = "MySql.Data.MySqlClient"; - } - - var fac = DbProviderFactoriesHelper.GetFactory(providerName, "MySql.Data", "MySql.Data.MySqlClient.MySqlClientFactory"); - _connection = fac.CreateConnection(); //new MySqlConnection(_connectionString) {ConnectionString = _connectionString}; - _connection.ConnectionString = _connectionString; - _connection.Open(); - } - - public MySqlTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) - : base(dialect, connection, null, scope) - { - } - - public override void RemoveForeignKey(string table, string name) - { - if (ForeignKeyExists(table, name)) - { - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP FOREIGN KEY {1}", table, _dialect.QuoteIdentifier(name))); - } - } - - public override void RemoveAllIndexes(string table) - { - var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME, i.CONSTRAINT_TYPE - FROM information_schema.KEY_COLUMN_USAGE k - INNER JOIN information_schema.TABLE_CONSTRAINTS i - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME - WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND - (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), table); - - var l = new List>(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, qry)) - { - while (reader.Read()) - { - l.Add(new Tuple(reader.GetString(0), reader.GetString(1), reader.GetString(2))); - } - } - - foreach (var tuple in l) - { - if (tuple.Item3 == "FOREIGN KEY") - { - RemoveForeignKey(tuple.Item1, tuple.Item2); - } - else if (tuple.Item3 == "PRIMARY KEY") - { - try - { - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP PRIMARY KEY", table)); - } - catch (Exception) - { } - } - else if (tuple.Item3 == "UNIQUE") - { - RemoveIndex(tuple.Item1, tuple.Item2); - } - } - } - - public override void RemoveAllForeignKeys(string tableName, string columnName) - { - var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME - FROM information_schema.KEY_COLUMN_USAGE k - INNER JOIN information_schema.TABLE_CONSTRAINTS i - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME - WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND - (k.REFERENCED_TABLE_NAME='{1}' AND REFERENCED_COLUMN_NAME='{2}') OR (k.TABLE_NAME='{1}' AND COLUMN_NAME='{2}')", GetDatabase(), tableName, columnName); - - if (string.IsNullOrEmpty(columnName)) - { - qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME - FROM information_schema.KEY_COLUMN_USAGE k - INNER JOIN information_schema.TABLE_CONSTRAINTS i - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME - WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND - (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), tableName); - } - var l = new List>(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, qry)) - { - while (reader.Read()) - { - l.Add(new Tuple(reader.GetString(0), reader.GetString(1))); - } - } - - foreach (var tuple in l) - { - RemoveForeignKey(tuple.Item1, tuple.Item2); - } - } - - public override void RemoveConstraint(string table, string name) - { - var type = Convert.ToString(ExecuteScalar($"SELECT CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")); - var action = type switch - { - "PRIMARY KEY" => "DROP PRIMARY KEY", - "FOREIGN KEY" => "DROP FOREIGN KEY " + _dialect.QuoteIdentifier(name), - "UNIQUE" => "DROP INDEX " + _dialect.QuoteIdentifier(name), - "CHECK" => (_dialect is MariaDBDialect ? "DROP CONSTRAINT " : "DROP CHECK ") + _dialect.QuoteIdentifier(name), - _ => throw new MigrationException($"Constraint '{name}' does not exist") - }; - ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(table)} {action}"); - } - - public override bool ConstraintExists(string table, string name) - { - return Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")) > 0; - } - - public bool ForeignKeyExists(string table, string name) - { - if (!TableExists(table)) - { - return false; - } - - var sqlConstraint = string.Format(@"SELECT distinct i.CONSTRAINT_NAME - FROM information_schema.TABLE_CONSTRAINTS i - INNER JOIN information_schema.KEY_COLUMN_USAGE k - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME - WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY' - AND i.TABLE_SCHEMA = '{1}' - AND i.TABLE_NAME = '{0}';", table, GetDatabase()); - - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, sqlConstraint); - - while (reader.Read()) - { - if (reader["CONSTRAINT_NAME"].ToString().ToLower() == name.ToLower()) - { - return true; - } - } - - return false; - } - - public override Index[] GetIndexes(string table) - { - if (!TableExists(table)) return []; - var constraints = ExecuteStringQuery($"SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_TYPE='UNIQUE'").ToHashSet(StringComparer.OrdinalIgnoreCase); - var indexes = new Dictionary(); - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, $"SHOW INDEX FROM {_dialect.Quote(table)}"); - var columns = new Dictionary>(); - while (reader.Read()) - { - var name = Convert.ToString(reader["Key_name"]); - if (!indexes.ContainsKey(name)) - { - indexes[name] = new Index { Name = name, PrimaryKey = name == "PRIMARY", UniqueConstraint = constraints.Contains(name), Unique = Convert.ToInt32(reader["Non_unique"]) == 0 }; - columns[name] = new SortedDictionary(); - } - columns[name][Convert.ToInt32(reader["Seq_in_index"])] = Convert.ToString(reader["Column_name"]); - } - foreach (var item in indexes) item.Value.KeyColumns = columns[item.Key].Values.ToArray(); - return indexes.Values.ToArray(); - } - - public override bool PrimaryKeyExists(string table, string name) - { - return ConstraintExists(table, "PRIMARY"); - } - - public override Column[] GetColumns(string table) - { - var columns = new List(); - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, $"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH, COLUMN_KEY, COLUMN_TYPE, NUMERIC_PRECISION, NUMERIC_SCALE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' ORDER BY ORDINAL_POSITION"); - while (reader.Read()) - { - var type = reader.GetString(1) switch - { - "smallint" => DbType.Int16, "int" or "integer" or "mediumint" => DbType.Int32, - "bigint" => DbType.Int64, "tinyint" => reader.GetString(7).StartsWith("tinyint(1)", StringComparison.OrdinalIgnoreCase) ? DbType.Boolean : DbType.Byte, - "decimal" or "numeric" => DbType.Decimal, "double" => DbType.Double, "float" => DbType.Single, - "date" => DbType.Date, "datetime" or "timestamp" => DbType.DateTime, "time" => DbType.Time, - "tinyblob" or "mediumblob" or "blob" or "binary" or "varbinary" or "longblob" => DbType.Binary, _ => DbType.String - }; - var column = new Column(reader.GetString(0), type); - column.IsNullable = reader.GetString(2) == "YES"; - if (reader.GetString(4).Contains("auto_increment")) column.IsIdentity = true; - if (!reader.IsDBNull(3)) column.DefaultValue = ReadDefault(reader.GetString(3), type, reader.GetString(4)); - if (type == DbType.Decimal) - { - if (!reader.IsDBNull(8)) column.Precision = Convert.ToInt32(reader.GetValue(8)); - if (!reader.IsDBNull(9)) column.Scale = Convert.ToInt32(reader.GetValue(9)); - } - if (!reader.IsDBNull(5)) column.Size = (int)Math.Min(int.MaxValue, Convert.ToInt64(reader.GetValue(5))); - columns.Add(column); - } - return columns.ToArray(); - } - - // Non-string objects retain SQL expression semantics in Dialect.Default. - private sealed record DatabaseDefault(string Sql) - { - public override string ToString() => Sql; - } - - private object ReadDefault(string value, DbType type, string extra) - { - if (_dialect is MariaDBDialect) - { - if (value.Equals("NULL", StringComparison.OrdinalIgnoreCase)) return null; - if (value.StartsWith("'") && value.EndsWith("'")) - value = value[1..^1].Replace("''", "'").Replace("\\'", "'").Replace("\\\\", "\\"); - else if (type == DbType.String) return new DatabaseDefault(value); - } - if (extra.Contains("DEFAULT_GENERATED", StringComparison.OrdinalIgnoreCase) || - (type == DbType.DateTime && value.StartsWith("current_timestamp", StringComparison.OrdinalIgnoreCase))) - return new DatabaseDefault(value); - return type switch - { - DbType.Time => TimeOnly.Parse(value, CultureInfo.InvariantCulture), - DbType.Boolean => value != "0", - DbType.Byte => byte.Parse(value, CultureInfo.InvariantCulture), - DbType.Int16 => short.Parse(value, CultureInfo.InvariantCulture), - DbType.Int32 => int.Parse(value, CultureInfo.InvariantCulture), - DbType.Int64 => long.Parse(value, CultureInfo.InvariantCulture), - DbType.Decimal => decimal.Parse(value, CultureInfo.InvariantCulture), - DbType.Double => double.Parse(value, CultureInfo.InvariantCulture), - DbType.Single => float.Parse(value, CultureInfo.InvariantCulture), - DbType.Date or DbType.DateTime => DateTime.SpecifyKind(DateTime.Parse(value, CultureInfo.InvariantCulture), DateTimeKind.Utc), - _ => value - }; - } - - public override string[] GetTables() - { - var tables = new List(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, "SHOW TABLES")) - { - while (reader.Read()) - { - tables.Add((string)reader[0]); - } - } - - return tables.ToArray(); - } - - public override void ChangeColumn(string table, string sqlColumn) - { - ExecuteNonQuery(string.Format("ALTER TABLE {0} MODIFY {1}", table, sqlColumn)); - } - - public override void AddTable(string name, params IDbField[] columns) - { - AddTable(name, "INNODB", columns); - } - - public override void AddTable(string name, string engine, string columns) - { - var sqlCreate = string.Format("CREATE TABLE {0} ({1}) ENGINE = {2}", name, columns, engine); - ExecuteNonQuery(sqlCreate); - } - - public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - if (!ColumnExists(tableName, oldColumnName) || ColumnExists(tableName, newColumnName)) - throw new MigrationException("Source column must exist and destination column must not exist."); - ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(tableName)} RENAME COLUMN {_dialect.Quote(oldColumnName)} TO {_dialect.Quote(newColumnName)}"); - } - - public string GetDatabase() - { - return ExecuteScalar("SELECT DATABASE()") as string; - } - - public override void RemoveIndex(string table, string name) - { - if (IndexExists(table, name)) - { - ExecuteNonQuery(string.Format("DROP INDEX {1} ON {0}", table, _dialect.QuoteIdentifier(name))); - } - } - - public override List GetDatabases() - { - return ExecuteStringQuery("SHOW DATABASES"); - } - - public override bool IndexExists(string table, string name) - { - return GetIndexes(table).Any(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - } - - public override string Concatenate(params string[] strings) - { - return "CONCAT(" + string.Join(", ", strings) + ")"; - } - public override bool TableExists(string table) => - Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_TYPE='BASE TABLE' AND TABLE_NAME='{table.Replace("'", "''")}'")) > 0; - - public override bool ViewExists(string view) => - Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.VIEWS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{view.Replace("'", "''")}'")) > 0; - - public override string AddIndex(string table, Index index) - { - if (index.KeyColumns.Length == 0) throw new ArgumentException("An index needs key columns.", nameof(index)); - if (index.IncludeColumns.Length != 0 || index.FilterItems.Count != 0 || index.Clustered) - throw new NotSupportedException("MySQL and MariaDB do not support included columns, filtered indexes or explicit clustered indexes."); - var name = index.Name ?? $"IX_{table}_{string.Join("_", index.KeyColumns)}"; - ExecuteNonQuery($"CREATE {(index.Unique ? "UNIQUE " : "")}INDEX {_dialect.QuoteIdentifier(name)} ON {_dialect.Quote(table)} ({string.Join(", ", index.KeyColumns.Select(_dialect.Quote))})"); - return name; - } - - protected override string GetPrimaryKeyConstraintName(string table) => - ConstraintExists(table, "PRIMARY") ? "PRIMARY" : null; - -} +using DotNetProjects.Migrator.Framework; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace DotNetProjects.Migrator.Providers.Impl.Mysql; + +/// +/// MySql transformation provider +/// +public class MySqlTransformationProvider : TransformationProvider +{ + public override void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) + { + var definition = PrepareColumnWithPrimaryKey(table, column, primaryKey); + // AUTO_INCREMENT must be indexed in the same ALTER statement, including on MariaDB. + ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ADD COLUMN {_dialect.GetAndMapColumnProperties(definition).ColumnSql}, ADD {_dialect.GetTableConstraintSql(primaryKey)}"); + } + + public MySqlTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) + : base(dialect, connectionString, null, scope) // we ignore schemas for MySql (schema == database for MySql) + { + if (string.IsNullOrEmpty(providerName)) + { + providerName = "MySql.Data.MySqlClient"; + } + + var fac = DbProviderFactoriesHelper.GetFactory(providerName, "MySql.Data", "MySql.Data.MySqlClient.MySqlClientFactory"); + _connection = fac.CreateConnection(); //new MySqlConnection(_connectionString) {ConnectionString = _connectionString}; + _connection.ConnectionString = _connectionString; + _connection.Open(); + } + + public MySqlTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) + : base(dialect, connection, null, scope) + { + } + + public override void RemoveForeignKey(string table, string name) + { + if (ForeignKeyExists(table, name)) + { + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP FOREIGN KEY {1}", table, _dialect.QuoteIdentifier(name))); + } + } + + public override void RemoveAllIndexes(string table) + { + var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME, i.CONSTRAINT_TYPE + FROM information_schema.KEY_COLUMN_USAGE k + INNER JOIN information_schema.TABLE_CONSTRAINTS i + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME + WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND + (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), table); + + var l = new List>(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, qry)) + { + while (reader.Read()) + { + l.Add(new Tuple(reader.GetString(0), reader.GetString(1), reader.GetString(2))); + } + } + + foreach (var tuple in l) + { + if (tuple.Item3 == "FOREIGN KEY") + { + RemoveForeignKey(tuple.Item1, tuple.Item2); + } + else if (tuple.Item3 == "PRIMARY KEY") + { + try + { + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP PRIMARY KEY", table)); + } + catch (Exception) + { } + } + else if (tuple.Item3 == "UNIQUE") + { + RemoveIndex(tuple.Item1, tuple.Item2); + } + } + } + + public override void RemoveAllForeignKeys(string tableName, string columnName) + { + var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE k + INNER JOIN information_schema.TABLE_CONSTRAINTS i + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME + WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND + (k.REFERENCED_TABLE_NAME='{1}' AND REFERENCED_COLUMN_NAME='{2}') OR (k.TABLE_NAME='{1}' AND COLUMN_NAME='{2}')", GetDatabase(), tableName, columnName); + + if (string.IsNullOrEmpty(columnName)) + { + qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE k + INNER JOIN information_schema.TABLE_CONSTRAINTS i + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME + WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND + (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), tableName); + } + var l = new List>(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, qry)) + { + while (reader.Read()) + { + l.Add(new Tuple(reader.GetString(0), reader.GetString(1))); + } + } + + foreach (var tuple in l) + { + RemoveForeignKey(tuple.Item1, tuple.Item2); + } + } + + public override void RemoveConstraint(string table, string name) + { + var type = Convert.ToString(ExecuteScalar($"SELECT CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")); + var action = type switch + { + "PRIMARY KEY" => "DROP PRIMARY KEY", + "FOREIGN KEY" => "DROP FOREIGN KEY " + _dialect.QuoteIdentifier(name), + "UNIQUE" => "DROP INDEX " + _dialect.QuoteIdentifier(name), + "CHECK" => (_dialect is MariaDBDialect ? "DROP CONSTRAINT " : "DROP CHECK ") + _dialect.QuoteIdentifier(name), + _ => throw new MigrationException($"Constraint '{name}' does not exist") + }; + ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(table)} {action}"); + } + + public override bool ConstraintExists(string table, string name) + { + return Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")) > 0; + } + + public bool ForeignKeyExists(string table, string name) + { + if (!TableExists(table)) + { + return false; + } + + var sqlConstraint = string.Format(@"SELECT distinct i.CONSTRAINT_NAME + FROM information_schema.TABLE_CONSTRAINTS i + INNER JOIN information_schema.KEY_COLUMN_USAGE k + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME + WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY' + AND i.TABLE_SCHEMA = '{1}' + AND i.TABLE_NAME = '{0}';", table, GetDatabase()); + + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, sqlConstraint); + + while (reader.Read()) + { + if (reader["CONSTRAINT_NAME"].ToString().ToLower() == name.ToLower()) + { + return true; + } + } + + return false; + } + + public override Index[] GetIndexes(string table) + { + if (!TableExists(table)) return []; + var constraints = ExecuteStringQuery($"SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_TYPE='UNIQUE'").ToHashSet(StringComparer.OrdinalIgnoreCase); + var indexes = new Dictionary(); + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, $"SHOW INDEX FROM {_dialect.Quote(table)}"); + var columns = new Dictionary>(); + while (reader.Read()) + { + var name = Convert.ToString(reader["Key_name"]); + if (!indexes.ContainsKey(name)) + { + indexes[name] = new Index { Name = name, PrimaryKey = name == "PRIMARY", UniqueConstraint = constraints.Contains(name), Unique = Convert.ToInt32(reader["Non_unique"]) == 0 }; + columns[name] = new SortedDictionary(); + } + columns[name][Convert.ToInt32(reader["Seq_in_index"])] = Convert.ToString(reader["Column_name"]); + } + foreach (var item in indexes) item.Value.KeyColumns = columns[item.Key].Values.ToArray(); + return indexes.Values.ToArray(); + } + + public override bool PrimaryKeyExists(string table, string name) + { + return ConstraintExists(table, "PRIMARY"); + } + + public override Column[] GetColumns(string table) + { + var columns = new List(); + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, $"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH, COLUMN_KEY, COLUMN_TYPE, NUMERIC_PRECISION, NUMERIC_SCALE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' ORDER BY ORDINAL_POSITION"); + while (reader.Read()) + { + var type = reader.GetString(1) switch + { + "smallint" => DbType.Int16, "int" or "integer" or "mediumint" => DbType.Int32, + "bigint" => DbType.Int64, "tinyint" => reader.GetString(7).StartsWith("tinyint(1)", StringComparison.OrdinalIgnoreCase) ? DbType.Boolean : DbType.Byte, + "decimal" or "numeric" => DbType.Decimal, "double" => DbType.Double, "float" => DbType.Single, + "date" => DbType.Date, "datetime" or "timestamp" => DbType.DateTime, "time" => DbType.Time, + "tinyblob" or "mediumblob" or "blob" or "binary" or "varbinary" or "longblob" => DbType.Binary, _ => DbType.String + }; + var column = new Column(reader.GetString(0), type); + column.IsNullable = reader.GetString(2) == "YES"; + if (reader.GetString(4).Contains("auto_increment")) column.IsIdentity = true; + if (!reader.IsDBNull(3)) column.DefaultValue = ReadDefault(reader.GetString(3), type, reader.GetString(4)); + if (type == DbType.Decimal) + { + if (!reader.IsDBNull(8)) column.Precision = Convert.ToInt32(reader.GetValue(8)); + if (!reader.IsDBNull(9)) column.Scale = Convert.ToInt32(reader.GetValue(9)); + } + if (!reader.IsDBNull(5)) column.Size = (int)Math.Min(int.MaxValue, Convert.ToInt64(reader.GetValue(5))); + columns.Add(column); + } + return columns.ToArray(); + } + + // Non-string objects retain SQL expression semantics in Dialect.Default. + private sealed record DatabaseDefault(string Sql) + { + public override string ToString() => Sql; + } + + private object ReadDefault(string value, DbType type, string extra) + { + if (_dialect is MariaDBDialect) + { + if (value.Equals("NULL", StringComparison.OrdinalIgnoreCase)) return null; + if (value.StartsWith("'") && value.EndsWith("'")) + value = value[1..^1].Replace("''", "'").Replace("\\'", "'").Replace("\\\\", "\\"); + else if (type == DbType.String) return new DatabaseDefault(value); + } + if (extra.Contains("DEFAULT_GENERATED", StringComparison.OrdinalIgnoreCase) || + (type == DbType.DateTime && value.StartsWith("current_timestamp", StringComparison.OrdinalIgnoreCase))) + return new DatabaseDefault(value); + return type switch + { + DbType.Time => TimeOnly.Parse(value, CultureInfo.InvariantCulture), + DbType.Boolean => value != "0", + DbType.Byte => byte.Parse(value, CultureInfo.InvariantCulture), + DbType.Int16 => short.Parse(value, CultureInfo.InvariantCulture), + DbType.Int32 => int.Parse(value, CultureInfo.InvariantCulture), + DbType.Int64 => long.Parse(value, CultureInfo.InvariantCulture), + DbType.Decimal => decimal.Parse(value, CultureInfo.InvariantCulture), + DbType.Double => double.Parse(value, CultureInfo.InvariantCulture), + DbType.Single => float.Parse(value, CultureInfo.InvariantCulture), + DbType.Date or DbType.DateTime => DateTime.SpecifyKind(DateTime.Parse(value, CultureInfo.InvariantCulture), DateTimeKind.Utc), + _ => value + }; + } + + public override string[] GetTables() + { + var tables = new List(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, "SHOW TABLES")) + { + while (reader.Read()) + { + tables.Add((string)reader[0]); + } + } + + return tables.ToArray(); + } + + public override void ChangeColumn(string table, string sqlColumn) + { + ExecuteNonQuery(string.Format("ALTER TABLE {0} MODIFY {1}", table, sqlColumn)); + } + + public override void AddTable(string name, params IDbField[] columns) + { + AddTable(name, "INNODB", columns); + } + + public override void AddTable(string name, string engine, string columns) + { + var sqlCreate = string.Format("CREATE TABLE {0} ({1}) ENGINE = {2}", name, columns, engine); + ExecuteNonQuery(sqlCreate); + } + + public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + if (!ColumnExists(tableName, oldColumnName) || ColumnExists(tableName, newColumnName)) + throw new MigrationException("Source column must exist and destination column must not exist."); + ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(tableName)} RENAME COLUMN {_dialect.Quote(oldColumnName)} TO {_dialect.Quote(newColumnName)}"); + } + + public string GetDatabase() + { + return ExecuteScalar("SELECT DATABASE()") as string; + } + + public override void RemoveIndex(string table, string name) + { + if (IndexExists(table, name)) + { + ExecuteNonQuery(string.Format("DROP INDEX {1} ON {0}", table, _dialect.QuoteIdentifier(name))); + } + } + + public override List GetDatabases() + { + return ExecuteStringQuery("SHOW DATABASES"); + } + + public override bool IndexExists(string table, string name) + { + return GetIndexes(table).Any(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + + public override string Concatenate(params string[] strings) + { + return "CONCAT(" + string.Join(", ", strings) + ")"; + } + public override bool TableExists(string table) => + Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_TYPE='BASE TABLE' AND TABLE_NAME='{table.Replace("'", "''")}'")) > 0; + + public override bool ViewExists(string view) => + Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.VIEWS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{view.Replace("'", "''")}'")) > 0; + + public override string AddIndex(string table, Index index) + { + if (index.KeyColumns.Length == 0) throw new ArgumentException("An index needs key columns.", nameof(index)); + if (index.IncludeColumns.Length != 0 || index.FilterItems.Count != 0 || index.Clustered) + throw new NotSupportedException("MySQL and MariaDB do not support included columns, filtered indexes or explicit clustered indexes."); + var name = index.Name ?? $"IX_{table}_{string.Join("_", index.KeyColumns)}"; + ExecuteNonQuery($"CREATE {(index.Unique ? "UNIQUE " : "")}INDEX {_dialect.QuoteIdentifier(name)} ON {_dialect.Quote(table)} ({string.Join(", ", index.KeyColumns.Select(_dialect.Quote))})"); + return name; + } + + protected override string GetPrimaryKeyConstraintName(string table) => + ConstraintExists(table, "PRIMARY") ? "PRIMARY" : null; + +} diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index c1b1f4fd..9ed9b196 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1,398 +1,398 @@ -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Providers.Impl.SQLite.Models; -using System; -using System.Collections.Generic; -using System.Data; -using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; -using Index = DotNetProjects.Migrator.Framework.Index; -using DotNetProjects.Migrator.Framework.Extensions; -using DotNetProjects.Migrator.Providers.Models.Indexes; -using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; -using DotNetProjects.Migrator.Framework.Models; - -namespace DotNetProjects.Migrator.Providers.Impl.SQLite; - -/// -/// Summary description for SQLiteTransformationProvider. -/// -public partial class SQLiteTransformationProvider : TransformationProvider -{ - private const string IntermediateTableSuffix = "Temp"; - - public SQLiteTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) - : base(dialect, connectionString, null, scope) - { - CreateConnection(providerName); - } - - public SQLiteTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) - : base(dialect, connection, null, scope) - { - } - - protected virtual void CreateConnection(string providerName) - { - if (string.IsNullOrEmpty(providerName)) - { - providerName = "System.Data.SQLite"; - } - - var fac = DbProviderFactoriesHelper.GetFactory(providerName, "System.Data.SQLite", "System.Data.SQLite.SQLiteFactory"); - _connection = fac.CreateConnection(); // new SQLiteConnection(_connectionString); - _connection.ConnectionString = _connectionString; - _connection.Open(); - } - - public override void AddForeignKey( - string name, - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns, - ForeignKeyConstraintType constraint) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new Exception("The foreign key name is mandatory"); - } - - var sqliteTableInfo = GetSQLiteTableInfo(childTable); - - // Get all unique constraint names if available - var uniqueConstraintNames = sqliteTableInfo.Uniques.Select(x => x.Name).ToList(); - - // Get all FK constraint names if available - var foreignKeyNames = sqliteTableInfo.ForeignKeys.Select(x => x.Name).ToList(); - - var names = uniqueConstraintNames.Concat(foreignKeyNames) - .Distinct() - .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToList(); - - if (names.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - throw new Exception($"Constraint name {name} already exists"); - } - - var foreignKey = new ForeignKeyConstraint - { - ChildColumns = childColumns, - ChildTable = childTable, - Name = name, - ParentColumns = parentColumns, - ParentTable = parentTable, - OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(constraint), - }; - - sqliteTableInfo.ForeignKeys - .Add(foreignKey); - - RecreateTable(sqliteTableInfo); - } - - public override void AddForeignKey(string table, ForeignKeyConstraint fk) - { - if (fk == null) throw new ArgumentNullException(nameof(fk)); - SQLiteTableSql.ValidateMatch(fk.Match); - base.AddForeignKey(table, fk); - } - - public override void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, - ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) - { - var info = GetSQLiteTableInfo(childTable) ?? throw new MigrationException("Child table does not exist."); - if (string.IsNullOrWhiteSpace(name) || info.ForeignKeys.Select(f => f.Name).Concat(info.Uniques.Select(u => u.Name)) - .Any(existing => string.Equals(existing, name, StringComparison.OrdinalIgnoreCase))) - throw new MigrationException("A unique foreign key name is required."); - info.ForeignKeys.Add(new ForeignKeyConstraint(name, parentTable, (string[])parentColumns.Clone(), childTable, (string[])childColumns.Clone()) - { - OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(onDelete), - OnUpdate = new ForeignKeyConstraintMapper().SqlForConstraint(onUpdate) - }); - RecreateTable(info); - } - - public string[] GetColumnDefs(string table, out string compositeDefSql) - { - return ParseSqlColumnDefs(GetSqlCreateTableScript(table), out compositeDefSql); - } - - /// - /// Gets the SQL CREATE TABLE script. Case-insensitive - /// - /// - /// - public string GetSqlCreateTableScript(string table) - { - string sqlCreateTableScript = null; - - using var cmd = CreateCommand(); - var parameter = cmd.CreateParameter(); parameter.ParameterName = "@name"; parameter.Value = table; cmd.Parameters.Add(parameter); - using var reader = ExecuteQuery(cmd, "SELECT sql FROM sqlite_master WHERE type='table' AND name=@name COLLATE NOCASE"); - if (reader.Read()) sqlCreateTableScript = reader.IsDBNull(0) ? null : reader.GetString(0); - - return sqlCreateTableScript; - } - - public override TableConstraint[] GetTableConstraints(string table) - { - var script = GetSqlCreateTableScript(table); - if (string.IsNullOrWhiteSpace(script)) throw new MigrationException("Table does not exist: " + table); - var constraints = SQLiteConstraintParser.Parse(script); - foreach (var foreignKey in constraints.OfType()) foreignKey.ChildTable = table; - return constraints; - } - - public override ForeignKeyConstraint[] GetForeignKeyConstraints(string tableName) - { - List foreignKeyConstraints = []; - - var pragmaForeignKeyListItems = GetForeignKeyListItems(tableName); - var groups = pragmaForeignKeyListItems.GroupBy(x => x.Id); - - foreach (var group in groups) - { - var foreignKeyConstraint = new ForeignKeyConstraint - { - Id = group.First().Id, - // SQLite does not support FK names. - ChildColumns = group.OrderBy(x => x.Seq).Select(x => x.From).ToArray(), - ChildTable = tableName, - Match = group.First().Match, - Name = null, - OnDelete = group.First().OnDelete, - OnUpdate = group.First().OnUpdate, - ParentColumns = group.OrderBy(x => x.Seq).Select(x => x.To).ToArray(), - ParentTable = group.First().Table, - }; - - foreignKeyConstraints.Add(foreignKeyConstraint); - } - - if (foreignKeyConstraints.Count == 0) - { - return []; - } - - var declared = GetTableConstraints(tableName).OfType().ToList(); - foreach (var foreignKey in foreignKeyConstraints) - { - var definition = declared.FirstOrDefault(candidate => - candidate.ChildColumns.SequenceEqual(foreignKey.ChildColumns, StringComparer.OrdinalIgnoreCase) && - candidate.ParentTable.Equals(foreignKey.ParentTable, StringComparison.OrdinalIgnoreCase) && - (candidate.ParentColumns.Length == 0 || candidate.ParentColumns.SequenceEqual(foreignKey.ParentColumns, StringComparer.OrdinalIgnoreCase))); - if (definition == null) throw new MigrationException("Cannot match a SQLite foreign key to its declaration."); - foreignKey.Name = definition.Name; - // PRAGMA reports NONE even for an explicitly declared MATCH FULL. - // Retain the declaration so a rebuild cannot silently discard it. - foreignKey.Match = definition.Match ?? foreignKey.Match; - declared.Remove(definition); - } - - return foreignKeyConstraints.ToArray(); - } - - public override void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) - { - if (!TableExists(tableSourceNotQuoted)) - { - throw new Exception($"Table '{tableSourceNotQuoted}' given in '{nameof(tableSourceNotQuoted)}' does not exist"); - } - - if (!TableExists(tableTargetNotQuoted)) - { - throw new Exception($"Table '{tableTargetNotQuoted}' given in '{nameof(tableTargetNotQuoted)}' does not exist"); - } - - if (fromSourceToTargetColumnPairs.Length == 0) - { - throw new Exception($"{nameof(fromSourceToTargetColumnPairs)} is empty."); - } - - if (fromSourceToTargetColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) - { - throw new Exception($"One of the strings in {nameof(fromSourceToTargetColumnPairs)} is null or empty"); - } - - if (conditionColumnPairs.Length == 0) - { - throw new Exception($"{nameof(conditionColumnPairs)} is empty."); - } - - if (conditionColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) - { - throw new Exception($"One of the strings in {nameof(conditionColumnPairs)} is null or empty"); - } - - var tableNameSource = QuoteTableNameIfRequired(tableSourceNotQuoted); - var tableNameTarget = QuoteTableNameIfRequired(tableTargetNotQuoted); - - var assignStrings = fromSourceToTargetColumnPairs.Select(x => $"{QuoteColumnNameIfRequired(x.ColumnNameTarget)} = {tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)}").ToList(); - - var conditionStrings = conditionColumnPairs.Select(x => $"{tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)} = {tableNameTarget}.{QuoteColumnNameIfRequired(x.ColumnNameTarget)}"); - - var assignStringsJoined = string.Join(", ", assignStrings); - var conditionStringsJoined = string.Join(" AND ", conditionStrings); - - var sql = $"UPDATE {tableNameTarget} SET {assignStringsJoined} FROM {tableNameSource} WHERE {conditionStringsJoined}"; - ExecuteNonQuery(sql); - } - - private List GetForeignKeyListItems(string tableNameNotQuoted) - { - List pragmaForeignKeyListItems = []; - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA foreign_key_list('{QuoteTableNameIfRequired(tableNameNotQuoted)}')")) - { - while (reader.Read()) - { - var pragmaForeignKeyListItem = new PragmaForeignKeyListItem - { - Id = reader.GetInt32(reader.GetOrdinal("id")), - Seq = reader.GetInt32(reader.GetOrdinal("seq")), - Table = reader.GetString(reader.GetOrdinal("table")), - From = reader.GetString(reader.GetOrdinal("from")), - To = reader.GetString(reader.GetOrdinal("to")), - OnUpdate = reader.GetString(reader.GetOrdinal("on_update")), - OnDelete = reader.GetString(reader.GetOrdinal("on_delete")), - Match = reader.GetString(reader.GetOrdinal("match")), - }; - - pragmaForeignKeyListItems.Add(pragmaForeignKeyListItem); - } - } - - return pragmaForeignKeyListItems; - } - - public string[] ParseSqlColumnDefs(string sqldef, out string compositeDefSql) - { - if (string.IsNullOrEmpty(sqldef)) - { - compositeDefSql = null; - - return null; - } - - sqldef = sqldef.Replace(Environment.NewLine, " "); - var start = sqldef.IndexOf("("); - - // Code to handle composite primary keys /mol - var compositeDefIndex = sqldef.IndexOf("PRIMARY KEY ("); // Not ideal to search for a string like this but I'm lazy - - if (compositeDefIndex > -1) - { - compositeDefSql = sqldef.Substring(compositeDefIndex, sqldef.LastIndexOf(")") - compositeDefIndex); - sqldef = sqldef.Substring(0, compositeDefIndex).TrimEnd(',', ' ') + ")"; - } - else - { - compositeDefSql = null; - } - - var end = sqldef.LastIndexOf(")"); // Changed from 'IndexOf' to 'LastIndexOf' to handle foreign key definitions /mol - - sqldef = sqldef.Substring(0, end); - sqldef = sqldef.Substring(start + 1); - - var cols = sqldef.Split([',']); - - for (var i = 0; i < cols.Length; i++) - { - cols[i] = cols[i].Trim(); - } - - return cols; - } - - /// - /// Turn something like 'columnName INTEGER NOT NULL' into just 'columnName' - /// - public string[] ParseSqlForColumnNames(string sqldef, out string compositeDefSql) - { - var parts = ParseSqlColumnDefs(sqldef, out compositeDefSql); - - return ParseSqlForColumnNames(parts); - } - - public string[] ParseSqlForColumnNames(string[] parts) - { - if (null == parts) - { - return null; - } - - for (var i = 0; i < parts.Length; i++) - { - parts[i] = ExtractNameFromColumnDef(parts[i]); - } - - return parts; - } - - /// - /// Name is the first value before the space. - /// - /// - /// - public static string ExtractNameFromColumnDef(string columnDef) - { - var idx = columnDef.IndexOf(" "); - - if (idx > 0) - { - return columnDef.Substring(0, idx); - } - return null; - } - - public DbType ExtractTypeFromColumnDef(string columnDef) - { - var idx = columnDef.IndexOf(" ") + 1; - - if (idx > 0) - { - var idy = columnDef.IndexOf(" ", idx) - idx; - - if (idy > 0) - { - return _dialect.GetDbType(columnDef.Substring(idx, idy)); - } - else - { - return _dialect.GetDbType(columnDef.Substring(idx)); - } - } - else - { - throw new Exception("Error extracting type from column definition: '" + columnDef + "'"); - } - } - - public override void RemoveForeignKey(string table, string name) - { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - var sqliteTableInfo = GetSQLiteTableInfo(table); - if (!sqliteTableInfo.ForeignKeys.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException($"Foreign key '{name}' does not exist."); - } - - sqliteTableInfo.ForeignKeys.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - - RecreateTable(sqliteTableInfo); - } - +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Impl.SQLite.Models; +using System; +using System.Collections.Generic; +using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; +using Index = DotNetProjects.Migrator.Framework.Index; +using DotNetProjects.Migrator.Framework.Extensions; +using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; +using DotNetProjects.Migrator.Framework.Models; + +namespace DotNetProjects.Migrator.Providers.Impl.SQLite; + +/// +/// Summary description for SQLiteTransformationProvider. +/// +public partial class SQLiteTransformationProvider : TransformationProvider +{ + private const string IntermediateTableSuffix = "Temp"; + + public SQLiteTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) + : base(dialect, connectionString, null, scope) + { + CreateConnection(providerName); + } + + public SQLiteTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) + : base(dialect, connection, null, scope) + { + } + + protected virtual void CreateConnection(string providerName) + { + if (string.IsNullOrEmpty(providerName)) + { + providerName = "System.Data.SQLite"; + } + + var fac = DbProviderFactoriesHelper.GetFactory(providerName, "System.Data.SQLite", "System.Data.SQLite.SQLiteFactory"); + _connection = fac.CreateConnection(); // new SQLiteConnection(_connectionString); + _connection.ConnectionString = _connectionString; + _connection.Open(); + } + + public override void AddForeignKey( + string name, + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns, + ForeignKeyConstraintType constraint) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new Exception("The foreign key name is mandatory"); + } + + var sqliteTableInfo = GetSQLiteTableInfo(childTable); + + // Get all unique constraint names if available + var uniqueConstraintNames = sqliteTableInfo.Uniques.Select(x => x.Name).ToList(); + + // Get all FK constraint names if available + var foreignKeyNames = sqliteTableInfo.ForeignKeys.Select(x => x.Name).ToList(); + + var names = uniqueConstraintNames.Concat(foreignKeyNames) + .Distinct() + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToList(); + + if (names.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new Exception($"Constraint name {name} already exists"); + } + + var foreignKey = new ForeignKeyConstraint + { + ChildColumns = childColumns, + ChildTable = childTable, + Name = name, + ParentColumns = parentColumns, + ParentTable = parentTable, + OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(constraint), + }; + + sqliteTableInfo.ForeignKeys + .Add(foreignKey); + + RecreateTable(sqliteTableInfo); + } + + public override void AddForeignKey(string table, ForeignKeyConstraint fk) + { + if (fk == null) throw new ArgumentNullException(nameof(fk)); + SQLiteTableSql.ValidateMatch(fk.Match); + base.AddForeignKey(table, fk); + } + + public override void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, + ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) + { + var info = GetSQLiteTableInfo(childTable) ?? throw new MigrationException("Child table does not exist."); + if (string.IsNullOrWhiteSpace(name) || info.ForeignKeys.Select(f => f.Name).Concat(info.Uniques.Select(u => u.Name)) + .Any(existing => string.Equals(existing, name, StringComparison.OrdinalIgnoreCase))) + throw new MigrationException("A unique foreign key name is required."); + info.ForeignKeys.Add(new ForeignKeyConstraint(name, parentTable, (string[])parentColumns.Clone(), childTable, (string[])childColumns.Clone()) + { + OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(onDelete), + OnUpdate = new ForeignKeyConstraintMapper().SqlForConstraint(onUpdate) + }); + RecreateTable(info); + } + + public string[] GetColumnDefs(string table, out string compositeDefSql) + { + return ParseSqlColumnDefs(GetSqlCreateTableScript(table), out compositeDefSql); + } + + /// + /// Gets the SQL CREATE TABLE script. Case-insensitive + /// + /// + /// + public string GetSqlCreateTableScript(string table) + { + string sqlCreateTableScript = null; + + using var cmd = CreateCommand(); + var parameter = cmd.CreateParameter(); parameter.ParameterName = "@name"; parameter.Value = table; cmd.Parameters.Add(parameter); + using var reader = ExecuteQuery(cmd, "SELECT sql FROM sqlite_master WHERE type='table' AND name=@name COLLATE NOCASE"); + if (reader.Read()) sqlCreateTableScript = reader.IsDBNull(0) ? null : reader.GetString(0); + + return sqlCreateTableScript; + } + + public override TableConstraint[] GetTableConstraints(string table) + { + var script = GetSqlCreateTableScript(table); + if (string.IsNullOrWhiteSpace(script)) throw new MigrationException("Table does not exist: " + table); + var constraints = SQLiteConstraintParser.Parse(script); + foreach (var foreignKey in constraints.OfType()) foreignKey.ChildTable = table; + return constraints; + } + + public override ForeignKeyConstraint[] GetForeignKeyConstraints(string tableName) + { + List foreignKeyConstraints = []; + + var pragmaForeignKeyListItems = GetForeignKeyListItems(tableName); + var groups = pragmaForeignKeyListItems.GroupBy(x => x.Id); + + foreach (var group in groups) + { + var foreignKeyConstraint = new ForeignKeyConstraint + { + Id = group.First().Id, + // SQLite does not support FK names. + ChildColumns = group.OrderBy(x => x.Seq).Select(x => x.From).ToArray(), + ChildTable = tableName, + Match = group.First().Match, + Name = null, + OnDelete = group.First().OnDelete, + OnUpdate = group.First().OnUpdate, + ParentColumns = group.OrderBy(x => x.Seq).Select(x => x.To).ToArray(), + ParentTable = group.First().Table, + }; + + foreignKeyConstraints.Add(foreignKeyConstraint); + } + + if (foreignKeyConstraints.Count == 0) + { + return []; + } + + var declared = GetTableConstraints(tableName).OfType().ToList(); + foreach (var foreignKey in foreignKeyConstraints) + { + var definition = declared.FirstOrDefault(candidate => + candidate.ChildColumns.SequenceEqual(foreignKey.ChildColumns, StringComparer.OrdinalIgnoreCase) && + candidate.ParentTable.Equals(foreignKey.ParentTable, StringComparison.OrdinalIgnoreCase) && + (candidate.ParentColumns.Length == 0 || candidate.ParentColumns.SequenceEqual(foreignKey.ParentColumns, StringComparer.OrdinalIgnoreCase))); + if (definition == null) throw new MigrationException("Cannot match a SQLite foreign key to its declaration."); + foreignKey.Name = definition.Name; + // PRAGMA reports NONE even for an explicitly declared MATCH FULL. + // Retain the declaration so a rebuild cannot silently discard it. + foreignKey.Match = definition.Match ?? foreignKey.Match; + declared.Remove(definition); + } + + return foreignKeyConstraints.ToArray(); + } + + public override void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) + { + if (!TableExists(tableSourceNotQuoted)) + { + throw new Exception($"Table '{tableSourceNotQuoted}' given in '{nameof(tableSourceNotQuoted)}' does not exist"); + } + + if (!TableExists(tableTargetNotQuoted)) + { + throw new Exception($"Table '{tableTargetNotQuoted}' given in '{nameof(tableTargetNotQuoted)}' does not exist"); + } + + if (fromSourceToTargetColumnPairs.Length == 0) + { + throw new Exception($"{nameof(fromSourceToTargetColumnPairs)} is empty."); + } + + if (fromSourceToTargetColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) + { + throw new Exception($"One of the strings in {nameof(fromSourceToTargetColumnPairs)} is null or empty"); + } + + if (conditionColumnPairs.Length == 0) + { + throw new Exception($"{nameof(conditionColumnPairs)} is empty."); + } + + if (conditionColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) + { + throw new Exception($"One of the strings in {nameof(conditionColumnPairs)} is null or empty"); + } + + var tableNameSource = QuoteTableNameIfRequired(tableSourceNotQuoted); + var tableNameTarget = QuoteTableNameIfRequired(tableTargetNotQuoted); + + var assignStrings = fromSourceToTargetColumnPairs.Select(x => $"{QuoteColumnNameIfRequired(x.ColumnNameTarget)} = {tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)}").ToList(); + + var conditionStrings = conditionColumnPairs.Select(x => $"{tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)} = {tableNameTarget}.{QuoteColumnNameIfRequired(x.ColumnNameTarget)}"); + + var assignStringsJoined = string.Join(", ", assignStrings); + var conditionStringsJoined = string.Join(" AND ", conditionStrings); + + var sql = $"UPDATE {tableNameTarget} SET {assignStringsJoined} FROM {tableNameSource} WHERE {conditionStringsJoined}"; + ExecuteNonQuery(sql); + } + + private List GetForeignKeyListItems(string tableNameNotQuoted) + { + List pragmaForeignKeyListItems = []; + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA foreign_key_list('{QuoteTableNameIfRequired(tableNameNotQuoted)}')")) + { + while (reader.Read()) + { + var pragmaForeignKeyListItem = new PragmaForeignKeyListItem + { + Id = reader.GetInt32(reader.GetOrdinal("id")), + Seq = reader.GetInt32(reader.GetOrdinal("seq")), + Table = reader.GetString(reader.GetOrdinal("table")), + From = reader.GetString(reader.GetOrdinal("from")), + To = reader.GetString(reader.GetOrdinal("to")), + OnUpdate = reader.GetString(reader.GetOrdinal("on_update")), + OnDelete = reader.GetString(reader.GetOrdinal("on_delete")), + Match = reader.GetString(reader.GetOrdinal("match")), + }; + + pragmaForeignKeyListItems.Add(pragmaForeignKeyListItem); + } + } + + return pragmaForeignKeyListItems; + } + + public string[] ParseSqlColumnDefs(string sqldef, out string compositeDefSql) + { + if (string.IsNullOrEmpty(sqldef)) + { + compositeDefSql = null; + + return null; + } + + sqldef = sqldef.Replace(Environment.NewLine, " "); + var start = sqldef.IndexOf("("); + + // Code to handle composite primary keys /mol + var compositeDefIndex = sqldef.IndexOf("PRIMARY KEY ("); // Not ideal to search for a string like this but I'm lazy + + if (compositeDefIndex > -1) + { + compositeDefSql = sqldef.Substring(compositeDefIndex, sqldef.LastIndexOf(")") - compositeDefIndex); + sqldef = sqldef.Substring(0, compositeDefIndex).TrimEnd(',', ' ') + ")"; + } + else + { + compositeDefSql = null; + } + + var end = sqldef.LastIndexOf(")"); // Changed from 'IndexOf' to 'LastIndexOf' to handle foreign key definitions /mol + + sqldef = sqldef.Substring(0, end); + sqldef = sqldef.Substring(start + 1); + + var cols = sqldef.Split([',']); + + for (var i = 0; i < cols.Length; i++) + { + cols[i] = cols[i].Trim(); + } + + return cols; + } + + /// + /// Turn something like 'columnName INTEGER NOT NULL' into just 'columnName' + /// + public string[] ParseSqlForColumnNames(string sqldef, out string compositeDefSql) + { + var parts = ParseSqlColumnDefs(sqldef, out compositeDefSql); + + return ParseSqlForColumnNames(parts); + } + + public string[] ParseSqlForColumnNames(string[] parts) + { + if (null == parts) + { + return null; + } + + for (var i = 0; i < parts.Length; i++) + { + parts[i] = ExtractNameFromColumnDef(parts[i]); + } + + return parts; + } + + /// + /// Name is the first value before the space. + /// + /// + /// + public static string ExtractNameFromColumnDef(string columnDef) + { + var idx = columnDef.IndexOf(" "); + + if (idx > 0) + { + return columnDef.Substring(0, idx); + } + return null; + } + + public DbType ExtractTypeFromColumnDef(string columnDef) + { + var idx = columnDef.IndexOf(" ") + 1; + + if (idx > 0) + { + var idy = columnDef.IndexOf(" ", idx) - idx; + + if (idy > 0) + { + return _dialect.GetDbType(columnDef.Substring(idx, idy)); + } + else + { + return _dialect.GetDbType(columnDef.Substring(idx)); + } + } + else + { + throw new Exception("Error extracting type from column definition: '" + columnDef + "'"); + } + } + + public override void RemoveForeignKey(string table, string name) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + var sqliteTableInfo = GetSQLiteTableInfo(table); + if (!sqliteTableInfo.ForeignKeys.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException($"Foreign key '{name}' does not exist."); + } + + sqliteTableInfo.ForeignKeys.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + + RecreateTable(sqliteTableInfo); + } + public override void RemoveAllForeignKeys(string tableName, string columnName) { bool Matches(string name) => string.Equals(name, tableName, StringComparison.OrdinalIgnoreCase); @@ -436,55 +436,55 @@ public override void RemoveAllForeignKeys(string tableName, string columnName) } } - public string[] GetCreateIndexSqlStrings(string table) - { - var sqlStrings = new List(); - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, string.Format("SELECT sql FROM sqlite_master WHERE type='index' AND sql NOT NULL AND lower(tbl_name)=lower('{0}')", table))) - { - while (reader.Read()) - { - sqlStrings.Add((string)reader[0]); - } - } - - return [.. sqlStrings]; - } - - public void MoveIndexesFromOriginalTable(string origTable, string newTable) - { - var indexSqls = GetCreateIndexSqlStrings(origTable); - - foreach (var indexSql in indexSqls) - { - var origTableStart = indexSql.IndexOf(" ON ", StringComparison.OrdinalIgnoreCase) + 4; - var origTableEnd = indexSql.IndexOf("(", origTableStart); - - // First remove original index, because names have to be unique - var createIndexDef = " INDEX "; - var indexNameStart = indexSql.IndexOf(createIndexDef, StringComparison.OrdinalIgnoreCase) + createIndexDef.Length; - ExecuteNonQuery("DROP INDEX " + indexSql.Substring(indexNameStart, origTableStart - 4 - indexNameStart)); - - // Create index on new table - ExecuteNonQuery(indexSql.Substring(0, origTableStart) + newTable + " " + indexSql.Substring(origTableEnd)); - } - } - - public override void RemoveColumn(string tableName, string column) - { - if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 35, 0) + public string[] GetCreateIndexSqlStrings(string table) + { + var sqlStrings = new List(); + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, string.Format("SELECT sql FROM sqlite_master WHERE type='index' AND sql NOT NULL AND lower(tbl_name)=lower('{0}')", table))) + { + while (reader.Read()) + { + sqlStrings.Add((string)reader[0]); + } + } + + return [.. sqlStrings]; + } + + public void MoveIndexesFromOriginalTable(string origTable, string newTable) + { + var indexSqls = GetCreateIndexSqlStrings(origTable); + + foreach (var indexSql in indexSqls) + { + var origTableStart = indexSql.IndexOf(" ON ", StringComparison.OrdinalIgnoreCase) + 4; + var origTableEnd = indexSql.IndexOf("(", origTableStart); + + // First remove original index, because names have to be unique + var createIndexDef = " INDEX "; + var indexNameStart = indexSql.IndexOf(createIndexDef, StringComparison.OrdinalIgnoreCase) + createIndexDef.Length; + ExecuteNonQuery("DROP INDEX " + indexSql.Substring(indexNameStart, origTableStart - 4 - indexNameStart)); + + // Create index on new table + ExecuteNonQuery(indexSql.Substring(0, origTableStart) + newTable + " " + indexSql.Substring(origTableEnd)); + } + } + + public override void RemoveColumn(string tableName, string column) + { + if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 35, 0) && TableExists(tableName) && CanDropColumnNatively(tableName, column)) - { + { // Native SQLite validates trigger and view dependencies atomically. ExecuteNonQuery($"ALTER TABLE {Dialect.Quote(tableName)} DROP COLUMN {Dialect.QuoteIdentifier(column)}"); return; - } + } if (IsPragmaForeignKeysOn()) throw new Exception($"{nameof(RemoveColumn)} requires foreign keys off."); if (!TableExists(tableName)) throw new MigrationException($"The table '{tableName}' does not exist"); if (!ColumnExists(tableName, column)) throw new MigrationException($"The table '{tableName}' does not have a column named '{column}'"); - - var sqliteInfoMainTable = GetSQLiteTableInfo(tableName); + + var sqliteInfoMainTable = GetSQLiteTableInfo(tableName); ValidateColumnRemoval(sqliteInfoMainTable, column); var affected = new List(); foreach (var name in GetTables()) @@ -525,165 +525,165 @@ private bool CanDropColumnNatively(string tableName, string column) private static void ValidateColumnRemoval(SQLiteTableInfo sqliteInfoMainTable, string column) { - if (sqliteInfoMainTable.PrimaryKey?.KeyColumns.Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)) == true) - throw new MigrationException("Remove the named primary-key constraint before removing one of its columns."); - - var checkConstraints = sqliteInfoMainTable.CheckConstraints; - - if (checkConstraints.Any(x => x.CheckConstraintString.Contains(column, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException("A check constraint contains the column you want to remove. Remove the check constraint first"); - } - + if (sqliteInfoMainTable.PrimaryKey?.KeyColumns.Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)) == true) + throw new MigrationException("Remove the named primary-key constraint before removing one of its columns."); + + var checkConstraints = sqliteInfoMainTable.CheckConstraints; + + if (checkConstraints.Any(x => x.CheckConstraintString.Contains(column, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException("A check constraint contains the column you want to remove. Remove the check constraint first"); + } + if (!sqliteInfoMainTable.ColumnMappings.Any(x => x.OldName.Equals(column, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException("Column not found"); - } - - // We throw if all of the conditions are fulfilled: - // - the unique constraint is a composite constraint (more than one column) - // - the column to be removed is part of the constraint - // In case of single constraint we remove it silently as it is not needed any more - var isColumnInUniqueConstraint = sqliteInfoMainTable.Uniques - .Where(x => x.KeyColumns.Length > 1) - .SelectMany(x => x.KeyColumns) - .Distinct() - .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); - - if (isColumnInUniqueConstraint) - { - StringBuilder stringBuilder = new(); - stringBuilder.Append("Found composite unique constraint where the column that you want to remove is part of. Remove the unique constraints first before you remove the column."); - stringBuilder.Append("Other unique constraints(if exists) that contains only the column to be removed are dropped silently."); - - throw new Exception(stringBuilder.ToString()); - } - - var isColumnInIndex = sqliteInfoMainTable.Indexes - .Where(x => x.KeyColumns.Length > 1) - .SelectMany(x => x.KeyColumns) - .Distinct() - .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); - - if (isColumnInIndex) - { - StringBuilder stringBuilder = new(); - stringBuilder.Append("Found composite index where the column that you want to remove is part of. Remove the indexes first before you remove the column."); - stringBuilder.Append("Other indexes(if exists) that contains only the column to be removed are dropped silently."); - - throw new Exception(stringBuilder.ToString()); - } - - var isColumnInForeignKey = sqliteInfoMainTable.ForeignKeys - .Where(x => x.ChildColumns.Length > 1) - .SelectMany(x => x.ChildColumns) - .Distinct() - .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); - - if (isColumnInForeignKey) - { - StringBuilder stringBuilder = new(); - stringBuilder.Append("Found foreign key with more than two columns with one column is the column you want to remove. Remove the foreign key before you "); - stringBuilder.Append("remove the column. Other foreign keys (if exists) that contain only the column to be removed are dropped silently."); - - throw new Exception(stringBuilder.ToString()); - } - - } - + { + throw new MigrationException("Column not found"); + } + + // We throw if all of the conditions are fulfilled: + // - the unique constraint is a composite constraint (more than one column) + // - the column to be removed is part of the constraint + // In case of single constraint we remove it silently as it is not needed any more + var isColumnInUniqueConstraint = sqliteInfoMainTable.Uniques + .Where(x => x.KeyColumns.Length > 1) + .SelectMany(x => x.KeyColumns) + .Distinct() + .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); + + if (isColumnInUniqueConstraint) + { + StringBuilder stringBuilder = new(); + stringBuilder.Append("Found composite unique constraint where the column that you want to remove is part of. Remove the unique constraints first before you remove the column."); + stringBuilder.Append("Other unique constraints(if exists) that contains only the column to be removed are dropped silently."); + + throw new Exception(stringBuilder.ToString()); + } + + var isColumnInIndex = sqliteInfoMainTable.Indexes + .Where(x => x.KeyColumns.Length > 1) + .SelectMany(x => x.KeyColumns) + .Distinct() + .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); + + if (isColumnInIndex) + { + StringBuilder stringBuilder = new(); + stringBuilder.Append("Found composite index where the column that you want to remove is part of. Remove the indexes first before you remove the column."); + stringBuilder.Append("Other indexes(if exists) that contains only the column to be removed are dropped silently."); + + throw new Exception(stringBuilder.ToString()); + } + + var isColumnInForeignKey = sqliteInfoMainTable.ForeignKeys + .Where(x => x.ChildColumns.Length > 1) + .SelectMany(x => x.ChildColumns) + .Distinct() + .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); + + if (isColumnInForeignKey) + { + StringBuilder stringBuilder = new(); + stringBuilder.Append("Found foreign key with more than two columns with one column is the column you want to remove. Remove the foreign key before you "); + stringBuilder.Append("remove the column. Other foreign keys (if exists) that contain only the column to be removed are dropped silently."); + + throw new Exception(stringBuilder.ToString()); + } + + } + private void RecreateTablesAtomically(IEnumerable tables) { var ownsTransaction = !HasActiveTransaction; if (ownsTransaction) BeginTransaction(); try - { + { foreach (var info in tables) RecreateTable(info); if (ownsTransaction) - { + { if (!CheckForeignKeyIntegrity()) throw new MigrationException("SQLite column removal would leave invalid foreign keys."); Commit(); - } - } + } + } catch (Exception ex) { if (ownsTransaction) try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } throw; } - } - - public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - if (!TableExists(tableName)) - { - throw new Exception($"Table {tableName} does not exist"); - } - - if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 26, 0)) - { - if (string.IsNullOrWhiteSpace(newColumnName)) throw new ArgumentException("A column name is required."); - ExecuteNonQuery($"ALTER TABLE {Dialect.Quote(tableName)} RENAME COLUMN {Dialect.QuoteIdentifier(oldColumnName)} TO {Dialect.QuoteIdentifier(newColumnName)}"); - return; - } - - var isPragmaForeignKeysOn = IsPragmaForeignKeysOn(); - - if (isPragmaForeignKeysOn) - { - throw new Exception($"{nameof(RenameColumn)} requires foreign keys off."); - } - - // Due to old .Net versions we cannot use ThrowIfNullOrWhitespace - if (string.IsNullOrWhiteSpace(newColumnName)) - { - throw new Exception("New column name is null or empty"); - } - - if (ColumnExists(tableName, newColumnName)) - { - throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); - } - - if (ColumnExists(tableName, oldColumnName)) - { - var sqliteTableInfo = GetSQLiteTableInfo(tableName); - - var columnMapping = sqliteTableInfo.ColumnMappings.First(x => x.OldName.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); - columnMapping.NewName = newColumnName; - - var column = sqliteTableInfo.Columns.First(x => x.Name.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); - column.Name = newColumnName; - if (sqliteTableInfo.PrimaryKey != null) - sqliteTableInfo.PrimaryKey.KeyColumns = sqliteTableInfo.PrimaryKey.KeyColumns - .Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); - - foreach (var foreignKey in sqliteTableInfo.ForeignKeys) - { - foreignKey.ChildColumns = [.. foreignKey.ChildColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; + } + + public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + if (!TableExists(tableName)) + { + throw new Exception($"Table {tableName} does not exist"); + } + + if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 26, 0)) + { + if (string.IsNullOrWhiteSpace(newColumnName)) throw new ArgumentException("A column name is required."); + ExecuteNonQuery($"ALTER TABLE {Dialect.Quote(tableName)} RENAME COLUMN {Dialect.QuoteIdentifier(oldColumnName)} TO {Dialect.QuoteIdentifier(newColumnName)}"); + return; + } + + var isPragmaForeignKeysOn = IsPragmaForeignKeysOn(); + + if (isPragmaForeignKeysOn) + { + throw new Exception($"{nameof(RenameColumn)} requires foreign keys off."); + } + + // Due to old .Net versions we cannot use ThrowIfNullOrWhitespace + if (string.IsNullOrWhiteSpace(newColumnName)) + { + throw new Exception("New column name is null or empty"); + } + + if (ColumnExists(tableName, newColumnName)) + { + throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); + } + + if (ColumnExists(tableName, oldColumnName)) + { + var sqliteTableInfo = GetSQLiteTableInfo(tableName); + + var columnMapping = sqliteTableInfo.ColumnMappings.First(x => x.OldName.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); + columnMapping.NewName = newColumnName; + + var column = sqliteTableInfo.Columns.First(x => x.Name.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); + column.Name = newColumnName; + if (sqliteTableInfo.PrimaryKey != null) + sqliteTableInfo.PrimaryKey.KeyColumns = sqliteTableInfo.PrimaryKey.KeyColumns + .Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); + + foreach (var foreignKey in sqliteTableInfo.ForeignKeys) + { + foreignKey.ChildColumns = [.. foreignKey.ChildColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; if (string.Equals(foreignKey.ParentTable, tableName, StringComparison.OrdinalIgnoreCase)) foreignKey.ParentColumns = foreignKey.ParentColumns.Select(x => string.Equals(x, oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); - } - - foreach (var index in sqliteTableInfo.Indexes) - { - index.KeyColumns = [.. index.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; - } - - foreach (var unique in sqliteTableInfo.Uniques) - { - unique.KeyColumns = [.. unique.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; - } - + } + + foreach (var index in sqliteTableInfo.Indexes) + { + index.KeyColumns = [.. index.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; + } + + foreach (var unique in sqliteTableInfo.Uniques) + { + unique.KeyColumns = [.. unique.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; + } + // Rebuild the parent and every dependent table atomically. Checking integrity // between those rebuilds would see references to the parent's old column name. var ownsTransaction = !HasActiveTransaction; if (ownsTransaction) BeginTransaction(); try - { + { RecreateTable(sqliteTableInfo); foreach (var otherTable in GetTables()) - { + { if (string.Equals(otherTable, tableName, StringComparison.OrdinalIgnoreCase)) continue; var otherInfo = GetSQLiteTableInfo(otherTable); var references = otherInfo.ForeignKeys.Where(f => @@ -694,922 +694,952 @@ public override void RenameColumn(string tableName, string oldColumnName, string foreignKey.ParentColumns = foreignKey.ParentColumns.Select(x => string.Equals(x, oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); RecreateTable(otherInfo); - } + } if (ownsTransaction) - { + { if (!CheckForeignKeyIntegrity()) throw new MigrationException("SQLite rename would leave invalid foreign keys."); Commit(); - } + } } catch (Exception ex) { if (ownsTransaction) try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } throw; - } - } - else - { - throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); - } - } - - public override void RemoveColumnDefaultValue(string tableName, string columnName) - { - if (!TableExists(tableName)) - { - throw new Exception("Table does not exist"); - } - - if (!ColumnExists(table: tableName, column: columnName)) - { - throw new Exception("Column does not exist"); - } - - var sqliteTableInfo = GetSQLiteTableInfo(tableName); - - var column = sqliteTableInfo.Columns.First(x => x.Name == columnName); - column.DefaultValue = null; - - RecreateTable(sqliteTableInfo); - } - - public override void AddPrimaryKey(string name, string tableName, params string[] columnNames) - { - var info = GetSQLiteTableInfo(tableName) ?? throw new MigrationException("Table does not exist."); - if (info.PrimaryKey != null) throw new MigrationException("The table already has a primary key. Remove it explicitly first."); - ValidateKeyColumns(name, columnNames, info.Columns.ToArray()); - info.PrimaryKey = new PrimaryKeyConstraint(name, columnNames); - RecreateTable(info); - } - - public override bool PrimaryKeyExists(string table, string name) - { - var key = GetTableConstraints(table).OfType().SingleOrDefault(); - return key != null && string.Equals(key.Name, name, StringComparison.OrdinalIgnoreCase); - } - - public override void AddUniqueConstraint(string name, string table, params string[] columns) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new MigrationException("Providing a constraint name is obligatory."); - } - - var sqliteTableInfo = GetSQLiteTableInfo(table); - - if (sqliteTableInfo.Uniques.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException("A unique constraint with the same name already exists."); - } - - var uniqueConstraint = new UniqueConstraint() { KeyColumns = columns, Name = name }; - sqliteTableInfo.Uniques.Add(uniqueConstraint); - - RecreateTable(sqliteTableInfo); - } - - public override void RemoveConstraint(string table, string name) - { - var sqliteTableInfo = GetSQLiteTableInfo(table); - sqliteTableInfo.Uniques.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - sqliteTableInfo.CheckConstraints.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - - RecreateTable(sqliteTableInfo); - } - - public SQLiteTableInfo GetSQLiteTableInfo(string tableName) - { - if (!TableExists(tableName)) - { - return null; - } - - var sqliteTable = new SQLiteTableInfo - { - TableNameMapping = new MappingInfo { OldName = tableName, NewName = tableName }, - Columns = GetColumns(tableName).ToList(), - PrimaryKey = GetTableConstraints(tableName).OfType().SingleOrDefault(), - ForeignKeys = GetForeignKeyConstraints(tableName).ToList(), - Indexes = GetIndexes(tableName).ToList(), - Uniques = GetUniques(tableName).ToList(), - CheckConstraints = GetCheckConstraints(tableName) - }; - - if (sqliteTable.PrimaryKey != null) - { - var columnOrder = GetPragmaTableInfoItems(tableName).ToDictionary(c => c.Name, c => c.Cid, StringComparer.OrdinalIgnoreCase); - sqliteTable.Columns = sqliteTable.Columns.OrderBy(c => columnOrder[c.Name]).ToList(); - } - - sqliteTable.ColumnMappings = sqliteTable.Columns - .Select(x => - new MappingInfo - { - OldName = x.Name, - NewName = x.Name - }) - .ToList(); - - return sqliteTable; - } - - public bool CheckForeignKeyIntegrity() - { - - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, "PRAGMA foreign_key_check"); - - if (reader.Read()) - { - return false; - } - - return true; - } - - public bool IsPragmaForeignKeysOn() - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, "PRAGMA foreign_keys"); - reader.Read(); - var isOn = reader.GetInt32(0) == 1; - - return isOn; - } - - public void SetPragmaForeignKeys(bool isOn) - { - var onOffString = isOn ? "ON" : "OFF"; - - using var cmd = CreateCommand(); - ExecuteNonQuery($"PRAGMA foreign_keys = {onOffString}"); - } - - private static string ValidateForeignKeyAction(string action) - { - var normalized = action.ToUpperInvariant(); - if (normalized is not ("CASCADE" or "RESTRICT" or "SET NULL" or "SET DEFAULT" or "NO ACTION")) - throw new MigrationException("Unsupported foreign key action: " + action); - return normalized; - } - - public void RecreateTable(SQLiteTableInfo sqliteTableInfo) - { - foreach (var foreignKey in sqliteTableInfo.ForeignKeys) SQLiteTableSql.ValidateMatch(foreignKey.Match); - var oldName = sqliteTableInfo.TableNameMapping.OldName; - var script = GetSqlCreateTableScript(oldName); - if (SQLiteConstraintParser.HasUnsupportedRebuildFeatures(script)) - throw new NotSupportedException("This table contains SQLite features that cannot be reconstructed faithfully. Use native SQL."); - if (GetCreateIndexSqlStrings(oldName).Any(sql => SQLiteConstraintParser.HasKeyword(sql, "COLLATE"))) - throw new NotSupportedException("Rebuilding indexes with explicit collations requires native SQL."); - var triggers = ExecuteStringQuery("SELECT sql FROM sqlite_master WHERE type='trigger' AND lower(tbl_name)=lower('{0}')", oldName.Replace("'", "''")); - if (triggers.Count > 0 && (oldName != sqliteTableInfo.TableNameMapping.NewName || sqliteTableInfo.ColumnMappings.Any(m => m.OldName != null && m.OldName != m.NewName))) - throw new NotSupportedException("Use native SQLite rename when triggers reference renamed objects."); - var originalColumns = GetColumns(oldName); - if (triggers.Count > 0 && originalColumns.Any(c => !sqliteTableInfo.Columns.Any(n => n.Name.Equals(c.Name, StringComparison.OrdinalIgnoreCase)))) - throw new NotSupportedException("Removing columns from a table with triggers requires native SQLite alteration or explicit trigger recreation."); - var sequence = TableExists("sqlite_sequence") - ? ExecuteScalar("SELECT seq FROM sqlite_sequence WHERE name='" + oldName.Replace("'", "''") + "'") : null; - var highWater = sequence == null || sequence == DBNull.Value ? (long?)null : Convert.ToInt64(sequence); - var foreignKeys = IsPragmaForeignKeysOn(); - if (HasActiveTransaction && foreignKeys) - throw new MigrationException("SQLite rebuild requires foreign keys to be disabled before beginning the transaction. Use the migration runner."); - var ownsTransaction = !HasActiveTransaction; - Exception failure = null; - try - { - if (ownsTransaction) - { - if (foreignKeys) SetPragmaForeignKeys(false); - BeginTransaction(); - } - RecreateTableCore(sqliteTableInfo); - if (highWater.HasValue && sqliteTableInfo.Columns.Any(c => c.IsIdentity)) - { - var sequenceName = sqliteTableInfo.TableNameMapping.NewName.Replace("'", "''"); - var sequenceValue = highWater.Value.ToString(CultureInfo.InvariantCulture); - ExecuteNonQuery($"UPDATE sqlite_sequence SET seq=MAX(seq, {sequenceValue}) WHERE name='{sequenceName}'"); - ExecuteNonQuery($"INSERT INTO sqlite_sequence(name, seq) SELECT '{sequenceName}', {sequenceValue} WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name='{sequenceName}')"); - } - foreach (var trigger in triggers) ExecuteNonQuery(trigger); - if (ownsTransaction && !CheckForeignKeyIntegrity()) throw new MigrationException("SQLite rebuild would leave invalid foreign keys."); - if (ownsTransaction) Commit(); - } - catch (Exception ex) - { - failure = ex; - if (ownsTransaction) - { - try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } - } - throw; - } - finally - { - try { if (ownsTransaction && foreignKeys) SetPragmaForeignKeys(true); } - catch (Exception restore) { if (failure == null) throw; failure.Data["ConnectionRestoreException"] = restore; } - } - } - - private void RecreateTableCore(SQLiteTableInfo sqliteTableInfo) - { - var sourceTableQuoted = QuoteTableNameIfRequired(sqliteTableInfo.TableNameMapping.OldName); - var targetIntermediateTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}{IntermediateTableSuffix}"); - var targetTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}"); - - var columns = sqliteTableInfo.Columns.Select(c => c.CopyDefinition()).ToArray(); - var columnDbFields = columns.Cast(); - var foreignKeyDbFields = sqliteTableInfo.ForeignKeys.Cast(); - var indexDbFields = sqliteTableInfo.Indexes.Cast(); - var uniqueDbFields = sqliteTableInfo.Uniques.Cast(); - var checkConstraintDbFields = sqliteTableInfo.CheckConstraints.Cast(); - - var dbFields = columnDbFields.Concat(foreignKeyDbFields) - .Concat(uniqueDbFields) - .Concat(checkConstraintDbFields) - .Concat(sqliteTableInfo.PrimaryKey == null ? Array.Empty() : new IDbField[] { sqliteTableInfo.PrimaryKey }) - .ToArray(); - - // ToHashSet() not available in older .NET versions so we create it old-fashioned. - var uniqueColumnNames = new HashSet(sqliteTableInfo.Uniques - .SelectMany(x => x.KeyColumns) - .Distinct() - ); - - // ToHashSet() not available in older .NET versions so we create it old-fashioned. - var columnNames = new HashSet(sqliteTableInfo.Columns - .Select(x => x.Name) - ); - - // ToHashSet() not available in older .NET versions so we create it old-fashioned. - var newColumnNamesInMapping = new HashSet(sqliteTableInfo.ColumnMappings - .Select(x => x.NewName) - ); - - if (!columnNames.SetEquals(newColumnNamesInMapping)) - { - throw new Exception($"{nameof(columnNames)} and {nameof(newColumnNamesInMapping)} are not equal regarding length and content"); - } - - if (uniqueColumnNames.Except(columnNames).Any()) - { - var firstMissing = uniqueColumnNames.Except(columnNames).First(); - throw new Exception($"Detected missing column names OR unique key columns that do not exist in the column list/column mapping. E.g. {firstMissing}"); - } - - AddTable(targetIntermediateTableQuoted, null, dbFields); - - var columnMappings = sqliteTableInfo.ColumnMappings - .Where(x => x.OldName != null) - .OrderBy(x => x.OldName) - .ToList(); - - var sourceColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.OldName))); - var targetColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.NewName))); - - using (var cmd = CreateCommand()) - { - var sql = $"INSERT INTO {targetIntermediateTableQuoted} ({targetColumnsQuotedString}) SELECT {sourceColumnsQuotedString} FROM {sourceTableQuoted}"; - ExecuteNonQuery(sql); - } - - RemoveTable(sourceTableQuoted); - - using (var cmd = CreateCommand()) - { - // Rename to original name - var sql = $"ALTER TABLE {targetIntermediateTableQuoted} RENAME TO {targetTableQuoted}"; - ExecuteNonQuery(sql); - } - - foreach (var index in sqliteTableInfo.Indexes) - { - AddIndex(sqliteTableInfo.TableNameMapping.NewName, index); - } - } - - [Obsolete] - public override void AddTable(string table, string engine, string columns) - { - throw new NotSupportedException(); - } - - public override void AddColumn(string table, Column column) - { - if (!TableExists(table)) - { - throw new Exception("Table does not exist."); - } - - var sqliteInfo = GetSQLiteTableInfo(table); - - if (sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) - { - throw new Exception("Column already exists."); - } - - sqliteInfo.ColumnMappings.Add(new MappingInfo { OldName = null, NewName = column.Name }); - sqliteInfo.Columns.Add(column); - - RecreateTable(sqliteInfo); - } - - public override void AddColumn(string table, string columnName, DbType type, int size) - { - var column = new Column(columnName, type, size); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, MigratorDbType type, int size) - { - var column = new Column(columnName, type, size); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, DbType type) - { - var column = new Column(columnName, type); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, MigratorDbType type) - { - var column = new Column(columnName, type); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, DbType type, object defaultValue) - { - var column = new Column(columnName, type, defaultValue); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string sqlColumn) - { - var column = new Column(sqlColumn); - AddColumn(table, column); - } - - public override void ChangeColumn(string table, Column column) - { - if (!TableExists(table)) - { - throw new Exception("Table does not exist."); - } - - var sqliteInfo = GetSQLiteTableInfo(table); - - if (!sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) - { - throw new Exception("Column does not exists."); - } - - var columnIndex = sqliteInfo.Columns.FindIndex(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); - sqliteInfo.Columns[columnIndex] = column.CopyDefinition(); - - RecreateTable(sqliteInfo); - } - - public override int TruncateTable(string table) - { - return ExecuteNonQuery(string.Format("DELETE FROM {0} ", table)); - } - - public override bool TableExists(string table) - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='table' and lower(name)=lower('{0}')", table)); - - return reader.Read(); - } - - public override bool ViewExists(string view) - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='view' and lower(name)=lower('{0}')", view)); - - return reader.Read(); - } - - public override List GetDatabases() - { - throw new NotSupportedException("SQLite is a file-based database. You cannot list other databases."); - } - - public override bool ConstraintExists(string table, string name) - { - if (!TableExists(table)) - { - throw new Exception($"Table '{table}' does not exist."); - } - - var constraintNames = GetConstraints(table); - - var exists = constraintNames.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)); - - return exists; - } - - public override string[] GetConstraints(string table) - { - var names = GetTableConstraints(table).Select(c => c.Name).Where(n => !string.IsNullOrWhiteSpace(n)).ToArray(); - if (names.Distinct(StringComparer.OrdinalIgnoreCase).Count() != names.Length) - throw new MigrationException("Duplicate constraint names in table: " + table); - return names; - } - - public override string[] GetTables() - { - var tables = new List(); - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")) - { - while (reader.Read()) - { - tables.Add((string)reader[0]); - } - } - - return [.. tables]; - } - - public override Column[] GetColumns(string tableName) - { - var pragmaTableInfoItems = GetPragmaTableInfoItems(tableName); - var tableScript = GetSqlCreateTableScript(tableName); - var collations = SQLiteConstraintParser.ColumnCollations(tableScript); - - var tableInfoPrimaryKeys = pragmaTableInfoItems.Where(x => x.Pk > 0).ToList(); - var pragmaTableInfoItemsSorted = pragmaTableInfoItems.OrderBy(x => x.Cid).ToList(); - - var columns = new List(); - - foreach (var pragmaTableInfoItem in pragmaTableInfoItemsSorted) - { - var column = new Column(pragmaTableInfoItem.Name) - { - Type = _dialect.GetDbTypeFromString(pragmaTableInfoItem.Type), - Collation = collations.TryGetValue(pragmaTableInfoItem.Name, out var collation) ? collation : null - }; - - if (pragmaTableInfoItem.NotNull) - { - column.IsNullable = false; - } - else - { - column.IsNullable = true; - } - - var defValue = pragmaTableInfoItem.DfltValue == DBNull.Value ? null : pragmaTableInfoItem.DfltValue; - - // Keep legacy text GUID defaults as text during unrelated rebuilds. New Guid - // values render as blobs, so parsing an old SQL literal into Guid would change its storage class. - column.DefaultValue = defValue is string sqlDefault - ? column.Type == DbType.Guid ? RawSql.Insert(sqlDefault) : CatalogDefaultValue.Parse(sqlDefault, column.Type) - : defValue; - - var columnTableInfoItem = pragmaTableInfoItems.First(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); - - var hasCompoundPrimaryKey = tableInfoPrimaryKeys.Count > 1; - - // Implicit in SQLite - if (columnTableInfoItem.Type == "INTEGER" && columnTableInfoItem.Pk == 1 && !hasCompoundPrimaryKey && Regex.IsMatch(tableScript, @"\bAUTOINCREMENT\b", RegexOptions.IgnoreCase)) - { - column.IsIdentity = true; - } - - columns.Add(column); - } - - - return [.. columns]; - } - - public bool IsNullable(string columnDef) - { - return !columnDef.Contains("NOT NULL"); - } - - public bool ColumnMatch(string column, string columnDef) - { - return columnDef.StartsWith(column + " ") || columnDef.StartsWith(_dialect.QuoteIdentifier(column)); - } - - public override bool IndexExists(string table, string name) - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='index' and lower(name)=lower('{0}')", name)); - - return reader.Read(); - } - - public override Index[] GetIndexes(string table) - { - var afterWhereRegex = new Regex("(?<= WHERE ).+"); - List indexes = []; - - var indexCreateScripts = GetCreateIndexSqlStrings(table); - - var pragmaIndexListItems = GetPragmaIndexListItems(table).Where(x => x.Origin == "c"); - - var columns = GetColumns(table); - - foreach (var pragmaIndexListItem in pragmaIndexListItems) - { - var indexInfos = GetPragmaIndexInfo(pragmaIndexListItem.Name); - - var columnNames = indexInfos.OrderBy(x => x.SeqNo) - .Select(x => x.Name) - .ToArray(); - - var index = new Index - { - // At this moment in time the migrator does not support clustered indexes for SQLITE - // Since SQLite 3.8.2 WITHOUT ROWID is supported but not in this migrator - Clustered = false, - - // SQLite does not support include colums - IncludeColumns = [], - KeyColumns = columnNames, - Name = pragmaIndexListItem.Name, - Unique = pragmaIndexListItem.Unique - }; - - var script = indexCreateScripts.FirstOrDefault(x => x.Contains(pragmaIndexListItem.Name, StringComparison.OrdinalIgnoreCase)); - - if (script != null) - { - if (afterWhereRegex.Match(script) is Match match && match.Success) - { - // We cannot use GeneratedRegexAttribute due to old .NET version - var andSplitted = Regex.Split(match.Value, " AND "); - - var filterSingleStrings = andSplitted - .Select(x => x.Trim()) - .ToList(); - - foreach (var filterSingleString in filterSingleStrings) - { - var splitted = filterSingleString.Split(' ') - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => x.Trim()) - .ToList(); - - var filterItem = new FilterItem { ColumnName = splitted[0], Filter = _dialect.GetFilterTypeByComparisonString(splitted[1]) }; - - var column = columns.Single(x => x.Name.Equals(splitted[0], StringComparison.OrdinalIgnoreCase)); - - var sqliteIntegerDataTypes = new[] { - MigratorDbType.Int16, - MigratorDbType.Int32, - MigratorDbType.Int64, - MigratorDbType.UInt16, - MigratorDbType.UInt32, - MigratorDbType.UInt64 - }; - - if (sqliteIntegerDataTypes.Contains(column.MigratorDbType)) - { - if (long.TryParse(splitted[2], out var longValue)) - { - filterItem.Value = longValue; - } - else if (ulong.TryParse(splitted[2], out var uLongValue)) - { - filterItem.Value = uLongValue; - } - else - { - throw new Exception(); - } - } - else - { - filterItem.Value = column.MigratorDbType switch - { - MigratorDbType.Boolean => splitted[2] == "1" || splitted[2].Equals("true", StringComparison.OrdinalIgnoreCase), - MigratorDbType.String => splitted[2].Substring(1, splitted[2].Length - 2), - _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), - }; - } - - index.FilterItems.Add(filterItem); - } - } - } - - indexes.Add(index); - } - - return [.. indexes]; - } - - public override void AddTable(string name, string engine, params IDbField[] fields) - { - if (engine != null) throw new NotSupportedException("SQLite does not support table engines."); - var table = _dialect.TableNameNeedsQuote ? _dialect.Quote(name) : QuoteTableNameIfRequired(name); - ExecuteNonQuery(SQLiteTableSql.Generate(_dialect, table, fields)); - foreach (var index in fields.OfType()) AddIndex(name, index); - } - - public override string AddIndex(string table, Index index) - { - ValidateIndex(table, index); - - var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; - - if (hasIncludedColumns) - { - // This will be actived in the future. - // throw new MigrationException($"SQLite does not support included columns. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); - } - - if (index.Clustered) - { - throw new MigrationException($"For SQLite this migrator does not support clustered indexes at this point in time, sorry. File an issue if needed. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); - } - - var name = QuoteConstraintNameIfRequired(index.Name); - table = QuoteTableNameIfRequired(table); - var columns = QuoteColumnNamesIfRequired(index.KeyColumns); - - var uniqueString = index.Unique ? "UNIQUE" : null; - var columnsString = $"({string.Join(", ", columns)})"; - var filterString = string.Empty; - - if (index.FilterItems != null && index.FilterItems.Count > 0) - { - List singleFilterStrings = []; - - foreach (var filterItem in index.FilterItems) - { - var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); - - var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); - string value = null; - - value = filterItem.Value switch - { - bool booleanValue => booleanValue ? "1" : "0", - string stringValue => $"'{stringValue.Replace("'", "''")}'", - byte or short or int or long => Convert.ToInt64(filterItem.Value).ToString(), - sbyte or ushort or uint or ulong => Convert.ToUInt64(filterItem.Value).ToString(), - _ => throw new NotImplementedException("Given type is not implemented. Please file an issue."), - }; - - if ((filterItem.Value is string || filterItem.Value is bool) && filterItem.Filter != FilterType.EqualTo && filterItem.Filter != FilterType.NotEqualTo) - { - throw new MigrationException($"Bool and string in {nameof(FilterItem)} can only be used with '{nameof(FilterType.EqualTo)}' or '{nameof(FilterType.EqualTo)}'."); - } - - var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; - - singleFilterStrings.Add(singleFilterString); - } - - filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; - } - - List list = ["CREATE", uniqueString, "INDEX", name, "ON", table, columnsString, filterString]; - - var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); - - ExecuteNonQuery(sql); - - return sql; - } - - protected override string GetPrimaryKeyConstraintName(string table) - { - return GetTableConstraints(table).OfType().SingleOrDefault()?.Name; - } - - public override void RemoveAllConstraints(string table) - { - var info = GetSQLiteTableInfo(table); - info.PrimaryKey = null; - info.Uniques.Clear(); - info.ForeignKeys.Clear(); - info.CheckConstraints.Clear(); - foreach (var column in info.Columns) column.IsIdentity = false; - RecreateTable(info); - } - - public override void RemovePrimaryKey(string tableName) - { - if (!TableExists(tableName)) return; - var info = GetSQLiteTableInfo(tableName); - info.PrimaryKey = null; - foreach (var column in info.Columns) column.IsIdentity = false; - RecreateTable(info); - } - - public override void RemoveAllIndexes(string tableName) - { - if (!TableExists(tableName)) - { - return; - } - - var sqliteInfoTable = GetSQLiteTableInfo(tableName); - - sqliteInfoTable.Indexes = []; - - RecreateTable(sqliteInfoTable); - } - - public List GetUniques(string tableName) => GetTableConstraints(tableName) - .OfType().ToList(); - - public List GetPragmaIndexInfo(string indexNameNotQuoted) - { - List pragmaIndexInfoItems = []; - - var quotedIndexName = QuoteTableNameIfRequired(indexNameNotQuoted); - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA index_info({quotedIndexName})")) - { - while (reader.Read()) - { - var pragmaIndexInfoItem = new PragmaIndexInfoItem - { - SeqNo = reader.GetInt32(reader.GetOrdinal("seqno")), - Cid = reader.GetInt32(reader.GetOrdinal("cid")), - Name = reader.GetString(reader.GetOrdinal("name")), - }; - - pragmaIndexInfoItems.Add(pragmaIndexInfoItem); - } - } - - return pragmaIndexInfoItems; - } - - public List GetPragmaIndexListItems(string tableNameNotQuoted) - { - List pragmaIndexListItems = []; - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA index_list({QuoteTableNameIfRequired(tableNameNotQuoted)})")) - { - while (reader.Read()) - { - var pragmaIndexListItem = new PragmaIndexListItem - { - Seq = reader.GetInt32(reader.GetOrdinal("seq")), - Name = reader.GetString(reader.GetOrdinal("name")), - Unique = reader.GetInt32(reader.GetOrdinal("unique")) == 1, - Origin = reader.GetString(reader.GetOrdinal("origin")), - Partial = reader.GetInt32(reader.GetOrdinal("partial")) == 1 - }; - - pragmaIndexListItems.Add(pragmaIndexListItem); - } - } - - return pragmaIndexListItems; - } - - public List GetPragmaTableInfoItems(string tableNameNotQuoted) - { - List pragmaTableInfoItems = []; - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA table_info({QuoteTableNameIfRequired(tableNameNotQuoted)})")) - { - while (reader.Read()) - { - var pragmaTableInfoItem = new PragmaTableInfoItem - { - Cid = reader.GetInt32(reader.GetOrdinal("cid")), - DfltValue = reader[reader.GetOrdinal("dflt_value")], - Name = reader.GetString(reader.GetOrdinal("name")), - NotNull = reader.GetInt32(reader.GetOrdinal("notnull")) == 1, - Pk = reader.GetInt32(reader.GetOrdinal("pk")), - Type = reader.GetString(reader.GetOrdinal("type")), - }; - - pragmaTableInfoItems.Add(pragmaTableInfoItem); - } - } - - return pragmaTableInfoItems; - } - - public override void AddCheckConstraint(string constraintName, string tableName, string checkSql) - { - var sqliteTableInfo = GetSQLiteTableInfo(tableName); - - var checkConstraint = new CheckConstraint(constraintName, checkSql); - sqliteTableInfo.CheckConstraints.Add(checkConstraint); - - RecreateTable(sqliteTableInfo); - } - - public override void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) - { - orderBySourceColumns ??= []; - - if (!TableExists(sourceTableName)) - { - throw new Exception($"Source table '{QuoteTableNameIfRequired(sourceTableName)}' does not exist"); - } - - if (!TableExists(targetTableName)) - { - throw new Exception($"Target table '{QuoteTableNameIfRequired(targetTableName)}' does not exist"); - } - - var sourceColumnsConcatenated = sourceColumnNames.Concat(orderBySourceColumns); - - foreach (var column in sourceColumnsConcatenated) - { - if (!ColumnExists(sourceTableName, column)) - { - throw new Exception($"Column {column} in source table does not exist."); - } - } - - foreach (var column in targetColumnNames) - { - if (!ColumnExists(targetTableName, column)) - { - throw new Exception($"Column {column} in target table does not exist."); - } - } - - if (!orderBySourceColumns.All(x => sourceColumnNames.Contains(x))) - { - throw new Exception($"All columns in {nameof(orderBySourceColumns)} must be in {nameof(sourceColumnNames)}"); - } - - var sourceTableNameQuoted = QuoteTableNameIfRequired(sourceTableName); - var targetTableNameQuoted = QuoteTableNameIfRequired(targetTableName); - - var sourceColumnNamesQuoted = sourceColumnNames.Select(QuoteColumnNameIfRequired).ToList(); - var targetColumnNamesQuoted = targetColumnNames.Select(QuoteColumnNameIfRequired).ToList(); - var orderBySourceColumnsQuoted = orderBySourceColumns.Select(QuoteColumnNameIfRequired).ToList(); - - var sourceColumnsJoined = string.Join(", ", sourceColumnNamesQuoted); - var targetColumnsJoined = string.Join(", ", targetColumnNamesQuoted); - var orderBySourceColumnsJoined = string.Join(", ", orderBySourceColumnsQuoted); - - var orderByComponent = !string.IsNullOrWhiteSpace(orderBySourceColumnsJoined) ? $"ORDER BY {orderBySourceColumnsJoined}" : null; - - List sqlComponents = - [ - $"INSERT INTO {targetTableNameQuoted} ({targetColumnsJoined}) SELECT {sourceColumnsJoined} FROM {sourceTableNameQuoted}", - orderByComponent - ]; - - var sql = string.Join(" ", sqlComponents.Where(x => x != null)); - ExecuteNonQuery(sql); - } - - public List GetCheckConstraints(string tableName) => GetTableConstraints(tableName).OfType().ToList(); - - protected override void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) - { - if (value is TimeOnly time) - { - // SQLite stores times as text; System.Data.SQLite cannot bind TimeSpan as DbType.Time. - parameter.DbType = DbType.String; - parameter.Value = time.ToString("HH:mm:ss.fffffff", CultureInfo.InvariantCulture); - } - else if (value is ushort) - { - parameter.DbType = DbType.Int32; - parameter.Value = Convert.ToInt32(value); - } - else if (value is uint) - { - parameter.DbType = DbType.Int64; - parameter.Value = Convert.ToInt64(value); - } - else if (value is ulong unsigned) + } + } + else { - // SQLite INTEGER cannot represent the upper half of UInt64. Do not let - // a driver wrap it to a negative integer or round it through a REAL. - parameter.DbType = DbType.Int64; - parameter.Value = checked((long)unsigned); + throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); } - else if (value is Guid || value is Guid?) - { - parameter.DbType = DbType.Binary; - parameter.Value = ((Guid)value).ToByteArray(); - } - else - { - base.ConfigureParameterWithValue(parameter, index, value); - } - } -} + } + + public override void RemoveColumnDefaultValue(string tableName, string columnName) + { + if (!TableExists(tableName)) + { + throw new Exception("Table does not exist"); + } + + if (!ColumnExists(table: tableName, column: columnName)) + { + throw new Exception("Column does not exist"); + } + + var sqliteTableInfo = GetSQLiteTableInfo(tableName); + + var column = sqliteTableInfo.Columns.First(x => x.Name == columnName); + column.DefaultValue = null; + + RecreateTable(sqliteTableInfo); + } + + public override void AddPrimaryKey(string name, string tableName, params string[] columnNames) + { + var info = GetSQLiteTableInfo(tableName) ?? throw new MigrationException("Table does not exist."); + if (info.PrimaryKey != null) throw new MigrationException("The table already has a primary key. Remove it explicitly first."); + ValidateKeyColumns(name, columnNames, info.Columns.ToArray()); + info.PrimaryKey = new PrimaryKeyConstraint(name, columnNames); + RecreateTable(info); + } + + public override bool PrimaryKeyExists(string table, string name) + { + var key = GetTableConstraints(table).OfType().SingleOrDefault(); + return key != null && string.Equals(key.Name, name, StringComparison.OrdinalIgnoreCase); + } + + public override void AddUniqueConstraint(string name, string table, params string[] columns) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new MigrationException("Providing a constraint name is obligatory."); + } + + var sqliteTableInfo = GetSQLiteTableInfo(table); + + if (sqliteTableInfo.Uniques.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException("A unique constraint with the same name already exists."); + } + + var uniqueConstraint = new UniqueConstraint() { KeyColumns = columns, Name = name }; + sqliteTableInfo.Uniques.Add(uniqueConstraint); + + RecreateTable(sqliteTableInfo); + } + + public override void RemoveConstraint(string table, string name) + { + var sqliteTableInfo = GetSQLiteTableInfo(table); + sqliteTableInfo.Uniques.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + sqliteTableInfo.CheckConstraints.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + + RecreateTable(sqliteTableInfo); + } + + public SQLiteTableInfo GetSQLiteTableInfo(string tableName) + { + if (!TableExists(tableName)) + { + return null; + } + + var sqliteTable = new SQLiteTableInfo + { + TableNameMapping = new MappingInfo { OldName = tableName, NewName = tableName }, + Columns = GetColumns(tableName).ToList(), + PrimaryKey = GetTableConstraints(tableName).OfType().SingleOrDefault(), + ForeignKeys = GetForeignKeyConstraints(tableName).ToList(), + Indexes = GetIndexes(tableName).ToList(), + Uniques = GetUniques(tableName).ToList(), + CheckConstraints = GetCheckConstraints(tableName) + }; + + if (sqliteTable.PrimaryKey != null) + { + var columnOrder = GetPragmaTableInfoItems(tableName).ToDictionary(c => c.Name, c => c.Cid, StringComparer.OrdinalIgnoreCase); + sqliteTable.Columns = sqliteTable.Columns.OrderBy(c => columnOrder[c.Name]).ToList(); + } + + sqliteTable.ColumnMappings = sqliteTable.Columns + .Select(x => + new MappingInfo + { + OldName = x.Name, + NewName = x.Name + }) + .ToList(); + + return sqliteTable; + } + + public bool CheckForeignKeyIntegrity() + { + + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, "PRAGMA foreign_key_check"); + + if (reader.Read()) + { + return false; + } + + return true; + } + + public bool IsPragmaForeignKeysOn() + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, "PRAGMA foreign_keys"); + reader.Read(); + var isOn = reader.GetInt32(0) == 1; + + return isOn; + } + + public void SetPragmaForeignKeys(bool isOn) + { + var onOffString = isOn ? "ON" : "OFF"; + + using var cmd = CreateCommand(); + ExecuteNonQuery($"PRAGMA foreign_keys = {onOffString}"); + } + + private static string ValidateForeignKeyAction(string action) + { + var normalized = action.ToUpperInvariant(); + if (normalized is not ("CASCADE" or "RESTRICT" or "SET NULL" or "SET DEFAULT" or "NO ACTION")) + throw new MigrationException("Unsupported foreign key action: " + action); + return normalized; + } + + public void RecreateTable(SQLiteTableInfo sqliteTableInfo) + { + foreach (var foreignKey in sqliteTableInfo.ForeignKeys) SQLiteTableSql.ValidateMatch(foreignKey.Match); + var oldName = sqliteTableInfo.TableNameMapping.OldName; + var script = GetSqlCreateTableScript(oldName); + if (SQLiteConstraintParser.HasUnsupportedRebuildFeatures(script)) + throw new NotSupportedException("This table contains SQLite features that cannot be reconstructed faithfully. Use native SQL."); + if (GetCreateIndexSqlStrings(oldName).Any(sql => SQLiteConstraintParser.HasKeyword(sql, "COLLATE"))) + throw new NotSupportedException("Rebuilding indexes with explicit collations requires native SQL."); + var triggers = ExecuteStringQuery("SELECT sql FROM sqlite_master WHERE type='trigger' AND lower(tbl_name)=lower('{0}')", oldName.Replace("'", "''")); + if (triggers.Count > 0 && (oldName != sqliteTableInfo.TableNameMapping.NewName || sqliteTableInfo.ColumnMappings.Any(m => m.OldName != null && m.OldName != m.NewName))) + throw new NotSupportedException("Use native SQLite rename when triggers reference renamed objects."); + var originalColumns = GetColumns(oldName); + if (triggers.Count > 0 && originalColumns.Any(c => !sqliteTableInfo.Columns.Any(n => n.Name.Equals(c.Name, StringComparison.OrdinalIgnoreCase)))) + throw new NotSupportedException("Removing columns from a table with triggers requires native SQLite alteration or explicit trigger recreation."); + var sequence = TableExists("sqlite_sequence") + ? ExecuteScalar("SELECT seq FROM sqlite_sequence WHERE name='" + oldName.Replace("'", "''") + "'") : null; + var highWater = sequence == null || sequence == DBNull.Value ? (long?)null : Convert.ToInt64(sequence); + var foreignKeys = IsPragmaForeignKeysOn(); + if (HasActiveTransaction && foreignKeys) + throw new MigrationException("SQLite rebuild requires foreign keys to be disabled before beginning the transaction. Use the migration runner."); + var ownsTransaction = !HasActiveTransaction; + Exception failure = null; + try + { + if (ownsTransaction) + { + if (foreignKeys) SetPragmaForeignKeys(false); + BeginTransaction(); + } + RecreateTableCore(sqliteTableInfo); + if (highWater.HasValue && sqliteTableInfo.Columns.Any(c => c.IsIdentity)) + { + var sequenceName = sqliteTableInfo.TableNameMapping.NewName.Replace("'", "''"); + var sequenceValue = highWater.Value.ToString(CultureInfo.InvariantCulture); + ExecuteNonQuery($"UPDATE sqlite_sequence SET seq=MAX(seq, {sequenceValue}) WHERE name='{sequenceName}'"); + ExecuteNonQuery($"INSERT INTO sqlite_sequence(name, seq) SELECT '{sequenceName}', {sequenceValue} WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name='{sequenceName}')"); + } + foreach (var trigger in triggers) ExecuteNonQuery(trigger); + if (ownsTransaction && !CheckForeignKeyIntegrity()) throw new MigrationException("SQLite rebuild would leave invalid foreign keys."); + if (ownsTransaction) Commit(); + } + catch (Exception ex) + { + failure = ex; + if (ownsTransaction) + { + try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } + } + throw; + } + finally + { + try { if (ownsTransaction && foreignKeys) SetPragmaForeignKeys(true); } + catch (Exception restore) { if (failure == null) throw; failure.Data["ConnectionRestoreException"] = restore; } + } + } + + private void RecreateTableCore(SQLiteTableInfo sqliteTableInfo) + { + var sourceTableQuoted = QuoteTableNameIfRequired(sqliteTableInfo.TableNameMapping.OldName); + var targetIntermediateTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}{IntermediateTableSuffix}"); + var targetTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}"); + + var columns = sqliteTableInfo.Columns.Select(c => c.CopyDefinition()).ToArray(); + var columnDbFields = columns.Cast(); + var foreignKeyDbFields = sqliteTableInfo.ForeignKeys.Cast(); + var indexDbFields = sqliteTableInfo.Indexes.Cast(); + var uniqueDbFields = sqliteTableInfo.Uniques.Cast(); + var checkConstraintDbFields = sqliteTableInfo.CheckConstraints.Cast(); + + var dbFields = columnDbFields.Concat(foreignKeyDbFields) + .Concat(uniqueDbFields) + .Concat(checkConstraintDbFields) + .Concat(sqliteTableInfo.PrimaryKey == null ? Array.Empty() : new IDbField[] { sqliteTableInfo.PrimaryKey }) + .ToArray(); + + // ToHashSet() not available in older .NET versions so we create it old-fashioned. + var uniqueColumnNames = new HashSet(sqliteTableInfo.Uniques + .SelectMany(x => x.KeyColumns) + .Distinct() + ); + + // ToHashSet() not available in older .NET versions so we create it old-fashioned. + var columnNames = new HashSet(sqliteTableInfo.Columns + .Select(x => x.Name) + ); + + // ToHashSet() not available in older .NET versions so we create it old-fashioned. + var newColumnNamesInMapping = new HashSet(sqliteTableInfo.ColumnMappings + .Select(x => x.NewName) + ); + + if (!columnNames.SetEquals(newColumnNamesInMapping)) + { + throw new Exception($"{nameof(columnNames)} and {nameof(newColumnNamesInMapping)} are not equal regarding length and content"); + } + + if (uniqueColumnNames.Except(columnNames).Any()) + { + var firstMissing = uniqueColumnNames.Except(columnNames).First(); + throw new Exception($"Detected missing column names OR unique key columns that do not exist in the column list/column mapping. E.g. {firstMissing}"); + } + + AddTable(targetIntermediateTableQuoted, null, dbFields); + + var columnMappings = sqliteTableInfo.ColumnMappings + .Where(x => x.OldName != null) + .OrderBy(x => x.OldName) + .ToList(); + + var sourceColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.OldName))); + var targetColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.NewName))); + + using (var cmd = CreateCommand()) + { + var sql = $"INSERT INTO {targetIntermediateTableQuoted} ({targetColumnsQuotedString}) SELECT {sourceColumnsQuotedString} FROM {sourceTableQuoted}"; + ExecuteNonQuery(sql); + } + + RemoveTable(sourceTableQuoted); + + using (var cmd = CreateCommand()) + { + // Rename to original name + var sql = $"ALTER TABLE {targetIntermediateTableQuoted} RENAME TO {targetTableQuoted}"; + ExecuteNonQuery(sql); + } + + foreach (var index in sqliteTableInfo.Indexes) + { + AddIndex(sqliteTableInfo.TableNameMapping.NewName, index); + } + } + + [Obsolete] + public override void AddTable(string table, string engine, string columns) + { + throw new NotSupportedException(); + } + + public override void AddColumn(string table, Column column) + { + if (!TableExists(table)) + { + throw new Exception("Table does not exist."); + } + + var sqliteInfo = GetSQLiteTableInfo(table); + + if (sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) + { + throw new Exception("Column already exists."); + } + + sqliteInfo.ColumnMappings.Add(new MappingInfo { OldName = null, NewName = column.Name }); + sqliteInfo.Columns.Add(column); + + RecreateTable(sqliteInfo); + } + + public override void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) + { + ArgumentException.ThrowIfNullOrWhiteSpace(table); + ArgumentNullException.ThrowIfNull(column); + ArgumentNullException.ThrowIfNull(primaryKey); + ArgumentException.ThrowIfNullOrWhiteSpace(primaryKey.Name); + var definition = GetSQLiteTableInfo(table); + if (definition.Columns.Any(existing => existing.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase))) + throw new MigrationException("Column already exists."); + if (definition.PrimaryKey != null) throw new MigrationException("The table already has a primary key."); + definition.Columns.Add(column.CopyDefinition()); + ValidateKeyColumns(primaryKey.Name, primaryKey.KeyColumns, definition.Columns.ToArray()); + definition.ColumnMappings.Add(new MappingInfo { NewName = column.Name }); + definition.PrimaryKey = new PrimaryKeyConstraint(primaryKey.Name, primaryKey.KeyColumns) { NonClustered = primaryKey.NonClustered }; + RecreateTable(definition); + } + + public override void RemoveUniqueConstraint(string table, UniqueConstraint constraint) + { + ArgumentException.ThrowIfNullOrWhiteSpace(table); + ArgumentNullException.ThrowIfNull(constraint); + var definition = GetSQLiteTableInfo(table); + var matches = definition.Uniques.Where(candidate => + string.Equals(candidate.Name, constraint.Name, StringComparison.OrdinalIgnoreCase) && + candidate.KeyColumns.SequenceEqual(constraint.KeyColumns, StringComparer.OrdinalIgnoreCase)).ToArray(); + if (matches.Length != 1) throw new MigrationException("Unique constraint selection must match exactly one definition."); + definition.Uniques.Remove(matches[0]); + RecreateTable(definition); + } + + public override void AddColumn(string table, string columnName, DbType type, int size) + { + var column = new Column(columnName, type, size); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type, int size) + { + var column = new Column(columnName, type, size); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type) + { + var column = new Column(columnName, type); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type) + { + var column = new Column(columnName, type); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type, object defaultValue) + { + var column = new Column(columnName, type, defaultValue); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string sqlColumn) + { + var column = new Column(sqlColumn); + AddColumn(table, column); + } + + public override void ChangeColumn(string table, Column column) + { + if (!TableExists(table)) + { + throw new Exception("Table does not exist."); + } + + var sqliteInfo = GetSQLiteTableInfo(table); + + if (!sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) + { + throw new Exception("Column does not exists."); + } + + var columnIndex = sqliteInfo.Columns.FindIndex(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); + sqliteInfo.Columns[columnIndex] = column.CopyDefinition(); + + RecreateTable(sqliteInfo); + } + + public override int TruncateTable(string table) + { + return ExecuteNonQuery(string.Format("DELETE FROM {0} ", table)); + } + + public override bool TableExists(string table) + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='table' and lower(name)=lower('{0}')", table)); + + return reader.Read(); + } + + public override bool ViewExists(string view) + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='view' and lower(name)=lower('{0}')", view)); + + return reader.Read(); + } + + public override List GetDatabases() + { + throw new NotSupportedException("SQLite is a file-based database. You cannot list other databases."); + } + + public override bool ConstraintExists(string table, string name) + { + if (!TableExists(table)) + { + throw new Exception($"Table '{table}' does not exist."); + } + + var constraintNames = GetConstraints(table); + + var exists = constraintNames.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)); + + return exists; + } + + public override string[] GetConstraints(string table) + { + var names = GetTableConstraints(table).Select(c => c.Name).Where(n => !string.IsNullOrWhiteSpace(n)).ToArray(); + if (names.Distinct(StringComparer.OrdinalIgnoreCase).Count() != names.Length) + throw new MigrationException("Duplicate constraint names in table: " + table); + return names; + } + + public override string[] GetTables() + { + var tables = new List(); + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")) + { + while (reader.Read()) + { + tables.Add((string)reader[0]); + } + } + + return [.. tables]; + } + + public override Column[] GetColumns(string tableName) + { + var pragmaTableInfoItems = GetPragmaTableInfoItems(tableName); + var tableScript = GetSqlCreateTableScript(tableName); + var collations = SQLiteConstraintParser.ColumnCollations(tableScript); + + var tableInfoPrimaryKeys = pragmaTableInfoItems.Where(x => x.Pk > 0).ToList(); + var pragmaTableInfoItemsSorted = pragmaTableInfoItems.OrderBy(x => x.Cid).ToList(); + + var columns = new List(); + + foreach (var pragmaTableInfoItem in pragmaTableInfoItemsSorted) + { + var column = new Column(pragmaTableInfoItem.Name) + { + Type = _dialect.GetDbTypeFromString(pragmaTableInfoItem.Type), + Collation = collations.TryGetValue(pragmaTableInfoItem.Name, out var collation) ? collation : null + }; + + if (pragmaTableInfoItem.NotNull) + { + column.IsNullable = false; + } + else + { + column.IsNullable = true; + } + + var defValue = pragmaTableInfoItem.DfltValue == DBNull.Value ? null : pragmaTableInfoItem.DfltValue; + + // Keep legacy text GUID defaults as text during unrelated rebuilds. New Guid + // values render as blobs, so parsing an old SQL literal into Guid would change its storage class. + column.DefaultValue = defValue is string sqlDefault + ? column.Type == DbType.Guid ? RawSql.Insert(sqlDefault) : CatalogDefaultValue.Parse(sqlDefault, column.Type) + : defValue; + + var columnTableInfoItem = pragmaTableInfoItems.First(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); + + var hasCompoundPrimaryKey = tableInfoPrimaryKeys.Count > 1; + + // Implicit in SQLite + if (columnTableInfoItem.Type == "INTEGER" && columnTableInfoItem.Pk == 1 && !hasCompoundPrimaryKey && Regex.IsMatch(tableScript, @"\bAUTOINCREMENT\b", RegexOptions.IgnoreCase)) + { + column.IsIdentity = true; + } + + columns.Add(column); + } + + + return [.. columns]; + } + + public bool IsNullable(string columnDef) + { + return !columnDef.Contains("NOT NULL"); + } + + public bool ColumnMatch(string column, string columnDef) + { + return columnDef.StartsWith(column + " ") || columnDef.StartsWith(_dialect.QuoteIdentifier(column)); + } + + public override bool IndexExists(string table, string name) + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='index' and lower(name)=lower('{0}')", name)); + + return reader.Read(); + } + + public override Index[] GetIndexes(string table) + { + var afterWhereRegex = new Regex("(?<= WHERE ).+"); + List indexes = []; + + var indexCreateScripts = GetCreateIndexSqlStrings(table); + + var pragmaIndexListItems = GetPragmaIndexListItems(table).Where(x => x.Origin == "c"); + + var columns = GetColumns(table); + + foreach (var pragmaIndexListItem in pragmaIndexListItems) + { + var indexInfos = GetPragmaIndexInfo(pragmaIndexListItem.Name); + + var columnNames = indexInfos.OrderBy(x => x.SeqNo) + .Select(x => x.Name) + .ToArray(); + + var index = new Index + { + // At this moment in time the migrator does not support clustered indexes for SQLITE + // Since SQLite 3.8.2 WITHOUT ROWID is supported but not in this migrator + Clustered = false, + + // SQLite does not support include colums + IncludeColumns = [], + KeyColumns = columnNames, + Name = pragmaIndexListItem.Name, + Unique = pragmaIndexListItem.Unique + }; + + var script = indexCreateScripts.FirstOrDefault(x => x.Contains(pragmaIndexListItem.Name, StringComparison.OrdinalIgnoreCase)); + + if (script != null) + { + if (afterWhereRegex.Match(script) is Match match && match.Success) + { + // We cannot use GeneratedRegexAttribute due to old .NET version + var andSplitted = Regex.Split(match.Value, " AND "); + + var filterSingleStrings = andSplitted + .Select(x => x.Trim()) + .ToList(); + + foreach (var filterSingleString in filterSingleStrings) + { + var splitted = filterSingleString.Split(' ') + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .ToList(); + + var filterItem = new FilterItem { ColumnName = splitted[0], Filter = _dialect.GetFilterTypeByComparisonString(splitted[1]) }; + + var column = columns.Single(x => x.Name.Equals(splitted[0], StringComparison.OrdinalIgnoreCase)); + + var sqliteIntegerDataTypes = new[] { + MigratorDbType.Int16, + MigratorDbType.Int32, + MigratorDbType.Int64, + MigratorDbType.UInt16, + MigratorDbType.UInt32, + MigratorDbType.UInt64 + }; + + if (sqliteIntegerDataTypes.Contains(column.MigratorDbType)) + { + if (long.TryParse(splitted[2], out var longValue)) + { + filterItem.Value = longValue; + } + else if (ulong.TryParse(splitted[2], out var uLongValue)) + { + filterItem.Value = uLongValue; + } + else + { + throw new Exception(); + } + } + else + { + filterItem.Value = column.MigratorDbType switch + { + MigratorDbType.Boolean => splitted[2] == "1" || splitted[2].Equals("true", StringComparison.OrdinalIgnoreCase), + MigratorDbType.String => splitted[2].Substring(1, splitted[2].Length - 2), + _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + }; + } + + index.FilterItems.Add(filterItem); + } + } + } + + indexes.Add(index); + } + + return [.. indexes]; + } + + public override void AddTable(string name, string engine, params IDbField[] fields) + { + if (engine != null) throw new NotSupportedException("SQLite does not support table engines."); + var table = _dialect.TableNameNeedsQuote ? _dialect.Quote(name) : QuoteTableNameIfRequired(name); + ExecuteNonQuery(SQLiteTableSql.Generate(_dialect, table, fields)); + foreach (var index in fields.OfType()) AddIndex(name, index); + } + + public override string AddIndex(string table, Index index) + { + ValidateIndex(table, index); + + var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; + + if (hasIncludedColumns) + { + // This will be actived in the future. + // throw new MigrationException($"SQLite does not support included columns. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); + } + + if (index.Clustered) + { + throw new MigrationException($"For SQLite this migrator does not support clustered indexes at this point in time, sorry. File an issue if needed. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); + } + + var name = QuoteConstraintNameIfRequired(index.Name); + table = QuoteTableNameIfRequired(table); + var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + + var uniqueString = index.Unique ? "UNIQUE" : null; + var columnsString = $"({string.Join(", ", columns)})"; + var filterString = string.Empty; + + if (index.FilterItems != null && index.FilterItems.Count > 0) + { + List singleFilterStrings = []; + + foreach (var filterItem in index.FilterItems) + { + var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); + + var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); + string value = null; + + value = filterItem.Value switch + { + bool booleanValue => booleanValue ? "1" : "0", + string stringValue => $"'{stringValue.Replace("'", "''")}'", + byte or short or int or long => Convert.ToInt64(filterItem.Value).ToString(), + sbyte or ushort or uint or ulong => Convert.ToUInt64(filterItem.Value).ToString(), + _ => throw new NotImplementedException("Given type is not implemented. Please file an issue."), + }; + + if ((filterItem.Value is string || filterItem.Value is bool) && filterItem.Filter != FilterType.EqualTo && filterItem.Filter != FilterType.NotEqualTo) + { + throw new MigrationException($"Bool and string in {nameof(FilterItem)} can only be used with '{nameof(FilterType.EqualTo)}' or '{nameof(FilterType.EqualTo)}'."); + } + + var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; + + singleFilterStrings.Add(singleFilterString); + } + + filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; + } + + List list = ["CREATE", uniqueString, "INDEX", name, "ON", table, columnsString, filterString]; + + var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); + + ExecuteNonQuery(sql); + + return sql; + } + + protected override string GetPrimaryKeyConstraintName(string table) + { + return GetTableConstraints(table).OfType().SingleOrDefault()?.Name; + } + + public override void RemoveAllConstraints(string table) + { + var info = GetSQLiteTableInfo(table); + info.PrimaryKey = null; + info.Uniques.Clear(); + info.ForeignKeys.Clear(); + info.CheckConstraints.Clear(); + foreach (var column in info.Columns) column.IsIdentity = false; + RecreateTable(info); + } + + public override void RemovePrimaryKey(string tableName) + { + if (!TableExists(tableName)) return; + var info = GetSQLiteTableInfo(tableName); + info.PrimaryKey = null; + foreach (var column in info.Columns) column.IsIdentity = false; + RecreateTable(info); + } + + public override void RemoveAllIndexes(string tableName) + { + if (!TableExists(tableName)) + { + return; + } + + var sqliteInfoTable = GetSQLiteTableInfo(tableName); + + sqliteInfoTable.Indexes = []; + + RecreateTable(sqliteInfoTable); + } + + public List GetUniques(string tableName) => GetTableConstraints(tableName) + .OfType().ToList(); + + public List GetPragmaIndexInfo(string indexNameNotQuoted) + { + List pragmaIndexInfoItems = []; + + var quotedIndexName = QuoteTableNameIfRequired(indexNameNotQuoted); + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA index_info({quotedIndexName})")) + { + while (reader.Read()) + { + var pragmaIndexInfoItem = new PragmaIndexInfoItem + { + SeqNo = reader.GetInt32(reader.GetOrdinal("seqno")), + Cid = reader.GetInt32(reader.GetOrdinal("cid")), + Name = reader.GetString(reader.GetOrdinal("name")), + }; + + pragmaIndexInfoItems.Add(pragmaIndexInfoItem); + } + } + + return pragmaIndexInfoItems; + } + + public List GetPragmaIndexListItems(string tableNameNotQuoted) + { + List pragmaIndexListItems = []; + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA index_list({QuoteTableNameIfRequired(tableNameNotQuoted)})")) + { + while (reader.Read()) + { + var pragmaIndexListItem = new PragmaIndexListItem + { + Seq = reader.GetInt32(reader.GetOrdinal("seq")), + Name = reader.GetString(reader.GetOrdinal("name")), + Unique = reader.GetInt32(reader.GetOrdinal("unique")) == 1, + Origin = reader.GetString(reader.GetOrdinal("origin")), + Partial = reader.GetInt32(reader.GetOrdinal("partial")) == 1 + }; + + pragmaIndexListItems.Add(pragmaIndexListItem); + } + } + + return pragmaIndexListItems; + } + + public List GetPragmaTableInfoItems(string tableNameNotQuoted) + { + List pragmaTableInfoItems = []; + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA table_info({QuoteTableNameIfRequired(tableNameNotQuoted)})")) + { + while (reader.Read()) + { + var pragmaTableInfoItem = new PragmaTableInfoItem + { + Cid = reader.GetInt32(reader.GetOrdinal("cid")), + DfltValue = reader[reader.GetOrdinal("dflt_value")], + Name = reader.GetString(reader.GetOrdinal("name")), + NotNull = reader.GetInt32(reader.GetOrdinal("notnull")) == 1, + Pk = reader.GetInt32(reader.GetOrdinal("pk")), + Type = reader.GetString(reader.GetOrdinal("type")), + }; + + pragmaTableInfoItems.Add(pragmaTableInfoItem); + } + } + + return pragmaTableInfoItems; + } + + public override void AddCheckConstraint(string constraintName, string tableName, string checkSql) + { + var sqliteTableInfo = GetSQLiteTableInfo(tableName); + + var checkConstraint = new CheckConstraint(constraintName, checkSql); + sqliteTableInfo.CheckConstraints.Add(checkConstraint); + + RecreateTable(sqliteTableInfo); + } + + public override void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) + { + orderBySourceColumns ??= []; + + if (!TableExists(sourceTableName)) + { + throw new Exception($"Source table '{QuoteTableNameIfRequired(sourceTableName)}' does not exist"); + } + + if (!TableExists(targetTableName)) + { + throw new Exception($"Target table '{QuoteTableNameIfRequired(targetTableName)}' does not exist"); + } + + var sourceColumnsConcatenated = sourceColumnNames.Concat(orderBySourceColumns); + + foreach (var column in sourceColumnsConcatenated) + { + if (!ColumnExists(sourceTableName, column)) + { + throw new Exception($"Column {column} in source table does not exist."); + } + } + + foreach (var column in targetColumnNames) + { + if (!ColumnExists(targetTableName, column)) + { + throw new Exception($"Column {column} in target table does not exist."); + } + } + + if (!orderBySourceColumns.All(x => sourceColumnNames.Contains(x))) + { + throw new Exception($"All columns in {nameof(orderBySourceColumns)} must be in {nameof(sourceColumnNames)}"); + } + + var sourceTableNameQuoted = QuoteTableNameIfRequired(sourceTableName); + var targetTableNameQuoted = QuoteTableNameIfRequired(targetTableName); + + var sourceColumnNamesQuoted = sourceColumnNames.Select(QuoteColumnNameIfRequired).ToList(); + var targetColumnNamesQuoted = targetColumnNames.Select(QuoteColumnNameIfRequired).ToList(); + var orderBySourceColumnsQuoted = orderBySourceColumns.Select(QuoteColumnNameIfRequired).ToList(); + + var sourceColumnsJoined = string.Join(", ", sourceColumnNamesQuoted); + var targetColumnsJoined = string.Join(", ", targetColumnNamesQuoted); + var orderBySourceColumnsJoined = string.Join(", ", orderBySourceColumnsQuoted); + + var orderByComponent = !string.IsNullOrWhiteSpace(orderBySourceColumnsJoined) ? $"ORDER BY {orderBySourceColumnsJoined}" : null; + + List sqlComponents = + [ + $"INSERT INTO {targetTableNameQuoted} ({targetColumnsJoined}) SELECT {sourceColumnsJoined} FROM {sourceTableNameQuoted}", + orderByComponent + ]; + + var sql = string.Join(" ", sqlComponents.Where(x => x != null)); + ExecuteNonQuery(sql); + } + + public List GetCheckConstraints(string tableName) => GetTableConstraints(tableName).OfType().ToList(); + + protected override void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) + { + if (value is TimeOnly time) + { + // SQLite stores times as text; System.Data.SQLite cannot bind TimeSpan as DbType.Time. + parameter.DbType = DbType.String; + parameter.Value = time.ToString("HH:mm:ss.fffffff", CultureInfo.InvariantCulture); + } + else if (value is ushort) + { + parameter.DbType = DbType.Int32; + parameter.Value = Convert.ToInt32(value); + } + else if (value is uint) + { + parameter.DbType = DbType.Int64; + parameter.Value = Convert.ToInt64(value); + } + else if (value is ulong unsigned) + { + // SQLite INTEGER cannot represent the upper half of UInt64. Do not let + // a driver wrap it to a negative integer or round it through a REAL. + parameter.DbType = DbType.Int64; + parameter.Value = checked((long)unsigned); + } + else if (value is Guid || value is Guid?) + { + parameter.DbType = DbType.Binary; + parameter.Value = ((Guid)value).ToByteArray(); + } + else + { + base.ConfigureParameterWithValue(parameter, index, value); + } + } +} diff --git a/src/Migrator/Providers/NoOpTransformationProvider.cs b/src/Migrator/Providers/NoOpTransformationProvider.cs index b81592b2..6165c5eb 100644 --- a/src/Migrator/Providers/NoOpTransformationProvider.cs +++ b/src/Migrator/Providers/NoOpTransformationProvider.cs @@ -1,572 +1,575 @@ -using System; -using System.Collections.Generic; -using System.Data; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Models; - -using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; -using Index = DotNetProjects.Migrator.Framework.Index; - -namespace DotNetProjects.Migrator.Providers; - -/// -/// No Op (Null Object Pattern) implementation of the ITransformationProvider -/// -public class NoOpTransformationProvider : ITransformationProvider -{ - public TableConstraint[] GetTableConstraints(string table) => []; - - public static readonly NoOpTransformationProvider Instance = new NoOpTransformationProvider(); - - private NoOpTransformationProvider() - { - } - - public int? CommandTimeout { get; set; } - - public IDialect Dialect - { - get { return null; } - } - - public bool IsMigrationApplied(long version, string scope) - { - throw new NotImplementedException(); - } - - public string ConnectionString - { - get { return string.Empty; } - } - - public virtual ILogger Logger - { - get { return null; } - set { } - } - - public string[] GetTables() - { - return null; - } - - public ForeignKeyConstraint[] GetForeignKeyConstraints(string table) - { - return null; - } - - public int Insert(string table, string[] columns, object[] values) - { - return 0; - } - - public int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - return 0; - } - - public List ExecuteStringQuery(string sql, params object[] args) - { - return new List(); - } - - public Index[] GetIndexes(string table) - { - return null; - } - - public Column[] GetColumns(string table) - { - return null; - } - - public Column GetColumnByName(string table, string column) - { - return null; - } - - public void RemoveForeignKey(string table, string name) - { - // No Op - } - - public void RemoveConstraint(string table, string name) - { - // No Op - } - - public void RemoveAllConstraints(string table) - { - // No Op - } - - public void RemovePrimaryKey(string table) - { - // No Op - } - - public void AddView(string name, string tableName, params IViewElement[] viewElements) - { - // No Op - } - - public void AddView(string name, string tableName, params IViewField[] fields) - { - throw new NotImplementedException(); - } - - public void AddTable(string name, params IDbField[] columns) - { - // No Op - } - - public void AddTable(string name, string engine, params IDbField[] columns) - { - // No Op - } - - public void RemoveTable(string name) - { - // No Op - } - - public void RenameTable(string oldName, string newName) - { - // No Op - } - - public void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - // No Op - } - - public void RemoveColumn(string table, string column) - { - // No Op - } - - public void RemoveColumnDefaultValue(string table, string column) - { - // No Op - } - - public bool ColumnExists(string table, string column) - { - return false; - } - - public bool TableExists(string table) - { - return false; - } - - public bool ViewExists(string view) - { - return false; - } - - public void AddColumn(string table, string column, DbType type) - { - // No Op - } - - public void AddColumn(string table, string column, DbType type, object defaultValue) - { - // No Op - } - - public void AddColumn(string table, string column, DbType type, int size) - { - // No Op - } - - public void AddPrimaryKey(string name, string table, params string[] columns) - { - // No Op - } - public void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) - { - // No Op - } - public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, - string[] refColumns, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, - string refColumn) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, - string[] refColumns, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void AddUniqueConstraint(string name, string table, params string[] columns) - { - // No Op - } - - public void AddCheckConstraint(string name, string table, string checkSql) - { - // No Op - } - - public bool ConstraintExists(string table, string name) - { - return false; - } - - public void ChangeColumn(string table, Column column) - { - // No Op - } - - public bool PrimaryKeyExists(string table, string name) - { - return false; - } - - public int ExecuteNonQuery(string sql) - { - return 0; - } - public int ExecuteNonQuery(string sql, int timeout) - { - return 0; - } - public int ExecuteNonQuery(string sql, int timeout, object[] parameters) - { - return 0; - } - - public IDataReader ExecuteQuery(IDbCommand cmd, string sql) - { - return null; - } - - public IDbCommand CreateCommand() - { - throw new NotImplementedException(); - } - - public object ExecuteScalar(string sql) - { - return null; - } - - public IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns, object[] whereValues) - { - return null; - } - - public IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, - object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) - { - return null; - } - - public IDataReader Select(IDbCommand cmd, string what, string from) - { - return null; - } - - public IDataReader Select(IDbCommand cmd, string what, string from, string where) - { - return null; - } - - public object SelectScalar(string what, string from) - { - return null; - } - - public object SelectScalar(string what, string from, string where) - { - return null; - } - - public int Update(string table, string[] columns, object[] values) - { - return 0; - } - - public int Update(string table, string[] columns, object[] values, string where) - { - return 0; - } - - public int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - return 0; - } - - public int Delete(string table, string[] columns = null, object[] columnValues = null) - { - return 0; - } - - public int Delete(string table, string column, string value) - { - return 0; - } - - public int TruncateTable(string table) - { - return 0; - } - - public void BeginTransaction() - { - // No Op - } - - public void Rollback() - { - // No Op - } - - public void Commit() - { - // No Op - } - - public ITransformationProvider this[string provider] - { - get { return this; } - } - - public string SchemaInfoTable { get; set; } - - public void MigrationApplied(long version, string scope) - { - //no op - } - - public void MigrationUnApplied(long version, string scope) - { - //no op - } - - public List AppliedMigrations - { - get { return new List(); } - } - - public void AddColumn(string table, Column column) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string refTable) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) - { - // No Op - } - - public IDbCommand GetCommand() - { - return null; - } - - public void RemoveAllForeignKeys(string tableName, string columnName) - { - - } - - public bool IsThisProvider(string provider) - { - return false; - } - - public string[] QuoteColumnNamesIfRequired(params string[] columnNames) - { - throw new NotImplementedException(); - } - - public string QuoteColumnNameIfRequired(string name) - { - throw new NotImplementedException(); - } - - public string QuoteTableNameIfRequired(string name) - { - throw new NotImplementedException(); - } - - public string Encode(Guid guid) - { - return guid.ToString(); - } - - public void SwitchDatabase(string databaseName) - { - - } - - public List GetDatabases() - { - return new List(); - } - - public bool DatabaseExists(string name) - { - return true; - } - - public void CreateDatabases(string databaseName) - { - - } - - public void KillDatabaseConnections(string databaseName) - { - - } - - public void DropDatabases(string databaseName) - { - - } - - public string AddIndex(string table, Index index) - { - // Don't know what this is for... - - return string.Empty; - } - - public void Dispose() - { - //No Op - } - - public void AddColumn(string table, string sqlColumn) - { - // No Op - } - - public int Insert(string table, string[] columns, string[] columnValues) - { - return 0; - } - - protected void CreateSchemaInfoTable() - { - } - - public void RemoveIndex(string table, string name) - { - // No Op - } - - public string AddIndex(string name, string table, params string[] columns) - { - // No Op - - // Don't know what this is for... - - return string.Empty; - } - - public bool IndexExists(string table, string name) - { - return false; - } - - public string GenerateParameterName(int index) - { - return "@p" + index; - } - - public void RemoveAllIndexes(string table) - { - // No Op - } - - public string Concatenate(params string[] strings) - { - return ""; - } - - public IDbConnection Connection - { - get - { - return null; - } - } - - public IEnumerable GetTables(string schema) - { - throw new NotImplementedException(); - } - - public IEnumerable GetColumns(string schema, string table) - { - throw new NotImplementedException(); - } - - public int GetColumnContentSize(string table, string columnName) - { - throw new NotImplementedException(); - } - - public void AddColumn(string table, string column, MigratorDbType type) - { - throw new NotImplementedException(); - } - - public void AddColumn(string table, string column, MigratorDbType type, int size) - { - throw new NotImplementedException(); - } - - public void AddColumn(string table, string column, MigratorDbType type, object defaultValue) - { - throw new NotImplementedException(); - } - - public void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) - { - throw new NotImplementedException(); - } - - public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns) - { - throw new NotImplementedException(); - } -} +using System; +using System.Collections.Generic; +using System.Data; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Models; + +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; +using Index = DotNetProjects.Migrator.Framework.Index; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; + +namespace DotNetProjects.Migrator.Providers; + +/// +/// No Op (Null Object Pattern) implementation of the ITransformationProvider +/// +public class NoOpTransformationProvider : ITransformationProvider +{ + public TableConstraint[] GetTableConstraints(string table) => []; + public void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) { } + public void RemoveUniqueConstraint(string table, UniqueConstraint constraint) { } + + public static readonly NoOpTransformationProvider Instance = new NoOpTransformationProvider(); + + private NoOpTransformationProvider() + { + } + + public int? CommandTimeout { get; set; } + + public IDialect Dialect + { + get { return null; } + } + + public bool IsMigrationApplied(long version, string scope) + { + throw new NotImplementedException(); + } + + public string ConnectionString + { + get { return string.Empty; } + } + + public virtual ILogger Logger + { + get { return null; } + set { } + } + + public string[] GetTables() + { + return null; + } + + public ForeignKeyConstraint[] GetForeignKeyConstraints(string table) + { + return null; + } + + public int Insert(string table, string[] columns, object[] values) + { + return 0; + } + + public int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + return 0; + } + + public List ExecuteStringQuery(string sql, params object[] args) + { + return new List(); + } + + public Index[] GetIndexes(string table) + { + return null; + } + + public Column[] GetColumns(string table) + { + return null; + } + + public Column GetColumnByName(string table, string column) + { + return null; + } + + public void RemoveForeignKey(string table, string name) + { + // No Op + } + + public void RemoveConstraint(string table, string name) + { + // No Op + } + + public void RemoveAllConstraints(string table) + { + // No Op + } + + public void RemovePrimaryKey(string table) + { + // No Op + } + + public void AddView(string name, string tableName, params IViewElement[] viewElements) + { + // No Op + } + + public void AddView(string name, string tableName, params IViewField[] fields) + { + throw new NotImplementedException(); + } + + public void AddTable(string name, params IDbField[] columns) + { + // No Op + } + + public void AddTable(string name, string engine, params IDbField[] columns) + { + // No Op + } + + public void RemoveTable(string name) + { + // No Op + } + + public void RenameTable(string oldName, string newName) + { + // No Op + } + + public void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + // No Op + } + + public void RemoveColumn(string table, string column) + { + // No Op + } + + public void RemoveColumnDefaultValue(string table, string column) + { + // No Op + } + + public bool ColumnExists(string table, string column) + { + return false; + } + + public bool TableExists(string table) + { + return false; + } + + public bool ViewExists(string view) + { + return false; + } + + public void AddColumn(string table, string column, DbType type) + { + // No Op + } + + public void AddColumn(string table, string column, DbType type, object defaultValue) + { + // No Op + } + + public void AddColumn(string table, string column, DbType type, int size) + { + // No Op + } + + public void AddPrimaryKey(string name, string table, params string[] columns) + { + // No Op + } + public void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) + { + // No Op + } + public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, + string[] refColumns, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, + string refColumn) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, + string[] refColumns, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void AddUniqueConstraint(string name, string table, params string[] columns) + { + // No Op + } + + public void AddCheckConstraint(string name, string table, string checkSql) + { + // No Op + } + + public bool ConstraintExists(string table, string name) + { + return false; + } + + public void ChangeColumn(string table, Column column) + { + // No Op + } + + public bool PrimaryKeyExists(string table, string name) + { + return false; + } + + public int ExecuteNonQuery(string sql) + { + return 0; + } + public int ExecuteNonQuery(string sql, int timeout) + { + return 0; + } + public int ExecuteNonQuery(string sql, int timeout, object[] parameters) + { + return 0; + } + + public IDataReader ExecuteQuery(IDbCommand cmd, string sql) + { + return null; + } + + public IDbCommand CreateCommand() + { + throw new NotImplementedException(); + } + + public object ExecuteScalar(string sql) + { + return null; + } + + public IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns, object[] whereValues) + { + return null; + } + + public IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, + object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) + { + return null; + } + + public IDataReader Select(IDbCommand cmd, string what, string from) + { + return null; + } + + public IDataReader Select(IDbCommand cmd, string what, string from, string where) + { + return null; + } + + public object SelectScalar(string what, string from) + { + return null; + } + + public object SelectScalar(string what, string from, string where) + { + return null; + } + + public int Update(string table, string[] columns, object[] values) + { + return 0; + } + + public int Update(string table, string[] columns, object[] values, string where) + { + return 0; + } + + public int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + return 0; + } + + public int Delete(string table, string[] columns = null, object[] columnValues = null) + { + return 0; + } + + public int Delete(string table, string column, string value) + { + return 0; + } + + public int TruncateTable(string table) + { + return 0; + } + + public void BeginTransaction() + { + // No Op + } + + public void Rollback() + { + // No Op + } + + public void Commit() + { + // No Op + } + + public ITransformationProvider this[string provider] + { + get { return this; } + } + + public string SchemaInfoTable { get; set; } + + public void MigrationApplied(long version, string scope) + { + //no op + } + + public void MigrationUnApplied(long version, string scope) + { + //no op + } + + public List AppliedMigrations + { + get { return new List(); } + } + + public void AddColumn(string table, Column column) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string refTable) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) + { + // No Op + } + + public IDbCommand GetCommand() + { + return null; + } + + public void RemoveAllForeignKeys(string tableName, string columnName) + { + + } + + public bool IsThisProvider(string provider) + { + return false; + } + + public string[] QuoteColumnNamesIfRequired(params string[] columnNames) + { + throw new NotImplementedException(); + } + + public string QuoteColumnNameIfRequired(string name) + { + throw new NotImplementedException(); + } + + public string QuoteTableNameIfRequired(string name) + { + throw new NotImplementedException(); + } + + public string Encode(Guid guid) + { + return guid.ToString(); + } + + public void SwitchDatabase(string databaseName) + { + + } + + public List GetDatabases() + { + return new List(); + } + + public bool DatabaseExists(string name) + { + return true; + } + + public void CreateDatabases(string databaseName) + { + + } + + public void KillDatabaseConnections(string databaseName) + { + + } + + public void DropDatabases(string databaseName) + { + + } + + public string AddIndex(string table, Index index) + { + // Don't know what this is for... + + return string.Empty; + } + + public void Dispose() + { + //No Op + } + + public void AddColumn(string table, string sqlColumn) + { + // No Op + } + + public int Insert(string table, string[] columns, string[] columnValues) + { + return 0; + } + + protected void CreateSchemaInfoTable() + { + } + + public void RemoveIndex(string table, string name) + { + // No Op + } + + public string AddIndex(string name, string table, params string[] columns) + { + // No Op + + // Don't know what this is for... + + return string.Empty; + } + + public bool IndexExists(string table, string name) + { + return false; + } + + public string GenerateParameterName(int index) + { + return "@p" + index; + } + + public void RemoveAllIndexes(string table) + { + // No Op + } + + public string Concatenate(params string[] strings) + { + return ""; + } + + public IDbConnection Connection + { + get + { + return null; + } + } + + public IEnumerable GetTables(string schema) + { + throw new NotImplementedException(); + } + + public IEnumerable GetColumns(string schema, string table) + { + throw new NotImplementedException(); + } + + public int GetColumnContentSize(string table, string columnName) + { + throw new NotImplementedException(); + } + + public void AddColumn(string table, string column, MigratorDbType type) + { + throw new NotImplementedException(); + } + + public void AddColumn(string table, string column, MigratorDbType type, int size) + { + throw new NotImplementedException(); + } + + public void AddColumn(string table, string column, MigratorDbType type, object defaultValue) + { + throw new NotImplementedException(); + } + + public void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) + { + throw new NotImplementedException(); + } + + public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns) + { + throw new NotImplementedException(); + } +} diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 8e0b36b0..4b5a6299 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -1,1040 +1,1040 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Loggers; -using DotNetProjects.Migrator.Framework.Models; - -using DotNetProjects.Migrator.Providers.Impl.SQLite; -using DotNetProjects.Migrator.Providers.Models; -using System; -using System.Collections.Generic; -using System.Data; -using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; -using System.Data.Common; -using System.IO; -using System.Linq; -using System.Text; -using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; -using ForeignKeyConstraintType = DotNetProjects.Migrator.Framework.ForeignKeyConstraintType; -using Index = DotNetProjects.Migrator.Framework.Index; - -namespace DotNetProjects.Migrator.Providers; - -/// -/// Base class for every transformation providers. -/// A 'tranformation' is an operation that modifies the database. -/// -public abstract class TransformationProvider : ITransformationProvider, IMigrationHistory, IForeignKeyActions -{ - private string _scope; - protected readonly string _connectionString; - protected readonly string _defaultSchema; - private readonly ForeignKeyConstraintMapper constraintMapper = new(); - protected List _appliedMigrations; - protected IDbConnection _connection; - protected bool _outsideConnection = false; - protected Dialect _dialect; - private ILogger _logger; - private IDbTransaction _transaction; - - protected TransformationProvider(Dialect dialect, string connectionString, string defaultSchema, string scope) - { - _dialect = dialect; - _connectionString = connectionString; - _defaultSchema = defaultSchema; - _logger = new Logger(false); - _scope = scope; - } - - protected TransformationProvider(Dialect dialect, IDbConnection connection, string defaultSchema, string scope) - { - _dialect = dialect; - _connection = connection; - _outsideConnection = true; - _defaultSchema = defaultSchema; - _logger = new Logger(false); - _scope = scope; - } - - public IMigration CurrentMigration { get; set; } - - private string _schemaInfotable = "SchemaInfo"; - public string SchemaInfoTable - { - get - { - return _schemaInfotable; - } - set - { - _schemaInfotable = value; - InvalidateHistory(); - } - } - - public int? CommandTimeout { get; set; } - - public IDialect Dialect - { - get { return _dialect; } - } - - public string ConnectionString { get { return _connectionString; } } - - /// - /// Returns the event logger - /// - public virtual ILogger Logger - { - get { return _logger; } - set { _logger = value; } - } - - public virtual ITransformationProvider this[string provider] - { - get - { - if (null != provider && IsThisProvider(provider)) - { - return this; - } - - return NoOpTransformationProvider.Instance; - } - } - - public virtual Index[] GetIndexes(string table) - { - throw new NotImplementedException(); - } - - public virtual Column[] GetColumns(string table) - { - var columns = new List(); - using (var cmd = CreateCommand()) - using ( - var reader = - ExecuteQuery( - cmd, string.Format("select COLUMN_NAME, IS_NULLABLE from INFORMATION_SCHEMA.COLUMNS where table_name = '{0}'", table))) - { - while (reader.Read()) - { - var column = new Column(reader.GetString(0), DbType.String); - var nullableStr = reader.GetString(1); - var isNullable = nullableStr == "YES"; - column.IsNullable = isNullable; - - columns.Add(column); - } - } - - return columns.ToArray(); - } - - /// - /// Basic implementation works for Postgre and probably for MySQL (not tested). For Oracle it should be overridden - /// - /// - /// - /// - public virtual ForeignKeyConstraint[] GetForeignKeyConstraints(string table) => ForeignKeyMetadataReader.Read(this, table); - - public virtual TableConstraint[] GetTableConstraints(string table) => ConstraintMetadataReader.Read(this, table); - - public virtual string[] GetConstraints(string table) - { - var constraints = new List(); - using (var cmd = CreateCommand()) - using ( - var reader = - ExecuteQuery( - cmd, string.Format("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE LOWER(TABLE_NAME) = LOWER('{0}')", table))) - { - while (reader.Read()) - { - constraints.Add(reader.GetString(0)); - } - } - - return constraints.ToArray(); - } - - public virtual Column GetColumnByName(string table, string columnName) - { - var columns = GetColumns(table); - var column = columns.FirstOrDefault(x => x.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase)) ?? - throw new Exception($"Cannot find column '{columnName}' in table '{table}'"); - - return column; - } - - public virtual int GetColumnContentSize(string table, string columnName) - { - var result = this.ExecuteScalar("SELECT MAX(LENGTH(" + this.QuoteColumnNameIfRequired(columnName) + ")) FROM " + this.QuoteTableNameIfRequired(table)); - - if (result == DBNull.Value) - { - return 0; - } - - return Convert.ToInt32(result); - } - - public virtual string[] GetTables() - { - var tables = new List(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, "SELECT table_name FROM INFORMATION_SCHEMA.TABLES")) - { - while (reader.Read()) - { - tables.Add((string)reader[0]); - } - } - return tables.ToArray(); - } - - public virtual void RemoveForeignKey(string table, string name) - { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - RemoveConstraint(table, name); - } - - public virtual void RemoveConstraint(string table, string name) - { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{name}' does not exist"); - } - - if (!ConstraintExists(table, name)) - { - throw new MigrationException($"Constraint '{name}' does not exist"); - } - - var names = GetConstraints(table); - var actual = names.FirstOrDefault(n => n == name) - ?? names.SingleOrDefault(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)) - ?? throw new MigrationException("Constraint was not found in the requested table: " + name); - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP CONSTRAINT {1}", QuoteTableNameIfRequired(table), _dialect.QuoteIdentifier(actual))); - } - - public virtual void RemoveAllConstraints(string table) - { - foreach (var constraint in GetConstraints(table)) - { - RemoveConstraint(table, constraint); - } - } - - public virtual void AddView(string name, string tableName, params IViewField[] fields) - { - var lst = - fields.Where(x => string.IsNullOrEmpty(x.TableName) || x.TableName == tableName) +#region License + +//The contents of this file are subject to the Mozilla Public License +//Version 1.1 (the "License"); you may not use this file except in +//compliance with the License. You may obtain a copy of the License at +//http://www.mozilla.org/MPL/ +//Software distributed under the License is distributed on an "AS IS" +//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +//License for the specific language governing rights and limitations +//under the License. + +#endregion + +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using DotNetProjects.Migrator.Framework.Models; + +using DotNetProjects.Migrator.Providers.Impl.SQLite; +using DotNetProjects.Migrator.Providers.Models; +using System; +using System.Collections.Generic; +using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Text; +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; +using ForeignKeyConstraintType = DotNetProjects.Migrator.Framework.ForeignKeyConstraintType; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace DotNetProjects.Migrator.Providers; + +/// +/// Base class for every transformation providers. +/// A 'tranformation' is an operation that modifies the database. +/// +public abstract class TransformationProvider : ITransformationProvider, IMigrationHistory, IForeignKeyActions +{ + private string _scope; + protected readonly string _connectionString; + protected readonly string _defaultSchema; + private readonly ForeignKeyConstraintMapper constraintMapper = new(); + protected List _appliedMigrations; + protected IDbConnection _connection; + protected bool _outsideConnection = false; + protected Dialect _dialect; + private ILogger _logger; + private IDbTransaction _transaction; + + protected TransformationProvider(Dialect dialect, string connectionString, string defaultSchema, string scope) + { + _dialect = dialect; + _connectionString = connectionString; + _defaultSchema = defaultSchema; + _logger = new Logger(false); + _scope = scope; + } + + protected TransformationProvider(Dialect dialect, IDbConnection connection, string defaultSchema, string scope) + { + _dialect = dialect; + _connection = connection; + _outsideConnection = true; + _defaultSchema = defaultSchema; + _logger = new Logger(false); + _scope = scope; + } + + public IMigration CurrentMigration { get; set; } + + private string _schemaInfotable = "SchemaInfo"; + public string SchemaInfoTable + { + get + { + return _schemaInfotable; + } + set + { + _schemaInfotable = value; + InvalidateHistory(); + } + } + + public int? CommandTimeout { get; set; } + + public IDialect Dialect + { + get { return _dialect; } + } + + public string ConnectionString { get { return _connectionString; } } + + /// + /// Returns the event logger + /// + public virtual ILogger Logger + { + get { return _logger; } + set { _logger = value; } + } + + public virtual ITransformationProvider this[string provider] + { + get + { + if (null != provider && IsThisProvider(provider)) + { + return this; + } + + return NoOpTransformationProvider.Instance; + } + } + + public virtual Index[] GetIndexes(string table) + { + throw new NotImplementedException(); + } + + public virtual Column[] GetColumns(string table) + { + var columns = new List(); + using (var cmd = CreateCommand()) + using ( + var reader = + ExecuteQuery( + cmd, string.Format("select COLUMN_NAME, IS_NULLABLE from INFORMATION_SCHEMA.COLUMNS where table_name = '{0}'", table))) + { + while (reader.Read()) + { + var column = new Column(reader.GetString(0), DbType.String); + var nullableStr = reader.GetString(1); + var isNullable = nullableStr == "YES"; + column.IsNullable = isNullable; + + columns.Add(column); + } + } + + return columns.ToArray(); + } + + /// + /// Basic implementation works for Postgre and probably for MySQL (not tested). For Oracle it should be overridden + /// + /// + /// + /// + public virtual ForeignKeyConstraint[] GetForeignKeyConstraints(string table) => ForeignKeyMetadataReader.Read(this, table); + + public virtual TableConstraint[] GetTableConstraints(string table) => ConstraintMetadataReader.Read(this, table); + + public virtual string[] GetConstraints(string table) + { + var constraints = new List(); + using (var cmd = CreateCommand()) + using ( + var reader = + ExecuteQuery( + cmd, string.Format("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE LOWER(TABLE_NAME) = LOWER('{0}')", table))) + { + while (reader.Read()) + { + constraints.Add(reader.GetString(0)); + } + } + + return constraints.ToArray(); + } + + public virtual Column GetColumnByName(string table, string columnName) + { + var columns = GetColumns(table); + var column = columns.FirstOrDefault(x => x.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase)) ?? + throw new Exception($"Cannot find column '{columnName}' in table '{table}'"); + + return column; + } + + public virtual int GetColumnContentSize(string table, string columnName) + { + var result = this.ExecuteScalar("SELECT MAX(LENGTH(" + this.QuoteColumnNameIfRequired(columnName) + ")) FROM " + this.QuoteTableNameIfRequired(table)); + + if (result == DBNull.Value) + { + return 0; + } + + return Convert.ToInt32(result); + } + + public virtual string[] GetTables() + { + var tables = new List(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, "SELECT table_name FROM INFORMATION_SCHEMA.TABLES")) + { + while (reader.Read()) + { + tables.Add((string)reader[0]); + } + } + return tables.ToArray(); + } + + public virtual void RemoveForeignKey(string table, string name) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + RemoveConstraint(table, name); + } + + public virtual void RemoveConstraint(string table, string name) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{name}' does not exist"); + } + + if (!ConstraintExists(table, name)) + { + throw new MigrationException($"Constraint '{name}' does not exist"); + } + + var names = GetConstraints(table); + var actual = names.FirstOrDefault(n => n == name) + ?? names.SingleOrDefault(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)) + ?? throw new MigrationException("Constraint was not found in the requested table: " + name); + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP CONSTRAINT {1}", QuoteTableNameIfRequired(table), _dialect.QuoteIdentifier(actual))); + } + + public virtual void RemoveAllConstraints(string table) + { + foreach (var constraint in GetConstraints(table)) + { + RemoveConstraint(table, constraint); + } + } + + public virtual void AddView(string name, string tableName, params IViewField[] fields) + { + var lst = + fields.Where(x => string.IsNullOrEmpty(x.TableName) || x.TableName == tableName) .Select(x => tableName + "." + x.ColumnName) - .ToList(); - - var nr = 0; - var joins = ""; + .ToList(); + + var nr = 0; + var joins = ""; foreach (var joinTable in fields.Where(x => !string.IsNullOrEmpty(x.TableName) && x.TableName != tableName) .GroupBy(x => new { x.TableName, x.KeyColumnName, x.ParentTableName, x.ParentKeyColumnName })) - { + { var relationship = joinTable.Key; var alias = "T" + nr++; joins += $"JOIN {relationship.TableName} {alias} ON {alias}.{relationship.KeyColumnName} = {relationship.ParentTableName}.{relationship.ParentKeyColumnName} "; - foreach (var viewField in joinTable) - { + foreach (var viewField in joinTable) + { lst.Add(alias + "." + viewField.ColumnName); - } - } - - var select = string.Format("SELECT {0} FROM {1} {2}", string.Join(",", lst), tableName, joins); - - var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); - - ExecuteNonQuery(sql); - } - - public virtual void AddView(string name, string tableName, params IViewElement[] viewElements) - { - var selectedColumns = viewElements.Where(x => x is ViewColumn) - .Select(x => - { - var viewColumn = (ViewColumn)x; - return $"{viewColumn.Prefix}.{viewColumn.ColumnName} {viewColumn.Prefix}{viewColumn.ColumnName}"; - }) - .ToList(); - - var joins = string.Empty; - - foreach (var viewJoin in viewElements.Where(x => x is ViewJoin).Cast()) - { - var joinType = string.Empty; - - switch (viewJoin.JoinType) - { - case JoinType.LeftJoin: - joinType = "LEFT JOIN"; - break; - case JoinType.Join: - joinType = "JOIN"; - break; - } - - var tableAlias = string.IsNullOrEmpty(viewJoin.TableAlias) ? viewJoin.TableName : viewJoin.TableAlias; + } + } + + var select = string.Format("SELECT {0} FROM {1} {2}", string.Join(",", lst), tableName, joins); + + var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); + + ExecuteNonQuery(sql); + } + + public virtual void AddView(string name, string tableName, params IViewElement[] viewElements) + { + var selectedColumns = viewElements.Where(x => x is ViewColumn) + .Select(x => + { + var viewColumn = (ViewColumn)x; + return $"{viewColumn.Prefix}.{viewColumn.ColumnName} {viewColumn.Prefix}{viewColumn.ColumnName}"; + }) + .ToList(); + + var joins = string.Empty; + + foreach (var viewJoin in viewElements.Where(x => x is ViewJoin).Cast()) + { + var joinType = string.Empty; + + switch (viewJoin.JoinType) + { + case JoinType.LeftJoin: + joinType = "LEFT JOIN"; + break; + case JoinType.Join: + joinType = "JOIN"; + break; + } + + var tableAlias = string.IsNullOrEmpty(viewJoin.TableAlias) ? viewJoin.TableName : viewJoin.TableAlias; var parentAlias = string.IsNullOrEmpty(viewJoin.ParentTableAlias) ? viewJoin.ParentTableName : viewJoin.ParentTableAlias; - - joins += string.Format("{0} {1} {2} ON {2}.{3} = {4}.{5} ", joinType, viewJoin.TableName, tableAlias, + + joins += string.Format("{0} {1} {2} ON {2}.{3} = {4}.{5} ", joinType, viewJoin.TableName, tableAlias, viewJoin.ColumnName, parentAlias, viewJoin.ParentColumnName); - } - - var select = string.Format("SELECT {0} FROM {1} {1} {2}", string.Join(",", selectedColumns), tableName, joins); - var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); - - - // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. - try - { - ExecuteNonQuery($"DROP VIEW {name}"); - } - catch - { - // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. - } - - ExecuteNonQuery(sql); - } - - /// - /// Add a new table - /// - /// Table name - /// Columns - public virtual void AddTable(string name, params IDbField[] columns) - { - // Most databases don't have the concept of a storage engine, so default is to not use it. - AddTable(name, null, columns); - } - - /// - /// Adds a new table - /// - /// Table name - /// Columns - /// the database storage engine to use - public virtual void AddTable(string name, string engine, params IDbField[] fields) - { - var columns = fields.OfType().Select(c => c.CopyDefinition()).ToArray(); - var keys = fields.OfType().ToArray(); - if (keys.Length > 1) throw new MigrationException("A table can have only one primary key."); - foreach (var key in keys) - { - ValidateKeyColumns(key.Name, key.KeyColumns, columns); - foreach (var column in columns.Where(c => key.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) - column.IsNullable = false; - } - foreach (var unique in fields.OfType()) ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); - var sql = columns.Select(c => _dialect.GetAndMapColumnProperties(c).ColumnSql) - .Concat(fields.OfType().Where(c => c is not ForeignKeyConstraint).Select(_dialect.GetTableConstraintSql)); - AddTable(name, engine, string.Join(", ", sql)); - foreach (var index in fields.OfType()) AddIndex(name, index); - foreach (var foreignKey in fields.OfType()) AddForeignKey(name, foreignKey); - } - - protected internal static void ValidateKeyColumns(string name, string[] keys, Column[] columns) - { - if (name != null && string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name must not be empty."); - if (keys == null || keys.Length == 0 || keys.Any(string.IsNullOrWhiteSpace) || keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != keys.Length) - throw new MigrationException("A key needs distinct, non-empty column names."); - if (keys.Any(key => !columns.Any(c => c.Name.Equals(key, StringComparison.OrdinalIgnoreCase)))) - throw new MigrationException("A constraint references a column that is absent from the table definition."); - } - - protected virtual string GetPrimaryKeyname(string tableName) - { - return "PK_" + tableName; - } - - public virtual void RemoveTable(string name) - { - if (!TableExists(name)) - { - throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", name)); - } - - ExecuteNonQuery(string.Format("DROP TABLE {0}", name)); - } - - public virtual void RenameTable(string oldName, string newName) - { - oldName = QuoteTableNameIfRequired(oldName); - newName = QuoteTableNameIfRequired(newName); - - if (TableExists(newName)) - { - throw new MigrationException(string.Format("Table with name '{0}' already exists", newName)); - } - - if (!TableExists(oldName)) - { - throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", oldName)); - } - - ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME TO {1}", oldName, newName)); - } - - public virtual void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - if (ColumnExists(tableName, newColumnName)) - { - throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); - } - - if (!ColumnExists(tableName, oldColumnName)) - { - throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); - } - - var column = GetColumnByName(tableName, oldColumnName); - - var quotedNewColumnName = QuoteColumnNameIfRequired(newColumnName); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME COLUMN {1} TO {2}", tableName, Dialect.Quote(column.Name), quotedNewColumnName)); - } - - public virtual void RemoveColumn(string tableName, string column) - { - if (!TableExists(tableName)) - { - throw new MigrationException($"The table '{tableName}' does not exist"); - } - - if (!ColumnExists(tableName, column, true)) - { - throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, column)); - } - - var existingColumn = GetColumnByName(tableName, column); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP COLUMN {1} ", tableName, Dialect.Quote(existingColumn.Name))); - } - - public virtual bool ColumnExists(string table, string column) - { - return ColumnExists(table, column, true); - } - - public virtual bool ColumnExists(string table, string column, bool ignoreCase) - { - if (ignoreCase) - { - return GetColumns(table).Any(x => x.Name.Equals(column, StringComparison.OrdinalIgnoreCase)); - } - - return GetColumns(table).Any(x => x.Name == column); - } - - public virtual void ChangeColumn(string table, Column column) - { - column = column.CopyDefinition(); - - - var mapper = _dialect.GetAndMapColumnProperties(column); - - ChangeColumn(table, mapper.ColumnSql); - - - } - - public virtual void RemoveColumnDefaultValue(string table, string column) - { - var sql = string.Format("ALTER TABLE {0} ALTER {1} DROP DEFAULT", table, column); - ExecuteNonQuery(sql); - } - - public virtual bool TableExists(string table) - { - throw new NotImplementedException(); - } - - public virtual bool ViewExists(string view) - { - throw new NotImplementedException(); - } - - public virtual void SwitchDatabase(string databaseName) - { - _connection.ChangeDatabase(databaseName); - } - - public abstract List GetDatabases(); - - public bool DatabaseExists(string name) - { - return GetDatabases().Any(c => string.Equals(name, c, StringComparison.OrdinalIgnoreCase)); - } - - public virtual void CreateDatabases(string databaseName) - { - ExecuteNonQuery(string.Format("CREATE DATABASE {0}", databaseName)); - } - - public virtual void KillDatabaseConnections(string databaseName) - { - //todo, implement this for each DB, no default implementation possible!!! - } - - public virtual void DropDatabases(string databaseName) - { - ExecuteNonQuery(string.Format("DROP DATABASE {0}", databaseName)); - } - - public virtual void AddColumn(string table, string column, DbType type) - { - AddColumn(table, new Column(column, type)); - } - - public virtual void AddColumn(string table, string column, MigratorDbType type) - { - AddColumn(table, new Column(column, type)); - } - - public virtual void AddColumn(string table, string column, DbType type, int size) - { - AddColumn(table, new Column(column, type, size)); - } - - public virtual void AddColumn(string table, string column, MigratorDbType type, int size) - { - AddColumn(table, new Column(column, type, size)); - } - - public virtual void AddColumn(string table, string column, DbType type, object defaultValue) - { - AddColumn(table, column, (MigratorDbType)type, defaultValue); - } - - public virtual void AddColumn(string table, string column, MigratorDbType type, object defaultValue) - { - var mapper = - _dialect.GetAndMapColumnProperties(new Column(column, type, defaultValue)); - - AddColumn(table, mapper.ColumnSql); - } - - /// - /// Append a primary key to a table. - /// - /// Constraint name - /// Table name - /// Primary column names - public virtual void AddPrimaryKey(string name, string table, params string[] columns) - { - table = QuoteTableNameIfRequired(table); - - ExecuteNonQuery( - string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} PRIMARY KEY ({2}) ", table, QuoteConstraintNameIfRequired(name), - string.Join(",", QuoteColumnNamesIfRequired(columns)))); - } - public virtual void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) - { - this.AddPrimaryKey(name, table, columns); - } - public virtual void AddUniqueConstraint(string name, string table, params string[] columns) - { - table = QuoteTableNameIfRequired(table); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE({2}) ", table, QuoteConstraintNameIfRequired(name), - string.Join(", ", QuoteColumnNamesIfRequired(columns)))); - } - - public virtual void AddCheckConstraint(string name, string table, string checkSql) - { - table = QuoteTableNameIfRequired(table); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} CHECK ({2}) ", table, QuoteConstraintNameIfRequired(name), checkSql)); - } - - /// - /// Guesses the name of the foreign key and adds it - /// - public virtual void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn); - } - - /// - /// Guesses the name of the foreign key and adds it - /// - /// - public virtual void GenerateForeignKey( - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns); - } - - /// - /// Guesses the name of the foreign key and adds it - /// - public virtual void GenerateForeignKey( - string childTable, - string childColumn, - string parentTable, - string parentColumn, - ForeignKeyConstraintType constraint) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn, constraint); - } - - /// - /// Guesses the name of the foreign key and add it - /// - /// - public virtual void GenerateForeignKey( - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns, - ForeignKeyConstraintType constraint) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns, constraint); - } - - public virtual void AddForeignKey(string table, ForeignKeyConstraint fk) - { - if (string.IsNullOrWhiteSpace(fk.OnDelete) && string.IsNullOrWhiteSpace(fk.OnUpdate)) - AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone()); - else - AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone(), ParseAction(fk.OnDelete), ParseAction(fk.OnUpdate)); - - static ForeignKeyConstraintType ParseAction(string action) - { - if (string.IsNullOrWhiteSpace(action)) return ForeignKeyConstraintType.NoAction; - return Enum.TryParse(action.Replace(" ", ""), true, out var parsed) && Enum.IsDefined(parsed) - ? parsed : throw new ArgumentException("Unsupported foreign-key action.", nameof(fk)); - } - } - - public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn) - { - try - { - AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn]); - } - catch (Exception ex) - { - throw new Exception(string.Format("Error occured while adding foreign key: \"{0}\" between table: \"{1}\" and table: \"{2}\" - see inner exception for details", name, parentTable, childTable), ex); - } - } - - public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns) - { - AddForeignKey(name, childTable, childColumns, parentTable, parentColumns, ForeignKeyConstraintType.NoAction); - } - - public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint) - { - AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn], constraint); - } - - public virtual void AddForeignKey( - string name, - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns, - ForeignKeyConstraintType constraint) - { - childTable = QuoteTableNameIfRequired(childTable); - parentTable = QuoteTableNameIfRequired(parentTable); - parentColumns = QuoteColumnNamesIfRequired(parentColumns); - childColumns = QuoteColumnNamesIfRequired(childColumns); - name = QuoteConstraintNameIfRequired(name); - - var constraintResolved = constraintMapper.SqlForConstraint(constraint); - - // Legacy overload preserves one action for both clauses; IForeignKeyActions provides independent actions. - var childColumnsString = string.Join(", ", childColumns); - var parentColumnsString = string.Join(", ", parentColumns); - - var stringBuilder = new StringBuilder(); - stringBuilder.Append($"ALTER TABLE {childTable} ADD CONSTRAINT {name} FOREIGN KEY ({childColumnsString}) REFERENCES {parentTable} ({parentColumnsString})"); - stringBuilder.Append($"ON DELETE {constraintResolved} ON UPDATE {constraintResolved}"); - - ExecuteNonQuery(stringBuilder.ToString()); - } - - /// - /// Determines if a constraint exists. - /// - /// Constraint name - /// Table owning the constraint - /// true if the constraint exists. - public abstract bool ConstraintExists(string table, string name); - - public virtual bool PrimaryKeyExists(string table, string name) - { - return ConstraintExists(table, name); - } - - public virtual int ExecuteNonQuery(string sql) - { - return ExecuteNonQuery(sql, CommandTimeout ?? 30); - } - - public virtual int ExecuteNonQuery(string sql, int timeout) - { - return ExecuteNonQuery(sql, timeout, null); - } - - public virtual int ExecuteNonQuery(string sql, int timeout, params object[] args) - { - if (args == null) - { - Logger.Trace(sql); - Logger.ApplyingDBChange(sql); - } - else - { - Logger.Trace(string.Format(sql, args)); - Logger.ApplyingDBChange(string.Format(sql, args)); - } - - using var cmd = BuildCommand(sql); - - try - { - cmd.CommandTimeout = timeout; - - if (args != null) - { - var index = 0; - - foreach (var obj in args) - { - var parameter = cmd.CreateParameter(); - ConfigureParameterWithValue(parameter, index, obj); - parameter.ParameterName = GenerateParameterNameParameter(index); - cmd.Parameters.Add(parameter); - ++index; - } - } - - Logger.Trace(cmd.CommandText); - return cmd.ExecuteNonQuery(); - } - catch (Exception ex) - { - Logger.Warn(ex.Message); - throw new MigrationException(string.Format("Error occured executing sql: {0}, see inner exception for details, error: " + ex, sql), ex); - } - } - - public List ExecuteStringQuery(string sql, params object[] args) - { - var values = new List(); - - using (var cmd = CreateCommand()) - { - using var reader = ExecuteQuery(cmd, string.Format(sql, args)); - while (reader.Read()) - { - var value = reader[0]; - - if (value == null || value == DBNull.Value) - { - values.Add(null); - } - else - { - values.Add(value.ToString()); - } - } - } - - return values; - } - - public virtual void ExecuteScript(string fileName) - { - if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("A script path is required.", nameof(fileName)); - var root = CurrentMigration == null ? AppContext.BaseDirectory : Path.GetDirectoryName(CurrentMigration.GetType().Assembly.Location); - var path = Path.IsPathRooted(fileName) ? fileName : Path.Combine(root ?? AppContext.BaseDirectory, fileName); - this.ExecuteSqlScript(File.ReadAllText(path)); - } - - public virtual void ExecuteResourceScript(System.Reflection.Assembly assembly, string resourceName) - { - using var stream = assembly.GetManifestResourceStream(resourceName) - ?? throw new FileNotFoundException("Embedded SQL resource not found.", resourceName); - using var reader = new StreamReader(stream); - this.ExecuteSqlScript(reader.ReadToEnd()); - } - - /// - /// Execute an SQL query returning results. - /// - /// The SQL text. - /// The IDbCommand. - /// A data iterator, IDataReader. - public virtual IDataReader ExecuteQuery(IDbCommand cmd, string sql) - { - Logger.Trace(sql); - cmd.CommandText = sql; - try - { - return cmd.ExecuteReader(); - } - catch (Exception ex) - { - Logger.Warn("query failed: {0}", cmd.CommandText); - throw new Exception("Failed to execute sql statement: " + sql, ex); - } - } - - public virtual object ExecuteScalar(string sql) - { - Logger.Trace(sql); - using var cmd = BuildCommand(sql); - try - { - return cmd.ExecuteScalar(); - } - catch - { - Logger.Warn("Query failed: {0}", cmd.CommandText); - throw; - } - } - - public virtual IDataReader Select(IDbCommand cmd, string what, string from) - { - return Select(cmd, what, from, "1=1"); - } - - public virtual IDataReader Select(IDbCommand cmd, string what, string from, string where) - { - return ExecuteQuery(cmd, string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); - } - - public virtual IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null) - { - return SelectComplex(cmd, table, columns, whereColumns, whereValues); - } - - public virtual IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, - object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - table = QuoteTableNameIfRequired(table); - - var builder = new StringBuilder(); - for (var i = 0; i < columns.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(QuoteColumnNameIfRequired(columns[i])); - } - - - cmd.Transaction = _transaction; - - var query = string.Format("SELECT {0} FROM {1}", builder.ToString(), table); - - if (whereColumns != null || nullWhereColumns != null || notNullWhereColumns != null) - { - query = string.Format("SELECT {0} FROM {1} WHERE ", builder.ToString(), table); - } - - var andNeeded = false; - if (whereColumns != null) - { - query += GetWhereString(whereColumns, whereValues); - andNeeded = true; - } - if (nullWhereColumns != null) - { - if (andNeeded) - { - query += " AND "; - } - - query += GetWhereStringIsNull(nullWhereColumns); - andNeeded = true; - } - if (notNullWhereColumns != null) - { - if (andNeeded) - { - query += " AND "; - } - - query += GetWhereStringIsNotNull(notNullWhereColumns); - andNeeded = true; - } - - cmd.CommandText = query; - cmd.CommandType = CommandType.Text; - - var paramCount = 0; - - if (whereColumns != null) - { - foreach (var value in whereValues) - { - var parameter = cmd.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - cmd.Parameters.Add(parameter); - - paramCount++; - } - } - - Logger.Trace(cmd.CommandText); - return cmd.ExecuteReader(); - - } - - public object SelectScalar(string what, string from) - { - return SelectScalar(what, from, "1=1"); - } - - public virtual object SelectScalar(string what, string from, string where) - { - return ExecuteScalar(string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); - } - - public virtual object SelectScalar(string what, string from, string[] whereColumns, object[] whereValues) - { - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - var query = string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, GetWhereString(whereColumns, whereValues)); - - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - - foreach (var value in whereValues) - { - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - Logger.Trace(command.CommandText); - return command.ExecuteScalar(); - } - - public virtual int Update(string table, string[] columns, object[] values) - { - return Update(table, columns, values, null); - } - - public virtual int Update(string table, string[] columns, object[] values, string where) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - if (values == null) - { - throw new ArgumentNullException("values"); - } - - if (columns.Length != values.Length) - { - throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); - } - - table = QuoteTableNameIfRequired(table); - - var builder = new StringBuilder(); - for (var i = 0; i < values.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(QuoteColumnNameIfRequired(columns[i])); - builder.Append(" = "); + } + + var select = string.Format("SELECT {0} FROM {1} {1} {2}", string.Join(",", selectedColumns), tableName, joins); + var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); + + + // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. + try + { + ExecuteNonQuery($"DROP VIEW {name}"); + } + catch + { + // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. + } + + ExecuteNonQuery(sql); + } + + /// + /// Add a new table + /// + /// Table name + /// Columns + public virtual void AddTable(string name, params IDbField[] columns) + { + // Most databases don't have the concept of a storage engine, so default is to not use it. + AddTable(name, null, columns); + } + + /// + /// Adds a new table + /// + /// Table name + /// Columns + /// the database storage engine to use + public virtual void AddTable(string name, string engine, params IDbField[] fields) + { + var columns = fields.OfType().Select(c => c.CopyDefinition()).ToArray(); + var keys = fields.OfType().ToArray(); + if (keys.Length > 1) throw new MigrationException("A table can have only one primary key."); + foreach (var key in keys) + { + ValidateKeyColumns(key.Name, key.KeyColumns, columns); + foreach (var column in columns.Where(c => key.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) + column.IsNullable = false; + } + foreach (var unique in fields.OfType()) ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); + var sql = columns.Select(c => _dialect.GetAndMapColumnProperties(c).ColumnSql) + .Concat(fields.OfType().Where(c => c is not ForeignKeyConstraint).Select(_dialect.GetTableConstraintSql)); + AddTable(name, engine, string.Join(", ", sql)); + foreach (var index in fields.OfType()) AddIndex(name, index); + foreach (var foreignKey in fields.OfType()) AddForeignKey(name, foreignKey); + } + + protected internal static void ValidateKeyColumns(string name, string[] keys, Column[] columns) + { + if (name != null && string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name must not be empty."); + if (keys == null || keys.Length == 0 || keys.Any(string.IsNullOrWhiteSpace) || keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != keys.Length) + throw new MigrationException("A key needs distinct, non-empty column names."); + if (keys.Any(key => !columns.Any(c => c.Name.Equals(key, StringComparison.OrdinalIgnoreCase)))) + throw new MigrationException("A constraint references a column that is absent from the table definition."); + } + + protected virtual string GetPrimaryKeyname(string tableName) + { + return "PK_" + tableName; + } + + public virtual void RemoveTable(string name) + { + if (!TableExists(name)) + { + throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", name)); + } + + ExecuteNonQuery(string.Format("DROP TABLE {0}", name)); + } + + public virtual void RenameTable(string oldName, string newName) + { + oldName = QuoteTableNameIfRequired(oldName); + newName = QuoteTableNameIfRequired(newName); + + if (TableExists(newName)) + { + throw new MigrationException(string.Format("Table with name '{0}' already exists", newName)); + } + + if (!TableExists(oldName)) + { + throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", oldName)); + } + + ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME TO {1}", oldName, newName)); + } + + public virtual void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + if (ColumnExists(tableName, newColumnName)) + { + throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); + } + + if (!ColumnExists(tableName, oldColumnName)) + { + throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); + } + + var column = GetColumnByName(tableName, oldColumnName); + + var quotedNewColumnName = QuoteColumnNameIfRequired(newColumnName); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME COLUMN {1} TO {2}", tableName, Dialect.Quote(column.Name), quotedNewColumnName)); + } + + public virtual void RemoveColumn(string tableName, string column) + { + if (!TableExists(tableName)) + { + throw new MigrationException($"The table '{tableName}' does not exist"); + } + + if (!ColumnExists(tableName, column, true)) + { + throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, column)); + } + + var existingColumn = GetColumnByName(tableName, column); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP COLUMN {1} ", tableName, Dialect.Quote(existingColumn.Name))); + } + + public virtual bool ColumnExists(string table, string column) + { + return ColumnExists(table, column, true); + } + + public virtual bool ColumnExists(string table, string column, bool ignoreCase) + { + if (ignoreCase) + { + return GetColumns(table).Any(x => x.Name.Equals(column, StringComparison.OrdinalIgnoreCase)); + } + + return GetColumns(table).Any(x => x.Name == column); + } + + public virtual void ChangeColumn(string table, Column column) + { + column = column.CopyDefinition(); + + + var mapper = _dialect.GetAndMapColumnProperties(column); + + ChangeColumn(table, mapper.ColumnSql); + + + } + + public virtual void RemoveColumnDefaultValue(string table, string column) + { + var sql = string.Format("ALTER TABLE {0} ALTER {1} DROP DEFAULT", table, column); + ExecuteNonQuery(sql); + } + + public virtual bool TableExists(string table) + { + throw new NotImplementedException(); + } + + public virtual bool ViewExists(string view) + { + throw new NotImplementedException(); + } + + public virtual void SwitchDatabase(string databaseName) + { + _connection.ChangeDatabase(databaseName); + } + + public abstract List GetDatabases(); + + public bool DatabaseExists(string name) + { + return GetDatabases().Any(c => string.Equals(name, c, StringComparison.OrdinalIgnoreCase)); + } + + public virtual void CreateDatabases(string databaseName) + { + ExecuteNonQuery(string.Format("CREATE DATABASE {0}", databaseName)); + } + + public virtual void KillDatabaseConnections(string databaseName) + { + //todo, implement this for each DB, no default implementation possible!!! + } + + public virtual void DropDatabases(string databaseName) + { + ExecuteNonQuery(string.Format("DROP DATABASE {0}", databaseName)); + } + + public virtual void AddColumn(string table, string column, DbType type) + { + AddColumn(table, new Column(column, type)); + } + + public virtual void AddColumn(string table, string column, MigratorDbType type) + { + AddColumn(table, new Column(column, type)); + } + + public virtual void AddColumn(string table, string column, DbType type, int size) + { + AddColumn(table, new Column(column, type, size)); + } + + public virtual void AddColumn(string table, string column, MigratorDbType type, int size) + { + AddColumn(table, new Column(column, type, size)); + } + + public virtual void AddColumn(string table, string column, DbType type, object defaultValue) + { + AddColumn(table, column, (MigratorDbType)type, defaultValue); + } + + public virtual void AddColumn(string table, string column, MigratorDbType type, object defaultValue) + { + var mapper = + _dialect.GetAndMapColumnProperties(new Column(column, type, defaultValue)); + + AddColumn(table, mapper.ColumnSql); + } + + /// + /// Append a primary key to a table. + /// + /// Constraint name + /// Table name + /// Primary column names + public virtual void AddPrimaryKey(string name, string table, params string[] columns) + { + table = QuoteTableNameIfRequired(table); + + ExecuteNonQuery( + string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} PRIMARY KEY ({2}) ", table, QuoteConstraintNameIfRequired(name), + string.Join(",", QuoteColumnNamesIfRequired(columns)))); + } + public virtual void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) + { + this.AddPrimaryKey(name, table, columns); + } + public virtual void AddUniqueConstraint(string name, string table, params string[] columns) + { + table = QuoteTableNameIfRequired(table); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE({2}) ", table, QuoteConstraintNameIfRequired(name), + string.Join(", ", QuoteColumnNamesIfRequired(columns)))); + } + + public virtual void AddCheckConstraint(string name, string table, string checkSql) + { + table = QuoteTableNameIfRequired(table); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} CHECK ({2}) ", table, QuoteConstraintNameIfRequired(name), checkSql)); + } + + /// + /// Guesses the name of the foreign key and adds it + /// + public virtual void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn); + } + + /// + /// Guesses the name of the foreign key and adds it + /// + /// + public virtual void GenerateForeignKey( + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns); + } + + /// + /// Guesses the name of the foreign key and adds it + /// + public virtual void GenerateForeignKey( + string childTable, + string childColumn, + string parentTable, + string parentColumn, + ForeignKeyConstraintType constraint) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn, constraint); + } + + /// + /// Guesses the name of the foreign key and add it + /// + /// + public virtual void GenerateForeignKey( + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns, + ForeignKeyConstraintType constraint) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns, constraint); + } + + public virtual void AddForeignKey(string table, ForeignKeyConstraint fk) + { + if (string.IsNullOrWhiteSpace(fk.OnDelete) && string.IsNullOrWhiteSpace(fk.OnUpdate)) + AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone()); + else + AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone(), ParseAction(fk.OnDelete), ParseAction(fk.OnUpdate)); + + static ForeignKeyConstraintType ParseAction(string action) + { + if (string.IsNullOrWhiteSpace(action)) return ForeignKeyConstraintType.NoAction; + return Enum.TryParse(action.Replace(" ", ""), true, out var parsed) && Enum.IsDefined(parsed) + ? parsed : throw new ArgumentException("Unsupported foreign-key action.", nameof(fk)); + } + } + + public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn) + { + try + { + AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn]); + } + catch (Exception ex) + { + throw new Exception(string.Format("Error occured while adding foreign key: \"{0}\" between table: \"{1}\" and table: \"{2}\" - see inner exception for details", name, parentTable, childTable), ex); + } + } + + public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns) + { + AddForeignKey(name, childTable, childColumns, parentTable, parentColumns, ForeignKeyConstraintType.NoAction); + } + + public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint) + { + AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn], constraint); + } + + public virtual void AddForeignKey( + string name, + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns, + ForeignKeyConstraintType constraint) + { + childTable = QuoteTableNameIfRequired(childTable); + parentTable = QuoteTableNameIfRequired(parentTable); + parentColumns = QuoteColumnNamesIfRequired(parentColumns); + childColumns = QuoteColumnNamesIfRequired(childColumns); + name = QuoteConstraintNameIfRequired(name); + + var constraintResolved = constraintMapper.SqlForConstraint(constraint); + + // Legacy overload preserves one action for both clauses; IForeignKeyActions provides independent actions. + var childColumnsString = string.Join(", ", childColumns); + var parentColumnsString = string.Join(", ", parentColumns); + + var stringBuilder = new StringBuilder(); + stringBuilder.Append($"ALTER TABLE {childTable} ADD CONSTRAINT {name} FOREIGN KEY ({childColumnsString}) REFERENCES {parentTable} ({parentColumnsString})"); + stringBuilder.Append($"ON DELETE {constraintResolved} ON UPDATE {constraintResolved}"); + + ExecuteNonQuery(stringBuilder.ToString()); + } + + /// + /// Determines if a constraint exists. + /// + /// Constraint name + /// Table owning the constraint + /// true if the constraint exists. + public abstract bool ConstraintExists(string table, string name); + + public virtual bool PrimaryKeyExists(string table, string name) + { + return ConstraintExists(table, name); + } + + public virtual int ExecuteNonQuery(string sql) + { + return ExecuteNonQuery(sql, CommandTimeout ?? 30); + } + + public virtual int ExecuteNonQuery(string sql, int timeout) + { + return ExecuteNonQuery(sql, timeout, null); + } + + public virtual int ExecuteNonQuery(string sql, int timeout, params object[] args) + { + if (args == null) + { + Logger.Trace(sql); + Logger.ApplyingDBChange(sql); + } + else + { + Logger.Trace(string.Format(sql, args)); + Logger.ApplyingDBChange(string.Format(sql, args)); + } + + using var cmd = BuildCommand(sql); + + try + { + cmd.CommandTimeout = timeout; + + if (args != null) + { + var index = 0; + + foreach (var obj in args) + { + var parameter = cmd.CreateParameter(); + ConfigureParameterWithValue(parameter, index, obj); + parameter.ParameterName = GenerateParameterNameParameter(index); + cmd.Parameters.Add(parameter); + ++index; + } + } + + Logger.Trace(cmd.CommandText); + return cmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + Logger.Warn(ex.Message); + throw new MigrationException(string.Format("Error occured executing sql: {0}, see inner exception for details, error: " + ex, sql), ex); + } + } + + public List ExecuteStringQuery(string sql, params object[] args) + { + var values = new List(); + + using (var cmd = CreateCommand()) + { + using var reader = ExecuteQuery(cmd, string.Format(sql, args)); + while (reader.Read()) + { + var value = reader[0]; + + if (value == null || value == DBNull.Value) + { + values.Add(null); + } + else + { + values.Add(value.ToString()); + } + } + } + + return values; + } + + public virtual void ExecuteScript(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("A script path is required.", nameof(fileName)); + var root = CurrentMigration == null ? AppContext.BaseDirectory : Path.GetDirectoryName(CurrentMigration.GetType().Assembly.Location); + var path = Path.IsPathRooted(fileName) ? fileName : Path.Combine(root ?? AppContext.BaseDirectory, fileName); + this.ExecuteSqlScript(File.ReadAllText(path)); + } + + public virtual void ExecuteResourceScript(System.Reflection.Assembly assembly, string resourceName) + { + using var stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new FileNotFoundException("Embedded SQL resource not found.", resourceName); + using var reader = new StreamReader(stream); + this.ExecuteSqlScript(reader.ReadToEnd()); + } + + /// + /// Execute an SQL query returning results. + /// + /// The SQL text. + /// The IDbCommand. + /// A data iterator, IDataReader. + public virtual IDataReader ExecuteQuery(IDbCommand cmd, string sql) + { + Logger.Trace(sql); + cmd.CommandText = sql; + try + { + return cmd.ExecuteReader(); + } + catch (Exception ex) + { + Logger.Warn("query failed: {0}", cmd.CommandText); + throw new Exception("Failed to execute sql statement: " + sql, ex); + } + } + + public virtual object ExecuteScalar(string sql) + { + Logger.Trace(sql); + using var cmd = BuildCommand(sql); + try + { + return cmd.ExecuteScalar(); + } + catch + { + Logger.Warn("Query failed: {0}", cmd.CommandText); + throw; + } + } + + public virtual IDataReader Select(IDbCommand cmd, string what, string from) + { + return Select(cmd, what, from, "1=1"); + } + + public virtual IDataReader Select(IDbCommand cmd, string what, string from, string where) + { + return ExecuteQuery(cmd, string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); + } + + public virtual IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null) + { + return SelectComplex(cmd, table, columns, whereColumns, whereValues); + } + + public virtual IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, + object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + table = QuoteTableNameIfRequired(table); + + var builder = new StringBuilder(); + for (var i = 0; i < columns.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(QuoteColumnNameIfRequired(columns[i])); + } + + + cmd.Transaction = _transaction; + + var query = string.Format("SELECT {0} FROM {1}", builder.ToString(), table); + + if (whereColumns != null || nullWhereColumns != null || notNullWhereColumns != null) + { + query = string.Format("SELECT {0} FROM {1} WHERE ", builder.ToString(), table); + } + + var andNeeded = false; + if (whereColumns != null) + { + query += GetWhereString(whereColumns, whereValues); + andNeeded = true; + } + if (nullWhereColumns != null) + { + if (andNeeded) + { + query += " AND "; + } + + query += GetWhereStringIsNull(nullWhereColumns); + andNeeded = true; + } + if (notNullWhereColumns != null) + { + if (andNeeded) + { + query += " AND "; + } + + query += GetWhereStringIsNotNull(notNullWhereColumns); + andNeeded = true; + } + + cmd.CommandText = query; + cmd.CommandType = CommandType.Text; + + var paramCount = 0; + + if (whereColumns != null) + { + foreach (var value in whereValues) + { + var parameter = cmd.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + cmd.Parameters.Add(parameter); + + paramCount++; + } + } + + Logger.Trace(cmd.CommandText); + return cmd.ExecuteReader(); + + } + + public object SelectScalar(string what, string from) + { + return SelectScalar(what, from, "1=1"); + } + + public virtual object SelectScalar(string what, string from, string where) + { + return ExecuteScalar(string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); + } + + public virtual object SelectScalar(string what, string from, string[] whereColumns, object[] whereValues) + { + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + var query = string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, GetWhereString(whereColumns, whereValues)); + + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + + foreach (var value in whereValues) + { + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + Logger.Trace(command.CommandText); + return command.ExecuteScalar(); + } + + public virtual int Update(string table, string[] columns, object[] values) + { + return Update(table, columns, values, null); + } + + public virtual int Update(string table, string[] columns, object[] values, string where) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (columns.Length != values.Length) + { + throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); + } + + table = QuoteTableNameIfRequired(table); + + var builder = new StringBuilder(); + for (var i = 0; i < values.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(QuoteColumnNameIfRequired(columns[i])); + builder.Append(" = "); // A literal NULL has no driver-dependent inferred type (notably for // nullable LOBs). Non-null values remain fully parameterized. builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); - } - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - var query = string.Format("UPDATE {0} SET {1}", table, builder.ToString()); - if (!string.IsNullOrEmpty(where)) - { - query += " WHERE " + where; - } - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - + } + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + var query = string.Format("UPDATE {0} SET {1}", table, builder.ToString()); + if (!string.IsNullOrEmpty(where)) + { + query += " WHERE " + where; + } + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + foreach (var value in values) { if (value == null || value == DBNull.Value) @@ -1043,78 +1043,78 @@ public virtual int Update(string table, string[] columns, object[] values, strin continue; } var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - Logger.Trace(command.CommandText); - return command.ExecuteNonQuery(); - } - - public virtual int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - if (values == null) - { - throw new ArgumentNullException("values"); - } - - if (columns.Length != values.Length) - { - throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); - } - - if (whereColumns.Length != whereValues.Length) - { - throw new Exception(string.Format("The number of whereColumns: {0} does not match the number of supplied whereValues: {1}", whereColumns.Length, whereValues.Length)); - } - - table = QuoteTableNameIfRequired(table); - - var builder = new StringBuilder(); - - for (var i = 0; i < values.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(QuoteColumnNameIfRequired(columns[i])); - builder.Append(" = "); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + Logger.Trace(command.CommandText); + return command.ExecuteNonQuery(); + } + + public virtual int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (columns.Length != values.Length) + { + throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); + } + + if (whereColumns.Length != whereValues.Length) + { + throw new Exception(string.Format("The number of whereColumns: {0} does not match the number of supplied whereValues: {1}", whereColumns.Length, whereValues.Length)); + } + + table = QuoteTableNameIfRequired(table); + + var builder = new StringBuilder(); + + for (var i = 0; i < values.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(QuoteColumnNameIfRequired(columns[i])); + builder.Append(" = "); builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); - } - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - + } + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + var query = string.Format("UPDATE {0} SET {1} WHERE {2}", table, builder.ToString(), GetWhereStringWithNullCheck(whereColumns, whereValues, values.Length)); - - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - + + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + foreach (var value in values) { if (value == null || value == DBNull.Value) @@ -1123,97 +1123,97 @@ public virtual int Update(string table, string[] columns, object[] values, strin continue; } var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - foreach (var value in whereValues) - { - if (value == null || value == DBNull.Value) - { - continue; - } - - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - - Logger.Trace(command.CommandText); - return command.ExecuteNonQuery(); - } - - public virtual void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) - { - throw new NotImplementedException(); - } - - public virtual int Insert(string table, string[] columns, object[] values) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - if (values == null) - { - throw new ArgumentNullException("values"); - } - - if (columns.Length != values.Length) - { - throw new MigrationException(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); - } - - table = QuoteTableNameIfRequired(table); - + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + foreach (var value in whereValues) + { + if (value == null || value == DBNull.Value) + { + continue; + } + + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + + Logger.Trace(command.CommandText); + return command.ExecuteNonQuery(); + } + + public virtual void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) + { + throw new NotImplementedException(); + } + + public virtual int Insert(string table, string[] columns, object[] values) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (columns.Length != values.Length) + { + throw new MigrationException(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); + } + + table = QuoteTableNameIfRequired(table); + var columnNames = string.Join(", ", columns.Select(col => QuoteColumnNameIfRequired(col)).ToArray()); - - var builder = new StringBuilder(); - - for (var i = 0; i < values.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - + + var builder = new StringBuilder(); + + for (var i = 0; i < values.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); - } - - var parameterNames = builder.ToString(); - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - + } + + var parameterNames = builder.ToString(); + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + command.CommandText = string.Format("INSERT INTO {0} ({1}) VALUES ({2})", table, columnNames, parameterNames); - command.CommandType = CommandType.Text; - - var paramCount = 0; - + command.CommandType = CommandType.Text; + + var paramCount = 0; + foreach (var value in values) { if (value == null || value == DBNull.Value) @@ -1222,832 +1222,870 @@ public virtual int Insert(string table, string[] columns, object[] values) continue; } var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - return command.ExecuteNonQuery(); - } - - protected virtual string GetWhereStringWithNullCheck(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) - { - var builder2 = new StringBuilder(); - var parCnt = 0; - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - var val = whereValues[i]; - if (val == null || val == DBNull.Value) - { - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" is null "); - } - else - { - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" = "); - builder2.Append(GenerateParameterName(parCnt + parameterStartIndex)); - parCnt++; - } - } - - return builder2.ToString(); - } - - protected virtual string GetWhereString(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) - { - var builder2 = new StringBuilder(); - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" = "); - builder2.Append(GenerateParameterName(i + parameterStartIndex)); - } - - return builder2.ToString(); - } - - protected virtual string GetWhereStringIsNull(string[] whereColumns) - { - var builder2 = new StringBuilder(); - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" IS NULL"); - } - - return builder2.ToString(); - } - - protected virtual string GetWhereStringIsNotNull(string[] whereColumns) - { - var builder2 = new StringBuilder(); - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" IS NOT NULL"); - } - - return builder2.ToString(); - } - - public virtual int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - using var cmd = CreateCommand(); - using var reader = this.Select(cmd, table, [whereColumns[0]], whereColumns, whereValues); - if (!reader.Read()) - { - reader.Close(); - return this.Insert(table, columns, values); - } - else - { - reader.Close(); - return 0; - } - } - - public virtual int Delete(string table, string[] whereColumns = null, object[] whereValues = null) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + return command.ExecuteNonQuery(); + } + + protected virtual string GetWhereStringWithNullCheck(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) + { + var builder2 = new StringBuilder(); + var parCnt = 0; + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + var val = whereValues[i]; + if (val == null || val == DBNull.Value) + { + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" is null "); + } + else + { + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" = "); + builder2.Append(GenerateParameterName(parCnt + parameterStartIndex)); + parCnt++; + } + } + + return builder2.ToString(); + } + + protected virtual string GetWhereString(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) + { + var builder2 = new StringBuilder(); + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" = "); + builder2.Append(GenerateParameterName(i + parameterStartIndex)); + } + + return builder2.ToString(); + } + + protected virtual string GetWhereStringIsNull(string[] whereColumns) + { + var builder2 = new StringBuilder(); + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" IS NULL"); + } + + return builder2.ToString(); + } + + protected virtual string GetWhereStringIsNotNull(string[] whereColumns) + { + var builder2 = new StringBuilder(); + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" IS NOT NULL"); + } + + return builder2.ToString(); + } + + public virtual int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + using var cmd = CreateCommand(); + using var reader = this.Select(cmd, table, [whereColumns[0]], whereColumns, whereValues); + if (!reader.Read()) + { + reader.Close(); + return this.Insert(table, columns, values); + } + else + { + reader.Close(); + return 0; + } + } + + public virtual int Delete(string table, string[] whereColumns = null, object[] whereValues = null) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + if (whereColumns == null && whereValues == null) - { - return ExecuteNonQuery(string.Format("DELETE FROM {0}", table)); - } - else - { + { + return ExecuteNonQuery(string.Format("DELETE FROM {0}", table)); + } + else + { ArgumentNullException.ThrowIfNull(whereColumns); ArgumentNullException.ThrowIfNull(whereValues); if (whereColumns.Length == 0 || whereColumns.Length != whereValues.Length) throw new ArgumentException("Delete predicates need matching, non-empty column and value arrays."); - table = QuoteTableNameIfRequired(table); - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - var query = string.Format("DELETE FROM {0} WHERE ({1})", table, + table = QuoteTableNameIfRequired(table); + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + var query = string.Format("DELETE FROM {0} WHERE ({1})", table, GetWhereStringWithNullCheck(whereColumns, whereValues)); - - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - - foreach (var value in whereValues) - { + + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + + foreach (var value in whereValues) + { if (value == null || value == DBNull.Value) continue; - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - Logger.Trace(command.CommandText); - return command.ExecuteNonQuery(); - } - } - - public virtual int Delete(string table, string wherecolumn, string wherevalue) - { - if (string.IsNullOrEmpty(wherecolumn) && string.IsNullOrEmpty(wherevalue)) - { - return Delete(table, (string[])null, null); - } - - return ExecuteNonQuery(string.Format("DELETE FROM {0} WHERE {1} = {2}", table, wherecolumn, QuoteValues(wherevalue))); - } - - public virtual int TruncateTable(string table) - { - return ExecuteNonQuery(string.Format("TRUNCATE TABLE {0} ", table)); - } - - public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, - ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) - { - var deleteAction = constraintMapper.SqlForConstraint(onDelete); - var updateAction = constraintMapper.SqlForConstraint(onUpdate); - var oracle = _dialect is DotNetProjects.Migrator.Providers.Impl.Oracle.OracleDialect; - if (oracle && onUpdate != ForeignKeyConstraintType.NoAction) - throw new NotSupportedException("Oracle does not support ON UPDATE foreign key actions."); - if (oracle && onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict or ForeignKeyConstraintType.Cascade or ForeignKeyConstraintType.SetNull)) - throw new NotSupportedException("Oracle supports default restrictive, CASCADE or SET NULL deletion actions."); - var sql = $"ALTER TABLE {QuoteTableNameIfRequired(childTable)} ADD CONSTRAINT {QuoteConstraintNameIfRequired(name)} FOREIGN KEY ({string.Join(", ", QuoteColumnNamesIfRequired(childColumns))}) REFERENCES {QuoteTableNameIfRequired(parentTable)} ({string.Join(", ", QuoteColumnNamesIfRequired(parentColumns))})"; - if (!oracle || onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict)) sql += $" ON DELETE {deleteAction}"; - if (!oracle) sql += $" ON UPDATE {updateAction}"; - ExecuteNonQuery(sql); - } - - /// - /// Starts a transaction. Called by the migration mediator. - /// - public virtual void BeginTransaction() - { - if (_transaction == null && _connection != null) - { - EnsureHasConnection(); - _transaction = _connection.BeginTransaction(_dialect is DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect ? IsolationLevel.Serializable : IsolationLevel.ReadCommitted); - } - } - - /// - /// Rollback the current migration. Called by the migration mediator. - /// - public virtual void Rollback() => CompleteTransaction(false); - - public virtual void Commit() => CompleteTransaction(true); - - public bool HasActiveTransaction => _transaction != null; - public string Scope => _scope; - public void InvalidateHistory() => _appliedMigrations = null; - - private void CompleteTransaction(bool commit) - { - var transaction = _transaction; - try - { - if (transaction != null) - { - if (commit) transaction.Commit(); - else transaction.Rollback(); - _transaction = null; - transaction.Dispose(); - } - } - finally { InvalidateHistory(); } - } - - /// Reads existing history without creating or upgrading its table. - public virtual IReadOnlyList ReadAppliedMigrations() - { - var versions = new List(); - if (!TableExists(_schemaInfotable)) return versions; - var hasScope = ColumnExists(_schemaInfotable, "Scope"); - if (!hasScope && _scope != "default") return versions; - using var cmd = CreateCommand(); - var predicate = "1=1"; - if (hasScope) - { - var parameter = cmd.CreateParameter(); - parameter.ParameterName = GenerateParameterNameParameter(0); - parameter.Value = _scope; - cmd.Parameters.Add(parameter); - predicate = QuoteColumnNameIfRequired("Scope") + " = " + GenerateParameterName(0); - } - using var reader = Select(cmd, QuoteColumnNameIfRequired("Version"), QuoteTableNameIfRequired(_schemaInfotable), predicate); - while (reader.Read()) versions.Add(Convert.ToInt64(reader.GetValue(0))); - versions.Sort(); - return versions; - } - - public virtual List AppliedMigrations - { - get - { - if (_appliedMigrations == null) - { - CreateSchemaInfoTable(); // Preserve the legacy property contract. - _appliedMigrations = new List(ReadAppliedMigrations()); - } - return _appliedMigrations; - } - } - - public virtual bool IsMigrationApplied(long version, string scope) - { - var value = SelectScalar("Version", _schemaInfotable, ["Scope", "Version"], [scope, version]); - return Convert.ToInt64(value) == version; - } - - /// - /// Marks a Migration version number as having been applied - /// - /// The version number of the migration that was applied - public virtual void MigrationApplied(long version, string scope) - { - CreateSchemaInfoTable(); - Insert(_schemaInfotable, ["Scope", "Version", "TimeStamp"], [scope ?? _scope, version, DateTime.UtcNow]); - InvalidateHistory(); - } - - /// - /// Marks a Migration version number as having been rolled back from the database - /// - /// The version number of the migration that was removed - public virtual void MigrationUnApplied(long version, string scope) - { - CreateSchemaInfoTable(); - Delete(_schemaInfotable, ["Scope", "Version"], [scope ?? _scope, version]); - InvalidateHistory(); - } - - public virtual void AddColumn(string table, Column column) - { - AddColumn(table, _dialect.GetAndMapColumnProperties(column.CopyDefinition()).ColumnSql); - } - - public virtual void GenerateForeignKey(string primaryTable, string refTable) - { - GenerateForeignKey(primaryTable, refTable, ForeignKeyConstraintType.NoAction); - } - - public virtual void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) - { - GenerateForeignKey(primaryTable, refTable + "Id", refTable, "Id", constraint); - } - - public virtual IDbCommand GetCommand() - { - return BuildCommand(null); - } - - public void Dispose() - { - try { if (_transaction != null) Rollback(); } - finally - { - if (!_outsideConnection) _connection?.Dispose(); - _connection = null; - InvalidateHistory(); - } - } - - public virtual string QuoteColumnNameIfRequired(string name) - { - return _dialect.QuoteColumnNameIfRequired(name); - } - - public virtual string QuoteTableNameIfRequired(string name) - { - if (!string.IsNullOrWhiteSpace(_defaultSchema) && SqlIdentifier.Parse(name).Length == 1) - name = _defaultSchema + "." + name; - return _dialect.QuoteTableNameIfRequired(name); - } - - public virtual string Encode(Guid guid) - { - return guid.ToString(); - } - - public virtual string[] QuoteColumnNamesIfRequired(params string[] columnNames) - { - var quotedColumns = new string[columnNames.Length]; - - for (var i = 0; i < columnNames.Length; i++) - { - quotedColumns[i] = QuoteColumnNameIfRequired(columnNames[i]); - } - - return quotedColumns; - } - - public virtual bool IsThisProvider(string provider) - { - // XXX: This might need to be more sophisticated. Currently just a convention - return GetType().Name.ToLower().StartsWith(provider.ToLower()); - } - - public virtual void RemoveAllForeignKeys(string tableName, string columnName) - { } - - public virtual void AddTable(string table, string engine, string columns) - { - table = QuoteTableNameIfRequired(table); - var sqlCreate = string.Format("CREATE TABLE {0} ({1})", table, columns); - - ExecuteNonQuery(sqlCreate); - } - - - - public virtual void AddColumnDefaultValue(string table, string column, object defaultValue) - { - if (defaultValue is DateTime defaultValueDateTime) - { - if (defaultValueDateTime.Kind != DateTimeKind.Utc) - { - throw new Exception("Only UTC values are accepted as default DateTime values."); - } - } - - table = QuoteTableNameIfRequired(table); - column = QuoteColumnNameIfRequired(column); - var def = Dialect.Default(defaultValue); - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD DEFAULT('{1}') FOR {2}", table, def, column)); - } - - public virtual void AddColumn(string table, string sqlColumn) - { - table = QuoteTableNameIfRequired(table); - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD COLUMN {1}", table, sqlColumn)); - } - - public virtual void ChangeColumn(string table, string sqlColumn) - { - table = QuoteTableNameIfRequired(table); - ExecuteNonQuery(string.Format("ALTER TABLE {0} ALTER COLUMN {1}", table, sqlColumn)); - } - - protected virtual string JoinColumns(IEnumerable columns) - { - var columnStrings = new List(); - - foreach (var column in columns) - { - columnStrings.Add(column.ColumnSql); - } - - return string.Join(", ", columnStrings.ToArray()); - } - - public IDbCommand CreateCommand() - { - EnsureHasConnection(); - var cmd = _connection.CreateCommand(); - - if (CommandTimeout.HasValue) - { - cmd.CommandTimeout = CommandTimeout.Value; - } - - cmd.CommandType = CommandType.Text; - - if (_transaction != null) - { - cmd.Transaction = _transaction; - } - - if (CommandTimeout.HasValue) - { - cmd.CommandTimeout = CommandTimeout.Value; - } - return cmd; - } - - protected IDbCommand BuildCommand(string sql) - { - var cmd = CreateCommand(); - cmd.CommandText = sql; - return cmd; - } - - public virtual int Delete(string table) - { - return Delete(table, null, (string[])null); - } - - protected void EnsureHasConnection() - { - if (_connection.State != ConnectionState.Open) - { - _connection.Open(); - } - } - - protected virtual void CreateSchemaInfoTable() - { - EnsureHasConnection(); - if (!TableExists(_schemaInfotable)) - { - AddTable(_schemaInfotable, - new Column("Version",DbType.Int64){IsNullable = false}, - new Column("Scope",DbType.String,50,"default"){IsNullable = false}, - new Column("TimeStamp", DbType.DateTime)); - } - else - { - if (!ColumnExists(_schemaInfotable, "Scope")) - { - AddColumn(_schemaInfotable, new Column("Scope", DbType.String, 50) { IsNullable = false, DefaultValue = "default" }); - RemoveAllConstraints(_schemaInfotable); - AddPrimaryKey("PK_SchemaInfo", _schemaInfotable, ["Version", "Scope"]); - } - - if (!ColumnExists(_schemaInfotable, "TimeStamp")) - { - AddColumn(_schemaInfotable, "TimeStamp", DbType.DateTime); - } - } - } - - public virtual string QuoteValues(string values) - { - return QuoteValues([values])[0]; - } - - public virtual string[] QuoteValues(string[] values) - { - return values.Select(val => - { - if (null == val) - { - return "null"; - } - else - { - return string.Format("'{0}'", val.Replace("'", "''")); - } - }).ToArray(); - } - - public virtual string JoinColumnsAndValues(string[] columns, string[] values) - { - return JoinColumnsAndValues(columns, values, ", "); - } - - public virtual string JoinColumnsAndValues(string[] columns, string[] values, string joinSeperator) - { - var quotedValues = QuoteValues(values); - var namesAndValues = new string[columns.Length]; - for (var i = 0; i < columns.Length; i++) - { - namesAndValues[i] = string.Format("{0}={1}", columns[i], quotedValues[i]); - } - - return string.Join(joinSeperator, namesAndValues); - } - - public virtual string GenerateParameterNameParameter(int index) - { - return "@p" + index; - } - - public virtual string GenerateParameterName(int index) - { - return GenerateParameterNameParameter(index); - } - - protected virtual void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) - { - if (value == null || value == DBNull.Value) - { - parameter.Value = DBNull.Value; - } - else if (value is Guid || value is Guid?) - { - parameter.DbType = DbType.Guid; - parameter.Value = (Guid)value; - } - else if (value is byte[] bytes) - { - parameter.DbType = DbType.Binary; - parameter.Value = bytes; - } - else if (value is byte) - { - parameter.DbType = DbType.Byte; - parameter.Value = value; - } + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + Logger.Trace(command.CommandText); + return command.ExecuteNonQuery(); + } + } + + public virtual int Delete(string table, string wherecolumn, string wherevalue) + { + if (string.IsNullOrEmpty(wherecolumn) && string.IsNullOrEmpty(wherevalue)) + { + return Delete(table, (string[])null, null); + } + + return ExecuteNonQuery(string.Format("DELETE FROM {0} WHERE {1} = {2}", table, wherecolumn, QuoteValues(wherevalue))); + } + + public virtual int TruncateTable(string table) + { + return ExecuteNonQuery(string.Format("TRUNCATE TABLE {0} ", table)); + } + + public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, + ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) + { + var deleteAction = constraintMapper.SqlForConstraint(onDelete); + var updateAction = constraintMapper.SqlForConstraint(onUpdate); + var oracle = _dialect is DotNetProjects.Migrator.Providers.Impl.Oracle.OracleDialect; + if (oracle && onUpdate != ForeignKeyConstraintType.NoAction) + throw new NotSupportedException("Oracle does not support ON UPDATE foreign key actions."); + if (oracle && onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict or ForeignKeyConstraintType.Cascade or ForeignKeyConstraintType.SetNull)) + throw new NotSupportedException("Oracle supports default restrictive, CASCADE or SET NULL deletion actions."); + var sql = $"ALTER TABLE {QuoteTableNameIfRequired(childTable)} ADD CONSTRAINT {QuoteConstraintNameIfRequired(name)} FOREIGN KEY ({string.Join(", ", QuoteColumnNamesIfRequired(childColumns))}) REFERENCES {QuoteTableNameIfRequired(parentTable)} ({string.Join(", ", QuoteColumnNamesIfRequired(parentColumns))})"; + if (!oracle || onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict)) sql += $" ON DELETE {deleteAction}"; + if (!oracle) sql += $" ON UPDATE {updateAction}"; + ExecuteNonQuery(sql); + } + + /// + /// Starts a transaction. Called by the migration mediator. + /// + public virtual void BeginTransaction() + { + if (_transaction == null && _connection != null) + { + EnsureHasConnection(); + _transaction = _connection.BeginTransaction(_dialect is DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect ? IsolationLevel.Serializable : IsolationLevel.ReadCommitted); + } + } + + /// + /// Rollback the current migration. Called by the migration mediator. + /// + public virtual void Rollback() => CompleteTransaction(false); + + public virtual void Commit() => CompleteTransaction(true); + + public bool HasActiveTransaction => _transaction != null; + public string Scope => _scope; + public void InvalidateHistory() => _appliedMigrations = null; + + private void CompleteTransaction(bool commit) + { + var transaction = _transaction; + try + { + if (transaction != null) + { + if (commit) transaction.Commit(); + else transaction.Rollback(); + _transaction = null; + transaction.Dispose(); + } + } + finally { InvalidateHistory(); } + } + + /// Reads existing history without creating or upgrading its table. + public virtual IReadOnlyList ReadAppliedMigrations() + { + var versions = new List(); + if (!TableExists(_schemaInfotable)) return versions; + var hasScope = ColumnExists(_schemaInfotable, "Scope"); + if (!hasScope && _scope != "default") return versions; + using var cmd = CreateCommand(); + var predicate = "1=1"; + if (hasScope) + { + var parameter = cmd.CreateParameter(); + parameter.ParameterName = GenerateParameterNameParameter(0); + parameter.Value = _scope; + cmd.Parameters.Add(parameter); + predicate = QuoteColumnNameIfRequired("Scope") + " = " + GenerateParameterName(0); + } + using var reader = Select(cmd, QuoteColumnNameIfRequired("Version"), QuoteTableNameIfRequired(_schemaInfotable), predicate); + while (reader.Read()) versions.Add(Convert.ToInt64(reader.GetValue(0))); + versions.Sort(); + return versions; + } + + public virtual List AppliedMigrations + { + get + { + if (_appliedMigrations == null) + { + CreateSchemaInfoTable(); // Preserve the legacy property contract. + _appliedMigrations = new List(ReadAppliedMigrations()); + } + return _appliedMigrations; + } + } + + public virtual bool IsMigrationApplied(long version, string scope) + { + var value = SelectScalar("Version", _schemaInfotable, ["Scope", "Version"], [scope, version]); + return Convert.ToInt64(value) == version; + } + + /// + /// Marks a Migration version number as having been applied + /// + /// The version number of the migration that was applied + public virtual void MigrationApplied(long version, string scope) + { + CreateSchemaInfoTable(); + Insert(_schemaInfotable, ["Scope", "Version", "TimeStamp"], [scope ?? _scope, version, DateTime.UtcNow]); + InvalidateHistory(); + } + + /// + /// Marks a Migration version number as having been rolled back from the database + /// + /// The version number of the migration that was removed + public virtual void MigrationUnApplied(long version, string scope) + { + CreateSchemaInfoTable(); + Delete(_schemaInfotable, ["Scope", "Version"], [scope ?? _scope, version]); + InvalidateHistory(); + } + + public virtual void AddColumn(string table, Column column) + { + AddColumn(table, _dialect.GetAndMapColumnProperties(column.CopyDefinition()).ColumnSql); + } + + public virtual void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) + { + var definition = PrepareColumnWithPrimaryKey(table, column, primaryKey); + AddColumn(table, definition); + if (primaryKey.NonClustered) AddPrimaryKeyNonClustered(primaryKey.Name, table, primaryKey.KeyColumns); + else AddPrimaryKey(primaryKey.Name, table, primaryKey.KeyColumns); + } + + protected Column PrepareColumnWithPrimaryKey(string table, Column column, PrimaryKeyConstraint primaryKey) + { + ArgumentException.ThrowIfNullOrWhiteSpace(table); + ArgumentNullException.ThrowIfNull(column); + ArgumentNullException.ThrowIfNull(primaryKey); + ArgumentException.ThrowIfNullOrWhiteSpace(primaryKey.Name); + if (!TableExists(table)) throw new MigrationException("Table does not exist."); + var columns = GetColumns(table); + if (columns.Any(existing => existing.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase))) throw new MigrationException("Column already exists."); + if (GetTableConstraints(table).OfType().Any()) throw new MigrationException("The table already has a primary key."); + ValidateKeyColumns(primaryKey.Name, primaryKey.KeyColumns, columns.Append(column).ToArray()); + // Validate provider-specific key options before any DDL. + _dialect.GetTableConstraintSql(primaryKey); + var definition = column.CopyDefinition(); + if (primaryKey.KeyColumns.Contains(column.Name, StringComparer.OrdinalIgnoreCase)) definition.IsNullable = false; + return definition; + } + + public virtual void RemoveUniqueConstraint(string table, UniqueConstraint constraint) + { + ArgumentException.ThrowIfNullOrWhiteSpace(table); + ArgumentNullException.ThrowIfNull(constraint); + var matches = GetTableConstraints(table).OfType().Where(candidate => + string.Equals(candidate.Name, constraint.Name, StringComparison.OrdinalIgnoreCase) && + candidate.KeyColumns.SequenceEqual(constraint.KeyColumns, StringComparer.OrdinalIgnoreCase)).ToArray(); + if (matches.Length != 1) throw new MigrationException("Unique constraint selection must match exactly one definition."); + if (string.IsNullOrWhiteSpace(matches[0].Name)) throw new NotSupportedException("This provider cannot remove an unnamed unique constraint."); + RemoveConstraint(table, matches[0].Name); + } + + public virtual void GenerateForeignKey(string primaryTable, string refTable) + { + GenerateForeignKey(primaryTable, refTable, ForeignKeyConstraintType.NoAction); + } + + public virtual void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) + { + GenerateForeignKey(primaryTable, refTable + "Id", refTable, "Id", constraint); + } + + public virtual IDbCommand GetCommand() + { + return BuildCommand(null); + } + + public void Dispose() + { + try { if (_transaction != null) Rollback(); } + finally + { + if (!_outsideConnection) _connection?.Dispose(); + _connection = null; + InvalidateHistory(); + } + } + + public virtual string QuoteColumnNameIfRequired(string name) + { + return _dialect.QuoteColumnNameIfRequired(name); + } + + public virtual string QuoteTableNameIfRequired(string name) + { + if (!string.IsNullOrWhiteSpace(_defaultSchema) && SqlIdentifier.Parse(name).Length == 1) + name = _defaultSchema + "." + name; + return _dialect.QuoteTableNameIfRequired(name); + } + + public virtual string Encode(Guid guid) + { + return guid.ToString(); + } + + public virtual string[] QuoteColumnNamesIfRequired(params string[] columnNames) + { + var quotedColumns = new string[columnNames.Length]; + + for (var i = 0; i < columnNames.Length; i++) + { + quotedColumns[i] = QuoteColumnNameIfRequired(columnNames[i]); + } + + return quotedColumns; + } + + public virtual bool IsThisProvider(string provider) + { + // XXX: This might need to be more sophisticated. Currently just a convention + return GetType().Name.ToLower().StartsWith(provider.ToLower()); + } + + public virtual void RemoveAllForeignKeys(string tableName, string columnName) + { } + + public virtual void AddTable(string table, string engine, string columns) + { + table = QuoteTableNameIfRequired(table); + var sqlCreate = string.Format("CREATE TABLE {0} ({1})", table, columns); + + ExecuteNonQuery(sqlCreate); + } + + + + public virtual void AddColumnDefaultValue(string table, string column, object defaultValue) + { + if (defaultValue is DateTime defaultValueDateTime) + { + if (defaultValueDateTime.Kind != DateTimeKind.Utc) + { + throw new Exception("Only UTC values are accepted as default DateTime values."); + } + } + + table = QuoteTableNameIfRequired(table); + column = QuoteColumnNameIfRequired(column); + var def = Dialect.Default(defaultValue); + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD DEFAULT('{1}') FOR {2}", table, def, column)); + } + + public virtual void AddColumn(string table, string sqlColumn) + { + table = QuoteTableNameIfRequired(table); + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD COLUMN {1}", table, sqlColumn)); + } + + public virtual void ChangeColumn(string table, string sqlColumn) + { + table = QuoteTableNameIfRequired(table); + ExecuteNonQuery(string.Format("ALTER TABLE {0} ALTER COLUMN {1}", table, sqlColumn)); + } + + protected virtual string JoinColumns(IEnumerable columns) + { + var columnStrings = new List(); + + foreach (var column in columns) + { + columnStrings.Add(column.ColumnSql); + } + + return string.Join(", ", columnStrings.ToArray()); + } + + public IDbCommand CreateCommand() + { + EnsureHasConnection(); + var cmd = _connection.CreateCommand(); + + if (CommandTimeout.HasValue) + { + cmd.CommandTimeout = CommandTimeout.Value; + } + + cmd.CommandType = CommandType.Text; + + if (_transaction != null) + { + cmd.Transaction = _transaction; + } + + if (CommandTimeout.HasValue) + { + cmd.CommandTimeout = CommandTimeout.Value; + } + return cmd; + } + + protected IDbCommand BuildCommand(string sql) + { + var cmd = CreateCommand(); + cmd.CommandText = sql; + return cmd; + } + + public virtual int Delete(string table) + { + return Delete(table, null, (string[])null); + } + + protected void EnsureHasConnection() + { + if (_connection.State != ConnectionState.Open) + { + _connection.Open(); + } + } + + protected virtual void CreateSchemaInfoTable() + { + EnsureHasConnection(); + if (!TableExists(_schemaInfotable)) + { + AddTable(_schemaInfotable, + new Column("Version",DbType.Int64){IsNullable = false}, + new Column("Scope",DbType.String,50,"default"){IsNullable = false}, + new Column("TimeStamp", DbType.DateTime)); + } + else + { + if (!ColumnExists(_schemaInfotable, "Scope")) + { + AddColumn(_schemaInfotable, new Column("Scope", DbType.String, 50) { IsNullable = false, DefaultValue = "default" }); + RemoveAllConstraints(_schemaInfotable); + AddPrimaryKey("PK_SchemaInfo", _schemaInfotable, ["Version", "Scope"]); + } + + if (!ColumnExists(_schemaInfotable, "TimeStamp")) + { + AddColumn(_schemaInfotable, "TimeStamp", DbType.DateTime); + } + } + } + + public virtual string QuoteValues(string values) + { + return QuoteValues([values])[0]; + } + + public virtual string[] QuoteValues(string[] values) + { + return values.Select(val => + { + if (null == val) + { + return "null"; + } + else + { + return string.Format("'{0}'", val.Replace("'", "''")); + } + }).ToArray(); + } + + public virtual string JoinColumnsAndValues(string[] columns, string[] values) + { + return JoinColumnsAndValues(columns, values, ", "); + } + + public virtual string JoinColumnsAndValues(string[] columns, string[] values, string joinSeperator) + { + var quotedValues = QuoteValues(values); + var namesAndValues = new string[columns.Length]; + for (var i = 0; i < columns.Length; i++) + { + namesAndValues[i] = string.Format("{0}={1}", columns[i], quotedValues[i]); + } + + return string.Join(joinSeperator, namesAndValues); + } + + public virtual string GenerateParameterNameParameter(int index) + { + return "@p" + index; + } + + public virtual string GenerateParameterName(int index) + { + return GenerateParameterNameParameter(index); + } + + protected virtual void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) + { + if (value == null || value == DBNull.Value) + { + parameter.Value = DBNull.Value; + } + else if (value is Guid || value is Guid?) + { + parameter.DbType = DbType.Guid; + parameter.Value = (Guid)value; + } + else if (value is byte[] bytes) + { + parameter.DbType = DbType.Binary; + parameter.Value = bytes; + } + else if (value is byte) + { + parameter.DbType = DbType.Byte; + parameter.Value = value; + } else if (value is sbyte signedByte) { parameter.DbType = DbType.Int16; parameter.Value = (short)signedByte; } else if (value is short) - { - parameter.DbType = DbType.Int16; - parameter.Value = value; - } - else if (value is int) - { - parameter.DbType = DbType.Int32; - parameter.Value = value; - } - else if (value is long) - { - parameter.DbType = DbType.Int64; - parameter.Value = value; - } - else if (value is ushort) - { - parameter.DbType = DbType.UInt16; - parameter.Value = value; - } - else if (value is uint) - { - parameter.DbType = DbType.UInt32; - parameter.Value = value; - } - else if (value is ulong) - { - parameter.DbType = DbType.UInt64; - parameter.Value = value; - } + { + parameter.DbType = DbType.Int16; + parameter.Value = value; + } + else if (value is int) + { + parameter.DbType = DbType.Int32; + parameter.Value = value; + } + else if (value is long) + { + parameter.DbType = DbType.Int64; + parameter.Value = value; + } + else if (value is ushort) + { + parameter.DbType = DbType.UInt16; + parameter.Value = value; + } + else if (value is uint) + { + parameter.DbType = DbType.UInt32; + parameter.Value = value; + } + else if (value is ulong) + { + parameter.DbType = DbType.UInt64; + parameter.Value = value; + } else if (value is float) { parameter.DbType = DbType.Single; parameter.Value = value; } else if (value is double) - { - parameter.DbType = DbType.Double; - parameter.Value = value; - } - else if (value is decimal) - { - parameter.DbType = DbType.Decimal; - parameter.Value = value; - } - else if (value is string) - { - parameter.DbType = DbType.String; - parameter.Value = value; - } - else if (value is DateTime || value is DateTime?) - { - parameter.DbType = DbType.DateTime; - parameter.Value = value; - } - else if (value is TimeOnly time) - { - parameter.DbType = DbType.Time; - parameter.Value = time.ToTimeSpan(); // ADO.NET drivers commonly carry SQL TIME as TimeSpan. - } - else if (value is TimeSpan interval) - { - var type = _dialect.GetTypeName((DbType)MigratorDbType.Interval); - if (type is not ("BIGINT" or "INTEGER")) throw new NotSupportedException("This provider requires native interval parameter handling."); - parameter.DbType = DbType.Int64; - parameter.Value = interval.Ticks; - } - else if (value is DateTimeOffset dateTimeOffset) - { - parameter.DbType = DbType.DateTimeOffset; - parameter.Value = dateTimeOffset.ToUniversalTime(); - } - else if (value is DateTimeOffset?) - { - parameter.DbType = DbType.DateTimeOffset; - parameter.Value = value == null ? null : ((DateTimeOffset?)value).Value.ToUniversalTime(); - } - else if (value is bool || value is bool?) - { - parameter.DbType = DbType.Boolean; - parameter.Value = value; - } - else - { - throw new NotSupportedException(string.Format("TransformationProvider does not support value: {0} of type: {1}", value, value.GetType())); - } - } - - private string FormatValue(object value) - { - if (value == null) - { - return null; - } - - if (value is DateTime) - { - return ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss:fff"); - } - - return value.ToString(); - } - - private void QuoteColumnNames(string[] primaryColumns) - { - for (var i = 0; i < primaryColumns.Length; i++) - { - primaryColumns[i] = QuoteColumnNameIfRequired(primaryColumns[i]); - } - } - - public virtual void RemoveIndex(string table, string name) - { - if (TableExists(table) && IndexExists(table, name)) - { - name = QuoteConstraintNameIfRequired(name); - ExecuteNonQuery(string.Format("DROP INDEX {0}", name)); - } - } - - public virtual string AddIndex(string table, Index index) - { - throw new NotImplementedException($"{nameof(AddIndex)} is not overridden for the provider."); - } - - public virtual string AddIndex(string name, string table, params string[] columns) - { - var index = new Index { Name = name, KeyColumns = columns }; - - return AddIndex(table, index); - } - - protected string QuoteConstraintNameIfRequired(string name) - { - if (!_dialect.ConstraintNameNeedsQuote && !_dialect.IsReservedWord(name) - && System.Text.RegularExpressions.Regex.IsMatch(name, @"^[A-Za-z_][A-Za-z0-9_$#]*$")) return name; - var template = _dialect.QuoteTemplate; - var closing = template[^1].ToString(); - return string.Format(template, name.Replace(closing, closing + closing)); - } - - public abstract bool IndexExists(string table, string name); - - protected virtual string GetPrimaryKeyConstraintName(string table) - { - return null; - } - - public virtual void RemovePrimaryKey(string table) - { - if (!TableExists(table)) - { - return; - } - - var primaryKeyConstraintName = GetPrimaryKeyConstraintName(table); - - if (primaryKeyConstraintName == null || !ConstraintExists(table, primaryKeyConstraintName)) - { - return; - } - - RemoveConstraint(table, primaryKeyConstraintName); - } - - public virtual void RemoveAllIndexes(string table) - { - if (!TableExists(table)) - { - return; - } - - var indexes = GetIndexes(table); - - foreach (var index in indexes) - { - if (index.Name == null || !IndexExists(table, index.Name)) - { - continue; - } - - if (index.PrimaryKey || index.UniqueConstraint) - { - RemoveConstraint(table, index.Name); - } - else - { - RemoveIndex(table, index.Name); - } - } - } - - public virtual string Concatenate(params string[] strings) - { - return string.Join(" || ", strings); - } - - public IDbConnection Connection - { - get { return _connection; } - } - - public IEnumerable GetTables(string schema) - { - var tableRestrictions = new string[4]; - tableRestrictions[1] = schema; - - var c = _connection as DbConnection; - var tables = c.GetSchema("Tables", tableRestrictions); - return from DataRow row in tables.Rows select (row["TABLE_NAME"] as string); - } - - public IEnumerable GetColumns(string schema, string table) - { - var tableRestrictions = new string[4]; - tableRestrictions[1] = schema; - tableRestrictions[2] = table; - - var c = _connection as DbConnection; - var tables = c.GetSchema("Columns", tableRestrictions); + { + parameter.DbType = DbType.Double; + parameter.Value = value; + } + else if (value is decimal) + { + parameter.DbType = DbType.Decimal; + parameter.Value = value; + } + else if (value is string) + { + parameter.DbType = DbType.String; + parameter.Value = value; + } + else if (value is DateTime || value is DateTime?) + { + parameter.DbType = DbType.DateTime; + parameter.Value = value; + } + else if (value is TimeOnly time) + { + parameter.DbType = DbType.Time; + parameter.Value = time.ToTimeSpan(); // ADO.NET drivers commonly carry SQL TIME as TimeSpan. + } + else if (value is TimeSpan interval) + { + var type = _dialect.GetTypeName((DbType)MigratorDbType.Interval); + if (type is not ("BIGINT" or "INTEGER")) throw new NotSupportedException("This provider requires native interval parameter handling."); + parameter.DbType = DbType.Int64; + parameter.Value = interval.Ticks; + } + else if (value is DateTimeOffset dateTimeOffset) + { + parameter.DbType = DbType.DateTimeOffset; + parameter.Value = dateTimeOffset.ToUniversalTime(); + } + else if (value is DateTimeOffset?) + { + parameter.DbType = DbType.DateTimeOffset; + parameter.Value = value == null ? null : ((DateTimeOffset?)value).Value.ToUniversalTime(); + } + else if (value is bool || value is bool?) + { + parameter.DbType = DbType.Boolean; + parameter.Value = value; + } + else + { + throw new NotSupportedException(string.Format("TransformationProvider does not support value: {0} of type: {1}", value, value.GetType())); + } + } + + private string FormatValue(object value) + { + if (value == null) + { + return null; + } + + if (value is DateTime) + { + return ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss:fff"); + } + + return value.ToString(); + } + + private void QuoteColumnNames(string[] primaryColumns) + { + for (var i = 0; i < primaryColumns.Length; i++) + { + primaryColumns[i] = QuoteColumnNameIfRequired(primaryColumns[i]); + } + } + + public virtual void RemoveIndex(string table, string name) + { + if (TableExists(table) && IndexExists(table, name)) + { + name = QuoteConstraintNameIfRequired(name); + ExecuteNonQuery(string.Format("DROP INDEX {0}", name)); + } + } + + public virtual string AddIndex(string table, Index index) + { + throw new NotImplementedException($"{nameof(AddIndex)} is not overridden for the provider."); + } + + public virtual string AddIndex(string name, string table, params string[] columns) + { + var index = new Index { Name = name, KeyColumns = columns }; + + return AddIndex(table, index); + } + + protected string QuoteConstraintNameIfRequired(string name) + { + if (!_dialect.ConstraintNameNeedsQuote && !_dialect.IsReservedWord(name) + && System.Text.RegularExpressions.Regex.IsMatch(name, @"^[A-Za-z_][A-Za-z0-9_$#]*$")) return name; + var template = _dialect.QuoteTemplate; + var closing = template[^1].ToString(); + return string.Format(template, name.Replace(closing, closing + closing)); + } + + public abstract bool IndexExists(string table, string name); + + protected virtual string GetPrimaryKeyConstraintName(string table) + { + return null; + } + + public virtual void RemovePrimaryKey(string table) + { + if (!TableExists(table)) + { + return; + } + + var primaryKeyConstraintName = GetPrimaryKeyConstraintName(table); + + if (primaryKeyConstraintName == null || !ConstraintExists(table, primaryKeyConstraintName)) + { + return; + } + + RemoveConstraint(table, primaryKeyConstraintName); + } + + public virtual void RemoveAllIndexes(string table) + { + if (!TableExists(table)) + { + return; + } + + var indexes = GetIndexes(table); + + foreach (var index in indexes) + { + if (index.Name == null || !IndexExists(table, index.Name)) + { + continue; + } + + if (index.PrimaryKey || index.UniqueConstraint) + { + RemoveConstraint(table, index.Name); + } + else + { + RemoveIndex(table, index.Name); + } + } + } + + public virtual string Concatenate(params string[] strings) + { + return string.Join(" || ", strings); + } + + public IDbConnection Connection + { + get { return _connection; } + } + + public IEnumerable GetTables(string schema) + { + var tableRestrictions = new string[4]; + tableRestrictions[1] = schema; + + var c = _connection as DbConnection; + var tables = c.GetSchema("Tables", tableRestrictions); + return from DataRow row in tables.Rows select (row["TABLE_NAME"] as string); + } + + public IEnumerable GetColumns(string schema, string table) + { + var tableRestrictions = new string[4]; + tableRestrictions[1] = schema; + tableRestrictions[2] = table; + + var c = _connection as DbConnection; + var tables = c.GetSchema("Columns", tableRestrictions); return from DataRow row in tables.Rows select (row["COLUMN_NAME"] as string); - } - - protected void ValidateIndex(string tableName, Index index) - { - var hasFilterItems = index.FilterItems != null && index.FilterItems.Count > 0; - var columns = GetColumns(table: tableName); - - if (!TableExists(tableName)) - { - throw new MigrationException($"Table '{tableName}' does not exist."); - } - - foreach (var keyColumn in index.KeyColumns) - { - if (!index.KeyColumns.All(x => columns.Any(y => y.Name.Equals(x, StringComparison.OrdinalIgnoreCase)))) - { - throw new MigrationException($"Column '{keyColumn}' does not exist."); - } - } - - if (hasFilterItems) - { - if (!index.FilterItems.All(x => index.KeyColumns.Any(y => x.ColumnName.Equals(y, StringComparison.OrdinalIgnoreCase)))) - { - throw new MigrationException($"All columns in the {nameof(index.FilterItems)} should exist in the {nameof(index.KeyColumns)}."); - } - } - - if (IndexExists(tableName, index.Name)) - { - throw new MigrationException($"Index '{index.Name}' in table {tableName} already exists."); - } - - if (index.IncludeColumns != null && index.IncludeColumns.Length > 0) - { - if (index.IncludeColumns.Any(x => index.KeyColumns.Any(y => x.Equals(y, StringComparison.OrdinalIgnoreCase)))) - { - throw new MigrationException($"It is not allowed to use a column in {nameof(index.IncludeColumns)} that exist in {nameof(index.KeyColumns)}."); - } - } - } - - public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) - { - throw new NotImplementedException(); - } -} - + } + + protected void ValidateIndex(string tableName, Index index) + { + var hasFilterItems = index.FilterItems != null && index.FilterItems.Count > 0; + var columns = GetColumns(table: tableName); + + if (!TableExists(tableName)) + { + throw new MigrationException($"Table '{tableName}' does not exist."); + } + + foreach (var keyColumn in index.KeyColumns) + { + if (!index.KeyColumns.All(x => columns.Any(y => y.Name.Equals(x, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"Column '{keyColumn}' does not exist."); + } + } + + if (hasFilterItems) + { + if (!index.FilterItems.All(x => index.KeyColumns.Any(y => x.ColumnName.Equals(y, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"All columns in the {nameof(index.FilterItems)} should exist in the {nameof(index.KeyColumns)}."); + } + } + + if (IndexExists(tableName, index.Name)) + { + throw new MigrationException($"Index '{index.Name}' in table {tableName} already exists."); + } + + if (index.IncludeColumns != null && index.IncludeColumns.Length > 0) + { + if (index.IncludeColumns.Any(x => index.KeyColumns.Any(y => x.Equals(y, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"It is not allowed to use a column in {nameof(index.IncludeColumns)} that exist in {nameof(index.KeyColumns)}."); + } + } + } + + public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) + { + throw new NotImplementedException(); + } +} + From 6d3d1529fbb624df91de6c9759ca6b8989848c39 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Thu, 24 Sep 2026 11:01:46 +0200 Subject: [PATCH 2/3] Preserve original line endings around the schema API changes --- docs/migration-guide-12.1-to-13.md | 592 +-- .../Framework/ITransformationProvider.cs | 1462 +++--- .../Impl/Mysql/MySqlTransformationProvider.cs | 682 +-- .../SQLite/SQLiteTransformationProvider.cs | 2946 ++++++------ .../Providers/NoOpTransformationProvider.cs | 1144 ++--- .../Providers/TransformationProvider.cs | 4122 ++++++++--------- 6 files changed, 5474 insertions(+), 5474 deletions(-) diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index 43eaa56e..70acdd45 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -1,276 +1,276 @@ -# Migrating from 12.1 to 13 - +# Migrating from 12.1 to 13 + Version 13 is a breaking release. This guide covers compatibility changes when updating existing migrations. Validate the upgrade on a restored database before running a changed migration history against production. For current API usage, see the [migration manual](https://dotnetprojects.github.io/Migrator.NET/guide/). - -## Schema model - -Columns describe data type, length, precision/scale, nullability, identity generation and defaults. Primary keys, unique constraints, foreign keys and checks belong to the table. Indexes are separate schema objects: a unique index is not automatically a unique constraint. - -The v13 API accepts complete named constraint definitions when creating a table, and returns the same kinds of definitions from metadata inspection. Key column order is significant. A composite UNIQUE constraint must never mark each member column as individually unique. Altering a column must not infer that its table constraints should be removed. - -The old column flags and duplicate fluent builder are removed. These are source-breaking changes: update historical migration source before recompiling for v13; the runner preserves the existing history table format. - -## Implemented breaking changes - -### Column flags, constructors and inspection - -`ColumnProperty`, its extensions, `Column.ColumnProperty`, -`IColumn.ColumnProperty`, `IsPrimaryKey` and `IsPrimaryKeyNonClustered` are -removed. Constructors and `AddColumn` overloads taking flags are removed. - -| 12.1 | 13 | -| --- | --- | -| `ColumnProperty.Null` / `None` | `IsNullable = true` (the default) | -| `ColumnProperty.NotNull` | `IsNullable = false` | -| `ColumnProperty.Identity` | `IsIdentity = true` | -| `ColumnProperty.Unsigned` | `IsUnsigned = true` | -| `ColumnProperty.PrimaryKey` | `new PrimaryKeyConstraint(name, columns)` in the table definition | -| `PrimaryKeyWithIdentity` | Identity on the column plus a separate primary key | -| `PrimaryKeyNonClustered` | `new PrimaryKeyConstraint(name, columns) { NonClustered = true }` | -| `ColumnProperty.Unique` | `new UniqueConstraint(name, columns)` | -| `ColumnProperty.Indexed` | An explicit `Index` definition | -| `ColumnProperty.CaseSensitive` | `Collation = "provider_collation_name"` | - -For example, replace a flagged `AddColumn` call with: - -```csharp -Database.AddColumn("Users", new Column("Email", DbType.String, 200) -{ - IsNullable = false, - DefaultValue = "unknown" -}); -Database.AddUniqueConstraint("UQ_Users_Email", "Users", "Email"); -``` - -Use `DefaultValue = 10` for numeric defaults: a positional integer after the -type is the column **size**, not its default. Unsupported unsigned/collation -combinations produce diagnostics. Collation names are explicit; the SQL Server -provider no longer queries or guesses a case-sensitive database collation. -SQLite's former `CaseSensitive` flag emitted `NOCASE`; choose `BINARY` or -`NOCASE` explicitly for the desired behavior. - -Read primary/unique membership through `GetTableConstraints(table)`, retaining -the constraint's member order. `GetColumns` returns column attributes only. -SQLite column metadata now follows physical column order, not primary-key order. -A plain SQLite INTEGER primary key remains a rowid alias; `IsIdentity` indicates -explicit `AUTOINCREMENT`, which is preserved on rebuild. - -### One fluent authoring API - -The obsolete `Framework.SchemaBuilder` namespace and `ExecuteSchemaBuilder` -method are removed. Use `Framework.Fluent.MigrationBuilder` and -`builder.Apply(provider)`, or derive from `FluentMigration`. -Replace `AddTable/AddColumn` chains with `Create.Table(...).WithColumn(...)`. -Replace `WithProperty`, unnamed `PrimaryKey()` and `Unique()` with -`NotNullable()`, `Identity()`, `Unsigned()`, `WithCollation(name)`, -`WithPrimaryKey(name, columns)` and `WithUniqueConstraint(name, columns)`. -Use `Create.ForeignKey` with separate delete/update actions. - -Fluent expressions select their table in a separate step: - -```csharp -migration.Create.Column("Email").OnTable("Users").AsString(320).Nullable(); -migration.Alter.Column("Name").OnTable("Users").AsString(200).NotNullable(); -migration.Rename.Column("Name").OnTable("Users").To("DisplayName"); -migration.Delete.Column("Email").FromTable("Users"); -migration.Create.Index("IX_Users_Name").OnTable("Users").WithColumns("DisplayName"); -migration.Create.ForeignKey("FK_Orders_Users") - .FromTable("Orders").WithColumns("UserId") - .ToTable("Users").WithColumns("Id") - .OnDelete(ForeignKeyConstraintType.Cascade); -``` - -The unreleased positional fluent overloads are removed. Use -`Create.UniqueConstraint(name).OnTable(table).WithColumns(...)` and -`Create.CheckConstraint(name).OnTable(table).WithExpression(sql)` for named -constraints. Renames end with `.To(newName)`. All object removal expressions -except `Delete.Table(table)` end with `.FromTable(table)`; for example, -`Delete.PrimaryKey().FromTable(table)` and -`Delete.DefaultValue(column).FromTable(table)`. - -Every named fluent column must specify a type. Table creation, column creation -and alteration share the same type/options methods. `AsDateTime()` now means -`DbType.DateTime`; use `AsDateTime2()` to preserve the earlier helper's mapping. -Table-level methods return the table builder: configure column attributes before -adding a constraint, or retain the specific column builder in a variable. - -Insert exposes only `IntoTable(...).Row(...)[.IfNotExists(...)]`; issue another -insert expression for each row. Update exposes `Table(...).Set(...)` followed by -`Where(...)`, `WhereSql(...)` or `AllRows()`. Delete exposes `FromTable(...)` -followed by `Where(...)` or `AllRows()`. Incomplete expressions throw during -`Build`, `Apply` and `Preview`, before any operation executes. - -Provider conditions use `IfProvider(name, configure)`. Table reads use -`Schema.Table(table).Select(...)` and `.SelectScalar(columns, where)`. -Data transfer uses `Execute.CopyDataFromTable(source).ToTable(target) -.WithColumns(sourceColumns, targetColumns)[.OrderBy(...)]`; joined updates use -`Execute.UpdateTable(target).FromTable(source).Set(copyPairs).Match(keyPairs)`. - -### Column changes do not own constraints - -`ChangeColumn` changes attributes without inferring creation or removal of -unique constraints. SQL Server's `AdoptColumnUniqueConstraint` and the implicit -ownership marker mechanism are removed. Use explicit `AddUniqueConstraint` and -`RemoveConstraint` calls. Existing extended-property markers are harmless; -v13 does not use them to delete constraints. - -Oracle changes columns in place so native constraints remain attached; a type -conversion that Oracle cannot perform must be expressed as an explicit data -migration. Identity validation runs before table creation, and identity no longer -requires primary-key membership. - -SQLite `RemoveAllIndexes` now preserves table UNIQUE constraints. To remove -constraints too, call `RemoveAllConstraints` explicitly. SQLite -`PrimaryKeyExists(table, name)` checks the actual name (use null for an unnamed -legacy key), rather than returning true for any primary key. - -### `Unique` is renamed to `UniqueConstraint` - -Replace `new Unique { Name = "UQ_Users_Email", KeyColumns = ["Email"] }` with `new UniqueConstraint("UQ_Users_Email", "Email")`. When importing both `System.Data` and `DotNetProjects.Migrator.Framework`, use an alias for the latter's `UniqueConstraint` (ADO.NET also defines that name). - -### Explicit table keys and complete constraint definitions - -```csharp -Database.AddTable("Users", - new Column("TenantId", DbType.Int32), - new Column("Id", DbType.Int32), - new Column("Email", DbType.String, 200), - new PrimaryKeyConstraint("PK_Users", "TenantId", "Id"), - new UniqueConstraint("UQ_Users_Email", "TenantId", "Email"), - new CheckConstraint("CK_Users_Id", "Id > 0")); -``` - -The supplied key order is preserved. Explicit primary-key definitions make their columns non-nullable without mutating the caller's column objects. This also rejects NULL in a composite SQLite primary key; old flag-based composite SQLite keys allowed NULL. SQLite identity requires a single INTEGER primary key and rejects incompatible combinations instead of silently removing identity. - -Fluent equivalent: append `.WithPrimaryKey("PK_Users", "TenantId", "Id")`, `.WithUniqueConstraint("UQ_Users_Email", "TenantId", "Email")`, or `.WithCheckConstraint("CK_Users_Id", "Id > 0")` to the table builder. Each is part of the complete table definition. - -### Structured constraint inspection - -Use `Database.GetTableConstraints("Users")`, or `Schema.Table("Users").ConstraintDefinitions()`, then select `PrimaryKeyConstraint`, `UniqueConstraint`, `ForeignKeyConstraint` or `CheckConstraint`. Key column order belongs to the constraint. A unique index stays in index metadata. SQLite returns `Name == null` for unnamed legacy constraints; a backing autoindex name is not an invented constraint name. - -Structured readers cover SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Db2, Firebird, Informix and Sybase. Each reader is exercised in its live CI job; unsupported engines throw `NotSupportedException`. MySQL identifies the primary key as `PRIMARY` regardless of a supplied symbolic name. Quoted qualified Oracle/MySQL lookups are currently rejected explicitly. These limitations must not be interpreted as empty metadata. - -### Custom provider and dialect implementations - -`ITransformationProvider` now requires `GetTableConstraints(string)`. Return accurate typed definitions, including ordered key members, or throw `NotSupportedException`; do not return an empty array for an unsupported reader. `IDialect` replaces `RegisterProperty/SqlForProperty` with `RegisterColumnAttribute/SqlForColumnAttribute`, using the non-flag `ColumnAttribute` enum (Null, NotNull, Identity, Unsigned). Custom column mappers use explicit column attributes; removed helpers include `IndexSql`, `PropertySelected`, `AddPrimaryKey`, `AddUnique`, and `AddForeignKey`. `GetPrimaryKeys(IEnumerable)` and column/index joining helpers are removed: inspect table constraints and create indexes explicitly. `GetCollationSql` generates a supported collation clause or throws before DDL. `IDialect` also adds `QuoteIdentifier(string)` for one identifier atom and `GetTableConstraintSql(TableConstraint)` for pure SQL rendering. Implementations derived from `Dialect` inherit defaults. Constraint names containing quote delimiters are escaped; a dot within a constraint name is not a schema separator. - -### SQLite alterations preserve named primary keys - -Rebuilding a table now retains an explicitly named primary key and its declared -column order. Changing a column definition does not implicitly remove that key. -To drop a column belonging to a named primary key, first call -`RemovePrimaryKey(table)`, then remove the column, and explicitly create any -replacement key. A failed attempt leaves the original table intact. - -Rebuilds also retain physical column order for tables with named primary keys, -including when a column's type or size changes. Identity rebuilds retain the -constraint name and the sequence high-water mark. - -## Provider authors and dialects (design) - + +## Schema model + +Columns describe data type, length, precision/scale, nullability, identity generation and defaults. Primary keys, unique constraints, foreign keys and checks belong to the table. Indexes are separate schema objects: a unique index is not automatically a unique constraint. + +The v13 API accepts complete named constraint definitions when creating a table, and returns the same kinds of definitions from metadata inspection. Key column order is significant. A composite UNIQUE constraint must never mark each member column as individually unique. Altering a column must not infer that its table constraints should be removed. + +The old column flags and duplicate fluent builder are removed. These are source-breaking changes: update historical migration source before recompiling for v13; the runner preserves the existing history table format. + +## Implemented breaking changes + +### Column flags, constructors and inspection + +`ColumnProperty`, its extensions, `Column.ColumnProperty`, +`IColumn.ColumnProperty`, `IsPrimaryKey` and `IsPrimaryKeyNonClustered` are +removed. Constructors and `AddColumn` overloads taking flags are removed. + +| 12.1 | 13 | +| --- | --- | +| `ColumnProperty.Null` / `None` | `IsNullable = true` (the default) | +| `ColumnProperty.NotNull` | `IsNullable = false` | +| `ColumnProperty.Identity` | `IsIdentity = true` | +| `ColumnProperty.Unsigned` | `IsUnsigned = true` | +| `ColumnProperty.PrimaryKey` | `new PrimaryKeyConstraint(name, columns)` in the table definition | +| `PrimaryKeyWithIdentity` | Identity on the column plus a separate primary key | +| `PrimaryKeyNonClustered` | `new PrimaryKeyConstraint(name, columns) { NonClustered = true }` | +| `ColumnProperty.Unique` | `new UniqueConstraint(name, columns)` | +| `ColumnProperty.Indexed` | An explicit `Index` definition | +| `ColumnProperty.CaseSensitive` | `Collation = "provider_collation_name"` | + +For example, replace a flagged `AddColumn` call with: + +```csharp +Database.AddColumn("Users", new Column("Email", DbType.String, 200) +{ + IsNullable = false, + DefaultValue = "unknown" +}); +Database.AddUniqueConstraint("UQ_Users_Email", "Users", "Email"); +``` + +Use `DefaultValue = 10` for numeric defaults: a positional integer after the +type is the column **size**, not its default. Unsupported unsigned/collation +combinations produce diagnostics. Collation names are explicit; the SQL Server +provider no longer queries or guesses a case-sensitive database collation. +SQLite's former `CaseSensitive` flag emitted `NOCASE`; choose `BINARY` or +`NOCASE` explicitly for the desired behavior. + +Read primary/unique membership through `GetTableConstraints(table)`, retaining +the constraint's member order. `GetColumns` returns column attributes only. +SQLite column metadata now follows physical column order, not primary-key order. +A plain SQLite INTEGER primary key remains a rowid alias; `IsIdentity` indicates +explicit `AUTOINCREMENT`, which is preserved on rebuild. + +### One fluent authoring API + +The obsolete `Framework.SchemaBuilder` namespace and `ExecuteSchemaBuilder` +method are removed. Use `Framework.Fluent.MigrationBuilder` and +`builder.Apply(provider)`, or derive from `FluentMigration`. +Replace `AddTable/AddColumn` chains with `Create.Table(...).WithColumn(...)`. +Replace `WithProperty`, unnamed `PrimaryKey()` and `Unique()` with +`NotNullable()`, `Identity()`, `Unsigned()`, `WithCollation(name)`, +`WithPrimaryKey(name, columns)` and `WithUniqueConstraint(name, columns)`. +Use `Create.ForeignKey` with separate delete/update actions. + +Fluent expressions select their table in a separate step: + +```csharp +migration.Create.Column("Email").OnTable("Users").AsString(320).Nullable(); +migration.Alter.Column("Name").OnTable("Users").AsString(200).NotNullable(); +migration.Rename.Column("Name").OnTable("Users").To("DisplayName"); +migration.Delete.Column("Email").FromTable("Users"); +migration.Create.Index("IX_Users_Name").OnTable("Users").WithColumns("DisplayName"); +migration.Create.ForeignKey("FK_Orders_Users") + .FromTable("Orders").WithColumns("UserId") + .ToTable("Users").WithColumns("Id") + .OnDelete(ForeignKeyConstraintType.Cascade); +``` + +The unreleased positional fluent overloads are removed. Use +`Create.UniqueConstraint(name).OnTable(table).WithColumns(...)` and +`Create.CheckConstraint(name).OnTable(table).WithExpression(sql)` for named +constraints. Renames end with `.To(newName)`. All object removal expressions +except `Delete.Table(table)` end with `.FromTable(table)`; for example, +`Delete.PrimaryKey().FromTable(table)` and +`Delete.DefaultValue(column).FromTable(table)`. + +Every named fluent column must specify a type. Table creation, column creation +and alteration share the same type/options methods. `AsDateTime()` now means +`DbType.DateTime`; use `AsDateTime2()` to preserve the earlier helper's mapping. +Table-level methods return the table builder: configure column attributes before +adding a constraint, or retain the specific column builder in a variable. + +Insert exposes only `IntoTable(...).Row(...)[.IfNotExists(...)]`; issue another +insert expression for each row. Update exposes `Table(...).Set(...)` followed by +`Where(...)`, `WhereSql(...)` or `AllRows()`. Delete exposes `FromTable(...)` +followed by `Where(...)` or `AllRows()`. Incomplete expressions throw during +`Build`, `Apply` and `Preview`, before any operation executes. + +Provider conditions use `IfProvider(name, configure)`. Table reads use +`Schema.Table(table).Select(...)` and `.SelectScalar(columns, where)`. +Data transfer uses `Execute.CopyDataFromTable(source).ToTable(target) +.WithColumns(sourceColumns, targetColumns)[.OrderBy(...)]`; joined updates use +`Execute.UpdateTable(target).FromTable(source).Set(copyPairs).Match(keyPairs)`. + +### Column changes do not own constraints + +`ChangeColumn` changes attributes without inferring creation or removal of +unique constraints. SQL Server's `AdoptColumnUniqueConstraint` and the implicit +ownership marker mechanism are removed. Use explicit `AddUniqueConstraint` and +`RemoveConstraint` calls. Existing extended-property markers are harmless; +v13 does not use them to delete constraints. + +Oracle changes columns in place so native constraints remain attached; a type +conversion that Oracle cannot perform must be expressed as an explicit data +migration. Identity validation runs before table creation, and identity no longer +requires primary-key membership. + +SQLite `RemoveAllIndexes` now preserves table UNIQUE constraints. To remove +constraints too, call `RemoveAllConstraints` explicitly. SQLite +`PrimaryKeyExists(table, name)` checks the actual name (use null for an unnamed +legacy key), rather than returning true for any primary key. + +### `Unique` is renamed to `UniqueConstraint` + +Replace `new Unique { Name = "UQ_Users_Email", KeyColumns = ["Email"] }` with `new UniqueConstraint("UQ_Users_Email", "Email")`. When importing both `System.Data` and `DotNetProjects.Migrator.Framework`, use an alias for the latter's `UniqueConstraint` (ADO.NET also defines that name). + +### Explicit table keys and complete constraint definitions + +```csharp +Database.AddTable("Users", + new Column("TenantId", DbType.Int32), + new Column("Id", DbType.Int32), + new Column("Email", DbType.String, 200), + new PrimaryKeyConstraint("PK_Users", "TenantId", "Id"), + new UniqueConstraint("UQ_Users_Email", "TenantId", "Email"), + new CheckConstraint("CK_Users_Id", "Id > 0")); +``` + +The supplied key order is preserved. Explicit primary-key definitions make their columns non-nullable without mutating the caller's column objects. This also rejects NULL in a composite SQLite primary key; old flag-based composite SQLite keys allowed NULL. SQLite identity requires a single INTEGER primary key and rejects incompatible combinations instead of silently removing identity. + +Fluent equivalent: append `.WithPrimaryKey("PK_Users", "TenantId", "Id")`, `.WithUniqueConstraint("UQ_Users_Email", "TenantId", "Email")`, or `.WithCheckConstraint("CK_Users_Id", "Id > 0")` to the table builder. Each is part of the complete table definition. + +### Structured constraint inspection + +Use `Database.GetTableConstraints("Users")`, or `Schema.Table("Users").ConstraintDefinitions()`, then select `PrimaryKeyConstraint`, `UniqueConstraint`, `ForeignKeyConstraint` or `CheckConstraint`. Key column order belongs to the constraint. A unique index stays in index metadata. SQLite returns `Name == null` for unnamed legacy constraints; a backing autoindex name is not an invented constraint name. + +Structured readers cover SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Db2, Firebird, Informix and Sybase. Each reader is exercised in its live CI job; unsupported engines throw `NotSupportedException`. MySQL identifies the primary key as `PRIMARY` regardless of a supplied symbolic name. Quoted qualified Oracle/MySQL lookups are currently rejected explicitly. These limitations must not be interpreted as empty metadata. + +### Custom provider and dialect implementations + +`ITransformationProvider` now requires `GetTableConstraints(string)`. Return accurate typed definitions, including ordered key members, or throw `NotSupportedException`; do not return an empty array for an unsupported reader. `IDialect` replaces `RegisterProperty/SqlForProperty` with `RegisterColumnAttribute/SqlForColumnAttribute`, using the non-flag `ColumnAttribute` enum (Null, NotNull, Identity, Unsigned). Custom column mappers use explicit column attributes; removed helpers include `IndexSql`, `PropertySelected`, `AddPrimaryKey`, `AddUnique`, and `AddForeignKey`. `GetPrimaryKeys(IEnumerable)` and column/index joining helpers are removed: inspect table constraints and create indexes explicitly. `GetCollationSql` generates a supported collation clause or throws before DDL. `IDialect` also adds `QuoteIdentifier(string)` for one identifier atom and `GetTableConstraintSql(TableConstraint)` for pure SQL rendering. Implementations derived from `Dialect` inherit defaults. Constraint names containing quote delimiters are escaped; a dot within a constraint name is not a schema separator. + +### SQLite alterations preserve named primary keys + +Rebuilding a table now retains an explicitly named primary key and its declared +column order. Changing a column definition does not implicitly remove that key. +To drop a column belonging to a named primary key, first call +`RemovePrimaryKey(table)`, then remove the column, and explicitly create any +replacement key. A failed attempt leaves the original table intact. + +Rebuilds also retain physical column order for tables with named primary keys, +including when a column's type or size changes. Identity rebuilds retain the +constraint name and the sequence high-water mark. + +## Provider authors and dialects (design) + Keep SQL rendering independent of a live connection. A dialect defines identifier quoting, type/literal rendering and SQL capabilities. Metadata readers inspect existing schema; execution manages commands, transactions and history. Connected preview may read history and schema before rendering, but it must not mutate the database. Offline rendering uses an explicitly supplied schema context. - + The provider surface combines these concerns through execution and metadata contracts. SQL rendering uses a separate context. Unsupported combinations must fail explicitly before DDL, not disappear from generated SQL. - -## Design references - -Reviewed 2026-09-22: - -- [EF Core CreateTableOperation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.migrations.operations.createtableoperation?view=efcore-10.0) separates columns, primary key, unique constraints, checks and foreign keys. -- [FluentMigrator ColumnDefinition](https://github.com/fluentmigrator/fluentmigrator/blob/main/src/FluentMigrator.Abstractions/Model/ColumnDefinition.cs) still carries constraint flags; its expression/generator separation is useful, but its column model is not the target here. -- [Alembic operations](https://alembic.sqlalchemy.org/en/latest/ops.html) distinguish named table constraints from column alteration and use explicit batch reconstruction for SQLite. - + +## Design references + +Reviewed 2026-09-22: + +- [EF Core CreateTableOperation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.migrations.operations.createtableoperation?view=efcore-10.0) separates columns, primary key, unique constraints, checks and foreign keys. +- [FluentMigrator ColumnDefinition](https://github.com/fluentmigrator/fluentmigrator/blob/main/src/FluentMigrator.Abstractions/Model/ColumnDefinition.cs) still carries constraint flags; its expression/generator separation is useful, but its column model is not the target here. +- [Alembic operations](https://alembic.sqlalchemy.org/en/latest/ops.html) distinguish named table constraints from column alteration and use explicit batch reconstruction for SQLite. + ## Schema API changes - + Typed constraints with ordered metadata, the SQLite constraint tokenizer, explicit SQL defaults, semantic collations and the consolidated authoring API work together. Use explicit object names and check provider-specific operation behavior when upgrading a custom dialect. - -## Explicit SQL defaults and semantic collations - -`RawSql.Insert("ksuid_new()")` marks trusted SQL as an expression in either API: - -```csharp -new Column("Id", DbType.String, 27) { DefaultValue = RawSql.Insert("ksuid_new()") }; -// Fluent: -builder.Create.Table("Events").WithColumn("Id").AsString(27) - .WithDefaultValue(RawSql.Insert("ksuid_new()")); -``` - -The database must provide that function. Strings remain quoted values, so -`WithDefaultValue("ksuid_new()")` stores that text instead of calling a function. -SQLite wraps expressions in parentheses as required for expression defaults. -Metadata exposes unparsed SQL defaults as `RawSql`, replacing the previous private -expression object; inspect `RawSql.Sql` instead of assuming every default is a string. -Expression text is trusted migration code, not a parameter or a cross-database function abstraction. - -`Column.Collation` is now a typed `Collation` value. String assignment still selects -a provider name through an implicit conversion; `Collation.Named("name")` is explicit. -Fluent `.WithCollation(...)` takes the same type. - -```csharp -new Column("Name", DbType.String, 100) { Collation = Collation.CaseInsensitive }; -builder.Create.Table("Names").WithColumn("Name").AsString(100) - .WithCollation(Collation.CaseInsensitive); -``` - -| Preset | SQL Server | MySQL 8 | MariaDB 10.10+ | PostgreSQL | SQLite | -| --- | --- | --- | --- | --- | --- | -| `CaseInsensitive` (accent-sensitive) | Latin1 General 100 CI AS SC | utf8mb4 0900 as ci | utf8mb4 UCA1400 nopad as ci | Explicit installed name required | Unsupported | -| `CaseSensitive` (accent-sensitive) | Latin1 General 100 CS AS SC | utf8mb4 0900 as cs | utf8mb4 UCA1400 nopad as cs | Explicit installed name required | Explicit installed name required | -| `Binary` | Latin1 General 100 BIN2 | utf8mb4 0900 bin | utf8mb4 nopad bin | C | BINARY | -| `AsciiIgnoreCase` | Unsupported | Unsupported | Unsupported | Unsupported | NOCASE | - -These presets describe comparison intent, not identical sorting, normalization, -language tailoring, or trailing-space behavior across engines. Use a named collation -for a specific language or exact provider semantics. MySQL/MariaDB presets require -utf8mb4-compatible text columns and the listed engine versions. Other dialects reject -unmapped presets; custom dialects can override `ResolveCollation(CollationKind)`. -Unsupported requests fail during SQL generation, before executing the table operation. -SQLite never downgrades Unicode case-insensitivity to its ASCII-only NOCASE behavior. -SQLite rebuilds preserve declared column collations, including named custom -collations registered on the connection. `GetColumns` reports these names. -Changing a collation explicitly rebuilds the table; a resulting uniqueness -violation rolls back the change and preserves the original data. Index-level -`COLLATE` clauses remain unsupported for rebuilds and fail before replacing the table. - -## Consolidated migration history - -A consolidated baseline can use `Database.MigrationApplied(version, scope)` to -record versions whose schema it already includes. The runner rechecks the active -scope's history before each planned migration and skips versions now applied, -including their `AfterUp` callbacks. The same rule applies to downgrades when an -earlier `Down` removes another version from history. Recording the baseline's own -version does not insert it twice. History for another scope does not skip a step -in the current scope. Transaction rollback still applies to baseline schema and -history changes according to the selected transaction mode. - -## SQLite defaults and identity - -### SQLite GUID defaults - -SQLite now renders a CLR `Guid` default as a blob using `Guid.ToByteArray()`, -matching GUID parameters inserted by the provider. Previously a GUID default -was text, so a defaulted foreign-key value did not match an explicitly inserted -parent GUID even when both represented the same identifier. - -This fixes new table/column definitions, including backfilling a new column. -Existing text GUID defaults and data are preserved during unrelated rebuilds; -column inspection retains their SQL as `RawSql` so storage classes are not -silently converted. Databases already containing mixed text/blob GUIDs require -an explicit data migration that converts related keys consistently. A string -default remains text; use a CLR `Guid` when authoring a GUID default. - -### SQLite identity columns - + +## Explicit SQL defaults and semantic collations + +`RawSql.Insert("ksuid_new()")` marks trusted SQL as an expression in either API: + +```csharp +new Column("Id", DbType.String, 27) { DefaultValue = RawSql.Insert("ksuid_new()") }; +// Fluent: +builder.Create.Table("Events").WithColumn("Id").AsString(27) + .WithDefaultValue(RawSql.Insert("ksuid_new()")); +``` + +The database must provide that function. Strings remain quoted values, so +`WithDefaultValue("ksuid_new()")` stores that text instead of calling a function. +SQLite wraps expressions in parentheses as required for expression defaults. +Metadata exposes unparsed SQL defaults as `RawSql`, replacing the previous private +expression object; inspect `RawSql.Sql` instead of assuming every default is a string. +Expression text is trusted migration code, not a parameter or a cross-database function abstraction. + +`Column.Collation` is now a typed `Collation` value. String assignment still selects +a provider name through an implicit conversion; `Collation.Named("name")` is explicit. +Fluent `.WithCollation(...)` takes the same type. + +```csharp +new Column("Name", DbType.String, 100) { Collation = Collation.CaseInsensitive }; +builder.Create.Table("Names").WithColumn("Name").AsString(100) + .WithCollation(Collation.CaseInsensitive); +``` + +| Preset | SQL Server | MySQL 8 | MariaDB 10.10+ | PostgreSQL | SQLite | +| --- | --- | --- | --- | --- | --- | +| `CaseInsensitive` (accent-sensitive) | Latin1 General 100 CI AS SC | utf8mb4 0900 as ci | utf8mb4 UCA1400 nopad as ci | Explicit installed name required | Unsupported | +| `CaseSensitive` (accent-sensitive) | Latin1 General 100 CS AS SC | utf8mb4 0900 as cs | utf8mb4 UCA1400 nopad as cs | Explicit installed name required | Explicit installed name required | +| `Binary` | Latin1 General 100 BIN2 | utf8mb4 0900 bin | utf8mb4 nopad bin | C | BINARY | +| `AsciiIgnoreCase` | Unsupported | Unsupported | Unsupported | Unsupported | NOCASE | + +These presets describe comparison intent, not identical sorting, normalization, +language tailoring, or trailing-space behavior across engines. Use a named collation +for a specific language or exact provider semantics. MySQL/MariaDB presets require +utf8mb4-compatible text columns and the listed engine versions. Other dialects reject +unmapped presets; custom dialects can override `ResolveCollation(CollationKind)`. +Unsupported requests fail during SQL generation, before executing the table operation. +SQLite never downgrades Unicode case-insensitivity to its ASCII-only NOCASE behavior. +SQLite rebuilds preserve declared column collations, including named custom +collations registered on the connection. `GetColumns` reports these names. +Changing a collation explicitly rebuilds the table; a resulting uniqueness +violation rolls back the change and preserves the original data. Index-level +`COLLATE` clauses remain unsupported for rebuilds and fail before replacing the table. + +## Consolidated migration history + +A consolidated baseline can use `Database.MigrationApplied(version, scope)` to +record versions whose schema it already includes. The runner rechecks the active +scope's history before each planned migration and skips versions now applied, +including their `AfterUp` callbacks. The same rule applies to downgrades when an +earlier `Down` removes another version from history. Recording the baseline's own +version does not insert it twice. History for another scope does not skip a step +in the current scope. Transaction rollback still applies to baseline schema and +history changes according to the selected transaction mode. + +## SQLite defaults and identity + +### SQLite GUID defaults + +SQLite now renders a CLR `Guid` default as a blob using `Guid.ToByteArray()`, +matching GUID parameters inserted by the provider. Previously a GUID default +was text, so a defaulted foreign-key value did not match an explicitly inserted +parent GUID even when both represented the same identifier. + +This fixes new table/column definitions, including backfilling a new column. +Existing text GUID defaults and data are preserved during unrelated rebuilds; +column inspection retains their SQL as `RawSql` so storage classes are not +silently converted. Databases already containing mixed text/blob GUIDs require +an explicit data migration that converts related keys consistently. A string +default remains text; use a CLR `Guid` when authoring a GUID default. + +### SQLite identity columns + Use the provider-independent overload to add a column with an explicit primary key: - -```csharp + +```csharp Database.AddColumn("Settings", new Column("Id", DbType.Int32) { IsIdentity = true }, new PrimaryKeyConstraint("PK_Settings", "Id")); -``` - +``` + The key must have a nonempty name and valid, ordered column members. Existing primary keys are rejected rather than replaced, and caller-owned definitions are not mutated. SQLite adds both definitions in a single transactional table rebuild, preserving existing rows and generating IDs. MySQL/MariaDB add both in one ALTER statement because AUTO_INCREMENT must be indexed immediately. Other providers use their normal AddColumn/AddPrimaryKey operations and DDL transaction semantics; the overload does not promise cross-provider rollback if a later DDL statement fails. Existing columns in a composite key must already meet the provider's requirements. - + `IsIdentity` alone still does not imply a primary key. Separate AddColumn/AddPrimaryKey calls cannot introduce an SQLite identity column. SQLite downgrade can use RemovePrimaryKey followed by RemoveColumn; the migration runner manages SQLite's foreign-key state. ### Removing legacy unnamed unique constraints @@ -278,42 +278,42 @@ The key must have a nonempty name and valid, ordered column members. Existing pr Use `Database.RemoveUniqueConstraint(table, constraint)` with a `UniqueConstraint` returned by `GetTableConstraints`. The operation matches both the declared name and ordered columns, and requires exactly one match. SQLite rebuilds internally to remove unnamed legacy constraints without discarding other unique/check constraints. Other providers remove the verified named constraint using their existing DDL implementation. Unknown or ambiguous selections fail before mutation. Custom ITransformationProvider implementations must implement these two new methods; implementations derived from TransformationProvider inherit the portable defaults. NoOpTransformationProvider supports both as no-ops. These are direct provider APIs; fluent callers can use Database for these combined operations. -## Identifier quoting and renamed tables - -Use `QuoteColumnNameIfRequired` for columns in authored SQL and -`QuoteTableNameIfRequired` for tables. A table name may acquire a schema prefix; -using that API for a column can produce an invalid reference such as `dbo.Color`. - -Renaming a table does not rename its explicitly named constraints or backing -indexes. On SQL Server and PostgreSQL, recreating the old table with the old -primary-key name can therefore collide with the renamed table's key. Give the -replacement table a distinct key name (for example `PK_Client_New`), or explicitly -rename the retained key using provider-specific SQL before reusing its name. - -For PostgreSQL, create an ICU nondeterministic collation explicitly (for example -`CREATE COLLATION app_ci (provider=icu, locale='und-u-ks-level2', deterministic=false)`) -and use `Collation.Named("app_ci")`. The framework does not silently create shared -database objects while rendering a column or preview. - -MySQL/MariaDB expose unique indexes as unique constraints in their catalogs, so -metadata cannot recover whether the original author used CREATE UNIQUE INDEX or -a UNIQUE table clause. No ownership decision may be inferred from that syntax. - -## Oracle index options and constraint metadata - -Oracle now rejects nonempty `Index.IncludeColumns` and `Index.Clustered = true` before -DDL. Version 12.1 silently ignored them. Remove these options for an ordinary Oracle -index or author an explicit Oracle-specific design; a SQL Server clustered-index +## Identifier quoting and renamed tables + +Use `QuoteColumnNameIfRequired` for columns in authored SQL and +`QuoteTableNameIfRequired` for tables. A table name may acquire a schema prefix; +using that API for a column can produce an invalid reference such as `dbo.Color`. + +Renaming a table does not rename its explicitly named constraints or backing +indexes. On SQL Server and PostgreSQL, recreating the old table with the old +primary-key name can therefore collide with the renamed table's key. Give the +replacement table a distinct key name (for example `PK_Client_New`), or explicitly +rename the retained key using provider-specific SQL before reusing its name. + +For PostgreSQL, create an ICU nondeterministic collation explicitly (for example +`CREATE COLLATION app_ci (provider=icu, locale='und-u-ks-level2', deterministic=false)`) +and use `Collation.Named("app_ci")`. The framework does not silently create shared +database objects while rendering a column or preview. + +MySQL/MariaDB expose unique indexes as unique constraints in their catalogs, so +metadata cannot recover whether the original author used CREATE UNIQUE INDEX or +a UNIQUE table clause. No ownership decision may be inferred from that syntax. + +## Oracle index options and constraint metadata + +Oracle now rejects nonempty `Index.IncludeColumns` and `Index.Clustered = true` before +DDL. Version 12.1 silently ignored them. Remove these options for an ordinary Oracle +index or author an explicit Oracle-specific design; a SQL Server clustered-index request is not translated to an Oracle index-organized table. Oracle column changes and default removal use `MODIFY (...)`. This disambiguates valid column names such as `Element`, which Oracle can interpret as syntax in the unparenthesized form, rejecting the statement with ORA-00903 or ORA-01735. - -Structured metadata preserves SQL Server nonclustered primary keys and Oracle -ordered foreign-key pairs/delete actions. Foreign-key constructor arrays are copied, -matching primary/unique definitions, so later caller-array edits cannot change the key. - -## Build and package identity - + +Structured metadata preserves SQL Server nonclustered primary keys and Oracle +ordered foreign-key pairs/delete actions. Foreign-key constructor arrays are copied, +matching primary/unique definitions, so later caller-array edits cannot change the key. + +## Build and package identity + The core assembly and file versions are 13.0.0.0; generated assembly metadata preserves the existing title and description. Recompile consumers of the breaking API and update assembly/version binding assumptions. Keep the core, optional DI integration and CLI on compatible package versions. diff --git a/src/Migrator/Framework/ITransformationProvider.cs b/src/Migrator/Framework/ITransformationProvider.cs index abcff336..0e5fa3e5 100644 --- a/src/Migrator/Framework/ITransformationProvider.cs +++ b/src/Migrator/Framework/ITransformationProvider.cs @@ -1,737 +1,737 @@ -using System; -using System.Collections.Generic; -using System.Data; -using DotNetProjects.Migrator.Framework.Models; - -namespace DotNetProjects.Migrator.Framework; - -/// -/// The main interface to use in Migrations to make changes on a database schema. -/// -public interface ITransformationProvider : IDisposable -{ - /// Read named table constraints with ordered key columns; indexes are separate objects. - TableConstraint[] GetTableConstraints(string table); - - /// - /// Get this provider or a NoOp provider if you are not running in the context of 'provider'. - /// - ITransformationProvider this[string provider] { get; } - - string SchemaInfoTable { get; set; } - - int? CommandTimeout { get; set; } - - IDialect Dialect { get; } - - /// - /// The list of Migrations currently applied to the database. - /// - List AppliedMigrations { get; } - - bool IsMigrationApplied(long version, string scope); - - /// - /// Connection string to the database - /// - string ConnectionString { get; } - - /// - /// Logger used to log details of operations performed during migration - /// - ILogger Logger { get; set; } - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - void AddColumn(string table, string column, DbType type); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - void AddColumn(string table, string column, MigratorDbType type); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - void AddColumn(string table, string column, DbType type, int size); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - void AddColumn(string table, string column, MigratorDbType type, int size); - - /// - /// Add a column to an existing table with the default column size. - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The default value of the column if no value is given in a query - void AddColumn(string table, string column, DbType type, object defaultValue); - - /// - /// Add a column to an existing table with the default column size. - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The default value of the column if no value is given in a query - void AddColumn(string table, string column, MigratorDbType type, object defaultValue); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// An instance of a Column with the specified properties - void AddColumn(string table, Column column); +using System; +using System.Collections.Generic; +using System.Data; +using DotNetProjects.Migrator.Framework.Models; + +namespace DotNetProjects.Migrator.Framework; + +/// +/// The main interface to use in Migrations to make changes on a database schema. +/// +public interface ITransformationProvider : IDisposable +{ + /// Read named table constraints with ordered key columns; indexes are separate objects. + TableConstraint[] GetTableConstraints(string table); + + /// + /// Get this provider or a NoOp provider if you are not running in the context of 'provider'. + /// + ITransformationProvider this[string provider] { get; } + + string SchemaInfoTable { get; set; } + + int? CommandTimeout { get; set; } + + IDialect Dialect { get; } + + /// + /// The list of Migrations currently applied to the database. + /// + List AppliedMigrations { get; } + + bool IsMigrationApplied(long version, string scope); + + /// + /// Connection string to the database + /// + string ConnectionString { get; } + + /// + /// Logger used to log details of operations performed during migration + /// + ILogger Logger { get; set; } + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + void AddColumn(string table, string column, DbType type); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + void AddColumn(string table, string column, MigratorDbType type); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The precision or size of the column + void AddColumn(string table, string column, DbType type, int size); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The precision or size of the column + void AddColumn(string table, string column, MigratorDbType type, int size); + + /// + /// Add a column to an existing table with the default column size. + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The default value of the column if no value is given in a query + void AddColumn(string table, string column, DbType type, object defaultValue); + + /// + /// Add a column to an existing table with the default column size. + /// + /// The name of the table that will get the new column + /// The name of the new column + /// The data type for the new columnd + /// The default value of the column if no value is given in a query + void AddColumn(string table, string column, MigratorDbType type, object defaultValue); + + /// + /// Add a column to an existing table + /// + /// The name of the table that will get the new column + /// An instance of a Column with the specified properties + void AddColumn(string table, Column column); /// Add a column and an explicit primary key as one schema operation. SQLite rebuilds once; other providers use their normal DDL transaction semantics. void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey); /// Remove the exact unique constraint returned by metadata, including unnamed SQLite constraints. void RemoveUniqueConstraint(string table, UniqueConstraint constraint); - - /// - /// Add a foreign key constraint - /// - /// The name of the foreign key. e.g. FK_TABLE_REF - /// The table that the foreign key will be created in (e.g. Child) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary keys (e.g. Parent) - /// The columns that are the primary keys in the parent table (e.g. Id) - void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns); - - /// - /// Add a foreign key constraint - /// - /// The name of the foreign key. e.g. FK_TABLE_REF - /// The table that the foreign key will be created in (e.g. Child) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary keys (e.g. Parent) - /// The columns that are the primary keys in the parent table(e.g. Id) - /// Constraint parameters - void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint - /// - /// - /// The name of the foreign key. e.g. FK_TABLE_REF - /// The table that the foreign key will be created in (e.g. Child) - /// The column that is the foreign key (e.g. ParentId) - /// The table that holds the primary keys (e.g. Parent) - /// The column that is the primary key int the parent table (e.g. Id) - void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn); - - /// - /// Add a foreign key constraint - /// - /// The name of the foreign key. e.g. FK_CHILD_PARENT - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The column that is the foreign key (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table(e.g. Id) - /// Constraint parameters - void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The column that is the foreign key (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table(e.g. Id) - void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table (e.g. Id) - void GenerateForeignKey(string foreignTable, string[] foreignColumns, string primaryTable, string[] primaryColumns); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The columns that are the primary keys in the parent table (e.g. Id) - /// Constraint parameters - void GenerateForeignKey(string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The table that the foreign key will be created in (e.g. ChildTable) - /// The columns that are the foreign keys (e.g. ParentId) - /// The table that holds the primary key (e.g. Parent) - /// The column that is the primary key in the parent table (e.g. Id) - /// Constraint parameters - void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The current expectations are that there is a column named the same as the foreignTable present in - /// the table. This is subject to change because I think it's not a good convention. - /// - /// The table that the foreign key will be created in (eg. ChildTable.ParentId) - /// The table that holds the primary key (eg. Table.PK_id) - void GenerateForeignKey(string childTable, string parentTable); - - /// - /// Add a foreign key constraint when you don't care about the name of the constraint. - /// Warning: This will prevent you from dropping the constraint since you won't know the name. - /// - /// The current expectations are that there is a column named the same as the foreignTable present in - /// the table. This is subject to change because I think it's not a good convention. - /// - /// The table that the foreign key will be created in (eg. ChildTable.ParentId) - /// The table that holds the primary key (eg. Table.PK_id) - /// - void GenerateForeignKey(string foreignTable, string primaryTable, ForeignKeyConstraintType constraint); - - /// - /// Add a primary key to a table - /// - /// The name of the primary key to add. - /// The name of the table that will get the primary key. - /// The name of the column or columns that are in the primary key. - void AddPrimaryKey(string name, string table, params string[] columns); - - void AddPrimaryKeyNonClustered(string name, string table, params string[] columns); - /// - /// Add a constraint to a table - /// - /// The name of the constraint to add. - /// The name of the table that will get the constraint - /// The name of the column or columns that will get the constraint. - void AddUniqueConstraint(string name, string table, params string[] columns); - - /// - /// Add a constraint to a table - /// - /// The name of the constraint to add. - /// The name of the table that will get the constraint - /// The check constraint definition. - void AddCheckConstraint(string name, string table, string checkSql); - - void AddView(string name, string tableName, params IViewElement[] viewElements); - - void AddView(string name, string tableName, params IViewField[] fields); - - /// - /// Add a table - /// - /// The name of the table to add. - /// The columns that are part of the table. - void AddTable(string name, params IDbField[] columns); - - /// - /// Add a table - /// - /// The name of the table to add. - /// The name of the database engine to use. (MySQL) - /// The columns that are part of the table. - void AddTable(string name, string engine, params IDbField[] columns); - - /// - /// Start a transction - /// - void BeginTransaction(); - - /// - /// Change the definition of an existing column. - /// - /// The name of the table that will get the new column - /// An instance of a Column with the specified properties and the name of an existing column - void ChangeColumn(string table, Column column); - - void RemoveColumnDefaultValue(string table, string column); - - /// - /// Check to see if a column exists - /// - /// - /// - /// - bool ColumnExists(string table, string column); - - /// - /// Commit the running transction - /// - void Commit(); - - /// - /// Check to see if a constraint exists - /// - /// The name of the constraint - /// The table that the constraint lives on. - /// - bool ConstraintExists(string table, string name); - - /// - /// Copies data from source table to target table using INSERT INTO...SELECT..FROM - /// Be aware that the order of and matters. - /// - /// - /// - /// - /// - /// Sort source by these columns. must contain the . - void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null); - - /// - /// Check to see if a primary key constraint exists on the table - /// - /// The name of the primary key - /// The table that the constraint lives on. - /// - bool PrimaryKeyExists(string table, string name); - - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// timeout - /// Array of parameters of type object - /// - int ExecuteNonQuery(string sql, int timeout, object[] args); - - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// timeout - /// - int ExecuteNonQuery(string sql, int timeout); - - int ExecuteNonQuery(string sql); - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// - IDataReader ExecuteQuery(IDbCommand cmd, string sql); - - /// - /// Creates a DbCommand - /// - /// - IDbCommand CreateCommand(); - - /// - /// Execute an arbitrary SQL query - /// - /// The SQL to execute. - /// A single value that is returned. - object ExecuteScalar(string sql); - - List ExecuteStringQuery(string sql, params object[] args); - - /// - /// Oracle: The retrieval of filter items is not supported in this migrator. If functional expressions are used: they seem to be stored as separate columns (with generated names). - /// - /// - /// - Index[] GetIndexes(string table); - - /// - /// Get the information about the columns in a table. - /// and can in some cases only be guessed. Do not rely on them. Same for - /// - /// The table name that you want the columns for. - /// - [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] - Column[] GetColumns(string table); - - /// - /// Reads the MaxLength of the Data in the Column - /// - /// - /// - /// - int GetColumnContentSize(string table, string columnName); - - /// - /// Gets information about a single column in a table. - /// and can in some cases only be guessed. Do not rely on them. Same for - /// - /// The table name that you want the columns for. - /// The column name for which you want information. - /// - [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] - Column GetColumnByName(string table, string column); - - /// - /// Get the names of all of the tables - /// - /// The names of all the tables. - string[] GetTables(); - - /// - /// Get all foreign keys by the given table name. - /// ATTENTION: For Postgre SQL the result will be lower case if the names were not quoted on table creation of on FK creation! For Oracle they are uppercase! - /// - /// - /// - ForeignKeyConstraint[] GetForeignKeyConstraints(string table); - - /// - /// Insert data into a table - /// - /// The table that will get the new data - /// The names of the columns - /// The values in the same order as the columns - /// - int Insert(string table, string[] columns, object[] values); - - /// - /// Insert data into a table (if it not exists) - /// - /// The table that will get the new data - /// The names of the columns - /// The values in the same order as the columns - /// - int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); - - /// - /// Delete data from a table - /// - /// The table that will have the data deleted - /// The names of the columns used in a where clause - /// The values in the same order as the columns - /// - int Delete(string table, string[] whereColumns = null, object[] whereValues = null); - - /// - /// Delete data from a table - /// - /// The table that will have the data deleted - /// The name of the column used in a where clause - /// The value for the where clause - /// - int Delete(string table, string whereColumn, string whereValue); - - /// - /// Truncate data from a table - /// - /// The table that will have the data deleted - /// - int TruncateTable(string table); - - /// - /// Marks a Migration version number as having been applied - /// - /// The version number of the migration that was applied - void MigrationApplied(long version, string scope); - - /// - /// Marks a Migration version number as having been rolled back from the database - /// - /// The version number of the migration that was removed - void MigrationUnApplied(long version, string scope); - - /// - /// Remove an existing column from a table - /// - /// The name of the table to remove the column from - /// The column to remove - void RemoveColumn(string table, string column); - - /// - /// Remove an existing foreign key constraint. - /// - /// The table that contains the foreign key. - /// The name of the foreign key to remove - void RemoveForeignKey(string table, string name); - - /// - /// Remove an existing constraint. - /// - /// The table that contains the foreign key. - /// The name of the constraint to remove - void RemoveConstraint(string table, string name); - - /// - /// Removes PK, FKs, Unique and CHECK constraints. - /// - /// - [Obsolete("Drop all constraints separately.")] - void RemoveAllConstraints(string table); - - /// - /// Remove an existing primary key. - /// - /// The table that contains the primary key. - void RemovePrimaryKey(string table); - - /// - /// Drops an existing table. - /// - /// The name of the table - void RemoveTable(string tableName); - - /// - /// Rename an existing table - /// - /// The old name of the table - /// The new name of the table - void RenameTable(string oldName, string newName); - - /// - /// Rename an existing table - /// - /// The name of the table - /// The old name of the column - /// The new name of the column - void RenameColumn(string tableName, string oldColumnName, string newColumnName); - - /// - /// Rollback the currently running transaction. - /// - void Rollback(); - - /// - /// Get values from a table - /// - /// The columns to select - /// The table to select from - /// The where clause to limit the selection - /// - IDataReader Select(IDbCommand cmd, string what, string from, string where); - - /// - /// Get values from a table - /// - /// - /// - /// - /// - /// - IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null); - - /// - /// Get values from a table - /// - /// - /// - /// - /// - /// - /// - /// - IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, - object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null); - - /// - /// Get values from a table - /// - /// The columns to select - /// The table to select from - /// - IDataReader Select(IDbCommand cmd, string what, string from); - - /// - /// Get a single value from a table - /// - /// The columns to select - /// The table to select from - /// - /// - object SelectScalar(string what, string from, string where); - - /// - /// Get a single value from a table - /// - /// The columns to select - /// The table to select from - /// - object SelectScalar(string what, string from); - - /// - /// Check if a table already exists - /// - /// The name of the table that you want to check on. - /// - bool TableExists(string tableName); - - /// - /// Check if a view already exists - /// - /// The name of the view that you want to check on. - /// - bool ViewExists(string viewName); - - /// - /// Update the values in a table - /// - /// The name of the table to update - /// The names of the columns. - /// The values for the columns in the same order as the names. - /// - int Update(string table, string[] columns, object[] values); - - /// - /// Update the values in a table - /// - /// The name of the table to update - /// The names of the columns. - /// The values for the columns in the same order as the names. - /// A where clause to limit the update - /// - int Update(string table, string[] columns, object[] values, string where); - - int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); - - /// - /// Updates the target table with data from the source table. Make sure to use primary key or unique columns in - /// - /// Source table name (unquoted). - /// Target table name (unquoted). - /// Pairs of columns that are used to copy data from column in source table to column in target table. - /// Pairs of columns that are used to match rows in source and target table. - void UpdateTargetFromSource(string tableNameSource, string tableNameTarget, ColumnPair[] copyColumnPairs, ColumnPair[] matchColumnPairs); - - /// - /// Get a command instance - /// - /// - IDbCommand GetCommand(); - - - void RemoveAllForeignKeys(string tableName, string columnName); - - bool IsThisProvider(string provider); - - /// - /// Quote a multiple column names, if required - /// - /// - /// - string[] QuoteColumnNamesIfRequired(params string[] columnNames); - - /// - /// Quaote column if required - /// - /// - /// - string QuoteColumnNameIfRequired(string name); - - /// - /// Quote table name if required - /// - /// - /// - string QuoteTableNameIfRequired(string name); - - /// - /// Encodes a guid value as a string, suitable for inclusion in sql statement - /// - /// - /// - string Encode(Guid guid); - - /// - /// Change the target database - /// - /// Name of the new target database - void SwitchDatabase(string databaseName); - - - /// - /// Get a list of databases available on the server - /// - List GetDatabases(); - - /// - /// Checks to see if a database with specific name exists on the server - /// - bool DatabaseExists(string name); - - /// - /// Create a new database on the server - /// - /// Name of the new database - void CreateDatabases(string databaseName); - - /// - /// Close all Connections to the Database. Sometimes needed for DropDatabase or redefine PrimaryKey. - /// - /// Name of the database to close all Connections - void KillDatabaseConnections(string databaseName); - - /// - /// Delete a database from the server - /// - /// Name of the database to delete - void DropDatabases(string databaseName); - - string AddIndex(string table, Index index); - - /// - /// Add a multi-column index to a table - /// - /// The name of the index to add. - /// The name of the table that will get the index. - /// The name of the column or columns that are in the index. - string AddIndex(string name, string table, params string[] columns); - - /// - /// Check to see if an index exists - /// - /// The name of the index - /// The table that the index lives on. - /// - bool IndexExists(string table, string name); - - /// - /// Remove an existing index - /// - /// The table that contains the index. - /// The name of the index to remove - void RemoveIndex(string table, string name); - - /// - /// Generate parameter name based on an index number - /// - /// The index number of the parameter - string GenerateParameterName(int index); - - /// - /// Remove all indexes of a table - /// - /// The table name - void RemoveAllIndexes(string table); - - string Concatenate(params string[] strings); - - IDbConnection Connection { get; } - - IEnumerable GetTables(string schema); - - IEnumerable GetColumns(string schema, string table); -} + + /// + /// Add a foreign key constraint + /// + /// The name of the foreign key. e.g. FK_TABLE_REF + /// The table that the foreign key will be created in (e.g. Child) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary keys (e.g. Parent) + /// The columns that are the primary keys in the parent table (e.g. Id) + void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns); + + /// + /// Add a foreign key constraint + /// + /// The name of the foreign key. e.g. FK_TABLE_REF + /// The table that the foreign key will be created in (e.g. Child) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary keys (e.g. Parent) + /// The columns that are the primary keys in the parent table(e.g. Id) + /// Constraint parameters + void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint + /// + /// + /// The name of the foreign key. e.g. FK_TABLE_REF + /// The table that the foreign key will be created in (e.g. Child) + /// The column that is the foreign key (e.g. ParentId) + /// The table that holds the primary keys (e.g. Parent) + /// The column that is the primary key int the parent table (e.g. Id) + void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn); + + /// + /// Add a foreign key constraint + /// + /// The name of the foreign key. e.g. FK_CHILD_PARENT + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The column that is the foreign key (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table(e.g. Id) + /// Constraint parameters + void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The column that is the foreign key (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table(e.g. Id) + void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table (e.g. Id) + void GenerateForeignKey(string foreignTable, string[] foreignColumns, string primaryTable, string[] primaryColumns); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The columns that are the primary keys in the parent table (e.g. Id) + /// Constraint parameters + void GenerateForeignKey(string childTable, string[] childColumns, string parentTable, string[] parentColumns, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The table that the foreign key will be created in (e.g. ChildTable) + /// The columns that are the foreign keys (e.g. ParentId) + /// The table that holds the primary key (e.g. Parent) + /// The column that is the primary key in the parent table (e.g. Id) + /// Constraint parameters + void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The current expectations are that there is a column named the same as the foreignTable present in + /// the table. This is subject to change because I think it's not a good convention. + /// + /// The table that the foreign key will be created in (eg. ChildTable.ParentId) + /// The table that holds the primary key (eg. Table.PK_id) + void GenerateForeignKey(string childTable, string parentTable); + + /// + /// Add a foreign key constraint when you don't care about the name of the constraint. + /// Warning: This will prevent you from dropping the constraint since you won't know the name. + /// + /// The current expectations are that there is a column named the same as the foreignTable present in + /// the table. This is subject to change because I think it's not a good convention. + /// + /// The table that the foreign key will be created in (eg. ChildTable.ParentId) + /// The table that holds the primary key (eg. Table.PK_id) + /// + void GenerateForeignKey(string foreignTable, string primaryTable, ForeignKeyConstraintType constraint); + + /// + /// Add a primary key to a table + /// + /// The name of the primary key to add. + /// The name of the table that will get the primary key. + /// The name of the column or columns that are in the primary key. + void AddPrimaryKey(string name, string table, params string[] columns); + + void AddPrimaryKeyNonClustered(string name, string table, params string[] columns); + /// + /// Add a constraint to a table + /// + /// The name of the constraint to add. + /// The name of the table that will get the constraint + /// The name of the column or columns that will get the constraint. + void AddUniqueConstraint(string name, string table, params string[] columns); + + /// + /// Add a constraint to a table + /// + /// The name of the constraint to add. + /// The name of the table that will get the constraint + /// The check constraint definition. + void AddCheckConstraint(string name, string table, string checkSql); + + void AddView(string name, string tableName, params IViewElement[] viewElements); + + void AddView(string name, string tableName, params IViewField[] fields); + + /// + /// Add a table + /// + /// The name of the table to add. + /// The columns that are part of the table. + void AddTable(string name, params IDbField[] columns); + + /// + /// Add a table + /// + /// The name of the table to add. + /// The name of the database engine to use. (MySQL) + /// The columns that are part of the table. + void AddTable(string name, string engine, params IDbField[] columns); + + /// + /// Start a transction + /// + void BeginTransaction(); + + /// + /// Change the definition of an existing column. + /// + /// The name of the table that will get the new column + /// An instance of a Column with the specified properties and the name of an existing column + void ChangeColumn(string table, Column column); + + void RemoveColumnDefaultValue(string table, string column); + + /// + /// Check to see if a column exists + /// + /// + /// + /// + bool ColumnExists(string table, string column); + + /// + /// Commit the running transction + /// + void Commit(); + + /// + /// Check to see if a constraint exists + /// + /// The name of the constraint + /// The table that the constraint lives on. + /// + bool ConstraintExists(string table, string name); + + /// + /// Copies data from source table to target table using INSERT INTO...SELECT..FROM + /// Be aware that the order of and matters. + /// + /// + /// + /// + /// + /// Sort source by these columns. must contain the . + void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null); + + /// + /// Check to see if a primary key constraint exists on the table + /// + /// The name of the primary key + /// The table that the constraint lives on. + /// + bool PrimaryKeyExists(string table, string name); + + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// timeout + /// Array of parameters of type object + /// + int ExecuteNonQuery(string sql, int timeout, object[] args); + + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// timeout + /// + int ExecuteNonQuery(string sql, int timeout); + + int ExecuteNonQuery(string sql); + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// + IDataReader ExecuteQuery(IDbCommand cmd, string sql); + + /// + /// Creates a DbCommand + /// + /// + IDbCommand CreateCommand(); + + /// + /// Execute an arbitrary SQL query + /// + /// The SQL to execute. + /// A single value that is returned. + object ExecuteScalar(string sql); + + List ExecuteStringQuery(string sql, params object[] args); + + /// + /// Oracle: The retrieval of filter items is not supported in this migrator. If functional expressions are used: they seem to be stored as separate columns (with generated names). + /// + /// + /// + Index[] GetIndexes(string table); + + /// + /// Get the information about the columns in a table. + /// and can in some cases only be guessed. Do not rely on them. Same for + /// + /// The table name that you want the columns for. + /// + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] + Column[] GetColumns(string table); + + /// + /// Reads the MaxLength of the Data in the Column + /// + /// + /// + /// + int GetColumnContentSize(string table, string columnName); + + /// + /// Gets information about a single column in a table. + /// and can in some cases only be guessed. Do not rely on them. Same for + /// + /// The table name that you want the columns for. + /// The column name for which you want information. + /// + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] + Column GetColumnByName(string table, string column); + + /// + /// Get the names of all of the tables + /// + /// The names of all the tables. + string[] GetTables(); + + /// + /// Get all foreign keys by the given table name. + /// ATTENTION: For Postgre SQL the result will be lower case if the names were not quoted on table creation of on FK creation! For Oracle they are uppercase! + /// + /// + /// + ForeignKeyConstraint[] GetForeignKeyConstraints(string table); + + /// + /// Insert data into a table + /// + /// The table that will get the new data + /// The names of the columns + /// The values in the same order as the columns + /// + int Insert(string table, string[] columns, object[] values); + + /// + /// Insert data into a table (if it not exists) + /// + /// The table that will get the new data + /// The names of the columns + /// The values in the same order as the columns + /// + int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); + + /// + /// Delete data from a table + /// + /// The table that will have the data deleted + /// The names of the columns used in a where clause + /// The values in the same order as the columns + /// + int Delete(string table, string[] whereColumns = null, object[] whereValues = null); + + /// + /// Delete data from a table + /// + /// The table that will have the data deleted + /// The name of the column used in a where clause + /// The value for the where clause + /// + int Delete(string table, string whereColumn, string whereValue); + + /// + /// Truncate data from a table + /// + /// The table that will have the data deleted + /// + int TruncateTable(string table); + + /// + /// Marks a Migration version number as having been applied + /// + /// The version number of the migration that was applied + void MigrationApplied(long version, string scope); + + /// + /// Marks a Migration version number as having been rolled back from the database + /// + /// The version number of the migration that was removed + void MigrationUnApplied(long version, string scope); + + /// + /// Remove an existing column from a table + /// + /// The name of the table to remove the column from + /// The column to remove + void RemoveColumn(string table, string column); + + /// + /// Remove an existing foreign key constraint. + /// + /// The table that contains the foreign key. + /// The name of the foreign key to remove + void RemoveForeignKey(string table, string name); + + /// + /// Remove an existing constraint. + /// + /// The table that contains the foreign key. + /// The name of the constraint to remove + void RemoveConstraint(string table, string name); + + /// + /// Removes PK, FKs, Unique and CHECK constraints. + /// + /// + [Obsolete("Drop all constraints separately.")] + void RemoveAllConstraints(string table); + + /// + /// Remove an existing primary key. + /// + /// The table that contains the primary key. + void RemovePrimaryKey(string table); + + /// + /// Drops an existing table. + /// + /// The name of the table + void RemoveTable(string tableName); + + /// + /// Rename an existing table + /// + /// The old name of the table + /// The new name of the table + void RenameTable(string oldName, string newName); + + /// + /// Rename an existing table + /// + /// The name of the table + /// The old name of the column + /// The new name of the column + void RenameColumn(string tableName, string oldColumnName, string newColumnName); + + /// + /// Rollback the currently running transaction. + /// + void Rollback(); + + /// + /// Get values from a table + /// + /// The columns to select + /// The table to select from + /// The where clause to limit the selection + /// + IDataReader Select(IDbCommand cmd, string what, string from, string where); + + /// + /// Get values from a table + /// + /// + /// + /// + /// + /// + IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null); + + /// + /// Get values from a table + /// + /// + /// + /// + /// + /// + /// + /// + IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, + object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null); + + /// + /// Get values from a table + /// + /// The columns to select + /// The table to select from + /// + IDataReader Select(IDbCommand cmd, string what, string from); + + /// + /// Get a single value from a table + /// + /// The columns to select + /// The table to select from + /// + /// + object SelectScalar(string what, string from, string where); + + /// + /// Get a single value from a table + /// + /// The columns to select + /// The table to select from + /// + object SelectScalar(string what, string from); + + /// + /// Check if a table already exists + /// + /// The name of the table that you want to check on. + /// + bool TableExists(string tableName); + + /// + /// Check if a view already exists + /// + /// The name of the view that you want to check on. + /// + bool ViewExists(string viewName); + + /// + /// Update the values in a table + /// + /// The name of the table to update + /// The names of the columns. + /// The values for the columns in the same order as the names. + /// + int Update(string table, string[] columns, object[] values); + + /// + /// Update the values in a table + /// + /// The name of the table to update + /// The names of the columns. + /// The values for the columns in the same order as the names. + /// A where clause to limit the update + /// + int Update(string table, string[] columns, object[] values, string where); + + int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues); + + /// + /// Updates the target table with data from the source table. Make sure to use primary key or unique columns in + /// + /// Source table name (unquoted). + /// Target table name (unquoted). + /// Pairs of columns that are used to copy data from column in source table to column in target table. + /// Pairs of columns that are used to match rows in source and target table. + void UpdateTargetFromSource(string tableNameSource, string tableNameTarget, ColumnPair[] copyColumnPairs, ColumnPair[] matchColumnPairs); + + /// + /// Get a command instance + /// + /// + IDbCommand GetCommand(); + + + void RemoveAllForeignKeys(string tableName, string columnName); + + bool IsThisProvider(string provider); + + /// + /// Quote a multiple column names, if required + /// + /// + /// + string[] QuoteColumnNamesIfRequired(params string[] columnNames); + + /// + /// Quaote column if required + /// + /// + /// + string QuoteColumnNameIfRequired(string name); + + /// + /// Quote table name if required + /// + /// + /// + string QuoteTableNameIfRequired(string name); + + /// + /// Encodes a guid value as a string, suitable for inclusion in sql statement + /// + /// + /// + string Encode(Guid guid); + + /// + /// Change the target database + /// + /// Name of the new target database + void SwitchDatabase(string databaseName); + + + /// + /// Get a list of databases available on the server + /// + List GetDatabases(); + + /// + /// Checks to see if a database with specific name exists on the server + /// + bool DatabaseExists(string name); + + /// + /// Create a new database on the server + /// + /// Name of the new database + void CreateDatabases(string databaseName); + + /// + /// Close all Connections to the Database. Sometimes needed for DropDatabase or redefine PrimaryKey. + /// + /// Name of the database to close all Connections + void KillDatabaseConnections(string databaseName); + + /// + /// Delete a database from the server + /// + /// Name of the database to delete + void DropDatabases(string databaseName); + + string AddIndex(string table, Index index); + + /// + /// Add a multi-column index to a table + /// + /// The name of the index to add. + /// The name of the table that will get the index. + /// The name of the column or columns that are in the index. + string AddIndex(string name, string table, params string[] columns); + + /// + /// Check to see if an index exists + /// + /// The name of the index + /// The table that the index lives on. + /// + bool IndexExists(string table, string name); + + /// + /// Remove an existing index + /// + /// The table that contains the index. + /// The name of the index to remove + void RemoveIndex(string table, string name); + + /// + /// Generate parameter name based on an index number + /// + /// The index number of the parameter + string GenerateParameterName(int index); + + /// + /// Remove all indexes of a table + /// + /// The table name + void RemoveAllIndexes(string table); + + string Concatenate(params string[] strings); + + IDbConnection Connection { get; } + + IEnumerable GetTables(string schema); + + IEnumerable GetColumns(string schema, string table); +} diff --git a/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs b/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs index 5c3a6f3a..9162e48a 100644 --- a/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs @@ -1,18 +1,18 @@ -using DotNetProjects.Migrator.Framework; -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.Linq; -using Index = DotNetProjects.Migrator.Framework.Index; - -namespace DotNetProjects.Migrator.Providers.Impl.Mysql; - -/// -/// MySql transformation provider -/// -public class MySqlTransformationProvider : TransformationProvider -{ +using DotNetProjects.Migrator.Framework; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace DotNetProjects.Migrator.Providers.Impl.Mysql; + +/// +/// MySql transformation provider +/// +public class MySqlTransformationProvider : TransformationProvider +{ public override void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) { var definition = PrepareColumnWithPrimaryKey(table, column, primaryKey); @@ -20,329 +20,329 @@ public override void AddColumn(string table, Column column, PrimaryKeyConstraint ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ADD COLUMN {_dialect.GetAndMapColumnProperties(definition).ColumnSql}, ADD {_dialect.GetTableConstraintSql(primaryKey)}"); } - public MySqlTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) - : base(dialect, connectionString, null, scope) // we ignore schemas for MySql (schema == database for MySql) - { - if (string.IsNullOrEmpty(providerName)) - { - providerName = "MySql.Data.MySqlClient"; - } - - var fac = DbProviderFactoriesHelper.GetFactory(providerName, "MySql.Data", "MySql.Data.MySqlClient.MySqlClientFactory"); - _connection = fac.CreateConnection(); //new MySqlConnection(_connectionString) {ConnectionString = _connectionString}; - _connection.ConnectionString = _connectionString; - _connection.Open(); - } - - public MySqlTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) - : base(dialect, connection, null, scope) - { - } - - public override void RemoveForeignKey(string table, string name) - { - if (ForeignKeyExists(table, name)) - { - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP FOREIGN KEY {1}", table, _dialect.QuoteIdentifier(name))); - } - } - - public override void RemoveAllIndexes(string table) - { - var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME, i.CONSTRAINT_TYPE - FROM information_schema.KEY_COLUMN_USAGE k - INNER JOIN information_schema.TABLE_CONSTRAINTS i - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME - WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND - (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), table); - - var l = new List>(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, qry)) - { - while (reader.Read()) - { - l.Add(new Tuple(reader.GetString(0), reader.GetString(1), reader.GetString(2))); - } - } - - foreach (var tuple in l) - { - if (tuple.Item3 == "FOREIGN KEY") - { - RemoveForeignKey(tuple.Item1, tuple.Item2); - } - else if (tuple.Item3 == "PRIMARY KEY") - { - try - { - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP PRIMARY KEY", table)); - } - catch (Exception) - { } - } - else if (tuple.Item3 == "UNIQUE") - { - RemoveIndex(tuple.Item1, tuple.Item2); - } - } - } - - public override void RemoveAllForeignKeys(string tableName, string columnName) - { - var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME - FROM information_schema.KEY_COLUMN_USAGE k - INNER JOIN information_schema.TABLE_CONSTRAINTS i - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME - WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND - (k.REFERENCED_TABLE_NAME='{1}' AND REFERENCED_COLUMN_NAME='{2}') OR (k.TABLE_NAME='{1}' AND COLUMN_NAME='{2}')", GetDatabase(), tableName, columnName); - - if (string.IsNullOrEmpty(columnName)) - { - qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME - FROM information_schema.KEY_COLUMN_USAGE k - INNER JOIN information_schema.TABLE_CONSTRAINTS i - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME - WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND - (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), tableName); - } - var l = new List>(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, qry)) - { - while (reader.Read()) - { - l.Add(new Tuple(reader.GetString(0), reader.GetString(1))); - } - } - - foreach (var tuple in l) - { - RemoveForeignKey(tuple.Item1, tuple.Item2); - } - } - - public override void RemoveConstraint(string table, string name) - { - var type = Convert.ToString(ExecuteScalar($"SELECT CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")); - var action = type switch - { - "PRIMARY KEY" => "DROP PRIMARY KEY", - "FOREIGN KEY" => "DROP FOREIGN KEY " + _dialect.QuoteIdentifier(name), - "UNIQUE" => "DROP INDEX " + _dialect.QuoteIdentifier(name), - "CHECK" => (_dialect is MariaDBDialect ? "DROP CONSTRAINT " : "DROP CHECK ") + _dialect.QuoteIdentifier(name), - _ => throw new MigrationException($"Constraint '{name}' does not exist") - }; - ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(table)} {action}"); - } - - public override bool ConstraintExists(string table, string name) - { - return Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")) > 0; - } - - public bool ForeignKeyExists(string table, string name) - { - if (!TableExists(table)) - { - return false; - } - - var sqlConstraint = string.Format(@"SELECT distinct i.CONSTRAINT_NAME - FROM information_schema.TABLE_CONSTRAINTS i - INNER JOIN information_schema.KEY_COLUMN_USAGE k - ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME - WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY' - AND i.TABLE_SCHEMA = '{1}' - AND i.TABLE_NAME = '{0}';", table, GetDatabase()); - - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, sqlConstraint); - - while (reader.Read()) - { - if (reader["CONSTRAINT_NAME"].ToString().ToLower() == name.ToLower()) - { - return true; - } - } - - return false; - } - - public override Index[] GetIndexes(string table) - { - if (!TableExists(table)) return []; - var constraints = ExecuteStringQuery($"SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_TYPE='UNIQUE'").ToHashSet(StringComparer.OrdinalIgnoreCase); - var indexes = new Dictionary(); - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, $"SHOW INDEX FROM {_dialect.Quote(table)}"); - var columns = new Dictionary>(); - while (reader.Read()) - { - var name = Convert.ToString(reader["Key_name"]); - if (!indexes.ContainsKey(name)) - { - indexes[name] = new Index { Name = name, PrimaryKey = name == "PRIMARY", UniqueConstraint = constraints.Contains(name), Unique = Convert.ToInt32(reader["Non_unique"]) == 0 }; - columns[name] = new SortedDictionary(); - } - columns[name][Convert.ToInt32(reader["Seq_in_index"])] = Convert.ToString(reader["Column_name"]); - } - foreach (var item in indexes) item.Value.KeyColumns = columns[item.Key].Values.ToArray(); - return indexes.Values.ToArray(); - } - - public override bool PrimaryKeyExists(string table, string name) - { - return ConstraintExists(table, "PRIMARY"); - } - - public override Column[] GetColumns(string table) - { - var columns = new List(); - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, $"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH, COLUMN_KEY, COLUMN_TYPE, NUMERIC_PRECISION, NUMERIC_SCALE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' ORDER BY ORDINAL_POSITION"); - while (reader.Read()) - { - var type = reader.GetString(1) switch - { - "smallint" => DbType.Int16, "int" or "integer" or "mediumint" => DbType.Int32, - "bigint" => DbType.Int64, "tinyint" => reader.GetString(7).StartsWith("tinyint(1)", StringComparison.OrdinalIgnoreCase) ? DbType.Boolean : DbType.Byte, - "decimal" or "numeric" => DbType.Decimal, "double" => DbType.Double, "float" => DbType.Single, - "date" => DbType.Date, "datetime" or "timestamp" => DbType.DateTime, "time" => DbType.Time, - "tinyblob" or "mediumblob" or "blob" or "binary" or "varbinary" or "longblob" => DbType.Binary, _ => DbType.String - }; - var column = new Column(reader.GetString(0), type); - column.IsNullable = reader.GetString(2) == "YES"; - if (reader.GetString(4).Contains("auto_increment")) column.IsIdentity = true; - if (!reader.IsDBNull(3)) column.DefaultValue = ReadDefault(reader.GetString(3), type, reader.GetString(4)); - if (type == DbType.Decimal) - { - if (!reader.IsDBNull(8)) column.Precision = Convert.ToInt32(reader.GetValue(8)); - if (!reader.IsDBNull(9)) column.Scale = Convert.ToInt32(reader.GetValue(9)); - } - if (!reader.IsDBNull(5)) column.Size = (int)Math.Min(int.MaxValue, Convert.ToInt64(reader.GetValue(5))); - columns.Add(column); - } - return columns.ToArray(); - } - - // Non-string objects retain SQL expression semantics in Dialect.Default. - private sealed record DatabaseDefault(string Sql) - { - public override string ToString() => Sql; - } - - private object ReadDefault(string value, DbType type, string extra) - { - if (_dialect is MariaDBDialect) - { - if (value.Equals("NULL", StringComparison.OrdinalIgnoreCase)) return null; - if (value.StartsWith("'") && value.EndsWith("'")) - value = value[1..^1].Replace("''", "'").Replace("\\'", "'").Replace("\\\\", "\\"); - else if (type == DbType.String) return new DatabaseDefault(value); - } - if (extra.Contains("DEFAULT_GENERATED", StringComparison.OrdinalIgnoreCase) || - (type == DbType.DateTime && value.StartsWith("current_timestamp", StringComparison.OrdinalIgnoreCase))) - return new DatabaseDefault(value); - return type switch - { - DbType.Time => TimeOnly.Parse(value, CultureInfo.InvariantCulture), - DbType.Boolean => value != "0", - DbType.Byte => byte.Parse(value, CultureInfo.InvariantCulture), - DbType.Int16 => short.Parse(value, CultureInfo.InvariantCulture), - DbType.Int32 => int.Parse(value, CultureInfo.InvariantCulture), - DbType.Int64 => long.Parse(value, CultureInfo.InvariantCulture), - DbType.Decimal => decimal.Parse(value, CultureInfo.InvariantCulture), - DbType.Double => double.Parse(value, CultureInfo.InvariantCulture), - DbType.Single => float.Parse(value, CultureInfo.InvariantCulture), - DbType.Date or DbType.DateTime => DateTime.SpecifyKind(DateTime.Parse(value, CultureInfo.InvariantCulture), DateTimeKind.Utc), - _ => value - }; - } - - public override string[] GetTables() - { - var tables = new List(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, "SHOW TABLES")) - { - while (reader.Read()) - { - tables.Add((string)reader[0]); - } - } - - return tables.ToArray(); - } - - public override void ChangeColumn(string table, string sqlColumn) - { - ExecuteNonQuery(string.Format("ALTER TABLE {0} MODIFY {1}", table, sqlColumn)); - } - - public override void AddTable(string name, params IDbField[] columns) - { - AddTable(name, "INNODB", columns); - } - - public override void AddTable(string name, string engine, string columns) - { - var sqlCreate = string.Format("CREATE TABLE {0} ({1}) ENGINE = {2}", name, columns, engine); - ExecuteNonQuery(sqlCreate); - } - - public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - if (!ColumnExists(tableName, oldColumnName) || ColumnExists(tableName, newColumnName)) - throw new MigrationException("Source column must exist and destination column must not exist."); - ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(tableName)} RENAME COLUMN {_dialect.Quote(oldColumnName)} TO {_dialect.Quote(newColumnName)}"); - } - - public string GetDatabase() - { - return ExecuteScalar("SELECT DATABASE()") as string; - } - - public override void RemoveIndex(string table, string name) - { - if (IndexExists(table, name)) - { - ExecuteNonQuery(string.Format("DROP INDEX {1} ON {0}", table, _dialect.QuoteIdentifier(name))); - } - } - - public override List GetDatabases() - { - return ExecuteStringQuery("SHOW DATABASES"); - } - - public override bool IndexExists(string table, string name) - { - return GetIndexes(table).Any(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - } - - public override string Concatenate(params string[] strings) - { - return "CONCAT(" + string.Join(", ", strings) + ")"; - } - public override bool TableExists(string table) => - Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_TYPE='BASE TABLE' AND TABLE_NAME='{table.Replace("'", "''")}'")) > 0; - - public override bool ViewExists(string view) => - Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.VIEWS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{view.Replace("'", "''")}'")) > 0; - - public override string AddIndex(string table, Index index) - { - if (index.KeyColumns.Length == 0) throw new ArgumentException("An index needs key columns.", nameof(index)); - if (index.IncludeColumns.Length != 0 || index.FilterItems.Count != 0 || index.Clustered) - throw new NotSupportedException("MySQL and MariaDB do not support included columns, filtered indexes or explicit clustered indexes."); - var name = index.Name ?? $"IX_{table}_{string.Join("_", index.KeyColumns)}"; - ExecuteNonQuery($"CREATE {(index.Unique ? "UNIQUE " : "")}INDEX {_dialect.QuoteIdentifier(name)} ON {_dialect.Quote(table)} ({string.Join(", ", index.KeyColumns.Select(_dialect.Quote))})"); - return name; - } - - protected override string GetPrimaryKeyConstraintName(string table) => - ConstraintExists(table, "PRIMARY") ? "PRIMARY" : null; - -} + public MySqlTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) + : base(dialect, connectionString, null, scope) // we ignore schemas for MySql (schema == database for MySql) + { + if (string.IsNullOrEmpty(providerName)) + { + providerName = "MySql.Data.MySqlClient"; + } + + var fac = DbProviderFactoriesHelper.GetFactory(providerName, "MySql.Data", "MySql.Data.MySqlClient.MySqlClientFactory"); + _connection = fac.CreateConnection(); //new MySqlConnection(_connectionString) {ConnectionString = _connectionString}; + _connection.ConnectionString = _connectionString; + _connection.Open(); + } + + public MySqlTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) + : base(dialect, connection, null, scope) + { + } + + public override void RemoveForeignKey(string table, string name) + { + if (ForeignKeyExists(table, name)) + { + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP FOREIGN KEY {1}", table, _dialect.QuoteIdentifier(name))); + } + } + + public override void RemoveAllIndexes(string table) + { + var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME, i.CONSTRAINT_TYPE + FROM information_schema.KEY_COLUMN_USAGE k + INNER JOIN information_schema.TABLE_CONSTRAINTS i + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME + WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND + (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), table); + + var l = new List>(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, qry)) + { + while (reader.Read()) + { + l.Add(new Tuple(reader.GetString(0), reader.GetString(1), reader.GetString(2))); + } + } + + foreach (var tuple in l) + { + if (tuple.Item3 == "FOREIGN KEY") + { + RemoveForeignKey(tuple.Item1, tuple.Item2); + } + else if (tuple.Item3 == "PRIMARY KEY") + { + try + { + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP PRIMARY KEY", table)); + } + catch (Exception) + { } + } + else if (tuple.Item3 == "UNIQUE") + { + RemoveIndex(tuple.Item1, tuple.Item2); + } + } + } + + public override void RemoveAllForeignKeys(string tableName, string columnName) + { + var qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE k + INNER JOIN information_schema.TABLE_CONSTRAINTS i + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME + WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND + (k.REFERENCED_TABLE_NAME='{1}' AND REFERENCED_COLUMN_NAME='{2}') OR (k.TABLE_NAME='{1}' AND COLUMN_NAME='{2}')", GetDatabase(), tableName, columnName); + + if (string.IsNullOrEmpty(columnName)) + { + qry = string.Format(@"SELECT k.TABLE_NAME, i.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE k + INNER JOIN information_schema.TABLE_CONSTRAINTS i + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND i.TABLE_NAME = k.TABLE_NAME + WHERE k.REFERENCED_TABLE_SCHEMA='{0}' AND i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND + (k.REFERENCED_TABLE_NAME='{1}') OR (k.TABLE_NAME='{1}')", GetDatabase(), tableName); + } + var l = new List>(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, qry)) + { + while (reader.Read()) + { + l.Add(new Tuple(reader.GetString(0), reader.GetString(1))); + } + } + + foreach (var tuple in l) + { + RemoveForeignKey(tuple.Item1, tuple.Item2); + } + } + + public override void RemoveConstraint(string table, string name) + { + var type = Convert.ToString(ExecuteScalar($"SELECT CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")); + var action = type switch + { + "PRIMARY KEY" => "DROP PRIMARY KEY", + "FOREIGN KEY" => "DROP FOREIGN KEY " + _dialect.QuoteIdentifier(name), + "UNIQUE" => "DROP INDEX " + _dialect.QuoteIdentifier(name), + "CHECK" => (_dialect is MariaDBDialect ? "DROP CONSTRAINT " : "DROP CHECK ") + _dialect.QuoteIdentifier(name), + _ => throw new MigrationException($"Constraint '{name}' does not exist") + }; + ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(table)} {action}"); + } + + public override bool ConstraintExists(string table, string name) + { + return Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_NAME='{name.Replace("'", "''")}'")) > 0; + } + + public bool ForeignKeyExists(string table, string name) + { + if (!TableExists(table)) + { + return false; + } + + var sqlConstraint = string.Format(@"SELECT distinct i.CONSTRAINT_NAME + FROM information_schema.TABLE_CONSTRAINTS i + INNER JOIN information_schema.KEY_COLUMN_USAGE k + ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME + WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY' + AND i.TABLE_SCHEMA = '{1}' + AND i.TABLE_NAME = '{0}';", table, GetDatabase()); + + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, sqlConstraint); + + while (reader.Read()) + { + if (reader["CONSTRAINT_NAME"].ToString().ToLower() == name.ToLower()) + { + return true; + } + } + + return false; + } + + public override Index[] GetIndexes(string table) + { + if (!TableExists(table)) return []; + var constraints = ExecuteStringQuery($"SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' AND CONSTRAINT_TYPE='UNIQUE'").ToHashSet(StringComparer.OrdinalIgnoreCase); + var indexes = new Dictionary(); + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, $"SHOW INDEX FROM {_dialect.Quote(table)}"); + var columns = new Dictionary>(); + while (reader.Read()) + { + var name = Convert.ToString(reader["Key_name"]); + if (!indexes.ContainsKey(name)) + { + indexes[name] = new Index { Name = name, PrimaryKey = name == "PRIMARY", UniqueConstraint = constraints.Contains(name), Unique = Convert.ToInt32(reader["Non_unique"]) == 0 }; + columns[name] = new SortedDictionary(); + } + columns[name][Convert.ToInt32(reader["Seq_in_index"])] = Convert.ToString(reader["Column_name"]); + } + foreach (var item in indexes) item.Value.KeyColumns = columns[item.Key].Values.ToArray(); + return indexes.Values.ToArray(); + } + + public override bool PrimaryKeyExists(string table, string name) + { + return ConstraintExists(table, "PRIMARY"); + } + + public override Column[] GetColumns(string table) + { + var columns = new List(); + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, $"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH, COLUMN_KEY, COLUMN_TYPE, NUMERIC_PRECISION, NUMERIC_SCALE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{table.Replace("'", "''")}' ORDER BY ORDINAL_POSITION"); + while (reader.Read()) + { + var type = reader.GetString(1) switch + { + "smallint" => DbType.Int16, "int" or "integer" or "mediumint" => DbType.Int32, + "bigint" => DbType.Int64, "tinyint" => reader.GetString(7).StartsWith("tinyint(1)", StringComparison.OrdinalIgnoreCase) ? DbType.Boolean : DbType.Byte, + "decimal" or "numeric" => DbType.Decimal, "double" => DbType.Double, "float" => DbType.Single, + "date" => DbType.Date, "datetime" or "timestamp" => DbType.DateTime, "time" => DbType.Time, + "tinyblob" or "mediumblob" or "blob" or "binary" or "varbinary" or "longblob" => DbType.Binary, _ => DbType.String + }; + var column = new Column(reader.GetString(0), type); + column.IsNullable = reader.GetString(2) == "YES"; + if (reader.GetString(4).Contains("auto_increment")) column.IsIdentity = true; + if (!reader.IsDBNull(3)) column.DefaultValue = ReadDefault(reader.GetString(3), type, reader.GetString(4)); + if (type == DbType.Decimal) + { + if (!reader.IsDBNull(8)) column.Precision = Convert.ToInt32(reader.GetValue(8)); + if (!reader.IsDBNull(9)) column.Scale = Convert.ToInt32(reader.GetValue(9)); + } + if (!reader.IsDBNull(5)) column.Size = (int)Math.Min(int.MaxValue, Convert.ToInt64(reader.GetValue(5))); + columns.Add(column); + } + return columns.ToArray(); + } + + // Non-string objects retain SQL expression semantics in Dialect.Default. + private sealed record DatabaseDefault(string Sql) + { + public override string ToString() => Sql; + } + + private object ReadDefault(string value, DbType type, string extra) + { + if (_dialect is MariaDBDialect) + { + if (value.Equals("NULL", StringComparison.OrdinalIgnoreCase)) return null; + if (value.StartsWith("'") && value.EndsWith("'")) + value = value[1..^1].Replace("''", "'").Replace("\\'", "'").Replace("\\\\", "\\"); + else if (type == DbType.String) return new DatabaseDefault(value); + } + if (extra.Contains("DEFAULT_GENERATED", StringComparison.OrdinalIgnoreCase) || + (type == DbType.DateTime && value.StartsWith("current_timestamp", StringComparison.OrdinalIgnoreCase))) + return new DatabaseDefault(value); + return type switch + { + DbType.Time => TimeOnly.Parse(value, CultureInfo.InvariantCulture), + DbType.Boolean => value != "0", + DbType.Byte => byte.Parse(value, CultureInfo.InvariantCulture), + DbType.Int16 => short.Parse(value, CultureInfo.InvariantCulture), + DbType.Int32 => int.Parse(value, CultureInfo.InvariantCulture), + DbType.Int64 => long.Parse(value, CultureInfo.InvariantCulture), + DbType.Decimal => decimal.Parse(value, CultureInfo.InvariantCulture), + DbType.Double => double.Parse(value, CultureInfo.InvariantCulture), + DbType.Single => float.Parse(value, CultureInfo.InvariantCulture), + DbType.Date or DbType.DateTime => DateTime.SpecifyKind(DateTime.Parse(value, CultureInfo.InvariantCulture), DateTimeKind.Utc), + _ => value + }; + } + + public override string[] GetTables() + { + var tables = new List(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, "SHOW TABLES")) + { + while (reader.Read()) + { + tables.Add((string)reader[0]); + } + } + + return tables.ToArray(); + } + + public override void ChangeColumn(string table, string sqlColumn) + { + ExecuteNonQuery(string.Format("ALTER TABLE {0} MODIFY {1}", table, sqlColumn)); + } + + public override void AddTable(string name, params IDbField[] columns) + { + AddTable(name, "INNODB", columns); + } + + public override void AddTable(string name, string engine, string columns) + { + var sqlCreate = string.Format("CREATE TABLE {0} ({1}) ENGINE = {2}", name, columns, engine); + ExecuteNonQuery(sqlCreate); + } + + public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + if (!ColumnExists(tableName, oldColumnName) || ColumnExists(tableName, newColumnName)) + throw new MigrationException("Source column must exist and destination column must not exist."); + ExecuteNonQuery($"ALTER TABLE {_dialect.Quote(tableName)} RENAME COLUMN {_dialect.Quote(oldColumnName)} TO {_dialect.Quote(newColumnName)}"); + } + + public string GetDatabase() + { + return ExecuteScalar("SELECT DATABASE()") as string; + } + + public override void RemoveIndex(string table, string name) + { + if (IndexExists(table, name)) + { + ExecuteNonQuery(string.Format("DROP INDEX {1} ON {0}", table, _dialect.QuoteIdentifier(name))); + } + } + + public override List GetDatabases() + { + return ExecuteStringQuery("SHOW DATABASES"); + } + + public override bool IndexExists(string table, string name) + { + return GetIndexes(table).Any(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + + public override string Concatenate(params string[] strings) + { + return "CONCAT(" + string.Join(", ", strings) + ")"; + } + public override bool TableExists(string table) => + Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_TYPE='BASE TABLE' AND TABLE_NAME='{table.Replace("'", "''")}'")) > 0; + + public override bool ViewExists(string view) => + Convert.ToInt32(ExecuteScalar($"SELECT COUNT(*) FROM information_schema.VIEWS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{view.Replace("'", "''")}'")) > 0; + + public override string AddIndex(string table, Index index) + { + if (index.KeyColumns.Length == 0) throw new ArgumentException("An index needs key columns.", nameof(index)); + if (index.IncludeColumns.Length != 0 || index.FilterItems.Count != 0 || index.Clustered) + throw new NotSupportedException("MySQL and MariaDB do not support included columns, filtered indexes or explicit clustered indexes."); + var name = index.Name ?? $"IX_{table}_{string.Join("_", index.KeyColumns)}"; + ExecuteNonQuery($"CREATE {(index.Unique ? "UNIQUE " : "")}INDEX {_dialect.QuoteIdentifier(name)} ON {_dialect.Quote(table)} ({string.Join(", ", index.KeyColumns.Select(_dialect.Quote))})"); + return name; + } + + protected override string GetPrimaryKeyConstraintName(string table) => + ConstraintExists(table, "PRIMARY") ? "PRIMARY" : null; + +} diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 9ed9b196..923f98bb 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1,398 +1,398 @@ -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Providers.Impl.SQLite.Models; -using System; -using System.Collections.Generic; -using System.Data; -using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; -using Index = DotNetProjects.Migrator.Framework.Index; -using DotNetProjects.Migrator.Framework.Extensions; -using DotNetProjects.Migrator.Providers.Models.Indexes; -using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; -using DotNetProjects.Migrator.Framework.Models; - -namespace DotNetProjects.Migrator.Providers.Impl.SQLite; - -/// -/// Summary description for SQLiteTransformationProvider. -/// -public partial class SQLiteTransformationProvider : TransformationProvider -{ - private const string IntermediateTableSuffix = "Temp"; - - public SQLiteTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) - : base(dialect, connectionString, null, scope) - { - CreateConnection(providerName); - } - - public SQLiteTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) - : base(dialect, connection, null, scope) - { - } - - protected virtual void CreateConnection(string providerName) - { - if (string.IsNullOrEmpty(providerName)) - { - providerName = "System.Data.SQLite"; - } - - var fac = DbProviderFactoriesHelper.GetFactory(providerName, "System.Data.SQLite", "System.Data.SQLite.SQLiteFactory"); - _connection = fac.CreateConnection(); // new SQLiteConnection(_connectionString); - _connection.ConnectionString = _connectionString; - _connection.Open(); - } - - public override void AddForeignKey( - string name, - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns, - ForeignKeyConstraintType constraint) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new Exception("The foreign key name is mandatory"); - } - - var sqliteTableInfo = GetSQLiteTableInfo(childTable); - - // Get all unique constraint names if available - var uniqueConstraintNames = sqliteTableInfo.Uniques.Select(x => x.Name).ToList(); - - // Get all FK constraint names if available - var foreignKeyNames = sqliteTableInfo.ForeignKeys.Select(x => x.Name).ToList(); - - var names = uniqueConstraintNames.Concat(foreignKeyNames) - .Distinct() - .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToList(); - - if (names.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - throw new Exception($"Constraint name {name} already exists"); - } - - var foreignKey = new ForeignKeyConstraint - { - ChildColumns = childColumns, - ChildTable = childTable, - Name = name, - ParentColumns = parentColumns, - ParentTable = parentTable, - OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(constraint), - }; - - sqliteTableInfo.ForeignKeys - .Add(foreignKey); - - RecreateTable(sqliteTableInfo); - } - - public override void AddForeignKey(string table, ForeignKeyConstraint fk) - { - if (fk == null) throw new ArgumentNullException(nameof(fk)); - SQLiteTableSql.ValidateMatch(fk.Match); - base.AddForeignKey(table, fk); - } - - public override void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, - ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) - { - var info = GetSQLiteTableInfo(childTable) ?? throw new MigrationException("Child table does not exist."); - if (string.IsNullOrWhiteSpace(name) || info.ForeignKeys.Select(f => f.Name).Concat(info.Uniques.Select(u => u.Name)) - .Any(existing => string.Equals(existing, name, StringComparison.OrdinalIgnoreCase))) - throw new MigrationException("A unique foreign key name is required."); - info.ForeignKeys.Add(new ForeignKeyConstraint(name, parentTable, (string[])parentColumns.Clone(), childTable, (string[])childColumns.Clone()) - { - OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(onDelete), - OnUpdate = new ForeignKeyConstraintMapper().SqlForConstraint(onUpdate) - }); - RecreateTable(info); - } - - public string[] GetColumnDefs(string table, out string compositeDefSql) - { - return ParseSqlColumnDefs(GetSqlCreateTableScript(table), out compositeDefSql); - } - - /// - /// Gets the SQL CREATE TABLE script. Case-insensitive - /// - /// - /// - public string GetSqlCreateTableScript(string table) - { - string sqlCreateTableScript = null; - - using var cmd = CreateCommand(); - var parameter = cmd.CreateParameter(); parameter.ParameterName = "@name"; parameter.Value = table; cmd.Parameters.Add(parameter); - using var reader = ExecuteQuery(cmd, "SELECT sql FROM sqlite_master WHERE type='table' AND name=@name COLLATE NOCASE"); - if (reader.Read()) sqlCreateTableScript = reader.IsDBNull(0) ? null : reader.GetString(0); - - return sqlCreateTableScript; - } - - public override TableConstraint[] GetTableConstraints(string table) - { - var script = GetSqlCreateTableScript(table); - if (string.IsNullOrWhiteSpace(script)) throw new MigrationException("Table does not exist: " + table); - var constraints = SQLiteConstraintParser.Parse(script); - foreach (var foreignKey in constraints.OfType()) foreignKey.ChildTable = table; - return constraints; - } - - public override ForeignKeyConstraint[] GetForeignKeyConstraints(string tableName) - { - List foreignKeyConstraints = []; - - var pragmaForeignKeyListItems = GetForeignKeyListItems(tableName); - var groups = pragmaForeignKeyListItems.GroupBy(x => x.Id); - - foreach (var group in groups) - { - var foreignKeyConstraint = new ForeignKeyConstraint - { - Id = group.First().Id, - // SQLite does not support FK names. - ChildColumns = group.OrderBy(x => x.Seq).Select(x => x.From).ToArray(), - ChildTable = tableName, - Match = group.First().Match, - Name = null, - OnDelete = group.First().OnDelete, - OnUpdate = group.First().OnUpdate, - ParentColumns = group.OrderBy(x => x.Seq).Select(x => x.To).ToArray(), - ParentTable = group.First().Table, - }; - - foreignKeyConstraints.Add(foreignKeyConstraint); - } - - if (foreignKeyConstraints.Count == 0) - { - return []; - } - - var declared = GetTableConstraints(tableName).OfType().ToList(); - foreach (var foreignKey in foreignKeyConstraints) - { - var definition = declared.FirstOrDefault(candidate => - candidate.ChildColumns.SequenceEqual(foreignKey.ChildColumns, StringComparer.OrdinalIgnoreCase) && - candidate.ParentTable.Equals(foreignKey.ParentTable, StringComparison.OrdinalIgnoreCase) && - (candidate.ParentColumns.Length == 0 || candidate.ParentColumns.SequenceEqual(foreignKey.ParentColumns, StringComparer.OrdinalIgnoreCase))); - if (definition == null) throw new MigrationException("Cannot match a SQLite foreign key to its declaration."); - foreignKey.Name = definition.Name; - // PRAGMA reports NONE even for an explicitly declared MATCH FULL. - // Retain the declaration so a rebuild cannot silently discard it. - foreignKey.Match = definition.Match ?? foreignKey.Match; - declared.Remove(definition); - } - - return foreignKeyConstraints.ToArray(); - } - - public override void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) - { - if (!TableExists(tableSourceNotQuoted)) - { - throw new Exception($"Table '{tableSourceNotQuoted}' given in '{nameof(tableSourceNotQuoted)}' does not exist"); - } - - if (!TableExists(tableTargetNotQuoted)) - { - throw new Exception($"Table '{tableTargetNotQuoted}' given in '{nameof(tableTargetNotQuoted)}' does not exist"); - } - - if (fromSourceToTargetColumnPairs.Length == 0) - { - throw new Exception($"{nameof(fromSourceToTargetColumnPairs)} is empty."); - } - - if (fromSourceToTargetColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) - { - throw new Exception($"One of the strings in {nameof(fromSourceToTargetColumnPairs)} is null or empty"); - } - - if (conditionColumnPairs.Length == 0) - { - throw new Exception($"{nameof(conditionColumnPairs)} is empty."); - } - - if (conditionColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) - { - throw new Exception($"One of the strings in {nameof(conditionColumnPairs)} is null or empty"); - } - - var tableNameSource = QuoteTableNameIfRequired(tableSourceNotQuoted); - var tableNameTarget = QuoteTableNameIfRequired(tableTargetNotQuoted); - - var assignStrings = fromSourceToTargetColumnPairs.Select(x => $"{QuoteColumnNameIfRequired(x.ColumnNameTarget)} = {tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)}").ToList(); - - var conditionStrings = conditionColumnPairs.Select(x => $"{tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)} = {tableNameTarget}.{QuoteColumnNameIfRequired(x.ColumnNameTarget)}"); - - var assignStringsJoined = string.Join(", ", assignStrings); - var conditionStringsJoined = string.Join(" AND ", conditionStrings); - - var sql = $"UPDATE {tableNameTarget} SET {assignStringsJoined} FROM {tableNameSource} WHERE {conditionStringsJoined}"; - ExecuteNonQuery(sql); - } - - private List GetForeignKeyListItems(string tableNameNotQuoted) - { - List pragmaForeignKeyListItems = []; - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA foreign_key_list('{QuoteTableNameIfRequired(tableNameNotQuoted)}')")) - { - while (reader.Read()) - { - var pragmaForeignKeyListItem = new PragmaForeignKeyListItem - { - Id = reader.GetInt32(reader.GetOrdinal("id")), - Seq = reader.GetInt32(reader.GetOrdinal("seq")), - Table = reader.GetString(reader.GetOrdinal("table")), - From = reader.GetString(reader.GetOrdinal("from")), - To = reader.GetString(reader.GetOrdinal("to")), - OnUpdate = reader.GetString(reader.GetOrdinal("on_update")), - OnDelete = reader.GetString(reader.GetOrdinal("on_delete")), - Match = reader.GetString(reader.GetOrdinal("match")), - }; - - pragmaForeignKeyListItems.Add(pragmaForeignKeyListItem); - } - } - - return pragmaForeignKeyListItems; - } - - public string[] ParseSqlColumnDefs(string sqldef, out string compositeDefSql) - { - if (string.IsNullOrEmpty(sqldef)) - { - compositeDefSql = null; - - return null; - } - - sqldef = sqldef.Replace(Environment.NewLine, " "); - var start = sqldef.IndexOf("("); - - // Code to handle composite primary keys /mol - var compositeDefIndex = sqldef.IndexOf("PRIMARY KEY ("); // Not ideal to search for a string like this but I'm lazy - - if (compositeDefIndex > -1) - { - compositeDefSql = sqldef.Substring(compositeDefIndex, sqldef.LastIndexOf(")") - compositeDefIndex); - sqldef = sqldef.Substring(0, compositeDefIndex).TrimEnd(',', ' ') + ")"; - } - else - { - compositeDefSql = null; - } - - var end = sqldef.LastIndexOf(")"); // Changed from 'IndexOf' to 'LastIndexOf' to handle foreign key definitions /mol - - sqldef = sqldef.Substring(0, end); - sqldef = sqldef.Substring(start + 1); - - var cols = sqldef.Split([',']); - - for (var i = 0; i < cols.Length; i++) - { - cols[i] = cols[i].Trim(); - } - - return cols; - } - - /// - /// Turn something like 'columnName INTEGER NOT NULL' into just 'columnName' - /// - public string[] ParseSqlForColumnNames(string sqldef, out string compositeDefSql) - { - var parts = ParseSqlColumnDefs(sqldef, out compositeDefSql); - - return ParseSqlForColumnNames(parts); - } - - public string[] ParseSqlForColumnNames(string[] parts) - { - if (null == parts) - { - return null; - } - - for (var i = 0; i < parts.Length; i++) - { - parts[i] = ExtractNameFromColumnDef(parts[i]); - } - - return parts; - } - - /// - /// Name is the first value before the space. - /// - /// - /// - public static string ExtractNameFromColumnDef(string columnDef) - { - var idx = columnDef.IndexOf(" "); - - if (idx > 0) - { - return columnDef.Substring(0, idx); - } - return null; - } - - public DbType ExtractTypeFromColumnDef(string columnDef) - { - var idx = columnDef.IndexOf(" ") + 1; - - if (idx > 0) - { - var idy = columnDef.IndexOf(" ", idx) - idx; - - if (idy > 0) - { - return _dialect.GetDbType(columnDef.Substring(idx, idy)); - } - else - { - return _dialect.GetDbType(columnDef.Substring(idx)); - } - } - else - { - throw new Exception("Error extracting type from column definition: '" + columnDef + "'"); - } - } - - public override void RemoveForeignKey(string table, string name) - { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - var sqliteTableInfo = GetSQLiteTableInfo(table); - if (!sqliteTableInfo.ForeignKeys.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException($"Foreign key '{name}' does not exist."); - } - - sqliteTableInfo.ForeignKeys.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - - RecreateTable(sqliteTableInfo); - } - +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Impl.SQLite.Models; +using System; +using System.Collections.Generic; +using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; +using Index = DotNetProjects.Migrator.Framework.Index; +using DotNetProjects.Migrator.Framework.Extensions; +using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; +using DotNetProjects.Migrator.Framework.Models; + +namespace DotNetProjects.Migrator.Providers.Impl.SQLite; + +/// +/// Summary description for SQLiteTransformationProvider. +/// +public partial class SQLiteTransformationProvider : TransformationProvider +{ + private const string IntermediateTableSuffix = "Temp"; + + public SQLiteTransformationProvider(Dialect dialect, string connectionString, string scope, string providerName) + : base(dialect, connectionString, null, scope) + { + CreateConnection(providerName); + } + + public SQLiteTransformationProvider(Dialect dialect, IDbConnection connection, string scope, string providerName) + : base(dialect, connection, null, scope) + { + } + + protected virtual void CreateConnection(string providerName) + { + if (string.IsNullOrEmpty(providerName)) + { + providerName = "System.Data.SQLite"; + } + + var fac = DbProviderFactoriesHelper.GetFactory(providerName, "System.Data.SQLite", "System.Data.SQLite.SQLiteFactory"); + _connection = fac.CreateConnection(); // new SQLiteConnection(_connectionString); + _connection.ConnectionString = _connectionString; + _connection.Open(); + } + + public override void AddForeignKey( + string name, + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns, + ForeignKeyConstraintType constraint) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new Exception("The foreign key name is mandatory"); + } + + var sqliteTableInfo = GetSQLiteTableInfo(childTable); + + // Get all unique constraint names if available + var uniqueConstraintNames = sqliteTableInfo.Uniques.Select(x => x.Name).ToList(); + + // Get all FK constraint names if available + var foreignKeyNames = sqliteTableInfo.ForeignKeys.Select(x => x.Name).ToList(); + + var names = uniqueConstraintNames.Concat(foreignKeyNames) + .Distinct() + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToList(); + + if (names.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new Exception($"Constraint name {name} already exists"); + } + + var foreignKey = new ForeignKeyConstraint + { + ChildColumns = childColumns, + ChildTable = childTable, + Name = name, + ParentColumns = parentColumns, + ParentTable = parentTable, + OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(constraint), + }; + + sqliteTableInfo.ForeignKeys + .Add(foreignKey); + + RecreateTable(sqliteTableInfo); + } + + public override void AddForeignKey(string table, ForeignKeyConstraint fk) + { + if (fk == null) throw new ArgumentNullException(nameof(fk)); + SQLiteTableSql.ValidateMatch(fk.Match); + base.AddForeignKey(table, fk); + } + + public override void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, + ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) + { + var info = GetSQLiteTableInfo(childTable) ?? throw new MigrationException("Child table does not exist."); + if (string.IsNullOrWhiteSpace(name) || info.ForeignKeys.Select(f => f.Name).Concat(info.Uniques.Select(u => u.Name)) + .Any(existing => string.Equals(existing, name, StringComparison.OrdinalIgnoreCase))) + throw new MigrationException("A unique foreign key name is required."); + info.ForeignKeys.Add(new ForeignKeyConstraint(name, parentTable, (string[])parentColumns.Clone(), childTable, (string[])childColumns.Clone()) + { + OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(onDelete), + OnUpdate = new ForeignKeyConstraintMapper().SqlForConstraint(onUpdate) + }); + RecreateTable(info); + } + + public string[] GetColumnDefs(string table, out string compositeDefSql) + { + return ParseSqlColumnDefs(GetSqlCreateTableScript(table), out compositeDefSql); + } + + /// + /// Gets the SQL CREATE TABLE script. Case-insensitive + /// + /// + /// + public string GetSqlCreateTableScript(string table) + { + string sqlCreateTableScript = null; + + using var cmd = CreateCommand(); + var parameter = cmd.CreateParameter(); parameter.ParameterName = "@name"; parameter.Value = table; cmd.Parameters.Add(parameter); + using var reader = ExecuteQuery(cmd, "SELECT sql FROM sqlite_master WHERE type='table' AND name=@name COLLATE NOCASE"); + if (reader.Read()) sqlCreateTableScript = reader.IsDBNull(0) ? null : reader.GetString(0); + + return sqlCreateTableScript; + } + + public override TableConstraint[] GetTableConstraints(string table) + { + var script = GetSqlCreateTableScript(table); + if (string.IsNullOrWhiteSpace(script)) throw new MigrationException("Table does not exist: " + table); + var constraints = SQLiteConstraintParser.Parse(script); + foreach (var foreignKey in constraints.OfType()) foreignKey.ChildTable = table; + return constraints; + } + + public override ForeignKeyConstraint[] GetForeignKeyConstraints(string tableName) + { + List foreignKeyConstraints = []; + + var pragmaForeignKeyListItems = GetForeignKeyListItems(tableName); + var groups = pragmaForeignKeyListItems.GroupBy(x => x.Id); + + foreach (var group in groups) + { + var foreignKeyConstraint = new ForeignKeyConstraint + { + Id = group.First().Id, + // SQLite does not support FK names. + ChildColumns = group.OrderBy(x => x.Seq).Select(x => x.From).ToArray(), + ChildTable = tableName, + Match = group.First().Match, + Name = null, + OnDelete = group.First().OnDelete, + OnUpdate = group.First().OnUpdate, + ParentColumns = group.OrderBy(x => x.Seq).Select(x => x.To).ToArray(), + ParentTable = group.First().Table, + }; + + foreignKeyConstraints.Add(foreignKeyConstraint); + } + + if (foreignKeyConstraints.Count == 0) + { + return []; + } + + var declared = GetTableConstraints(tableName).OfType().ToList(); + foreach (var foreignKey in foreignKeyConstraints) + { + var definition = declared.FirstOrDefault(candidate => + candidate.ChildColumns.SequenceEqual(foreignKey.ChildColumns, StringComparer.OrdinalIgnoreCase) && + candidate.ParentTable.Equals(foreignKey.ParentTable, StringComparison.OrdinalIgnoreCase) && + (candidate.ParentColumns.Length == 0 || candidate.ParentColumns.SequenceEqual(foreignKey.ParentColumns, StringComparer.OrdinalIgnoreCase))); + if (definition == null) throw new MigrationException("Cannot match a SQLite foreign key to its declaration."); + foreignKey.Name = definition.Name; + // PRAGMA reports NONE even for an explicitly declared MATCH FULL. + // Retain the declaration so a rebuild cannot silently discard it. + foreignKey.Match = definition.Match ?? foreignKey.Match; + declared.Remove(definition); + } + + return foreignKeyConstraints.ToArray(); + } + + public override void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) + { + if (!TableExists(tableSourceNotQuoted)) + { + throw new Exception($"Table '{tableSourceNotQuoted}' given in '{nameof(tableSourceNotQuoted)}' does not exist"); + } + + if (!TableExists(tableTargetNotQuoted)) + { + throw new Exception($"Table '{tableTargetNotQuoted}' given in '{nameof(tableTargetNotQuoted)}' does not exist"); + } + + if (fromSourceToTargetColumnPairs.Length == 0) + { + throw new Exception($"{nameof(fromSourceToTargetColumnPairs)} is empty."); + } + + if (fromSourceToTargetColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) + { + throw new Exception($"One of the strings in {nameof(fromSourceToTargetColumnPairs)} is null or empty"); + } + + if (conditionColumnPairs.Length == 0) + { + throw new Exception($"{nameof(conditionColumnPairs)} is empty."); + } + + if (conditionColumnPairs.Any(x => string.IsNullOrWhiteSpace(x.ColumnNameSource) || string.IsNullOrWhiteSpace(x.ColumnNameTarget))) + { + throw new Exception($"One of the strings in {nameof(conditionColumnPairs)} is null or empty"); + } + + var tableNameSource = QuoteTableNameIfRequired(tableSourceNotQuoted); + var tableNameTarget = QuoteTableNameIfRequired(tableTargetNotQuoted); + + var assignStrings = fromSourceToTargetColumnPairs.Select(x => $"{QuoteColumnNameIfRequired(x.ColumnNameTarget)} = {tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)}").ToList(); + + var conditionStrings = conditionColumnPairs.Select(x => $"{tableNameSource}.{QuoteColumnNameIfRequired(x.ColumnNameSource)} = {tableNameTarget}.{QuoteColumnNameIfRequired(x.ColumnNameTarget)}"); + + var assignStringsJoined = string.Join(", ", assignStrings); + var conditionStringsJoined = string.Join(" AND ", conditionStrings); + + var sql = $"UPDATE {tableNameTarget} SET {assignStringsJoined} FROM {tableNameSource} WHERE {conditionStringsJoined}"; + ExecuteNonQuery(sql); + } + + private List GetForeignKeyListItems(string tableNameNotQuoted) + { + List pragmaForeignKeyListItems = []; + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA foreign_key_list('{QuoteTableNameIfRequired(tableNameNotQuoted)}')")) + { + while (reader.Read()) + { + var pragmaForeignKeyListItem = new PragmaForeignKeyListItem + { + Id = reader.GetInt32(reader.GetOrdinal("id")), + Seq = reader.GetInt32(reader.GetOrdinal("seq")), + Table = reader.GetString(reader.GetOrdinal("table")), + From = reader.GetString(reader.GetOrdinal("from")), + To = reader.GetString(reader.GetOrdinal("to")), + OnUpdate = reader.GetString(reader.GetOrdinal("on_update")), + OnDelete = reader.GetString(reader.GetOrdinal("on_delete")), + Match = reader.GetString(reader.GetOrdinal("match")), + }; + + pragmaForeignKeyListItems.Add(pragmaForeignKeyListItem); + } + } + + return pragmaForeignKeyListItems; + } + + public string[] ParseSqlColumnDefs(string sqldef, out string compositeDefSql) + { + if (string.IsNullOrEmpty(sqldef)) + { + compositeDefSql = null; + + return null; + } + + sqldef = sqldef.Replace(Environment.NewLine, " "); + var start = sqldef.IndexOf("("); + + // Code to handle composite primary keys /mol + var compositeDefIndex = sqldef.IndexOf("PRIMARY KEY ("); // Not ideal to search for a string like this but I'm lazy + + if (compositeDefIndex > -1) + { + compositeDefSql = sqldef.Substring(compositeDefIndex, sqldef.LastIndexOf(")") - compositeDefIndex); + sqldef = sqldef.Substring(0, compositeDefIndex).TrimEnd(',', ' ') + ")"; + } + else + { + compositeDefSql = null; + } + + var end = sqldef.LastIndexOf(")"); // Changed from 'IndexOf' to 'LastIndexOf' to handle foreign key definitions /mol + + sqldef = sqldef.Substring(0, end); + sqldef = sqldef.Substring(start + 1); + + var cols = sqldef.Split([',']); + + for (var i = 0; i < cols.Length; i++) + { + cols[i] = cols[i].Trim(); + } + + return cols; + } + + /// + /// Turn something like 'columnName INTEGER NOT NULL' into just 'columnName' + /// + public string[] ParseSqlForColumnNames(string sqldef, out string compositeDefSql) + { + var parts = ParseSqlColumnDefs(sqldef, out compositeDefSql); + + return ParseSqlForColumnNames(parts); + } + + public string[] ParseSqlForColumnNames(string[] parts) + { + if (null == parts) + { + return null; + } + + for (var i = 0; i < parts.Length; i++) + { + parts[i] = ExtractNameFromColumnDef(parts[i]); + } + + return parts; + } + + /// + /// Name is the first value before the space. + /// + /// + /// + public static string ExtractNameFromColumnDef(string columnDef) + { + var idx = columnDef.IndexOf(" "); + + if (idx > 0) + { + return columnDef.Substring(0, idx); + } + return null; + } + + public DbType ExtractTypeFromColumnDef(string columnDef) + { + var idx = columnDef.IndexOf(" ") + 1; + + if (idx > 0) + { + var idy = columnDef.IndexOf(" ", idx) - idx; + + if (idy > 0) + { + return _dialect.GetDbType(columnDef.Substring(idx, idy)); + } + else + { + return _dialect.GetDbType(columnDef.Substring(idx)); + } + } + else + { + throw new Exception("Error extracting type from column definition: '" + columnDef + "'"); + } + } + + public override void RemoveForeignKey(string table, string name) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + var sqliteTableInfo = GetSQLiteTableInfo(table); + if (!sqliteTableInfo.ForeignKeys.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException($"Foreign key '{name}' does not exist."); + } + + sqliteTableInfo.ForeignKeys.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + + RecreateTable(sqliteTableInfo); + } + public override void RemoveAllForeignKeys(string tableName, string columnName) { bool Matches(string name) => string.Equals(name, tableName, StringComparison.OrdinalIgnoreCase); @@ -436,55 +436,55 @@ public override void RemoveAllForeignKeys(string tableName, string columnName) } } - public string[] GetCreateIndexSqlStrings(string table) - { - var sqlStrings = new List(); - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, string.Format("SELECT sql FROM sqlite_master WHERE type='index' AND sql NOT NULL AND lower(tbl_name)=lower('{0}')", table))) - { - while (reader.Read()) - { - sqlStrings.Add((string)reader[0]); - } - } - - return [.. sqlStrings]; - } - - public void MoveIndexesFromOriginalTable(string origTable, string newTable) - { - var indexSqls = GetCreateIndexSqlStrings(origTable); - - foreach (var indexSql in indexSqls) - { - var origTableStart = indexSql.IndexOf(" ON ", StringComparison.OrdinalIgnoreCase) + 4; - var origTableEnd = indexSql.IndexOf("(", origTableStart); - - // First remove original index, because names have to be unique - var createIndexDef = " INDEX "; - var indexNameStart = indexSql.IndexOf(createIndexDef, StringComparison.OrdinalIgnoreCase) + createIndexDef.Length; - ExecuteNonQuery("DROP INDEX " + indexSql.Substring(indexNameStart, origTableStart - 4 - indexNameStart)); - - // Create index on new table - ExecuteNonQuery(indexSql.Substring(0, origTableStart) + newTable + " " + indexSql.Substring(origTableEnd)); - } - } - - public override void RemoveColumn(string tableName, string column) - { - if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 35, 0) + public string[] GetCreateIndexSqlStrings(string table) + { + var sqlStrings = new List(); + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, string.Format("SELECT sql FROM sqlite_master WHERE type='index' AND sql NOT NULL AND lower(tbl_name)=lower('{0}')", table))) + { + while (reader.Read()) + { + sqlStrings.Add((string)reader[0]); + } + } + + return [.. sqlStrings]; + } + + public void MoveIndexesFromOriginalTable(string origTable, string newTable) + { + var indexSqls = GetCreateIndexSqlStrings(origTable); + + foreach (var indexSql in indexSqls) + { + var origTableStart = indexSql.IndexOf(" ON ", StringComparison.OrdinalIgnoreCase) + 4; + var origTableEnd = indexSql.IndexOf("(", origTableStart); + + // First remove original index, because names have to be unique + var createIndexDef = " INDEX "; + var indexNameStart = indexSql.IndexOf(createIndexDef, StringComparison.OrdinalIgnoreCase) + createIndexDef.Length; + ExecuteNonQuery("DROP INDEX " + indexSql.Substring(indexNameStart, origTableStart - 4 - indexNameStart)); + + // Create index on new table + ExecuteNonQuery(indexSql.Substring(0, origTableStart) + newTable + " " + indexSql.Substring(origTableEnd)); + } + } + + public override void RemoveColumn(string tableName, string column) + { + if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 35, 0) && TableExists(tableName) && CanDropColumnNatively(tableName, column)) - { + { // Native SQLite validates trigger and view dependencies atomically. ExecuteNonQuery($"ALTER TABLE {Dialect.Quote(tableName)} DROP COLUMN {Dialect.QuoteIdentifier(column)}"); return; - } + } if (IsPragmaForeignKeysOn()) throw new Exception($"{nameof(RemoveColumn)} requires foreign keys off."); if (!TableExists(tableName)) throw new MigrationException($"The table '{tableName}' does not exist"); if (!ColumnExists(tableName, column)) throw new MigrationException($"The table '{tableName}' does not have a column named '{column}'"); - - var sqliteInfoMainTable = GetSQLiteTableInfo(tableName); + + var sqliteInfoMainTable = GetSQLiteTableInfo(tableName); ValidateColumnRemoval(sqliteInfoMainTable, column); var affected = new List(); foreach (var name in GetTables()) @@ -525,165 +525,165 @@ private bool CanDropColumnNatively(string tableName, string column) private static void ValidateColumnRemoval(SQLiteTableInfo sqliteInfoMainTable, string column) { - if (sqliteInfoMainTable.PrimaryKey?.KeyColumns.Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)) == true) - throw new MigrationException("Remove the named primary-key constraint before removing one of its columns."); - - var checkConstraints = sqliteInfoMainTable.CheckConstraints; - - if (checkConstraints.Any(x => x.CheckConstraintString.Contains(column, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException("A check constraint contains the column you want to remove. Remove the check constraint first"); - } - + if (sqliteInfoMainTable.PrimaryKey?.KeyColumns.Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)) == true) + throw new MigrationException("Remove the named primary-key constraint before removing one of its columns."); + + var checkConstraints = sqliteInfoMainTable.CheckConstraints; + + if (checkConstraints.Any(x => x.CheckConstraintString.Contains(column, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException("A check constraint contains the column you want to remove. Remove the check constraint first"); + } + if (!sqliteInfoMainTable.ColumnMappings.Any(x => x.OldName.Equals(column, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException("Column not found"); - } - - // We throw if all of the conditions are fulfilled: - // - the unique constraint is a composite constraint (more than one column) - // - the column to be removed is part of the constraint - // In case of single constraint we remove it silently as it is not needed any more - var isColumnInUniqueConstraint = sqliteInfoMainTable.Uniques - .Where(x => x.KeyColumns.Length > 1) - .SelectMany(x => x.KeyColumns) - .Distinct() - .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); - - if (isColumnInUniqueConstraint) - { - StringBuilder stringBuilder = new(); - stringBuilder.Append("Found composite unique constraint where the column that you want to remove is part of. Remove the unique constraints first before you remove the column."); - stringBuilder.Append("Other unique constraints(if exists) that contains only the column to be removed are dropped silently."); - - throw new Exception(stringBuilder.ToString()); - } - - var isColumnInIndex = sqliteInfoMainTable.Indexes - .Where(x => x.KeyColumns.Length > 1) - .SelectMany(x => x.KeyColumns) - .Distinct() - .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); - - if (isColumnInIndex) - { - StringBuilder stringBuilder = new(); - stringBuilder.Append("Found composite index where the column that you want to remove is part of. Remove the indexes first before you remove the column."); - stringBuilder.Append("Other indexes(if exists) that contains only the column to be removed are dropped silently."); - - throw new Exception(stringBuilder.ToString()); - } - - var isColumnInForeignKey = sqliteInfoMainTable.ForeignKeys - .Where(x => x.ChildColumns.Length > 1) - .SelectMany(x => x.ChildColumns) - .Distinct() - .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); - - if (isColumnInForeignKey) - { - StringBuilder stringBuilder = new(); - stringBuilder.Append("Found foreign key with more than two columns with one column is the column you want to remove. Remove the foreign key before you "); - stringBuilder.Append("remove the column. Other foreign keys (if exists) that contain only the column to be removed are dropped silently."); - - throw new Exception(stringBuilder.ToString()); - } - - } - + { + throw new MigrationException("Column not found"); + } + + // We throw if all of the conditions are fulfilled: + // - the unique constraint is a composite constraint (more than one column) + // - the column to be removed is part of the constraint + // In case of single constraint we remove it silently as it is not needed any more + var isColumnInUniqueConstraint = sqliteInfoMainTable.Uniques + .Where(x => x.KeyColumns.Length > 1) + .SelectMany(x => x.KeyColumns) + .Distinct() + .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); + + if (isColumnInUniqueConstraint) + { + StringBuilder stringBuilder = new(); + stringBuilder.Append("Found composite unique constraint where the column that you want to remove is part of. Remove the unique constraints first before you remove the column."); + stringBuilder.Append("Other unique constraints(if exists) that contains only the column to be removed are dropped silently."); + + throw new Exception(stringBuilder.ToString()); + } + + var isColumnInIndex = sqliteInfoMainTable.Indexes + .Where(x => x.KeyColumns.Length > 1) + .SelectMany(x => x.KeyColumns) + .Distinct() + .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); + + if (isColumnInIndex) + { + StringBuilder stringBuilder = new(); + stringBuilder.Append("Found composite index where the column that you want to remove is part of. Remove the indexes first before you remove the column."); + stringBuilder.Append("Other indexes(if exists) that contains only the column to be removed are dropped silently."); + + throw new Exception(stringBuilder.ToString()); + } + + var isColumnInForeignKey = sqliteInfoMainTable.ForeignKeys + .Where(x => x.ChildColumns.Length > 1) + .SelectMany(x => x.ChildColumns) + .Distinct() + .Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)); + + if (isColumnInForeignKey) + { + StringBuilder stringBuilder = new(); + stringBuilder.Append("Found foreign key with more than two columns with one column is the column you want to remove. Remove the foreign key before you "); + stringBuilder.Append("remove the column. Other foreign keys (if exists) that contain only the column to be removed are dropped silently."); + + throw new Exception(stringBuilder.ToString()); + } + + } + private void RecreateTablesAtomically(IEnumerable tables) { var ownsTransaction = !HasActiveTransaction; if (ownsTransaction) BeginTransaction(); try - { + { foreach (var info in tables) RecreateTable(info); if (ownsTransaction) - { + { if (!CheckForeignKeyIntegrity()) throw new MigrationException("SQLite column removal would leave invalid foreign keys."); Commit(); - } - } + } + } catch (Exception ex) { if (ownsTransaction) try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } throw; } - } - - public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - if (!TableExists(tableName)) - { - throw new Exception($"Table {tableName} does not exist"); - } - - if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 26, 0)) - { - if (string.IsNullOrWhiteSpace(newColumnName)) throw new ArgumentException("A column name is required."); - ExecuteNonQuery($"ALTER TABLE {Dialect.Quote(tableName)} RENAME COLUMN {Dialect.QuoteIdentifier(oldColumnName)} TO {Dialect.QuoteIdentifier(newColumnName)}"); - return; - } - - var isPragmaForeignKeysOn = IsPragmaForeignKeysOn(); - - if (isPragmaForeignKeysOn) - { - throw new Exception($"{nameof(RenameColumn)} requires foreign keys off."); - } - - // Due to old .Net versions we cannot use ThrowIfNullOrWhitespace - if (string.IsNullOrWhiteSpace(newColumnName)) - { - throw new Exception("New column name is null or empty"); - } - - if (ColumnExists(tableName, newColumnName)) - { - throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); - } - - if (ColumnExists(tableName, oldColumnName)) - { - var sqliteTableInfo = GetSQLiteTableInfo(tableName); - - var columnMapping = sqliteTableInfo.ColumnMappings.First(x => x.OldName.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); - columnMapping.NewName = newColumnName; - - var column = sqliteTableInfo.Columns.First(x => x.Name.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); - column.Name = newColumnName; - if (sqliteTableInfo.PrimaryKey != null) - sqliteTableInfo.PrimaryKey.KeyColumns = sqliteTableInfo.PrimaryKey.KeyColumns - .Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); - - foreach (var foreignKey in sqliteTableInfo.ForeignKeys) - { - foreignKey.ChildColumns = [.. foreignKey.ChildColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; + } + + public override void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + if (!TableExists(tableName)) + { + throw new Exception($"Table {tableName} does not exist"); + } + + if (Version.Parse(Convert.ToString(ExecuteScalar("SELECT sqlite_version()"))) >= new Version(3, 26, 0)) + { + if (string.IsNullOrWhiteSpace(newColumnName)) throw new ArgumentException("A column name is required."); + ExecuteNonQuery($"ALTER TABLE {Dialect.Quote(tableName)} RENAME COLUMN {Dialect.QuoteIdentifier(oldColumnName)} TO {Dialect.QuoteIdentifier(newColumnName)}"); + return; + } + + var isPragmaForeignKeysOn = IsPragmaForeignKeysOn(); + + if (isPragmaForeignKeysOn) + { + throw new Exception($"{nameof(RenameColumn)} requires foreign keys off."); + } + + // Due to old .Net versions we cannot use ThrowIfNullOrWhitespace + if (string.IsNullOrWhiteSpace(newColumnName)) + { + throw new Exception("New column name is null or empty"); + } + + if (ColumnExists(tableName, newColumnName)) + { + throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); + } + + if (ColumnExists(tableName, oldColumnName)) + { + var sqliteTableInfo = GetSQLiteTableInfo(tableName); + + var columnMapping = sqliteTableInfo.ColumnMappings.First(x => x.OldName.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); + columnMapping.NewName = newColumnName; + + var column = sqliteTableInfo.Columns.First(x => x.Name.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); + column.Name = newColumnName; + if (sqliteTableInfo.PrimaryKey != null) + sqliteTableInfo.PrimaryKey.KeyColumns = sqliteTableInfo.PrimaryKey.KeyColumns + .Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); + + foreach (var foreignKey in sqliteTableInfo.ForeignKeys) + { + foreignKey.ChildColumns = [.. foreignKey.ChildColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; if (string.Equals(foreignKey.ParentTable, tableName, StringComparison.OrdinalIgnoreCase)) foreignKey.ParentColumns = foreignKey.ParentColumns.Select(x => string.Equals(x, oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); - } - - foreach (var index in sqliteTableInfo.Indexes) - { - index.KeyColumns = [.. index.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; - } - - foreach (var unique in sqliteTableInfo.Uniques) - { - unique.KeyColumns = [.. unique.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; - } - + } + + foreach (var index in sqliteTableInfo.Indexes) + { + index.KeyColumns = [.. index.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; + } + + foreach (var unique in sqliteTableInfo.Uniques) + { + unique.KeyColumns = [.. unique.KeyColumns.Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x)]; + } + // Rebuild the parent and every dependent table atomically. Checking integrity // between those rebuilds would see references to the parent's old column name. var ownsTransaction = !HasActiveTransaction; if (ownsTransaction) BeginTransaction(); try - { + { RecreateTable(sqliteTableInfo); foreach (var otherTable in GetTables()) - { + { if (string.Equals(otherTable, tableName, StringComparison.OrdinalIgnoreCase)) continue; var otherInfo = GetSQLiteTableInfo(otherTable); var references = otherInfo.ForeignKeys.Where(f => @@ -694,328 +694,328 @@ public override void RenameColumn(string tableName, string oldColumnName, string foreignKey.ParentColumns = foreignKey.ParentColumns.Select(x => string.Equals(x, oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); RecreateTable(otherInfo); - } + } if (ownsTransaction) - { + { if (!CheckForeignKeyIntegrity()) throw new MigrationException("SQLite rename would leave invalid foreign keys."); Commit(); - } + } } catch (Exception ex) { if (ownsTransaction) try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } throw; - } - } - else - { - throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); - } - } - - public override void RemoveColumnDefaultValue(string tableName, string columnName) - { - if (!TableExists(tableName)) - { - throw new Exception("Table does not exist"); - } - - if (!ColumnExists(table: tableName, column: columnName)) - { - throw new Exception("Column does not exist"); - } - - var sqliteTableInfo = GetSQLiteTableInfo(tableName); - - var column = sqliteTableInfo.Columns.First(x => x.Name == columnName); - column.DefaultValue = null; - - RecreateTable(sqliteTableInfo); - } - - public override void AddPrimaryKey(string name, string tableName, params string[] columnNames) - { - var info = GetSQLiteTableInfo(tableName) ?? throw new MigrationException("Table does not exist."); - if (info.PrimaryKey != null) throw new MigrationException("The table already has a primary key. Remove it explicitly first."); - ValidateKeyColumns(name, columnNames, info.Columns.ToArray()); - info.PrimaryKey = new PrimaryKeyConstraint(name, columnNames); - RecreateTable(info); - } - - public override bool PrimaryKeyExists(string table, string name) - { - var key = GetTableConstraints(table).OfType().SingleOrDefault(); - return key != null && string.Equals(key.Name, name, StringComparison.OrdinalIgnoreCase); - } - - public override void AddUniqueConstraint(string name, string table, params string[] columns) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new MigrationException("Providing a constraint name is obligatory."); - } - - var sqliteTableInfo = GetSQLiteTableInfo(table); - - if (sqliteTableInfo.Uniques.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) - { - throw new MigrationException("A unique constraint with the same name already exists."); - } - - var uniqueConstraint = new UniqueConstraint() { KeyColumns = columns, Name = name }; - sqliteTableInfo.Uniques.Add(uniqueConstraint); - - RecreateTable(sqliteTableInfo); - } - - public override void RemoveConstraint(string table, string name) - { - var sqliteTableInfo = GetSQLiteTableInfo(table); - sqliteTableInfo.Uniques.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - sqliteTableInfo.CheckConstraints.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - - RecreateTable(sqliteTableInfo); - } - - public SQLiteTableInfo GetSQLiteTableInfo(string tableName) - { - if (!TableExists(tableName)) - { - return null; - } - - var sqliteTable = new SQLiteTableInfo - { - TableNameMapping = new MappingInfo { OldName = tableName, NewName = tableName }, - Columns = GetColumns(tableName).ToList(), - PrimaryKey = GetTableConstraints(tableName).OfType().SingleOrDefault(), - ForeignKeys = GetForeignKeyConstraints(tableName).ToList(), - Indexes = GetIndexes(tableName).ToList(), - Uniques = GetUniques(tableName).ToList(), - CheckConstraints = GetCheckConstraints(tableName) - }; - - if (sqliteTable.PrimaryKey != null) - { - var columnOrder = GetPragmaTableInfoItems(tableName).ToDictionary(c => c.Name, c => c.Cid, StringComparer.OrdinalIgnoreCase); - sqliteTable.Columns = sqliteTable.Columns.OrderBy(c => columnOrder[c.Name]).ToList(); - } - - sqliteTable.ColumnMappings = sqliteTable.Columns - .Select(x => - new MappingInfo - { - OldName = x.Name, - NewName = x.Name - }) - .ToList(); - - return sqliteTable; - } - - public bool CheckForeignKeyIntegrity() - { - - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, "PRAGMA foreign_key_check"); - - if (reader.Read()) - { - return false; - } - - return true; - } - - public bool IsPragmaForeignKeysOn() - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, "PRAGMA foreign_keys"); - reader.Read(); - var isOn = reader.GetInt32(0) == 1; - - return isOn; - } - - public void SetPragmaForeignKeys(bool isOn) - { - var onOffString = isOn ? "ON" : "OFF"; - - using var cmd = CreateCommand(); - ExecuteNonQuery($"PRAGMA foreign_keys = {onOffString}"); - } - - private static string ValidateForeignKeyAction(string action) - { - var normalized = action.ToUpperInvariant(); - if (normalized is not ("CASCADE" or "RESTRICT" or "SET NULL" or "SET DEFAULT" or "NO ACTION")) - throw new MigrationException("Unsupported foreign key action: " + action); - return normalized; - } - - public void RecreateTable(SQLiteTableInfo sqliteTableInfo) - { - foreach (var foreignKey in sqliteTableInfo.ForeignKeys) SQLiteTableSql.ValidateMatch(foreignKey.Match); - var oldName = sqliteTableInfo.TableNameMapping.OldName; - var script = GetSqlCreateTableScript(oldName); - if (SQLiteConstraintParser.HasUnsupportedRebuildFeatures(script)) - throw new NotSupportedException("This table contains SQLite features that cannot be reconstructed faithfully. Use native SQL."); - if (GetCreateIndexSqlStrings(oldName).Any(sql => SQLiteConstraintParser.HasKeyword(sql, "COLLATE"))) - throw new NotSupportedException("Rebuilding indexes with explicit collations requires native SQL."); - var triggers = ExecuteStringQuery("SELECT sql FROM sqlite_master WHERE type='trigger' AND lower(tbl_name)=lower('{0}')", oldName.Replace("'", "''")); - if (triggers.Count > 0 && (oldName != sqliteTableInfo.TableNameMapping.NewName || sqliteTableInfo.ColumnMappings.Any(m => m.OldName != null && m.OldName != m.NewName))) - throw new NotSupportedException("Use native SQLite rename when triggers reference renamed objects."); - var originalColumns = GetColumns(oldName); - if (triggers.Count > 0 && originalColumns.Any(c => !sqliteTableInfo.Columns.Any(n => n.Name.Equals(c.Name, StringComparison.OrdinalIgnoreCase)))) - throw new NotSupportedException("Removing columns from a table with triggers requires native SQLite alteration or explicit trigger recreation."); - var sequence = TableExists("sqlite_sequence") - ? ExecuteScalar("SELECT seq FROM sqlite_sequence WHERE name='" + oldName.Replace("'", "''") + "'") : null; - var highWater = sequence == null || sequence == DBNull.Value ? (long?)null : Convert.ToInt64(sequence); - var foreignKeys = IsPragmaForeignKeysOn(); - if (HasActiveTransaction && foreignKeys) - throw new MigrationException("SQLite rebuild requires foreign keys to be disabled before beginning the transaction. Use the migration runner."); - var ownsTransaction = !HasActiveTransaction; - Exception failure = null; - try - { - if (ownsTransaction) - { - if (foreignKeys) SetPragmaForeignKeys(false); - BeginTransaction(); - } - RecreateTableCore(sqliteTableInfo); - if (highWater.HasValue && sqliteTableInfo.Columns.Any(c => c.IsIdentity)) - { - var sequenceName = sqliteTableInfo.TableNameMapping.NewName.Replace("'", "''"); - var sequenceValue = highWater.Value.ToString(CultureInfo.InvariantCulture); - ExecuteNonQuery($"UPDATE sqlite_sequence SET seq=MAX(seq, {sequenceValue}) WHERE name='{sequenceName}'"); - ExecuteNonQuery($"INSERT INTO sqlite_sequence(name, seq) SELECT '{sequenceName}', {sequenceValue} WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name='{sequenceName}')"); - } - foreach (var trigger in triggers) ExecuteNonQuery(trigger); - if (ownsTransaction && !CheckForeignKeyIntegrity()) throw new MigrationException("SQLite rebuild would leave invalid foreign keys."); - if (ownsTransaction) Commit(); - } - catch (Exception ex) - { - failure = ex; - if (ownsTransaction) - { - try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } - } - throw; - } - finally - { - try { if (ownsTransaction && foreignKeys) SetPragmaForeignKeys(true); } - catch (Exception restore) { if (failure == null) throw; failure.Data["ConnectionRestoreException"] = restore; } - } - } - - private void RecreateTableCore(SQLiteTableInfo sqliteTableInfo) - { - var sourceTableQuoted = QuoteTableNameIfRequired(sqliteTableInfo.TableNameMapping.OldName); - var targetIntermediateTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}{IntermediateTableSuffix}"); - var targetTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}"); - - var columns = sqliteTableInfo.Columns.Select(c => c.CopyDefinition()).ToArray(); - var columnDbFields = columns.Cast(); - var foreignKeyDbFields = sqliteTableInfo.ForeignKeys.Cast(); - var indexDbFields = sqliteTableInfo.Indexes.Cast(); - var uniqueDbFields = sqliteTableInfo.Uniques.Cast(); - var checkConstraintDbFields = sqliteTableInfo.CheckConstraints.Cast(); - - var dbFields = columnDbFields.Concat(foreignKeyDbFields) - .Concat(uniqueDbFields) - .Concat(checkConstraintDbFields) - .Concat(sqliteTableInfo.PrimaryKey == null ? Array.Empty() : new IDbField[] { sqliteTableInfo.PrimaryKey }) - .ToArray(); - - // ToHashSet() not available in older .NET versions so we create it old-fashioned. - var uniqueColumnNames = new HashSet(sqliteTableInfo.Uniques - .SelectMany(x => x.KeyColumns) - .Distinct() - ); - - // ToHashSet() not available in older .NET versions so we create it old-fashioned. - var columnNames = new HashSet(sqliteTableInfo.Columns - .Select(x => x.Name) - ); - - // ToHashSet() not available in older .NET versions so we create it old-fashioned. - var newColumnNamesInMapping = new HashSet(sqliteTableInfo.ColumnMappings - .Select(x => x.NewName) - ); - - if (!columnNames.SetEquals(newColumnNamesInMapping)) - { - throw new Exception($"{nameof(columnNames)} and {nameof(newColumnNamesInMapping)} are not equal regarding length and content"); - } - - if (uniqueColumnNames.Except(columnNames).Any()) - { - var firstMissing = uniqueColumnNames.Except(columnNames).First(); - throw new Exception($"Detected missing column names OR unique key columns that do not exist in the column list/column mapping. E.g. {firstMissing}"); - } - - AddTable(targetIntermediateTableQuoted, null, dbFields); - - var columnMappings = sqliteTableInfo.ColumnMappings - .Where(x => x.OldName != null) - .OrderBy(x => x.OldName) - .ToList(); - - var sourceColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.OldName))); - var targetColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.NewName))); - - using (var cmd = CreateCommand()) - { - var sql = $"INSERT INTO {targetIntermediateTableQuoted} ({targetColumnsQuotedString}) SELECT {sourceColumnsQuotedString} FROM {sourceTableQuoted}"; - ExecuteNonQuery(sql); - } - - RemoveTable(sourceTableQuoted); - - using (var cmd = CreateCommand()) - { - // Rename to original name - var sql = $"ALTER TABLE {targetIntermediateTableQuoted} RENAME TO {targetTableQuoted}"; - ExecuteNonQuery(sql); - } - - foreach (var index in sqliteTableInfo.Indexes) - { - AddIndex(sqliteTableInfo.TableNameMapping.NewName, index); - } - } - - [Obsolete] - public override void AddTable(string table, string engine, string columns) - { - throw new NotSupportedException(); - } - - public override void AddColumn(string table, Column column) - { - if (!TableExists(table)) - { - throw new Exception("Table does not exist."); - } - - var sqliteInfo = GetSQLiteTableInfo(table); - - if (sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) - { - throw new Exception("Column already exists."); - } - - sqliteInfo.ColumnMappings.Add(new MappingInfo { OldName = null, NewName = column.Name }); - sqliteInfo.Columns.Add(column); - - RecreateTable(sqliteInfo); - } - - public override void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) + } + } + else + { + throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); + } + } + + public override void RemoveColumnDefaultValue(string tableName, string columnName) + { + if (!TableExists(tableName)) + { + throw new Exception("Table does not exist"); + } + + if (!ColumnExists(table: tableName, column: columnName)) + { + throw new Exception("Column does not exist"); + } + + var sqliteTableInfo = GetSQLiteTableInfo(tableName); + + var column = sqliteTableInfo.Columns.First(x => x.Name == columnName); + column.DefaultValue = null; + + RecreateTable(sqliteTableInfo); + } + + public override void AddPrimaryKey(string name, string tableName, params string[] columnNames) + { + var info = GetSQLiteTableInfo(tableName) ?? throw new MigrationException("Table does not exist."); + if (info.PrimaryKey != null) throw new MigrationException("The table already has a primary key. Remove it explicitly first."); + ValidateKeyColumns(name, columnNames, info.Columns.ToArray()); + info.PrimaryKey = new PrimaryKeyConstraint(name, columnNames); + RecreateTable(info); + } + + public override bool PrimaryKeyExists(string table, string name) + { + var key = GetTableConstraints(table).OfType().SingleOrDefault(); + return key != null && string.Equals(key.Name, name, StringComparison.OrdinalIgnoreCase); + } + + public override void AddUniqueConstraint(string name, string table, params string[] columns) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new MigrationException("Providing a constraint name is obligatory."); + } + + var sqliteTableInfo = GetSQLiteTableInfo(table); + + if (sqliteTableInfo.Uniques.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException("A unique constraint with the same name already exists."); + } + + var uniqueConstraint = new UniqueConstraint() { KeyColumns = columns, Name = name }; + sqliteTableInfo.Uniques.Add(uniqueConstraint); + + RecreateTable(sqliteTableInfo); + } + + public override void RemoveConstraint(string table, string name) + { + var sqliteTableInfo = GetSQLiteTableInfo(table); + sqliteTableInfo.Uniques.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + sqliteTableInfo.CheckConstraints.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + + RecreateTable(sqliteTableInfo); + } + + public SQLiteTableInfo GetSQLiteTableInfo(string tableName) + { + if (!TableExists(tableName)) + { + return null; + } + + var sqliteTable = new SQLiteTableInfo + { + TableNameMapping = new MappingInfo { OldName = tableName, NewName = tableName }, + Columns = GetColumns(tableName).ToList(), + PrimaryKey = GetTableConstraints(tableName).OfType().SingleOrDefault(), + ForeignKeys = GetForeignKeyConstraints(tableName).ToList(), + Indexes = GetIndexes(tableName).ToList(), + Uniques = GetUniques(tableName).ToList(), + CheckConstraints = GetCheckConstraints(tableName) + }; + + if (sqliteTable.PrimaryKey != null) + { + var columnOrder = GetPragmaTableInfoItems(tableName).ToDictionary(c => c.Name, c => c.Cid, StringComparer.OrdinalIgnoreCase); + sqliteTable.Columns = sqliteTable.Columns.OrderBy(c => columnOrder[c.Name]).ToList(); + } + + sqliteTable.ColumnMappings = sqliteTable.Columns + .Select(x => + new MappingInfo + { + OldName = x.Name, + NewName = x.Name + }) + .ToList(); + + return sqliteTable; + } + + public bool CheckForeignKeyIntegrity() + { + + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, "PRAGMA foreign_key_check"); + + if (reader.Read()) + { + return false; + } + + return true; + } + + public bool IsPragmaForeignKeysOn() + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, "PRAGMA foreign_keys"); + reader.Read(); + var isOn = reader.GetInt32(0) == 1; + + return isOn; + } + + public void SetPragmaForeignKeys(bool isOn) + { + var onOffString = isOn ? "ON" : "OFF"; + + using var cmd = CreateCommand(); + ExecuteNonQuery($"PRAGMA foreign_keys = {onOffString}"); + } + + private static string ValidateForeignKeyAction(string action) + { + var normalized = action.ToUpperInvariant(); + if (normalized is not ("CASCADE" or "RESTRICT" or "SET NULL" or "SET DEFAULT" or "NO ACTION")) + throw new MigrationException("Unsupported foreign key action: " + action); + return normalized; + } + + public void RecreateTable(SQLiteTableInfo sqliteTableInfo) + { + foreach (var foreignKey in sqliteTableInfo.ForeignKeys) SQLiteTableSql.ValidateMatch(foreignKey.Match); + var oldName = sqliteTableInfo.TableNameMapping.OldName; + var script = GetSqlCreateTableScript(oldName); + if (SQLiteConstraintParser.HasUnsupportedRebuildFeatures(script)) + throw new NotSupportedException("This table contains SQLite features that cannot be reconstructed faithfully. Use native SQL."); + if (GetCreateIndexSqlStrings(oldName).Any(sql => SQLiteConstraintParser.HasKeyword(sql, "COLLATE"))) + throw new NotSupportedException("Rebuilding indexes with explicit collations requires native SQL."); + var triggers = ExecuteStringQuery("SELECT sql FROM sqlite_master WHERE type='trigger' AND lower(tbl_name)=lower('{0}')", oldName.Replace("'", "''")); + if (triggers.Count > 0 && (oldName != sqliteTableInfo.TableNameMapping.NewName || sqliteTableInfo.ColumnMappings.Any(m => m.OldName != null && m.OldName != m.NewName))) + throw new NotSupportedException("Use native SQLite rename when triggers reference renamed objects."); + var originalColumns = GetColumns(oldName); + if (triggers.Count > 0 && originalColumns.Any(c => !sqliteTableInfo.Columns.Any(n => n.Name.Equals(c.Name, StringComparison.OrdinalIgnoreCase)))) + throw new NotSupportedException("Removing columns from a table with triggers requires native SQLite alteration or explicit trigger recreation."); + var sequence = TableExists("sqlite_sequence") + ? ExecuteScalar("SELECT seq FROM sqlite_sequence WHERE name='" + oldName.Replace("'", "''") + "'") : null; + var highWater = sequence == null || sequence == DBNull.Value ? (long?)null : Convert.ToInt64(sequence); + var foreignKeys = IsPragmaForeignKeysOn(); + if (HasActiveTransaction && foreignKeys) + throw new MigrationException("SQLite rebuild requires foreign keys to be disabled before beginning the transaction. Use the migration runner."); + var ownsTransaction = !HasActiveTransaction; + Exception failure = null; + try + { + if (ownsTransaction) + { + if (foreignKeys) SetPragmaForeignKeys(false); + BeginTransaction(); + } + RecreateTableCore(sqliteTableInfo); + if (highWater.HasValue && sqliteTableInfo.Columns.Any(c => c.IsIdentity)) + { + var sequenceName = sqliteTableInfo.TableNameMapping.NewName.Replace("'", "''"); + var sequenceValue = highWater.Value.ToString(CultureInfo.InvariantCulture); + ExecuteNonQuery($"UPDATE sqlite_sequence SET seq=MAX(seq, {sequenceValue}) WHERE name='{sequenceName}'"); + ExecuteNonQuery($"INSERT INTO sqlite_sequence(name, seq) SELECT '{sequenceName}', {sequenceValue} WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name='{sequenceName}')"); + } + foreach (var trigger in triggers) ExecuteNonQuery(trigger); + if (ownsTransaction && !CheckForeignKeyIntegrity()) throw new MigrationException("SQLite rebuild would leave invalid foreign keys."); + if (ownsTransaction) Commit(); + } + catch (Exception ex) + { + failure = ex; + if (ownsTransaction) + { + try { Rollback(); } catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } + } + throw; + } + finally + { + try { if (ownsTransaction && foreignKeys) SetPragmaForeignKeys(true); } + catch (Exception restore) { if (failure == null) throw; failure.Data["ConnectionRestoreException"] = restore; } + } + } + + private void RecreateTableCore(SQLiteTableInfo sqliteTableInfo) + { + var sourceTableQuoted = QuoteTableNameIfRequired(sqliteTableInfo.TableNameMapping.OldName); + var targetIntermediateTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}{IntermediateTableSuffix}"); + var targetTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}"); + + var columns = sqliteTableInfo.Columns.Select(c => c.CopyDefinition()).ToArray(); + var columnDbFields = columns.Cast(); + var foreignKeyDbFields = sqliteTableInfo.ForeignKeys.Cast(); + var indexDbFields = sqliteTableInfo.Indexes.Cast(); + var uniqueDbFields = sqliteTableInfo.Uniques.Cast(); + var checkConstraintDbFields = sqliteTableInfo.CheckConstraints.Cast(); + + var dbFields = columnDbFields.Concat(foreignKeyDbFields) + .Concat(uniqueDbFields) + .Concat(checkConstraintDbFields) + .Concat(sqliteTableInfo.PrimaryKey == null ? Array.Empty() : new IDbField[] { sqliteTableInfo.PrimaryKey }) + .ToArray(); + + // ToHashSet() not available in older .NET versions so we create it old-fashioned. + var uniqueColumnNames = new HashSet(sqliteTableInfo.Uniques + .SelectMany(x => x.KeyColumns) + .Distinct() + ); + + // ToHashSet() not available in older .NET versions so we create it old-fashioned. + var columnNames = new HashSet(sqliteTableInfo.Columns + .Select(x => x.Name) + ); + + // ToHashSet() not available in older .NET versions so we create it old-fashioned. + var newColumnNamesInMapping = new HashSet(sqliteTableInfo.ColumnMappings + .Select(x => x.NewName) + ); + + if (!columnNames.SetEquals(newColumnNamesInMapping)) + { + throw new Exception($"{nameof(columnNames)} and {nameof(newColumnNamesInMapping)} are not equal regarding length and content"); + } + + if (uniqueColumnNames.Except(columnNames).Any()) + { + var firstMissing = uniqueColumnNames.Except(columnNames).First(); + throw new Exception($"Detected missing column names OR unique key columns that do not exist in the column list/column mapping. E.g. {firstMissing}"); + } + + AddTable(targetIntermediateTableQuoted, null, dbFields); + + var columnMappings = sqliteTableInfo.ColumnMappings + .Where(x => x.OldName != null) + .OrderBy(x => x.OldName) + .ToList(); + + var sourceColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.OldName))); + var targetColumnsQuotedString = string.Join(", ", columnMappings.Select(x => QuoteColumnNameIfRequired(x.NewName))); + + using (var cmd = CreateCommand()) + { + var sql = $"INSERT INTO {targetIntermediateTableQuoted} ({targetColumnsQuotedString}) SELECT {sourceColumnsQuotedString} FROM {sourceTableQuoted}"; + ExecuteNonQuery(sql); + } + + RemoveTable(sourceTableQuoted); + + using (var cmd = CreateCommand()) + { + // Rename to original name + var sql = $"ALTER TABLE {targetIntermediateTableQuoted} RENAME TO {targetTableQuoted}"; + ExecuteNonQuery(sql); + } + + foreach (var index in sqliteTableInfo.Indexes) + { + AddIndex(sqliteTableInfo.TableNameMapping.NewName, index); + } + } + + [Obsolete] + public override void AddTable(string table, string engine, string columns) + { + throw new NotSupportedException(); + } + + public override void AddColumn(string table, Column column) + { + if (!TableExists(table)) + { + throw new Exception("Table does not exist."); + } + + var sqliteInfo = GetSQLiteTableInfo(table); + + if (sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) + { + throw new Exception("Column already exists."); + } + + sqliteInfo.ColumnMappings.Add(new MappingInfo { OldName = null, NewName = column.Name }); + sqliteInfo.Columns.Add(column); + + RecreateTable(sqliteInfo); + } + + public override void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) { ArgumentException.ThrowIfNullOrWhiteSpace(table); ArgumentNullException.ThrowIfNull(column); @@ -1045,586 +1045,586 @@ public override void RemoveUniqueConstraint(string table, UniqueConstraint const RecreateTable(definition); } - public override void AddColumn(string table, string columnName, DbType type, int size) - { - var column = new Column(columnName, type, size); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, MigratorDbType type, int size) - { - var column = new Column(columnName, type, size); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, DbType type) - { - var column = new Column(columnName, type); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, MigratorDbType type) - { - var column = new Column(columnName, type); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, DbType type, object defaultValue) - { - var column = new Column(columnName, type, defaultValue); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string sqlColumn) - { - var column = new Column(sqlColumn); - AddColumn(table, column); - } - - public override void ChangeColumn(string table, Column column) - { - if (!TableExists(table)) - { - throw new Exception("Table does not exist."); - } - - var sqliteInfo = GetSQLiteTableInfo(table); - - if (!sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) - { - throw new Exception("Column does not exists."); - } - - var columnIndex = sqliteInfo.Columns.FindIndex(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); - sqliteInfo.Columns[columnIndex] = column.CopyDefinition(); - - RecreateTable(sqliteInfo); - } - - public override int TruncateTable(string table) - { - return ExecuteNonQuery(string.Format("DELETE FROM {0} ", table)); - } - - public override bool TableExists(string table) - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='table' and lower(name)=lower('{0}')", table)); - - return reader.Read(); - } - - public override bool ViewExists(string view) - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='view' and lower(name)=lower('{0}')", view)); - - return reader.Read(); - } - - public override List GetDatabases() - { - throw new NotSupportedException("SQLite is a file-based database. You cannot list other databases."); - } - - public override bool ConstraintExists(string table, string name) - { - if (!TableExists(table)) - { - throw new Exception($"Table '{table}' does not exist."); - } - - var constraintNames = GetConstraints(table); - - var exists = constraintNames.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)); - - return exists; - } - - public override string[] GetConstraints(string table) - { - var names = GetTableConstraints(table).Select(c => c.Name).Where(n => !string.IsNullOrWhiteSpace(n)).ToArray(); - if (names.Distinct(StringComparer.OrdinalIgnoreCase).Count() != names.Length) - throw new MigrationException("Duplicate constraint names in table: " + table); - return names; - } - - public override string[] GetTables() - { - var tables = new List(); - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")) - { - while (reader.Read()) - { - tables.Add((string)reader[0]); - } - } - - return [.. tables]; - } - - public override Column[] GetColumns(string tableName) - { - var pragmaTableInfoItems = GetPragmaTableInfoItems(tableName); - var tableScript = GetSqlCreateTableScript(tableName); - var collations = SQLiteConstraintParser.ColumnCollations(tableScript); - - var tableInfoPrimaryKeys = pragmaTableInfoItems.Where(x => x.Pk > 0).ToList(); - var pragmaTableInfoItemsSorted = pragmaTableInfoItems.OrderBy(x => x.Cid).ToList(); - - var columns = new List(); - - foreach (var pragmaTableInfoItem in pragmaTableInfoItemsSorted) - { - var column = new Column(pragmaTableInfoItem.Name) - { - Type = _dialect.GetDbTypeFromString(pragmaTableInfoItem.Type), - Collation = collations.TryGetValue(pragmaTableInfoItem.Name, out var collation) ? collation : null - }; - - if (pragmaTableInfoItem.NotNull) - { - column.IsNullable = false; - } - else - { - column.IsNullable = true; - } - - var defValue = pragmaTableInfoItem.DfltValue == DBNull.Value ? null : pragmaTableInfoItem.DfltValue; - - // Keep legacy text GUID defaults as text during unrelated rebuilds. New Guid - // values render as blobs, so parsing an old SQL literal into Guid would change its storage class. - column.DefaultValue = defValue is string sqlDefault - ? column.Type == DbType.Guid ? RawSql.Insert(sqlDefault) : CatalogDefaultValue.Parse(sqlDefault, column.Type) - : defValue; - - var columnTableInfoItem = pragmaTableInfoItems.First(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); - - var hasCompoundPrimaryKey = tableInfoPrimaryKeys.Count > 1; - - // Implicit in SQLite - if (columnTableInfoItem.Type == "INTEGER" && columnTableInfoItem.Pk == 1 && !hasCompoundPrimaryKey && Regex.IsMatch(tableScript, @"\bAUTOINCREMENT\b", RegexOptions.IgnoreCase)) - { - column.IsIdentity = true; - } - - columns.Add(column); - } - - - return [.. columns]; - } - - public bool IsNullable(string columnDef) - { - return !columnDef.Contains("NOT NULL"); - } - - public bool ColumnMatch(string column, string columnDef) - { - return columnDef.StartsWith(column + " ") || columnDef.StartsWith(_dialect.QuoteIdentifier(column)); - } - - public override bool IndexExists(string table, string name) - { - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='index' and lower(name)=lower('{0}')", name)); - - return reader.Read(); - } - - public override Index[] GetIndexes(string table) - { - var afterWhereRegex = new Regex("(?<= WHERE ).+"); - List indexes = []; - - var indexCreateScripts = GetCreateIndexSqlStrings(table); - - var pragmaIndexListItems = GetPragmaIndexListItems(table).Where(x => x.Origin == "c"); - - var columns = GetColumns(table); - - foreach (var pragmaIndexListItem in pragmaIndexListItems) - { - var indexInfos = GetPragmaIndexInfo(pragmaIndexListItem.Name); - - var columnNames = indexInfos.OrderBy(x => x.SeqNo) - .Select(x => x.Name) - .ToArray(); - - var index = new Index - { - // At this moment in time the migrator does not support clustered indexes for SQLITE - // Since SQLite 3.8.2 WITHOUT ROWID is supported but not in this migrator - Clustered = false, - - // SQLite does not support include colums - IncludeColumns = [], - KeyColumns = columnNames, - Name = pragmaIndexListItem.Name, - Unique = pragmaIndexListItem.Unique - }; - - var script = indexCreateScripts.FirstOrDefault(x => x.Contains(pragmaIndexListItem.Name, StringComparison.OrdinalIgnoreCase)); - - if (script != null) - { - if (afterWhereRegex.Match(script) is Match match && match.Success) - { - // We cannot use GeneratedRegexAttribute due to old .NET version - var andSplitted = Regex.Split(match.Value, " AND "); - - var filterSingleStrings = andSplitted - .Select(x => x.Trim()) - .ToList(); - - foreach (var filterSingleString in filterSingleStrings) - { - var splitted = filterSingleString.Split(' ') - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => x.Trim()) - .ToList(); - - var filterItem = new FilterItem { ColumnName = splitted[0], Filter = _dialect.GetFilterTypeByComparisonString(splitted[1]) }; - - var column = columns.Single(x => x.Name.Equals(splitted[0], StringComparison.OrdinalIgnoreCase)); - - var sqliteIntegerDataTypes = new[] { - MigratorDbType.Int16, - MigratorDbType.Int32, - MigratorDbType.Int64, - MigratorDbType.UInt16, - MigratorDbType.UInt32, - MigratorDbType.UInt64 - }; - - if (sqliteIntegerDataTypes.Contains(column.MigratorDbType)) - { - if (long.TryParse(splitted[2], out var longValue)) - { - filterItem.Value = longValue; - } - else if (ulong.TryParse(splitted[2], out var uLongValue)) - { - filterItem.Value = uLongValue; - } - else - { - throw new Exception(); - } - } - else - { - filterItem.Value = column.MigratorDbType switch - { - MigratorDbType.Boolean => splitted[2] == "1" || splitted[2].Equals("true", StringComparison.OrdinalIgnoreCase), - MigratorDbType.String => splitted[2].Substring(1, splitted[2].Length - 2), - _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), - }; - } - - index.FilterItems.Add(filterItem); - } - } - } - - indexes.Add(index); - } - - return [.. indexes]; - } - - public override void AddTable(string name, string engine, params IDbField[] fields) - { - if (engine != null) throw new NotSupportedException("SQLite does not support table engines."); - var table = _dialect.TableNameNeedsQuote ? _dialect.Quote(name) : QuoteTableNameIfRequired(name); - ExecuteNonQuery(SQLiteTableSql.Generate(_dialect, table, fields)); - foreach (var index in fields.OfType()) AddIndex(name, index); - } - - public override string AddIndex(string table, Index index) - { - ValidateIndex(table, index); - - var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; - - if (hasIncludedColumns) - { - // This will be actived in the future. - // throw new MigrationException($"SQLite does not support included columns. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); - } - - if (index.Clustered) - { - throw new MigrationException($"For SQLite this migrator does not support clustered indexes at this point in time, sorry. File an issue if needed. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); - } - - var name = QuoteConstraintNameIfRequired(index.Name); - table = QuoteTableNameIfRequired(table); - var columns = QuoteColumnNamesIfRequired(index.KeyColumns); - - var uniqueString = index.Unique ? "UNIQUE" : null; - var columnsString = $"({string.Join(", ", columns)})"; - var filterString = string.Empty; - - if (index.FilterItems != null && index.FilterItems.Count > 0) - { - List singleFilterStrings = []; - - foreach (var filterItem in index.FilterItems) - { - var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); - - var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); - string value = null; - - value = filterItem.Value switch - { - bool booleanValue => booleanValue ? "1" : "0", - string stringValue => $"'{stringValue.Replace("'", "''")}'", - byte or short or int or long => Convert.ToInt64(filterItem.Value).ToString(), - sbyte or ushort or uint or ulong => Convert.ToUInt64(filterItem.Value).ToString(), - _ => throw new NotImplementedException("Given type is not implemented. Please file an issue."), - }; - - if ((filterItem.Value is string || filterItem.Value is bool) && filterItem.Filter != FilterType.EqualTo && filterItem.Filter != FilterType.NotEqualTo) - { - throw new MigrationException($"Bool and string in {nameof(FilterItem)} can only be used with '{nameof(FilterType.EqualTo)}' or '{nameof(FilterType.EqualTo)}'."); - } - - var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; - - singleFilterStrings.Add(singleFilterString); - } - - filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; - } - - List list = ["CREATE", uniqueString, "INDEX", name, "ON", table, columnsString, filterString]; - - var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); - - ExecuteNonQuery(sql); - - return sql; - } - - protected override string GetPrimaryKeyConstraintName(string table) - { - return GetTableConstraints(table).OfType().SingleOrDefault()?.Name; - } - - public override void RemoveAllConstraints(string table) - { - var info = GetSQLiteTableInfo(table); - info.PrimaryKey = null; - info.Uniques.Clear(); - info.ForeignKeys.Clear(); - info.CheckConstraints.Clear(); - foreach (var column in info.Columns) column.IsIdentity = false; - RecreateTable(info); - } - - public override void RemovePrimaryKey(string tableName) - { - if (!TableExists(tableName)) return; - var info = GetSQLiteTableInfo(tableName); - info.PrimaryKey = null; - foreach (var column in info.Columns) column.IsIdentity = false; - RecreateTable(info); - } - - public override void RemoveAllIndexes(string tableName) - { - if (!TableExists(tableName)) - { - return; - } - - var sqliteInfoTable = GetSQLiteTableInfo(tableName); - - sqliteInfoTable.Indexes = []; - - RecreateTable(sqliteInfoTable); - } - - public List GetUniques(string tableName) => GetTableConstraints(tableName) - .OfType().ToList(); - - public List GetPragmaIndexInfo(string indexNameNotQuoted) - { - List pragmaIndexInfoItems = []; - - var quotedIndexName = QuoteTableNameIfRequired(indexNameNotQuoted); - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA index_info({quotedIndexName})")) - { - while (reader.Read()) - { - var pragmaIndexInfoItem = new PragmaIndexInfoItem - { - SeqNo = reader.GetInt32(reader.GetOrdinal("seqno")), - Cid = reader.GetInt32(reader.GetOrdinal("cid")), - Name = reader.GetString(reader.GetOrdinal("name")), - }; - - pragmaIndexInfoItems.Add(pragmaIndexInfoItem); - } - } - - return pragmaIndexInfoItems; - } - - public List GetPragmaIndexListItems(string tableNameNotQuoted) - { - List pragmaIndexListItems = []; - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA index_list({QuoteTableNameIfRequired(tableNameNotQuoted)})")) - { - while (reader.Read()) - { - var pragmaIndexListItem = new PragmaIndexListItem - { - Seq = reader.GetInt32(reader.GetOrdinal("seq")), - Name = reader.GetString(reader.GetOrdinal("name")), - Unique = reader.GetInt32(reader.GetOrdinal("unique")) == 1, - Origin = reader.GetString(reader.GetOrdinal("origin")), - Partial = reader.GetInt32(reader.GetOrdinal("partial")) == 1 - }; - - pragmaIndexListItems.Add(pragmaIndexListItem); - } - } - - return pragmaIndexListItems; - } - - public List GetPragmaTableInfoItems(string tableNameNotQuoted) - { - List pragmaTableInfoItems = []; - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, $"PRAGMA table_info({QuoteTableNameIfRequired(tableNameNotQuoted)})")) - { - while (reader.Read()) - { - var pragmaTableInfoItem = new PragmaTableInfoItem - { - Cid = reader.GetInt32(reader.GetOrdinal("cid")), - DfltValue = reader[reader.GetOrdinal("dflt_value")], - Name = reader.GetString(reader.GetOrdinal("name")), - NotNull = reader.GetInt32(reader.GetOrdinal("notnull")) == 1, - Pk = reader.GetInt32(reader.GetOrdinal("pk")), - Type = reader.GetString(reader.GetOrdinal("type")), - }; - - pragmaTableInfoItems.Add(pragmaTableInfoItem); - } - } - - return pragmaTableInfoItems; - } - - public override void AddCheckConstraint(string constraintName, string tableName, string checkSql) - { - var sqliteTableInfo = GetSQLiteTableInfo(tableName); - - var checkConstraint = new CheckConstraint(constraintName, checkSql); - sqliteTableInfo.CheckConstraints.Add(checkConstraint); - - RecreateTable(sqliteTableInfo); - } - - public override void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) - { - orderBySourceColumns ??= []; - - if (!TableExists(sourceTableName)) - { - throw new Exception($"Source table '{QuoteTableNameIfRequired(sourceTableName)}' does not exist"); - } - - if (!TableExists(targetTableName)) - { - throw new Exception($"Target table '{QuoteTableNameIfRequired(targetTableName)}' does not exist"); - } - - var sourceColumnsConcatenated = sourceColumnNames.Concat(orderBySourceColumns); - - foreach (var column in sourceColumnsConcatenated) - { - if (!ColumnExists(sourceTableName, column)) - { - throw new Exception($"Column {column} in source table does not exist."); - } - } - - foreach (var column in targetColumnNames) - { - if (!ColumnExists(targetTableName, column)) - { - throw new Exception($"Column {column} in target table does not exist."); - } - } - - if (!orderBySourceColumns.All(x => sourceColumnNames.Contains(x))) - { - throw new Exception($"All columns in {nameof(orderBySourceColumns)} must be in {nameof(sourceColumnNames)}"); - } - - var sourceTableNameQuoted = QuoteTableNameIfRequired(sourceTableName); - var targetTableNameQuoted = QuoteTableNameIfRequired(targetTableName); - - var sourceColumnNamesQuoted = sourceColumnNames.Select(QuoteColumnNameIfRequired).ToList(); - var targetColumnNamesQuoted = targetColumnNames.Select(QuoteColumnNameIfRequired).ToList(); - var orderBySourceColumnsQuoted = orderBySourceColumns.Select(QuoteColumnNameIfRequired).ToList(); - - var sourceColumnsJoined = string.Join(", ", sourceColumnNamesQuoted); - var targetColumnsJoined = string.Join(", ", targetColumnNamesQuoted); - var orderBySourceColumnsJoined = string.Join(", ", orderBySourceColumnsQuoted); - - var orderByComponent = !string.IsNullOrWhiteSpace(orderBySourceColumnsJoined) ? $"ORDER BY {orderBySourceColumnsJoined}" : null; - - List sqlComponents = - [ - $"INSERT INTO {targetTableNameQuoted} ({targetColumnsJoined}) SELECT {sourceColumnsJoined} FROM {sourceTableNameQuoted}", - orderByComponent - ]; - - var sql = string.Join(" ", sqlComponents.Where(x => x != null)); - ExecuteNonQuery(sql); - } - - public List GetCheckConstraints(string tableName) => GetTableConstraints(tableName).OfType().ToList(); - - protected override void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) - { - if (value is TimeOnly time) - { - // SQLite stores times as text; System.Data.SQLite cannot bind TimeSpan as DbType.Time. - parameter.DbType = DbType.String; - parameter.Value = time.ToString("HH:mm:ss.fffffff", CultureInfo.InvariantCulture); - } - else if (value is ushort) - { - parameter.DbType = DbType.Int32; - parameter.Value = Convert.ToInt32(value); - } - else if (value is uint) - { - parameter.DbType = DbType.Int64; - parameter.Value = Convert.ToInt64(value); - } + public override void AddColumn(string table, string columnName, DbType type, int size) + { + var column = new Column(columnName, type, size); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type, int size) + { + var column = new Column(columnName, type, size); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type) + { + var column = new Column(columnName, type); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type) + { + var column = new Column(columnName, type); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type, object defaultValue) + { + var column = new Column(columnName, type, defaultValue); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string sqlColumn) + { + var column = new Column(sqlColumn); + AddColumn(table, column); + } + + public override void ChangeColumn(string table, Column column) + { + if (!TableExists(table)) + { + throw new Exception("Table does not exist."); + } + + var sqliteInfo = GetSQLiteTableInfo(table); + + if (!sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) + { + throw new Exception("Column does not exists."); + } + + var columnIndex = sqliteInfo.Columns.FindIndex(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); + sqliteInfo.Columns[columnIndex] = column.CopyDefinition(); + + RecreateTable(sqliteInfo); + } + + public override int TruncateTable(string table) + { + return ExecuteNonQuery(string.Format("DELETE FROM {0} ", table)); + } + + public override bool TableExists(string table) + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='table' and lower(name)=lower('{0}')", table)); + + return reader.Read(); + } + + public override bool ViewExists(string view) + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='view' and lower(name)=lower('{0}')", view)); + + return reader.Read(); + } + + public override List GetDatabases() + { + throw new NotSupportedException("SQLite is a file-based database. You cannot list other databases."); + } + + public override bool ConstraintExists(string table, string name) + { + if (!TableExists(table)) + { + throw new Exception($"Table '{table}' does not exist."); + } + + var constraintNames = GetConstraints(table); + + var exists = constraintNames.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)); + + return exists; + } + + public override string[] GetConstraints(string table) + { + var names = GetTableConstraints(table).Select(c => c.Name).Where(n => !string.IsNullOrWhiteSpace(n)).ToArray(); + if (names.Distinct(StringComparer.OrdinalIgnoreCase).Count() != names.Length) + throw new MigrationException("Duplicate constraint names in table: " + table); + return names; + } + + public override string[] GetTables() + { + var tables = new List(); + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")) + { + while (reader.Read()) + { + tables.Add((string)reader[0]); + } + } + + return [.. tables]; + } + + public override Column[] GetColumns(string tableName) + { + var pragmaTableInfoItems = GetPragmaTableInfoItems(tableName); + var tableScript = GetSqlCreateTableScript(tableName); + var collations = SQLiteConstraintParser.ColumnCollations(tableScript); + + var tableInfoPrimaryKeys = pragmaTableInfoItems.Where(x => x.Pk > 0).ToList(); + var pragmaTableInfoItemsSorted = pragmaTableInfoItems.OrderBy(x => x.Cid).ToList(); + + var columns = new List(); + + foreach (var pragmaTableInfoItem in pragmaTableInfoItemsSorted) + { + var column = new Column(pragmaTableInfoItem.Name) + { + Type = _dialect.GetDbTypeFromString(pragmaTableInfoItem.Type), + Collation = collations.TryGetValue(pragmaTableInfoItem.Name, out var collation) ? collation : null + }; + + if (pragmaTableInfoItem.NotNull) + { + column.IsNullable = false; + } + else + { + column.IsNullable = true; + } + + var defValue = pragmaTableInfoItem.DfltValue == DBNull.Value ? null : pragmaTableInfoItem.DfltValue; + + // Keep legacy text GUID defaults as text during unrelated rebuilds. New Guid + // values render as blobs, so parsing an old SQL literal into Guid would change its storage class. + column.DefaultValue = defValue is string sqlDefault + ? column.Type == DbType.Guid ? RawSql.Insert(sqlDefault) : CatalogDefaultValue.Parse(sqlDefault, column.Type) + : defValue; + + var columnTableInfoItem = pragmaTableInfoItems.First(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); + + var hasCompoundPrimaryKey = tableInfoPrimaryKeys.Count > 1; + + // Implicit in SQLite + if (columnTableInfoItem.Type == "INTEGER" && columnTableInfoItem.Pk == 1 && !hasCompoundPrimaryKey && Regex.IsMatch(tableScript, @"\bAUTOINCREMENT\b", RegexOptions.IgnoreCase)) + { + column.IsIdentity = true; + } + + columns.Add(column); + } + + + return [.. columns]; + } + + public bool IsNullable(string columnDef) + { + return !columnDef.Contains("NOT NULL"); + } + + public bool ColumnMatch(string column, string columnDef) + { + return columnDef.StartsWith(column + " ") || columnDef.StartsWith(_dialect.QuoteIdentifier(column)); + } + + public override bool IndexExists(string table, string name) + { + using var cmd = CreateCommand(); + using var reader = ExecuteQuery(cmd, string.Format("SELECT name FROM sqlite_master WHERE type='index' and lower(name)=lower('{0}')", name)); + + return reader.Read(); + } + + public override Index[] GetIndexes(string table) + { + var afterWhereRegex = new Regex("(?<= WHERE ).+"); + List indexes = []; + + var indexCreateScripts = GetCreateIndexSqlStrings(table); + + var pragmaIndexListItems = GetPragmaIndexListItems(table).Where(x => x.Origin == "c"); + + var columns = GetColumns(table); + + foreach (var pragmaIndexListItem in pragmaIndexListItems) + { + var indexInfos = GetPragmaIndexInfo(pragmaIndexListItem.Name); + + var columnNames = indexInfos.OrderBy(x => x.SeqNo) + .Select(x => x.Name) + .ToArray(); + + var index = new Index + { + // At this moment in time the migrator does not support clustered indexes for SQLITE + // Since SQLite 3.8.2 WITHOUT ROWID is supported but not in this migrator + Clustered = false, + + // SQLite does not support include colums + IncludeColumns = [], + KeyColumns = columnNames, + Name = pragmaIndexListItem.Name, + Unique = pragmaIndexListItem.Unique + }; + + var script = indexCreateScripts.FirstOrDefault(x => x.Contains(pragmaIndexListItem.Name, StringComparison.OrdinalIgnoreCase)); + + if (script != null) + { + if (afterWhereRegex.Match(script) is Match match && match.Success) + { + // We cannot use GeneratedRegexAttribute due to old .NET version + var andSplitted = Regex.Split(match.Value, " AND "); + + var filterSingleStrings = andSplitted + .Select(x => x.Trim()) + .ToList(); + + foreach (var filterSingleString in filterSingleStrings) + { + var splitted = filterSingleString.Split(' ') + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .ToList(); + + var filterItem = new FilterItem { ColumnName = splitted[0], Filter = _dialect.GetFilterTypeByComparisonString(splitted[1]) }; + + var column = columns.Single(x => x.Name.Equals(splitted[0], StringComparison.OrdinalIgnoreCase)); + + var sqliteIntegerDataTypes = new[] { + MigratorDbType.Int16, + MigratorDbType.Int32, + MigratorDbType.Int64, + MigratorDbType.UInt16, + MigratorDbType.UInt32, + MigratorDbType.UInt64 + }; + + if (sqliteIntegerDataTypes.Contains(column.MigratorDbType)) + { + if (long.TryParse(splitted[2], out var longValue)) + { + filterItem.Value = longValue; + } + else if (ulong.TryParse(splitted[2], out var uLongValue)) + { + filterItem.Value = uLongValue; + } + else + { + throw new Exception(); + } + } + else + { + filterItem.Value = column.MigratorDbType switch + { + MigratorDbType.Boolean => splitted[2] == "1" || splitted[2].Equals("true", StringComparison.OrdinalIgnoreCase), + MigratorDbType.String => splitted[2].Substring(1, splitted[2].Length - 2), + _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + }; + } + + index.FilterItems.Add(filterItem); + } + } + } + + indexes.Add(index); + } + + return [.. indexes]; + } + + public override void AddTable(string name, string engine, params IDbField[] fields) + { + if (engine != null) throw new NotSupportedException("SQLite does not support table engines."); + var table = _dialect.TableNameNeedsQuote ? _dialect.Quote(name) : QuoteTableNameIfRequired(name); + ExecuteNonQuery(SQLiteTableSql.Generate(_dialect, table, fields)); + foreach (var index in fields.OfType()) AddIndex(name, index); + } + + public override string AddIndex(string table, Index index) + { + ValidateIndex(table, index); + + var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; + + if (hasIncludedColumns) + { + // This will be actived in the future. + // throw new MigrationException($"SQLite does not support included columns. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); + } + + if (index.Clustered) + { + throw new MigrationException($"For SQLite this migrator does not support clustered indexes at this point in time, sorry. File an issue if needed. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); + } + + var name = QuoteConstraintNameIfRequired(index.Name); + table = QuoteTableNameIfRequired(table); + var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + + var uniqueString = index.Unique ? "UNIQUE" : null; + var columnsString = $"({string.Join(", ", columns)})"; + var filterString = string.Empty; + + if (index.FilterItems != null && index.FilterItems.Count > 0) + { + List singleFilterStrings = []; + + foreach (var filterItem in index.FilterItems) + { + var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); + + var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); + string value = null; + + value = filterItem.Value switch + { + bool booleanValue => booleanValue ? "1" : "0", + string stringValue => $"'{stringValue.Replace("'", "''")}'", + byte or short or int or long => Convert.ToInt64(filterItem.Value).ToString(), + sbyte or ushort or uint or ulong => Convert.ToUInt64(filterItem.Value).ToString(), + _ => throw new NotImplementedException("Given type is not implemented. Please file an issue."), + }; + + if ((filterItem.Value is string || filterItem.Value is bool) && filterItem.Filter != FilterType.EqualTo && filterItem.Filter != FilterType.NotEqualTo) + { + throw new MigrationException($"Bool and string in {nameof(FilterItem)} can only be used with '{nameof(FilterType.EqualTo)}' or '{nameof(FilterType.EqualTo)}'."); + } + + var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; + + singleFilterStrings.Add(singleFilterString); + } + + filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; + } + + List list = ["CREATE", uniqueString, "INDEX", name, "ON", table, columnsString, filterString]; + + var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); + + ExecuteNonQuery(sql); + + return sql; + } + + protected override string GetPrimaryKeyConstraintName(string table) + { + return GetTableConstraints(table).OfType().SingleOrDefault()?.Name; + } + + public override void RemoveAllConstraints(string table) + { + var info = GetSQLiteTableInfo(table); + info.PrimaryKey = null; + info.Uniques.Clear(); + info.ForeignKeys.Clear(); + info.CheckConstraints.Clear(); + foreach (var column in info.Columns) column.IsIdentity = false; + RecreateTable(info); + } + + public override void RemovePrimaryKey(string tableName) + { + if (!TableExists(tableName)) return; + var info = GetSQLiteTableInfo(tableName); + info.PrimaryKey = null; + foreach (var column in info.Columns) column.IsIdentity = false; + RecreateTable(info); + } + + public override void RemoveAllIndexes(string tableName) + { + if (!TableExists(tableName)) + { + return; + } + + var sqliteInfoTable = GetSQLiteTableInfo(tableName); + + sqliteInfoTable.Indexes = []; + + RecreateTable(sqliteInfoTable); + } + + public List GetUniques(string tableName) => GetTableConstraints(tableName) + .OfType().ToList(); + + public List GetPragmaIndexInfo(string indexNameNotQuoted) + { + List pragmaIndexInfoItems = []; + + var quotedIndexName = QuoteTableNameIfRequired(indexNameNotQuoted); + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA index_info({quotedIndexName})")) + { + while (reader.Read()) + { + var pragmaIndexInfoItem = new PragmaIndexInfoItem + { + SeqNo = reader.GetInt32(reader.GetOrdinal("seqno")), + Cid = reader.GetInt32(reader.GetOrdinal("cid")), + Name = reader.GetString(reader.GetOrdinal("name")), + }; + + pragmaIndexInfoItems.Add(pragmaIndexInfoItem); + } + } + + return pragmaIndexInfoItems; + } + + public List GetPragmaIndexListItems(string tableNameNotQuoted) + { + List pragmaIndexListItems = []; + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA index_list({QuoteTableNameIfRequired(tableNameNotQuoted)})")) + { + while (reader.Read()) + { + var pragmaIndexListItem = new PragmaIndexListItem + { + Seq = reader.GetInt32(reader.GetOrdinal("seq")), + Name = reader.GetString(reader.GetOrdinal("name")), + Unique = reader.GetInt32(reader.GetOrdinal("unique")) == 1, + Origin = reader.GetString(reader.GetOrdinal("origin")), + Partial = reader.GetInt32(reader.GetOrdinal("partial")) == 1 + }; + + pragmaIndexListItems.Add(pragmaIndexListItem); + } + } + + return pragmaIndexListItems; + } + + public List GetPragmaTableInfoItems(string tableNameNotQuoted) + { + List pragmaTableInfoItems = []; + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, $"PRAGMA table_info({QuoteTableNameIfRequired(tableNameNotQuoted)})")) + { + while (reader.Read()) + { + var pragmaTableInfoItem = new PragmaTableInfoItem + { + Cid = reader.GetInt32(reader.GetOrdinal("cid")), + DfltValue = reader[reader.GetOrdinal("dflt_value")], + Name = reader.GetString(reader.GetOrdinal("name")), + NotNull = reader.GetInt32(reader.GetOrdinal("notnull")) == 1, + Pk = reader.GetInt32(reader.GetOrdinal("pk")), + Type = reader.GetString(reader.GetOrdinal("type")), + }; + + pragmaTableInfoItems.Add(pragmaTableInfoItem); + } + } + + return pragmaTableInfoItems; + } + + public override void AddCheckConstraint(string constraintName, string tableName, string checkSql) + { + var sqliteTableInfo = GetSQLiteTableInfo(tableName); + + var checkConstraint = new CheckConstraint(constraintName, checkSql); + sqliteTableInfo.CheckConstraints.Add(checkConstraint); + + RecreateTable(sqliteTableInfo); + } + + public override void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) + { + orderBySourceColumns ??= []; + + if (!TableExists(sourceTableName)) + { + throw new Exception($"Source table '{QuoteTableNameIfRequired(sourceTableName)}' does not exist"); + } + + if (!TableExists(targetTableName)) + { + throw new Exception($"Target table '{QuoteTableNameIfRequired(targetTableName)}' does not exist"); + } + + var sourceColumnsConcatenated = sourceColumnNames.Concat(orderBySourceColumns); + + foreach (var column in sourceColumnsConcatenated) + { + if (!ColumnExists(sourceTableName, column)) + { + throw new Exception($"Column {column} in source table does not exist."); + } + } + + foreach (var column in targetColumnNames) + { + if (!ColumnExists(targetTableName, column)) + { + throw new Exception($"Column {column} in target table does not exist."); + } + } + + if (!orderBySourceColumns.All(x => sourceColumnNames.Contains(x))) + { + throw new Exception($"All columns in {nameof(orderBySourceColumns)} must be in {nameof(sourceColumnNames)}"); + } + + var sourceTableNameQuoted = QuoteTableNameIfRequired(sourceTableName); + var targetTableNameQuoted = QuoteTableNameIfRequired(targetTableName); + + var sourceColumnNamesQuoted = sourceColumnNames.Select(QuoteColumnNameIfRequired).ToList(); + var targetColumnNamesQuoted = targetColumnNames.Select(QuoteColumnNameIfRequired).ToList(); + var orderBySourceColumnsQuoted = orderBySourceColumns.Select(QuoteColumnNameIfRequired).ToList(); + + var sourceColumnsJoined = string.Join(", ", sourceColumnNamesQuoted); + var targetColumnsJoined = string.Join(", ", targetColumnNamesQuoted); + var orderBySourceColumnsJoined = string.Join(", ", orderBySourceColumnsQuoted); + + var orderByComponent = !string.IsNullOrWhiteSpace(orderBySourceColumnsJoined) ? $"ORDER BY {orderBySourceColumnsJoined}" : null; + + List sqlComponents = + [ + $"INSERT INTO {targetTableNameQuoted} ({targetColumnsJoined}) SELECT {sourceColumnsJoined} FROM {sourceTableNameQuoted}", + orderByComponent + ]; + + var sql = string.Join(" ", sqlComponents.Where(x => x != null)); + ExecuteNonQuery(sql); + } + + public List GetCheckConstraints(string tableName) => GetTableConstraints(tableName).OfType().ToList(); + + protected override void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) + { + if (value is TimeOnly time) + { + // SQLite stores times as text; System.Data.SQLite cannot bind TimeSpan as DbType.Time. + parameter.DbType = DbType.String; + parameter.Value = time.ToString("HH:mm:ss.fffffff", CultureInfo.InvariantCulture); + } + else if (value is ushort) + { + parameter.DbType = DbType.Int32; + parameter.Value = Convert.ToInt32(value); + } + else if (value is uint) + { + parameter.DbType = DbType.Int64; + parameter.Value = Convert.ToInt64(value); + } else if (value is ulong unsigned) { // SQLite INTEGER cannot represent the upper half of UInt64. Do not let @@ -1633,13 +1633,13 @@ protected override void ConfigureParameterWithValue(IDbDataParameter parameter, parameter.Value = checked((long)unsigned); } else if (value is Guid || value is Guid?) - { - parameter.DbType = DbType.Binary; - parameter.Value = ((Guid)value).ToByteArray(); - } - else - { - base.ConfigureParameterWithValue(parameter, index, value); - } - } -} + { + parameter.DbType = DbType.Binary; + parameter.Value = ((Guid)value).ToByteArray(); + } + else + { + base.ConfigureParameterWithValue(parameter, index, value); + } + } +} diff --git a/src/Migrator/Providers/NoOpTransformationProvider.cs b/src/Migrator/Providers/NoOpTransformationProvider.cs index 6165c5eb..670d004a 100644 --- a/src/Migrator/Providers/NoOpTransformationProvider.cs +++ b/src/Migrator/Providers/NoOpTransformationProvider.cs @@ -1,575 +1,575 @@ -using System; -using System.Collections.Generic; -using System.Data; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Models; - -using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; -using Index = DotNetProjects.Migrator.Framework.Index; +using System; +using System.Collections.Generic; +using System.Data; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Models; + +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; +using Index = DotNetProjects.Migrator.Framework.Index; using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; - -namespace DotNetProjects.Migrator.Providers; - -/// -/// No Op (Null Object Pattern) implementation of the ITransformationProvider -/// -public class NoOpTransformationProvider : ITransformationProvider -{ - public TableConstraint[] GetTableConstraints(string table) => []; + +namespace DotNetProjects.Migrator.Providers; + +/// +/// No Op (Null Object Pattern) implementation of the ITransformationProvider +/// +public class NoOpTransformationProvider : ITransformationProvider +{ + public TableConstraint[] GetTableConstraints(string table) => []; public void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) { } public void RemoveUniqueConstraint(string table, UniqueConstraint constraint) { } - - public static readonly NoOpTransformationProvider Instance = new NoOpTransformationProvider(); - - private NoOpTransformationProvider() - { - } - - public int? CommandTimeout { get; set; } - - public IDialect Dialect - { - get { return null; } - } - - public bool IsMigrationApplied(long version, string scope) - { - throw new NotImplementedException(); - } - - public string ConnectionString - { - get { return string.Empty; } - } - - public virtual ILogger Logger - { - get { return null; } - set { } - } - - public string[] GetTables() - { - return null; - } - - public ForeignKeyConstraint[] GetForeignKeyConstraints(string table) - { - return null; - } - - public int Insert(string table, string[] columns, object[] values) - { - return 0; - } - - public int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - return 0; - } - - public List ExecuteStringQuery(string sql, params object[] args) - { - return new List(); - } - - public Index[] GetIndexes(string table) - { - return null; - } - - public Column[] GetColumns(string table) - { - return null; - } - - public Column GetColumnByName(string table, string column) - { - return null; - } - - public void RemoveForeignKey(string table, string name) - { - // No Op - } - - public void RemoveConstraint(string table, string name) - { - // No Op - } - - public void RemoveAllConstraints(string table) - { - // No Op - } - - public void RemovePrimaryKey(string table) - { - // No Op - } - - public void AddView(string name, string tableName, params IViewElement[] viewElements) - { - // No Op - } - - public void AddView(string name, string tableName, params IViewField[] fields) - { - throw new NotImplementedException(); - } - - public void AddTable(string name, params IDbField[] columns) - { - // No Op - } - - public void AddTable(string name, string engine, params IDbField[] columns) - { - // No Op - } - - public void RemoveTable(string name) - { - // No Op - } - - public void RenameTable(string oldName, string newName) - { - // No Op - } - - public void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - // No Op - } - - public void RemoveColumn(string table, string column) - { - // No Op - } - - public void RemoveColumnDefaultValue(string table, string column) - { - // No Op - } - - public bool ColumnExists(string table, string column) - { - return false; - } - - public bool TableExists(string table) - { - return false; - } - - public bool ViewExists(string view) - { - return false; - } - - public void AddColumn(string table, string column, DbType type) - { - // No Op - } - - public void AddColumn(string table, string column, DbType type, object defaultValue) - { - // No Op - } - - public void AddColumn(string table, string column, DbType type, int size) - { - // No Op - } - - public void AddPrimaryKey(string name, string table, params string[] columns) - { - // No Op - } - public void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) - { - // No Op - } - public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, - string[] refColumns, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, - string refColumn) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, - string[] refColumns, ForeignKeyConstraintType constraint) - { - // No Op - } - - public void AddUniqueConstraint(string name, string table, params string[] columns) - { - // No Op - } - - public void AddCheckConstraint(string name, string table, string checkSql) - { - // No Op - } - - public bool ConstraintExists(string table, string name) - { - return false; - } - - public void ChangeColumn(string table, Column column) - { - // No Op - } - - public bool PrimaryKeyExists(string table, string name) - { - return false; - } - - public int ExecuteNonQuery(string sql) - { - return 0; - } - public int ExecuteNonQuery(string sql, int timeout) - { - return 0; - } - public int ExecuteNonQuery(string sql, int timeout, object[] parameters) - { - return 0; - } - - public IDataReader ExecuteQuery(IDbCommand cmd, string sql) - { - return null; - } - - public IDbCommand CreateCommand() - { - throw new NotImplementedException(); - } - - public object ExecuteScalar(string sql) - { - return null; - } - - public IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns, object[] whereValues) - { - return null; - } - - public IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, - object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) - { - return null; - } - - public IDataReader Select(IDbCommand cmd, string what, string from) - { - return null; - } - - public IDataReader Select(IDbCommand cmd, string what, string from, string where) - { - return null; - } - - public object SelectScalar(string what, string from) - { - return null; - } - - public object SelectScalar(string what, string from, string where) - { - return null; - } - - public int Update(string table, string[] columns, object[] values) - { - return 0; - } - - public int Update(string table, string[] columns, object[] values, string where) - { - return 0; - } - - public int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - return 0; - } - - public int Delete(string table, string[] columns = null, object[] columnValues = null) - { - return 0; - } - - public int Delete(string table, string column, string value) - { - return 0; - } - - public int TruncateTable(string table) - { - return 0; - } - - public void BeginTransaction() - { - // No Op - } - - public void Rollback() - { - // No Op - } - - public void Commit() - { - // No Op - } - - public ITransformationProvider this[string provider] - { - get { return this; } - } - - public string SchemaInfoTable { get; set; } - - public void MigrationApplied(long version, string scope) - { - //no op - } - - public void MigrationUnApplied(long version, string scope) - { - //no op - } - - public List AppliedMigrations - { - get { return new List(); } - } - - public void AddColumn(string table, Column column) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string refTable) - { - // No Op - } - - public void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) - { - // No Op - } - - public IDbCommand GetCommand() - { - return null; - } - - public void RemoveAllForeignKeys(string tableName, string columnName) - { - - } - - public bool IsThisProvider(string provider) - { - return false; - } - - public string[] QuoteColumnNamesIfRequired(params string[] columnNames) - { - throw new NotImplementedException(); - } - - public string QuoteColumnNameIfRequired(string name) - { - throw new NotImplementedException(); - } - - public string QuoteTableNameIfRequired(string name) - { - throw new NotImplementedException(); - } - - public string Encode(Guid guid) - { - return guid.ToString(); - } - - public void SwitchDatabase(string databaseName) - { - - } - - public List GetDatabases() - { - return new List(); - } - - public bool DatabaseExists(string name) - { - return true; - } - - public void CreateDatabases(string databaseName) - { - - } - - public void KillDatabaseConnections(string databaseName) - { - - } - - public void DropDatabases(string databaseName) - { - - } - - public string AddIndex(string table, Index index) - { - // Don't know what this is for... - - return string.Empty; - } - - public void Dispose() - { - //No Op - } - - public void AddColumn(string table, string sqlColumn) - { - // No Op - } - - public int Insert(string table, string[] columns, string[] columnValues) - { - return 0; - } - - protected void CreateSchemaInfoTable() - { - } - - public void RemoveIndex(string table, string name) - { - // No Op - } - - public string AddIndex(string name, string table, params string[] columns) - { - // No Op - - // Don't know what this is for... - - return string.Empty; - } - - public bool IndexExists(string table, string name) - { - return false; - } - - public string GenerateParameterName(int index) - { - return "@p" + index; - } - - public void RemoveAllIndexes(string table) - { - // No Op - } - - public string Concatenate(params string[] strings) - { - return ""; - } - - public IDbConnection Connection - { - get - { - return null; - } - } - - public IEnumerable GetTables(string schema) - { - throw new NotImplementedException(); - } - - public IEnumerable GetColumns(string schema, string table) - { - throw new NotImplementedException(); - } - - public int GetColumnContentSize(string table, string columnName) - { - throw new NotImplementedException(); - } - - public void AddColumn(string table, string column, MigratorDbType type) - { - throw new NotImplementedException(); - } - - public void AddColumn(string table, string column, MigratorDbType type, int size) - { - throw new NotImplementedException(); - } - - public void AddColumn(string table, string column, MigratorDbType type, object defaultValue) - { - throw new NotImplementedException(); - } - - public void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) - { - throw new NotImplementedException(); - } - - public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns) - { - throw new NotImplementedException(); - } -} + + public static readonly NoOpTransformationProvider Instance = new NoOpTransformationProvider(); + + private NoOpTransformationProvider() + { + } + + public int? CommandTimeout { get; set; } + + public IDialect Dialect + { + get { return null; } + } + + public bool IsMigrationApplied(long version, string scope) + { + throw new NotImplementedException(); + } + + public string ConnectionString + { + get { return string.Empty; } + } + + public virtual ILogger Logger + { + get { return null; } + set { } + } + + public string[] GetTables() + { + return null; + } + + public ForeignKeyConstraint[] GetForeignKeyConstraints(string table) + { + return null; + } + + public int Insert(string table, string[] columns, object[] values) + { + return 0; + } + + public int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + return 0; + } + + public List ExecuteStringQuery(string sql, params object[] args) + { + return new List(); + } + + public Index[] GetIndexes(string table) + { + return null; + } + + public Column[] GetColumns(string table) + { + return null; + } + + public Column GetColumnByName(string table, string column) + { + return null; + } + + public void RemoveForeignKey(string table, string name) + { + // No Op + } + + public void RemoveConstraint(string table, string name) + { + // No Op + } + + public void RemoveAllConstraints(string table) + { + // No Op + } + + public void RemovePrimaryKey(string table) + { + // No Op + } + + public void AddView(string name, string tableName, params IViewElement[] viewElements) + { + // No Op + } + + public void AddView(string name, string tableName, params IViewField[] fields) + { + throw new NotImplementedException(); + } + + public void AddTable(string name, params IDbField[] columns) + { + // No Op + } + + public void AddTable(string name, string engine, params IDbField[] columns) + { + // No Op + } + + public void RemoveTable(string name) + { + // No Op + } + + public void RenameTable(string oldName, string newName) + { + // No Op + } + + public void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + // No Op + } + + public void RemoveColumn(string table, string column) + { + // No Op + } + + public void RemoveColumnDefaultValue(string table, string column) + { + // No Op + } + + public bool ColumnExists(string table, string column) + { + return false; + } + + public bool TableExists(string table) + { + return false; + } + + public bool ViewExists(string view) + { + return false; + } + + public void AddColumn(string table, string column, DbType type) + { + // No Op + } + + public void AddColumn(string table, string column, DbType type, object defaultValue) + { + // No Op + } + + public void AddColumn(string table, string column, DbType type, int size) + { + // No Op + } + + public void AddPrimaryKey(string name, string table, params string[] columns) + { + // No Op + } + public void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) + { + // No Op + } + public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, + string[] refColumns, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, + string refColumn) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, + string[] refColumns, ForeignKeyConstraintType constraint) + { + // No Op + } + + public void AddUniqueConstraint(string name, string table, params string[] columns) + { + // No Op + } + + public void AddCheckConstraint(string name, string table, string checkSql) + { + // No Op + } + + public bool ConstraintExists(string table, string name) + { + return false; + } + + public void ChangeColumn(string table, Column column) + { + // No Op + } + + public bool PrimaryKeyExists(string table, string name) + { + return false; + } + + public int ExecuteNonQuery(string sql) + { + return 0; + } + public int ExecuteNonQuery(string sql, int timeout) + { + return 0; + } + public int ExecuteNonQuery(string sql, int timeout, object[] parameters) + { + return 0; + } + + public IDataReader ExecuteQuery(IDbCommand cmd, string sql) + { + return null; + } + + public IDbCommand CreateCommand() + { + throw new NotImplementedException(); + } + + public object ExecuteScalar(string sql) + { + return null; + } + + public IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns, object[] whereValues) + { + return null; + } + + public IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, + object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) + { + return null; + } + + public IDataReader Select(IDbCommand cmd, string what, string from) + { + return null; + } + + public IDataReader Select(IDbCommand cmd, string what, string from, string where) + { + return null; + } + + public object SelectScalar(string what, string from) + { + return null; + } + + public object SelectScalar(string what, string from, string where) + { + return null; + } + + public int Update(string table, string[] columns, object[] values) + { + return 0; + } + + public int Update(string table, string[] columns, object[] values, string where) + { + return 0; + } + + public int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + return 0; + } + + public int Delete(string table, string[] columns = null, object[] columnValues = null) + { + return 0; + } + + public int Delete(string table, string column, string value) + { + return 0; + } + + public int TruncateTable(string table) + { + return 0; + } + + public void BeginTransaction() + { + // No Op + } + + public void Rollback() + { + // No Op + } + + public void Commit() + { + // No Op + } + + public ITransformationProvider this[string provider] + { + get { return this; } + } + + public string SchemaInfoTable { get; set; } + + public void MigrationApplied(long version, string scope) + { + //no op + } + + public void MigrationUnApplied(long version, string scope) + { + //no op + } + + public List AppliedMigrations + { + get { return new List(); } + } + + public void AddColumn(string table, Column column) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string refTable) + { + // No Op + } + + public void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) + { + // No Op + } + + public IDbCommand GetCommand() + { + return null; + } + + public void RemoveAllForeignKeys(string tableName, string columnName) + { + + } + + public bool IsThisProvider(string provider) + { + return false; + } + + public string[] QuoteColumnNamesIfRequired(params string[] columnNames) + { + throw new NotImplementedException(); + } + + public string QuoteColumnNameIfRequired(string name) + { + throw new NotImplementedException(); + } + + public string QuoteTableNameIfRequired(string name) + { + throw new NotImplementedException(); + } + + public string Encode(Guid guid) + { + return guid.ToString(); + } + + public void SwitchDatabase(string databaseName) + { + + } + + public List GetDatabases() + { + return new List(); + } + + public bool DatabaseExists(string name) + { + return true; + } + + public void CreateDatabases(string databaseName) + { + + } + + public void KillDatabaseConnections(string databaseName) + { + + } + + public void DropDatabases(string databaseName) + { + + } + + public string AddIndex(string table, Index index) + { + // Don't know what this is for... + + return string.Empty; + } + + public void Dispose() + { + //No Op + } + + public void AddColumn(string table, string sqlColumn) + { + // No Op + } + + public int Insert(string table, string[] columns, string[] columnValues) + { + return 0; + } + + protected void CreateSchemaInfoTable() + { + } + + public void RemoveIndex(string table, string name) + { + // No Op + } + + public string AddIndex(string name, string table, params string[] columns) + { + // No Op + + // Don't know what this is for... + + return string.Empty; + } + + public bool IndexExists(string table, string name) + { + return false; + } + + public string GenerateParameterName(int index) + { + return "@p" + index; + } + + public void RemoveAllIndexes(string table) + { + // No Op + } + + public string Concatenate(params string[] strings) + { + return ""; + } + + public IDbConnection Connection + { + get + { + return null; + } + } + + public IEnumerable GetTables(string schema) + { + throw new NotImplementedException(); + } + + public IEnumerable GetColumns(string schema, string table) + { + throw new NotImplementedException(); + } + + public int GetColumnContentSize(string table, string columnName) + { + throw new NotImplementedException(); + } + + public void AddColumn(string table, string column, MigratorDbType type) + { + throw new NotImplementedException(); + } + + public void AddColumn(string table, string column, MigratorDbType type, int size) + { + throw new NotImplementedException(); + } + + public void AddColumn(string table, string column, MigratorDbType type, object defaultValue) + { + throw new NotImplementedException(); + } + + public void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) + { + throw new NotImplementedException(); + } + + public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns) + { + throw new NotImplementedException(); + } +} diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 4b5a6299..a14e2e59 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -1,2091 +1,2091 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Loggers; -using DotNetProjects.Migrator.Framework.Models; - -using DotNetProjects.Migrator.Providers.Impl.SQLite; -using DotNetProjects.Migrator.Providers.Models; -using System; -using System.Collections.Generic; -using System.Data; -using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; -using System.Data.Common; -using System.IO; -using System.Linq; -using System.Text; -using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; -using ForeignKeyConstraintType = DotNetProjects.Migrator.Framework.ForeignKeyConstraintType; -using Index = DotNetProjects.Migrator.Framework.Index; - -namespace DotNetProjects.Migrator.Providers; - -/// -/// Base class for every transformation providers. -/// A 'tranformation' is an operation that modifies the database. -/// -public abstract class TransformationProvider : ITransformationProvider, IMigrationHistory, IForeignKeyActions -{ - private string _scope; - protected readonly string _connectionString; - protected readonly string _defaultSchema; - private readonly ForeignKeyConstraintMapper constraintMapper = new(); - protected List _appliedMigrations; - protected IDbConnection _connection; - protected bool _outsideConnection = false; - protected Dialect _dialect; - private ILogger _logger; - private IDbTransaction _transaction; - - protected TransformationProvider(Dialect dialect, string connectionString, string defaultSchema, string scope) - { - _dialect = dialect; - _connectionString = connectionString; - _defaultSchema = defaultSchema; - _logger = new Logger(false); - _scope = scope; - } - - protected TransformationProvider(Dialect dialect, IDbConnection connection, string defaultSchema, string scope) - { - _dialect = dialect; - _connection = connection; - _outsideConnection = true; - _defaultSchema = defaultSchema; - _logger = new Logger(false); - _scope = scope; - } - - public IMigration CurrentMigration { get; set; } - - private string _schemaInfotable = "SchemaInfo"; - public string SchemaInfoTable - { - get - { - return _schemaInfotable; - } - set - { - _schemaInfotable = value; - InvalidateHistory(); - } - } - - public int? CommandTimeout { get; set; } - - public IDialect Dialect - { - get { return _dialect; } - } - - public string ConnectionString { get { return _connectionString; } } - - /// - /// Returns the event logger - /// - public virtual ILogger Logger - { - get { return _logger; } - set { _logger = value; } - } - - public virtual ITransformationProvider this[string provider] - { - get - { - if (null != provider && IsThisProvider(provider)) - { - return this; - } - - return NoOpTransformationProvider.Instance; - } - } - - public virtual Index[] GetIndexes(string table) - { - throw new NotImplementedException(); - } - - public virtual Column[] GetColumns(string table) - { - var columns = new List(); - using (var cmd = CreateCommand()) - using ( - var reader = - ExecuteQuery( - cmd, string.Format("select COLUMN_NAME, IS_NULLABLE from INFORMATION_SCHEMA.COLUMNS where table_name = '{0}'", table))) - { - while (reader.Read()) - { - var column = new Column(reader.GetString(0), DbType.String); - var nullableStr = reader.GetString(1); - var isNullable = nullableStr == "YES"; - column.IsNullable = isNullable; - - columns.Add(column); - } - } - - return columns.ToArray(); - } - - /// - /// Basic implementation works for Postgre and probably for MySQL (not tested). For Oracle it should be overridden - /// - /// - /// - /// - public virtual ForeignKeyConstraint[] GetForeignKeyConstraints(string table) => ForeignKeyMetadataReader.Read(this, table); - - public virtual TableConstraint[] GetTableConstraints(string table) => ConstraintMetadataReader.Read(this, table); - - public virtual string[] GetConstraints(string table) - { - var constraints = new List(); - using (var cmd = CreateCommand()) - using ( - var reader = - ExecuteQuery( - cmd, string.Format("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE LOWER(TABLE_NAME) = LOWER('{0}')", table))) - { - while (reader.Read()) - { - constraints.Add(reader.GetString(0)); - } - } - - return constraints.ToArray(); - } - - public virtual Column GetColumnByName(string table, string columnName) - { - var columns = GetColumns(table); - var column = columns.FirstOrDefault(x => x.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase)) ?? - throw new Exception($"Cannot find column '{columnName}' in table '{table}'"); - - return column; - } - - public virtual int GetColumnContentSize(string table, string columnName) - { - var result = this.ExecuteScalar("SELECT MAX(LENGTH(" + this.QuoteColumnNameIfRequired(columnName) + ")) FROM " + this.QuoteTableNameIfRequired(table)); - - if (result == DBNull.Value) - { - return 0; - } - - return Convert.ToInt32(result); - } - - public virtual string[] GetTables() - { - var tables = new List(); - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, "SELECT table_name FROM INFORMATION_SCHEMA.TABLES")) - { - while (reader.Read()) - { - tables.Add((string)reader[0]); - } - } - return tables.ToArray(); - } - - public virtual void RemoveForeignKey(string table, string name) - { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - RemoveConstraint(table, name); - } - - public virtual void RemoveConstraint(string table, string name) - { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{name}' does not exist"); - } - - if (!ConstraintExists(table, name)) - { - throw new MigrationException($"Constraint '{name}' does not exist"); - } - - var names = GetConstraints(table); - var actual = names.FirstOrDefault(n => n == name) - ?? names.SingleOrDefault(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)) - ?? throw new MigrationException("Constraint was not found in the requested table: " + name); - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP CONSTRAINT {1}", QuoteTableNameIfRequired(table), _dialect.QuoteIdentifier(actual))); - } - - public virtual void RemoveAllConstraints(string table) - { - foreach (var constraint in GetConstraints(table)) - { - RemoveConstraint(table, constraint); - } - } - - public virtual void AddView(string name, string tableName, params IViewField[] fields) - { - var lst = - fields.Where(x => string.IsNullOrEmpty(x.TableName) || x.TableName == tableName) - .Select(x => tableName + "." + x.ColumnName) - .ToList(); - - var nr = 0; - var joins = ""; - foreach (var joinTable in fields.Where(x => !string.IsNullOrEmpty(x.TableName) && x.TableName != tableName) - .GroupBy(x => new { x.TableName, x.KeyColumnName, x.ParentTableName, x.ParentKeyColumnName })) - { - var relationship = joinTable.Key; - var alias = "T" + nr++; - joins += $"JOIN {relationship.TableName} {alias} ON {alias}.{relationship.KeyColumnName} = {relationship.ParentTableName}.{relationship.ParentKeyColumnName} "; - foreach (var viewField in joinTable) - { - lst.Add(alias + "." + viewField.ColumnName); - } - } - - var select = string.Format("SELECT {0} FROM {1} {2}", string.Join(",", lst), tableName, joins); - - var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); - - ExecuteNonQuery(sql); - } - - public virtual void AddView(string name, string tableName, params IViewElement[] viewElements) - { - var selectedColumns = viewElements.Where(x => x is ViewColumn) - .Select(x => - { - var viewColumn = (ViewColumn)x; - return $"{viewColumn.Prefix}.{viewColumn.ColumnName} {viewColumn.Prefix}{viewColumn.ColumnName}"; - }) - .ToList(); - - var joins = string.Empty; - - foreach (var viewJoin in viewElements.Where(x => x is ViewJoin).Cast()) - { - var joinType = string.Empty; - - switch (viewJoin.JoinType) - { - case JoinType.LeftJoin: - joinType = "LEFT JOIN"; - break; - case JoinType.Join: - joinType = "JOIN"; - break; - } - - var tableAlias = string.IsNullOrEmpty(viewJoin.TableAlias) ? viewJoin.TableName : viewJoin.TableAlias; - var parentAlias = string.IsNullOrEmpty(viewJoin.ParentTableAlias) ? viewJoin.ParentTableName : viewJoin.ParentTableAlias; - - joins += string.Format("{0} {1} {2} ON {2}.{3} = {4}.{5} ", joinType, viewJoin.TableName, tableAlias, - viewJoin.ColumnName, parentAlias, viewJoin.ParentColumnName); - } - - var select = string.Format("SELECT {0} FROM {1} {1} {2}", string.Join(",", selectedColumns), tableName, joins); - var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); - - - // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. - try - { - ExecuteNonQuery($"DROP VIEW {name}"); - } - catch - { - // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. - } - - ExecuteNonQuery(sql); - } - - /// - /// Add a new table - /// - /// Table name - /// Columns - public virtual void AddTable(string name, params IDbField[] columns) - { - // Most databases don't have the concept of a storage engine, so default is to not use it. - AddTable(name, null, columns); - } - - /// - /// Adds a new table - /// - /// Table name - /// Columns - /// the database storage engine to use - public virtual void AddTable(string name, string engine, params IDbField[] fields) - { - var columns = fields.OfType().Select(c => c.CopyDefinition()).ToArray(); - var keys = fields.OfType().ToArray(); - if (keys.Length > 1) throw new MigrationException("A table can have only one primary key."); - foreach (var key in keys) - { - ValidateKeyColumns(key.Name, key.KeyColumns, columns); - foreach (var column in columns.Where(c => key.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) - column.IsNullable = false; - } - foreach (var unique in fields.OfType()) ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); - var sql = columns.Select(c => _dialect.GetAndMapColumnProperties(c).ColumnSql) - .Concat(fields.OfType().Where(c => c is not ForeignKeyConstraint).Select(_dialect.GetTableConstraintSql)); - AddTable(name, engine, string.Join(", ", sql)); - foreach (var index in fields.OfType()) AddIndex(name, index); - foreach (var foreignKey in fields.OfType()) AddForeignKey(name, foreignKey); - } - - protected internal static void ValidateKeyColumns(string name, string[] keys, Column[] columns) - { - if (name != null && string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name must not be empty."); - if (keys == null || keys.Length == 0 || keys.Any(string.IsNullOrWhiteSpace) || keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != keys.Length) - throw new MigrationException("A key needs distinct, non-empty column names."); - if (keys.Any(key => !columns.Any(c => c.Name.Equals(key, StringComparison.OrdinalIgnoreCase)))) - throw new MigrationException("A constraint references a column that is absent from the table definition."); - } - - protected virtual string GetPrimaryKeyname(string tableName) - { - return "PK_" + tableName; - } - - public virtual void RemoveTable(string name) - { - if (!TableExists(name)) - { - throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", name)); - } - - ExecuteNonQuery(string.Format("DROP TABLE {0}", name)); - } - - public virtual void RenameTable(string oldName, string newName) - { - oldName = QuoteTableNameIfRequired(oldName); - newName = QuoteTableNameIfRequired(newName); - - if (TableExists(newName)) - { - throw new MigrationException(string.Format("Table with name '{0}' already exists", newName)); - } - - if (!TableExists(oldName)) - { - throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", oldName)); - } - - ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME TO {1}", oldName, newName)); - } - - public virtual void RenameColumn(string tableName, string oldColumnName, string newColumnName) - { - if (ColumnExists(tableName, newColumnName)) - { - throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); - } - - if (!ColumnExists(tableName, oldColumnName)) - { - throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); - } - - var column = GetColumnByName(tableName, oldColumnName); - - var quotedNewColumnName = QuoteColumnNameIfRequired(newColumnName); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME COLUMN {1} TO {2}", tableName, Dialect.Quote(column.Name), quotedNewColumnName)); - } - - public virtual void RemoveColumn(string tableName, string column) - { - if (!TableExists(tableName)) - { - throw new MigrationException($"The table '{tableName}' does not exist"); - } - - if (!ColumnExists(tableName, column, true)) - { - throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, column)); - } - - var existingColumn = GetColumnByName(tableName, column); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP COLUMN {1} ", tableName, Dialect.Quote(existingColumn.Name))); - } - - public virtual bool ColumnExists(string table, string column) - { - return ColumnExists(table, column, true); - } - - public virtual bool ColumnExists(string table, string column, bool ignoreCase) - { - if (ignoreCase) - { - return GetColumns(table).Any(x => x.Name.Equals(column, StringComparison.OrdinalIgnoreCase)); - } - - return GetColumns(table).Any(x => x.Name == column); - } - - public virtual void ChangeColumn(string table, Column column) - { - column = column.CopyDefinition(); - - - var mapper = _dialect.GetAndMapColumnProperties(column); - - ChangeColumn(table, mapper.ColumnSql); - - - } - - public virtual void RemoveColumnDefaultValue(string table, string column) - { - var sql = string.Format("ALTER TABLE {0} ALTER {1} DROP DEFAULT", table, column); - ExecuteNonQuery(sql); - } - - public virtual bool TableExists(string table) - { - throw new NotImplementedException(); - } - - public virtual bool ViewExists(string view) - { - throw new NotImplementedException(); - } - - public virtual void SwitchDatabase(string databaseName) - { - _connection.ChangeDatabase(databaseName); - } - - public abstract List GetDatabases(); - - public bool DatabaseExists(string name) - { - return GetDatabases().Any(c => string.Equals(name, c, StringComparison.OrdinalIgnoreCase)); - } - - public virtual void CreateDatabases(string databaseName) - { - ExecuteNonQuery(string.Format("CREATE DATABASE {0}", databaseName)); - } - - public virtual void KillDatabaseConnections(string databaseName) - { - //todo, implement this for each DB, no default implementation possible!!! - } - - public virtual void DropDatabases(string databaseName) - { - ExecuteNonQuery(string.Format("DROP DATABASE {0}", databaseName)); - } - - public virtual void AddColumn(string table, string column, DbType type) - { - AddColumn(table, new Column(column, type)); - } - - public virtual void AddColumn(string table, string column, MigratorDbType type) - { - AddColumn(table, new Column(column, type)); - } - - public virtual void AddColumn(string table, string column, DbType type, int size) - { - AddColumn(table, new Column(column, type, size)); - } - - public virtual void AddColumn(string table, string column, MigratorDbType type, int size) - { - AddColumn(table, new Column(column, type, size)); - } - - public virtual void AddColumn(string table, string column, DbType type, object defaultValue) - { - AddColumn(table, column, (MigratorDbType)type, defaultValue); - } - - public virtual void AddColumn(string table, string column, MigratorDbType type, object defaultValue) - { - var mapper = - _dialect.GetAndMapColumnProperties(new Column(column, type, defaultValue)); - - AddColumn(table, mapper.ColumnSql); - } - - /// - /// Append a primary key to a table. - /// - /// Constraint name - /// Table name - /// Primary column names - public virtual void AddPrimaryKey(string name, string table, params string[] columns) - { - table = QuoteTableNameIfRequired(table); - - ExecuteNonQuery( - string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} PRIMARY KEY ({2}) ", table, QuoteConstraintNameIfRequired(name), - string.Join(",", QuoteColumnNamesIfRequired(columns)))); - } - public virtual void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) - { - this.AddPrimaryKey(name, table, columns); - } - public virtual void AddUniqueConstraint(string name, string table, params string[] columns) - { - table = QuoteTableNameIfRequired(table); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE({2}) ", table, QuoteConstraintNameIfRequired(name), - string.Join(", ", QuoteColumnNamesIfRequired(columns)))); - } - - public virtual void AddCheckConstraint(string name, string table, string checkSql) - { - table = QuoteTableNameIfRequired(table); - - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} CHECK ({2}) ", table, QuoteConstraintNameIfRequired(name), checkSql)); - } - - /// - /// Guesses the name of the foreign key and adds it - /// - public virtual void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn); - } - - /// - /// Guesses the name of the foreign key and adds it - /// - /// - public virtual void GenerateForeignKey( - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns); - } - - /// - /// Guesses the name of the foreign key and adds it - /// - public virtual void GenerateForeignKey( - string childTable, - string childColumn, - string parentTable, - string parentColumn, - ForeignKeyConstraintType constraint) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn, constraint); - } - - /// - /// Guesses the name of the foreign key and add it - /// - /// - public virtual void GenerateForeignKey( - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns, - ForeignKeyConstraintType constraint) - { - AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns, constraint); - } - - public virtual void AddForeignKey(string table, ForeignKeyConstraint fk) - { - if (string.IsNullOrWhiteSpace(fk.OnDelete) && string.IsNullOrWhiteSpace(fk.OnUpdate)) - AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone()); - else - AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone(), ParseAction(fk.OnDelete), ParseAction(fk.OnUpdate)); - - static ForeignKeyConstraintType ParseAction(string action) - { - if (string.IsNullOrWhiteSpace(action)) return ForeignKeyConstraintType.NoAction; - return Enum.TryParse(action.Replace(" ", ""), true, out var parsed) && Enum.IsDefined(parsed) - ? parsed : throw new ArgumentException("Unsupported foreign-key action.", nameof(fk)); - } - } - - public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn) - { - try - { - AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn]); - } - catch (Exception ex) - { - throw new Exception(string.Format("Error occured while adding foreign key: \"{0}\" between table: \"{1}\" and table: \"{2}\" - see inner exception for details", name, parentTable, childTable), ex); - } - } - - public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns) - { - AddForeignKey(name, childTable, childColumns, parentTable, parentColumns, ForeignKeyConstraintType.NoAction); - } - - public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint) - { - AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn], constraint); - } - - public virtual void AddForeignKey( - string name, - string childTable, - string[] childColumns, - string parentTable, - string[] parentColumns, - ForeignKeyConstraintType constraint) - { - childTable = QuoteTableNameIfRequired(childTable); - parentTable = QuoteTableNameIfRequired(parentTable); - parentColumns = QuoteColumnNamesIfRequired(parentColumns); - childColumns = QuoteColumnNamesIfRequired(childColumns); - name = QuoteConstraintNameIfRequired(name); - - var constraintResolved = constraintMapper.SqlForConstraint(constraint); - - // Legacy overload preserves one action for both clauses; IForeignKeyActions provides independent actions. - var childColumnsString = string.Join(", ", childColumns); - var parentColumnsString = string.Join(", ", parentColumns); - - var stringBuilder = new StringBuilder(); - stringBuilder.Append($"ALTER TABLE {childTable} ADD CONSTRAINT {name} FOREIGN KEY ({childColumnsString}) REFERENCES {parentTable} ({parentColumnsString})"); - stringBuilder.Append($"ON DELETE {constraintResolved} ON UPDATE {constraintResolved}"); - - ExecuteNonQuery(stringBuilder.ToString()); - } - - /// - /// Determines if a constraint exists. - /// - /// Constraint name - /// Table owning the constraint - /// true if the constraint exists. - public abstract bool ConstraintExists(string table, string name); - - public virtual bool PrimaryKeyExists(string table, string name) - { - return ConstraintExists(table, name); - } - - public virtual int ExecuteNonQuery(string sql) - { - return ExecuteNonQuery(sql, CommandTimeout ?? 30); - } - - public virtual int ExecuteNonQuery(string sql, int timeout) - { - return ExecuteNonQuery(sql, timeout, null); - } - - public virtual int ExecuteNonQuery(string sql, int timeout, params object[] args) - { - if (args == null) - { - Logger.Trace(sql); - Logger.ApplyingDBChange(sql); - } - else - { - Logger.Trace(string.Format(sql, args)); - Logger.ApplyingDBChange(string.Format(sql, args)); - } - - using var cmd = BuildCommand(sql); - - try - { - cmd.CommandTimeout = timeout; - - if (args != null) - { - var index = 0; - - foreach (var obj in args) - { - var parameter = cmd.CreateParameter(); - ConfigureParameterWithValue(parameter, index, obj); - parameter.ParameterName = GenerateParameterNameParameter(index); - cmd.Parameters.Add(parameter); - ++index; - } - } - - Logger.Trace(cmd.CommandText); - return cmd.ExecuteNonQuery(); - } - catch (Exception ex) - { - Logger.Warn(ex.Message); - throw new MigrationException(string.Format("Error occured executing sql: {0}, see inner exception for details, error: " + ex, sql), ex); - } - } - - public List ExecuteStringQuery(string sql, params object[] args) - { - var values = new List(); - - using (var cmd = CreateCommand()) - { - using var reader = ExecuteQuery(cmd, string.Format(sql, args)); - while (reader.Read()) - { - var value = reader[0]; - - if (value == null || value == DBNull.Value) - { - values.Add(null); - } - else - { - values.Add(value.ToString()); - } - } - } - - return values; - } - - public virtual void ExecuteScript(string fileName) - { - if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("A script path is required.", nameof(fileName)); - var root = CurrentMigration == null ? AppContext.BaseDirectory : Path.GetDirectoryName(CurrentMigration.GetType().Assembly.Location); - var path = Path.IsPathRooted(fileName) ? fileName : Path.Combine(root ?? AppContext.BaseDirectory, fileName); - this.ExecuteSqlScript(File.ReadAllText(path)); - } - - public virtual void ExecuteResourceScript(System.Reflection.Assembly assembly, string resourceName) - { - using var stream = assembly.GetManifestResourceStream(resourceName) - ?? throw new FileNotFoundException("Embedded SQL resource not found.", resourceName); - using var reader = new StreamReader(stream); - this.ExecuteSqlScript(reader.ReadToEnd()); - } - - /// - /// Execute an SQL query returning results. - /// - /// The SQL text. - /// The IDbCommand. - /// A data iterator, IDataReader. - public virtual IDataReader ExecuteQuery(IDbCommand cmd, string sql) - { - Logger.Trace(sql); - cmd.CommandText = sql; - try - { - return cmd.ExecuteReader(); - } - catch (Exception ex) - { - Logger.Warn("query failed: {0}", cmd.CommandText); - throw new Exception("Failed to execute sql statement: " + sql, ex); - } - } - - public virtual object ExecuteScalar(string sql) - { - Logger.Trace(sql); - using var cmd = BuildCommand(sql); - try - { - return cmd.ExecuteScalar(); - } - catch - { - Logger.Warn("Query failed: {0}", cmd.CommandText); - throw; - } - } - - public virtual IDataReader Select(IDbCommand cmd, string what, string from) - { - return Select(cmd, what, from, "1=1"); - } - - public virtual IDataReader Select(IDbCommand cmd, string what, string from, string where) - { - return ExecuteQuery(cmd, string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); - } - - public virtual IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null) - { - return SelectComplex(cmd, table, columns, whereColumns, whereValues); - } - - public virtual IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, - object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - table = QuoteTableNameIfRequired(table); - - var builder = new StringBuilder(); - for (var i = 0; i < columns.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(QuoteColumnNameIfRequired(columns[i])); - } - - - cmd.Transaction = _transaction; - - var query = string.Format("SELECT {0} FROM {1}", builder.ToString(), table); - - if (whereColumns != null || nullWhereColumns != null || notNullWhereColumns != null) - { - query = string.Format("SELECT {0} FROM {1} WHERE ", builder.ToString(), table); - } - - var andNeeded = false; - if (whereColumns != null) - { - query += GetWhereString(whereColumns, whereValues); - andNeeded = true; - } - if (nullWhereColumns != null) - { - if (andNeeded) - { - query += " AND "; - } - - query += GetWhereStringIsNull(nullWhereColumns); - andNeeded = true; - } - if (notNullWhereColumns != null) - { - if (andNeeded) - { - query += " AND "; - } - - query += GetWhereStringIsNotNull(notNullWhereColumns); - andNeeded = true; - } - - cmd.CommandText = query; - cmd.CommandType = CommandType.Text; - - var paramCount = 0; - - if (whereColumns != null) - { - foreach (var value in whereValues) - { - var parameter = cmd.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - cmd.Parameters.Add(parameter); - - paramCount++; - } - } - - Logger.Trace(cmd.CommandText); - return cmd.ExecuteReader(); - - } - - public object SelectScalar(string what, string from) - { - return SelectScalar(what, from, "1=1"); - } - - public virtual object SelectScalar(string what, string from, string where) - { - return ExecuteScalar(string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); - } - - public virtual object SelectScalar(string what, string from, string[] whereColumns, object[] whereValues) - { - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - var query = string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, GetWhereString(whereColumns, whereValues)); - - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - - foreach (var value in whereValues) - { - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - Logger.Trace(command.CommandText); - return command.ExecuteScalar(); - } - - public virtual int Update(string table, string[] columns, object[] values) - { - return Update(table, columns, values, null); - } - - public virtual int Update(string table, string[] columns, object[] values, string where) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - if (values == null) - { - throw new ArgumentNullException("values"); - } - - if (columns.Length != values.Length) - { - throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); - } - - table = QuoteTableNameIfRequired(table); - - var builder = new StringBuilder(); - for (var i = 0; i < values.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(QuoteColumnNameIfRequired(columns[i])); - builder.Append(" = "); - // A literal NULL has no driver-dependent inferred type (notably for - // nullable LOBs). Non-null values remain fully parameterized. - builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); - } - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - var query = string.Format("UPDATE {0} SET {1}", table, builder.ToString()); - if (!string.IsNullOrEmpty(where)) - { - query += " WHERE " + where; - } - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - - foreach (var value in values) - { - if (value == null || value == DBNull.Value) - { - paramCount++; - continue; - } - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - Logger.Trace(command.CommandText); - return command.ExecuteNonQuery(); - } - - public virtual int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - if (values == null) - { - throw new ArgumentNullException("values"); - } - - if (columns.Length != values.Length) - { - throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); - } - - if (whereColumns.Length != whereValues.Length) - { - throw new Exception(string.Format("The number of whereColumns: {0} does not match the number of supplied whereValues: {1}", whereColumns.Length, whereValues.Length)); - } - - table = QuoteTableNameIfRequired(table); - - var builder = new StringBuilder(); - - for (var i = 0; i < values.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(QuoteColumnNameIfRequired(columns[i])); - builder.Append(" = "); - builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); - } - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - var query = string.Format("UPDATE {0} SET {1} WHERE {2}", table, builder.ToString(), GetWhereStringWithNullCheck(whereColumns, whereValues, values.Length)); - - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - - foreach (var value in values) - { - if (value == null || value == DBNull.Value) - { - paramCount++; - continue; - } - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - foreach (var value in whereValues) - { - if (value == null || value == DBNull.Value) - { - continue; - } - - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - - Logger.Trace(command.CommandText); - return command.ExecuteNonQuery(); - } - - public virtual void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) - { - throw new NotImplementedException(); - } - - public virtual int Insert(string table, string[] columns, object[] values) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (columns == null) - { - throw new ArgumentNullException("columns"); - } - - if (values == null) - { - throw new ArgumentNullException("values"); - } - - if (columns.Length != values.Length) - { - throw new MigrationException(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); - } - - table = QuoteTableNameIfRequired(table); - - var columnNames = string.Join(", ", columns.Select(col => QuoteColumnNameIfRequired(col)).ToArray()); - - var builder = new StringBuilder(); - - for (var i = 0; i < values.Length; i++) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); - } - - var parameterNames = builder.ToString(); - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - command.CommandText = string.Format("INSERT INTO {0} ({1}) VALUES ({2})", table, columnNames, parameterNames); - command.CommandType = CommandType.Text; - - var paramCount = 0; - - foreach (var value in values) - { - if (value == null || value == DBNull.Value) - { - paramCount++; - continue; - } - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - return command.ExecuteNonQuery(); - } - - protected virtual string GetWhereStringWithNullCheck(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) - { - var builder2 = new StringBuilder(); - var parCnt = 0; - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - var val = whereValues[i]; - if (val == null || val == DBNull.Value) - { - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" is null "); - } - else - { - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" = "); - builder2.Append(GenerateParameterName(parCnt + parameterStartIndex)); - parCnt++; - } - } - - return builder2.ToString(); - } - - protected virtual string GetWhereString(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) - { - var builder2 = new StringBuilder(); - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" = "); - builder2.Append(GenerateParameterName(i + parameterStartIndex)); - } - - return builder2.ToString(); - } - - protected virtual string GetWhereStringIsNull(string[] whereColumns) - { - var builder2 = new StringBuilder(); - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" IS NULL"); - } - - return builder2.ToString(); - } - - protected virtual string GetWhereStringIsNotNull(string[] whereColumns) - { - var builder2 = new StringBuilder(); - for (var i = 0; i < whereColumns.Length; i++) - { - if (builder2.Length > 0) - { - builder2.Append(" AND "); - } - - builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); - builder2.Append(" IS NOT NULL"); - } - - return builder2.ToString(); - } - - public virtual int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) - { - using var cmd = CreateCommand(); - using var reader = this.Select(cmd, table, [whereColumns[0]], whereColumns, whereValues); - if (!reader.Read()) - { - reader.Close(); - return this.Insert(table, columns, values); - } - else - { - reader.Close(); - return 0; - } - } - - public virtual int Delete(string table, string[] whereColumns = null, object[] whereValues = null) - { - if (string.IsNullOrEmpty(table)) - { - throw new ArgumentNullException("table"); - } - - if (whereColumns == null && whereValues == null) - { - return ExecuteNonQuery(string.Format("DELETE FROM {0}", table)); - } - else - { - ArgumentNullException.ThrowIfNull(whereColumns); - ArgumentNullException.ThrowIfNull(whereValues); - if (whereColumns.Length == 0 || whereColumns.Length != whereValues.Length) - throw new ArgumentException("Delete predicates need matching, non-empty column and value arrays."); - table = QuoteTableNameIfRequired(table); - - using var command = CreateCommand(); - if (CommandTimeout.HasValue) - { - command.CommandTimeout = CommandTimeout.Value; - } - - command.Transaction = _transaction; - - var query = string.Format("DELETE FROM {0} WHERE ({1})", table, - GetWhereStringWithNullCheck(whereColumns, whereValues)); - - command.CommandText = query; - command.CommandType = CommandType.Text; - - var paramCount = 0; - - foreach (var value in whereValues) - { - if (value == null || value == DBNull.Value) continue; - var parameter = command.CreateParameter(); - - ConfigureParameterWithValue(parameter, paramCount, value); - - parameter.ParameterName = GenerateParameterNameParameter(paramCount); - - command.Parameters.Add(parameter); - - paramCount++; - } - - Logger.Trace(command.CommandText); - return command.ExecuteNonQuery(); - } - } - - public virtual int Delete(string table, string wherecolumn, string wherevalue) - { - if (string.IsNullOrEmpty(wherecolumn) && string.IsNullOrEmpty(wherevalue)) - { - return Delete(table, (string[])null, null); - } - - return ExecuteNonQuery(string.Format("DELETE FROM {0} WHERE {1} = {2}", table, wherecolumn, QuoteValues(wherevalue))); - } - - public virtual int TruncateTable(string table) - { - return ExecuteNonQuery(string.Format("TRUNCATE TABLE {0} ", table)); - } - - public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, - ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) - { - var deleteAction = constraintMapper.SqlForConstraint(onDelete); - var updateAction = constraintMapper.SqlForConstraint(onUpdate); - var oracle = _dialect is DotNetProjects.Migrator.Providers.Impl.Oracle.OracleDialect; - if (oracle && onUpdate != ForeignKeyConstraintType.NoAction) - throw new NotSupportedException("Oracle does not support ON UPDATE foreign key actions."); - if (oracle && onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict or ForeignKeyConstraintType.Cascade or ForeignKeyConstraintType.SetNull)) - throw new NotSupportedException("Oracle supports default restrictive, CASCADE or SET NULL deletion actions."); - var sql = $"ALTER TABLE {QuoteTableNameIfRequired(childTable)} ADD CONSTRAINT {QuoteConstraintNameIfRequired(name)} FOREIGN KEY ({string.Join(", ", QuoteColumnNamesIfRequired(childColumns))}) REFERENCES {QuoteTableNameIfRequired(parentTable)} ({string.Join(", ", QuoteColumnNamesIfRequired(parentColumns))})"; - if (!oracle || onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict)) sql += $" ON DELETE {deleteAction}"; - if (!oracle) sql += $" ON UPDATE {updateAction}"; - ExecuteNonQuery(sql); - } - - /// - /// Starts a transaction. Called by the migration mediator. - /// - public virtual void BeginTransaction() - { - if (_transaction == null && _connection != null) - { - EnsureHasConnection(); - _transaction = _connection.BeginTransaction(_dialect is DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect ? IsolationLevel.Serializable : IsolationLevel.ReadCommitted); - } - } - - /// - /// Rollback the current migration. Called by the migration mediator. - /// - public virtual void Rollback() => CompleteTransaction(false); - - public virtual void Commit() => CompleteTransaction(true); - - public bool HasActiveTransaction => _transaction != null; - public string Scope => _scope; - public void InvalidateHistory() => _appliedMigrations = null; - - private void CompleteTransaction(bool commit) - { - var transaction = _transaction; - try - { - if (transaction != null) - { - if (commit) transaction.Commit(); - else transaction.Rollback(); - _transaction = null; - transaction.Dispose(); - } - } - finally { InvalidateHistory(); } - } - - /// Reads existing history without creating or upgrading its table. - public virtual IReadOnlyList ReadAppliedMigrations() - { - var versions = new List(); - if (!TableExists(_schemaInfotable)) return versions; - var hasScope = ColumnExists(_schemaInfotable, "Scope"); - if (!hasScope && _scope != "default") return versions; - using var cmd = CreateCommand(); - var predicate = "1=1"; - if (hasScope) - { - var parameter = cmd.CreateParameter(); - parameter.ParameterName = GenerateParameterNameParameter(0); - parameter.Value = _scope; - cmd.Parameters.Add(parameter); - predicate = QuoteColumnNameIfRequired("Scope") + " = " + GenerateParameterName(0); - } - using var reader = Select(cmd, QuoteColumnNameIfRequired("Version"), QuoteTableNameIfRequired(_schemaInfotable), predicate); - while (reader.Read()) versions.Add(Convert.ToInt64(reader.GetValue(0))); - versions.Sort(); - return versions; - } - - public virtual List AppliedMigrations - { - get - { - if (_appliedMigrations == null) - { - CreateSchemaInfoTable(); // Preserve the legacy property contract. - _appliedMigrations = new List(ReadAppliedMigrations()); - } - return _appliedMigrations; - } - } - - public virtual bool IsMigrationApplied(long version, string scope) - { - var value = SelectScalar("Version", _schemaInfotable, ["Scope", "Version"], [scope, version]); - return Convert.ToInt64(value) == version; - } - - /// - /// Marks a Migration version number as having been applied - /// - /// The version number of the migration that was applied - public virtual void MigrationApplied(long version, string scope) - { - CreateSchemaInfoTable(); - Insert(_schemaInfotable, ["Scope", "Version", "TimeStamp"], [scope ?? _scope, version, DateTime.UtcNow]); - InvalidateHistory(); - } - - /// - /// Marks a Migration version number as having been rolled back from the database - /// - /// The version number of the migration that was removed - public virtual void MigrationUnApplied(long version, string scope) - { - CreateSchemaInfoTable(); - Delete(_schemaInfotable, ["Scope", "Version"], [scope ?? _scope, version]); - InvalidateHistory(); - } - - public virtual void AddColumn(string table, Column column) - { - AddColumn(table, _dialect.GetAndMapColumnProperties(column.CopyDefinition()).ColumnSql); - } - - public virtual void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) - { - var definition = PrepareColumnWithPrimaryKey(table, column, primaryKey); - AddColumn(table, definition); - if (primaryKey.NonClustered) AddPrimaryKeyNonClustered(primaryKey.Name, table, primaryKey.KeyColumns); - else AddPrimaryKey(primaryKey.Name, table, primaryKey.KeyColumns); - } - - protected Column PrepareColumnWithPrimaryKey(string table, Column column, PrimaryKeyConstraint primaryKey) - { - ArgumentException.ThrowIfNullOrWhiteSpace(table); - ArgumentNullException.ThrowIfNull(column); - ArgumentNullException.ThrowIfNull(primaryKey); - ArgumentException.ThrowIfNullOrWhiteSpace(primaryKey.Name); - if (!TableExists(table)) throw new MigrationException("Table does not exist."); - var columns = GetColumns(table); - if (columns.Any(existing => existing.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase))) throw new MigrationException("Column already exists."); - if (GetTableConstraints(table).OfType().Any()) throw new MigrationException("The table already has a primary key."); - ValidateKeyColumns(primaryKey.Name, primaryKey.KeyColumns, columns.Append(column).ToArray()); - // Validate provider-specific key options before any DDL. - _dialect.GetTableConstraintSql(primaryKey); - var definition = column.CopyDefinition(); - if (primaryKey.KeyColumns.Contains(column.Name, StringComparer.OrdinalIgnoreCase)) definition.IsNullable = false; - return definition; - } - - public virtual void RemoveUniqueConstraint(string table, UniqueConstraint constraint) - { - ArgumentException.ThrowIfNullOrWhiteSpace(table); - ArgumentNullException.ThrowIfNull(constraint); - var matches = GetTableConstraints(table).OfType().Where(candidate => - string.Equals(candidate.Name, constraint.Name, StringComparison.OrdinalIgnoreCase) && - candidate.KeyColumns.SequenceEqual(constraint.KeyColumns, StringComparer.OrdinalIgnoreCase)).ToArray(); - if (matches.Length != 1) throw new MigrationException("Unique constraint selection must match exactly one definition."); - if (string.IsNullOrWhiteSpace(matches[0].Name)) throw new NotSupportedException("This provider cannot remove an unnamed unique constraint."); - RemoveConstraint(table, matches[0].Name); - } - - public virtual void GenerateForeignKey(string primaryTable, string refTable) - { - GenerateForeignKey(primaryTable, refTable, ForeignKeyConstraintType.NoAction); - } - - public virtual void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) - { - GenerateForeignKey(primaryTable, refTable + "Id", refTable, "Id", constraint); - } - - public virtual IDbCommand GetCommand() - { - return BuildCommand(null); - } - - public void Dispose() - { - try { if (_transaction != null) Rollback(); } - finally - { - if (!_outsideConnection) _connection?.Dispose(); - _connection = null; - InvalidateHistory(); - } - } - - public virtual string QuoteColumnNameIfRequired(string name) - { - return _dialect.QuoteColumnNameIfRequired(name); - } - - public virtual string QuoteTableNameIfRequired(string name) - { - if (!string.IsNullOrWhiteSpace(_defaultSchema) && SqlIdentifier.Parse(name).Length == 1) - name = _defaultSchema + "." + name; - return _dialect.QuoteTableNameIfRequired(name); - } - - public virtual string Encode(Guid guid) - { - return guid.ToString(); - } - - public virtual string[] QuoteColumnNamesIfRequired(params string[] columnNames) - { - var quotedColumns = new string[columnNames.Length]; - - for (var i = 0; i < columnNames.Length; i++) - { - quotedColumns[i] = QuoteColumnNameIfRequired(columnNames[i]); - } - - return quotedColumns; - } - - public virtual bool IsThisProvider(string provider) - { - // XXX: This might need to be more sophisticated. Currently just a convention - return GetType().Name.ToLower().StartsWith(provider.ToLower()); - } - - public virtual void RemoveAllForeignKeys(string tableName, string columnName) - { } - - public virtual void AddTable(string table, string engine, string columns) - { - table = QuoteTableNameIfRequired(table); - var sqlCreate = string.Format("CREATE TABLE {0} ({1})", table, columns); - - ExecuteNonQuery(sqlCreate); - } - - - - public virtual void AddColumnDefaultValue(string table, string column, object defaultValue) - { - if (defaultValue is DateTime defaultValueDateTime) - { - if (defaultValueDateTime.Kind != DateTimeKind.Utc) - { - throw new Exception("Only UTC values are accepted as default DateTime values."); - } - } - - table = QuoteTableNameIfRequired(table); - column = QuoteColumnNameIfRequired(column); - var def = Dialect.Default(defaultValue); - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD DEFAULT('{1}') FOR {2}", table, def, column)); - } - - public virtual void AddColumn(string table, string sqlColumn) - { - table = QuoteTableNameIfRequired(table); - ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD COLUMN {1}", table, sqlColumn)); - } - - public virtual void ChangeColumn(string table, string sqlColumn) - { - table = QuoteTableNameIfRequired(table); - ExecuteNonQuery(string.Format("ALTER TABLE {0} ALTER COLUMN {1}", table, sqlColumn)); - } - - protected virtual string JoinColumns(IEnumerable columns) - { - var columnStrings = new List(); - - foreach (var column in columns) - { - columnStrings.Add(column.ColumnSql); - } - - return string.Join(", ", columnStrings.ToArray()); - } - - public IDbCommand CreateCommand() - { - EnsureHasConnection(); - var cmd = _connection.CreateCommand(); - - if (CommandTimeout.HasValue) - { - cmd.CommandTimeout = CommandTimeout.Value; - } - - cmd.CommandType = CommandType.Text; - - if (_transaction != null) - { - cmd.Transaction = _transaction; - } - - if (CommandTimeout.HasValue) - { - cmd.CommandTimeout = CommandTimeout.Value; - } - return cmd; - } - - protected IDbCommand BuildCommand(string sql) - { - var cmd = CreateCommand(); - cmd.CommandText = sql; - return cmd; - } - - public virtual int Delete(string table) - { - return Delete(table, null, (string[])null); - } - - protected void EnsureHasConnection() - { - if (_connection.State != ConnectionState.Open) - { - _connection.Open(); - } - } - - protected virtual void CreateSchemaInfoTable() - { - EnsureHasConnection(); - if (!TableExists(_schemaInfotable)) - { - AddTable(_schemaInfotable, - new Column("Version",DbType.Int64){IsNullable = false}, - new Column("Scope",DbType.String,50,"default"){IsNullable = false}, - new Column("TimeStamp", DbType.DateTime)); - } - else +#region License + +//The contents of this file are subject to the Mozilla Public License +//Version 1.1 (the "License"); you may not use this file except in +//compliance with the License. You may obtain a copy of the License at +//http://www.mozilla.org/MPL/ +//Software distributed under the License is distributed on an "AS IS" +//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +//License for the specific language governing rights and limitations +//under the License. + +#endregion + +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using DotNetProjects.Migrator.Framework.Models; + +using DotNetProjects.Migrator.Providers.Impl.SQLite; +using DotNetProjects.Migrator.Providers.Models; +using System; +using System.Collections.Generic; +using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Text; +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; +using ForeignKeyConstraintType = DotNetProjects.Migrator.Framework.ForeignKeyConstraintType; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace DotNetProjects.Migrator.Providers; + +/// +/// Base class for every transformation providers. +/// A 'tranformation' is an operation that modifies the database. +/// +public abstract class TransformationProvider : ITransformationProvider, IMigrationHistory, IForeignKeyActions +{ + private string _scope; + protected readonly string _connectionString; + protected readonly string _defaultSchema; + private readonly ForeignKeyConstraintMapper constraintMapper = new(); + protected List _appliedMigrations; + protected IDbConnection _connection; + protected bool _outsideConnection = false; + protected Dialect _dialect; + private ILogger _logger; + private IDbTransaction _transaction; + + protected TransformationProvider(Dialect dialect, string connectionString, string defaultSchema, string scope) + { + _dialect = dialect; + _connectionString = connectionString; + _defaultSchema = defaultSchema; + _logger = new Logger(false); + _scope = scope; + } + + protected TransformationProvider(Dialect dialect, IDbConnection connection, string defaultSchema, string scope) + { + _dialect = dialect; + _connection = connection; + _outsideConnection = true; + _defaultSchema = defaultSchema; + _logger = new Logger(false); + _scope = scope; + } + + public IMigration CurrentMigration { get; set; } + + private string _schemaInfotable = "SchemaInfo"; + public string SchemaInfoTable + { + get + { + return _schemaInfotable; + } + set + { + _schemaInfotable = value; + InvalidateHistory(); + } + } + + public int? CommandTimeout { get; set; } + + public IDialect Dialect + { + get { return _dialect; } + } + + public string ConnectionString { get { return _connectionString; } } + + /// + /// Returns the event logger + /// + public virtual ILogger Logger + { + get { return _logger; } + set { _logger = value; } + } + + public virtual ITransformationProvider this[string provider] + { + get + { + if (null != provider && IsThisProvider(provider)) + { + return this; + } + + return NoOpTransformationProvider.Instance; + } + } + + public virtual Index[] GetIndexes(string table) + { + throw new NotImplementedException(); + } + + public virtual Column[] GetColumns(string table) + { + var columns = new List(); + using (var cmd = CreateCommand()) + using ( + var reader = + ExecuteQuery( + cmd, string.Format("select COLUMN_NAME, IS_NULLABLE from INFORMATION_SCHEMA.COLUMNS where table_name = '{0}'", table))) + { + while (reader.Read()) + { + var column = new Column(reader.GetString(0), DbType.String); + var nullableStr = reader.GetString(1); + var isNullable = nullableStr == "YES"; + column.IsNullable = isNullable; + + columns.Add(column); + } + } + + return columns.ToArray(); + } + + /// + /// Basic implementation works for Postgre and probably for MySQL (not tested). For Oracle it should be overridden + /// + /// + /// + /// + public virtual ForeignKeyConstraint[] GetForeignKeyConstraints(string table) => ForeignKeyMetadataReader.Read(this, table); + + public virtual TableConstraint[] GetTableConstraints(string table) => ConstraintMetadataReader.Read(this, table); + + public virtual string[] GetConstraints(string table) + { + var constraints = new List(); + using (var cmd = CreateCommand()) + using ( + var reader = + ExecuteQuery( + cmd, string.Format("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE LOWER(TABLE_NAME) = LOWER('{0}')", table))) + { + while (reader.Read()) + { + constraints.Add(reader.GetString(0)); + } + } + + return constraints.ToArray(); + } + + public virtual Column GetColumnByName(string table, string columnName) + { + var columns = GetColumns(table); + var column = columns.FirstOrDefault(x => x.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase)) ?? + throw new Exception($"Cannot find column '{columnName}' in table '{table}'"); + + return column; + } + + public virtual int GetColumnContentSize(string table, string columnName) + { + var result = this.ExecuteScalar("SELECT MAX(LENGTH(" + this.QuoteColumnNameIfRequired(columnName) + ")) FROM " + this.QuoteTableNameIfRequired(table)); + + if (result == DBNull.Value) + { + return 0; + } + + return Convert.ToInt32(result); + } + + public virtual string[] GetTables() + { + var tables = new List(); + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, "SELECT table_name FROM INFORMATION_SCHEMA.TABLES")) + { + while (reader.Read()) + { + tables.Add((string)reader[0]); + } + } + return tables.ToArray(); + } + + public virtual void RemoveForeignKey(string table, string name) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + RemoveConstraint(table, name); + } + + public virtual void RemoveConstraint(string table, string name) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{name}' does not exist"); + } + + if (!ConstraintExists(table, name)) + { + throw new MigrationException($"Constraint '{name}' does not exist"); + } + + var names = GetConstraints(table); + var actual = names.FirstOrDefault(n => n == name) + ?? names.SingleOrDefault(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)) + ?? throw new MigrationException("Constraint was not found in the requested table: " + name); + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP CONSTRAINT {1}", QuoteTableNameIfRequired(table), _dialect.QuoteIdentifier(actual))); + } + + public virtual void RemoveAllConstraints(string table) + { + foreach (var constraint in GetConstraints(table)) + { + RemoveConstraint(table, constraint); + } + } + + public virtual void AddView(string name, string tableName, params IViewField[] fields) + { + var lst = + fields.Where(x => string.IsNullOrEmpty(x.TableName) || x.TableName == tableName) + .Select(x => tableName + "." + x.ColumnName) + .ToList(); + + var nr = 0; + var joins = ""; + foreach (var joinTable in fields.Where(x => !string.IsNullOrEmpty(x.TableName) && x.TableName != tableName) + .GroupBy(x => new { x.TableName, x.KeyColumnName, x.ParentTableName, x.ParentKeyColumnName })) + { + var relationship = joinTable.Key; + var alias = "T" + nr++; + joins += $"JOIN {relationship.TableName} {alias} ON {alias}.{relationship.KeyColumnName} = {relationship.ParentTableName}.{relationship.ParentKeyColumnName} "; + foreach (var viewField in joinTable) + { + lst.Add(alias + "." + viewField.ColumnName); + } + } + + var select = string.Format("SELECT {0} FROM {1} {2}", string.Join(",", lst), tableName, joins); + + var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); + + ExecuteNonQuery(sql); + } + + public virtual void AddView(string name, string tableName, params IViewElement[] viewElements) + { + var selectedColumns = viewElements.Where(x => x is ViewColumn) + .Select(x => + { + var viewColumn = (ViewColumn)x; + return $"{viewColumn.Prefix}.{viewColumn.ColumnName} {viewColumn.Prefix}{viewColumn.ColumnName}"; + }) + .ToList(); + + var joins = string.Empty; + + foreach (var viewJoin in viewElements.Where(x => x is ViewJoin).Cast()) + { + var joinType = string.Empty; + + switch (viewJoin.JoinType) + { + case JoinType.LeftJoin: + joinType = "LEFT JOIN"; + break; + case JoinType.Join: + joinType = "JOIN"; + break; + } + + var tableAlias = string.IsNullOrEmpty(viewJoin.TableAlias) ? viewJoin.TableName : viewJoin.TableAlias; + var parentAlias = string.IsNullOrEmpty(viewJoin.ParentTableAlias) ? viewJoin.ParentTableName : viewJoin.ParentTableAlias; + + joins += string.Format("{0} {1} {2} ON {2}.{3} = {4}.{5} ", joinType, viewJoin.TableName, tableAlias, + viewJoin.ColumnName, parentAlias, viewJoin.ParentColumnName); + } + + var select = string.Format("SELECT {0} FROM {1} {1} {2}", string.Join(",", selectedColumns), tableName, joins); + var sql = string.Format("CREATE VIEW {0} AS {1}", name, select); + + + // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. + try + { + ExecuteNonQuery($"DROP VIEW {name}"); + } + catch + { + // Works with all DBs. "CREATE OR REPLACE" does not work with SQLite. "DROP IF EXISTS" does not work with oracle. + } + + ExecuteNonQuery(sql); + } + + /// + /// Add a new table + /// + /// Table name + /// Columns + public virtual void AddTable(string name, params IDbField[] columns) + { + // Most databases don't have the concept of a storage engine, so default is to not use it. + AddTable(name, null, columns); + } + + /// + /// Adds a new table + /// + /// Table name + /// Columns + /// the database storage engine to use + public virtual void AddTable(string name, string engine, params IDbField[] fields) + { + var columns = fields.OfType().Select(c => c.CopyDefinition()).ToArray(); + var keys = fields.OfType().ToArray(); + if (keys.Length > 1) throw new MigrationException("A table can have only one primary key."); + foreach (var key in keys) + { + ValidateKeyColumns(key.Name, key.KeyColumns, columns); + foreach (var column in columns.Where(c => key.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) + column.IsNullable = false; + } + foreach (var unique in fields.OfType()) ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); + var sql = columns.Select(c => _dialect.GetAndMapColumnProperties(c).ColumnSql) + .Concat(fields.OfType().Where(c => c is not ForeignKeyConstraint).Select(_dialect.GetTableConstraintSql)); + AddTable(name, engine, string.Join(", ", sql)); + foreach (var index in fields.OfType()) AddIndex(name, index); + foreach (var foreignKey in fields.OfType()) AddForeignKey(name, foreignKey); + } + + protected internal static void ValidateKeyColumns(string name, string[] keys, Column[] columns) + { + if (name != null && string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name must not be empty."); + if (keys == null || keys.Length == 0 || keys.Any(string.IsNullOrWhiteSpace) || keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != keys.Length) + throw new MigrationException("A key needs distinct, non-empty column names."); + if (keys.Any(key => !columns.Any(c => c.Name.Equals(key, StringComparison.OrdinalIgnoreCase)))) + throw new MigrationException("A constraint references a column that is absent from the table definition."); + } + + protected virtual string GetPrimaryKeyname(string tableName) + { + return "PK_" + tableName; + } + + public virtual void RemoveTable(string name) + { + if (!TableExists(name)) + { + throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", name)); + } + + ExecuteNonQuery(string.Format("DROP TABLE {0}", name)); + } + + public virtual void RenameTable(string oldName, string newName) + { + oldName = QuoteTableNameIfRequired(oldName); + newName = QuoteTableNameIfRequired(newName); + + if (TableExists(newName)) + { + throw new MigrationException(string.Format("Table with name '{0}' already exists", newName)); + } + + if (!TableExists(oldName)) + { + throw new MigrationException(string.Format("Table with name '{0}' does not exist to rename", oldName)); + } + + ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME TO {1}", oldName, newName)); + } + + public virtual void RenameColumn(string tableName, string oldColumnName, string newColumnName) + { + if (ColumnExists(tableName, newColumnName)) + { + throw new MigrationException(string.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); + } + + if (!ColumnExists(tableName, oldColumnName)) + { + throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, oldColumnName)); + } + + var column = GetColumnByName(tableName, oldColumnName); + + var quotedNewColumnName = QuoteColumnNameIfRequired(newColumnName); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} RENAME COLUMN {1} TO {2}", tableName, Dialect.Quote(column.Name), quotedNewColumnName)); + } + + public virtual void RemoveColumn(string tableName, string column) + { + if (!TableExists(tableName)) + { + throw new MigrationException($"The table '{tableName}' does not exist"); + } + + if (!ColumnExists(tableName, column, true)) + { + throw new MigrationException(string.Format("The table '{0}' does not have a column named '{1}'", tableName, column)); + } + + var existingColumn = GetColumnByName(tableName, column); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP COLUMN {1} ", tableName, Dialect.Quote(existingColumn.Name))); + } + + public virtual bool ColumnExists(string table, string column) + { + return ColumnExists(table, column, true); + } + + public virtual bool ColumnExists(string table, string column, bool ignoreCase) + { + if (ignoreCase) + { + return GetColumns(table).Any(x => x.Name.Equals(column, StringComparison.OrdinalIgnoreCase)); + } + + return GetColumns(table).Any(x => x.Name == column); + } + + public virtual void ChangeColumn(string table, Column column) + { + column = column.CopyDefinition(); + + + var mapper = _dialect.GetAndMapColumnProperties(column); + + ChangeColumn(table, mapper.ColumnSql); + + + } + + public virtual void RemoveColumnDefaultValue(string table, string column) + { + var sql = string.Format("ALTER TABLE {0} ALTER {1} DROP DEFAULT", table, column); + ExecuteNonQuery(sql); + } + + public virtual bool TableExists(string table) + { + throw new NotImplementedException(); + } + + public virtual bool ViewExists(string view) + { + throw new NotImplementedException(); + } + + public virtual void SwitchDatabase(string databaseName) + { + _connection.ChangeDatabase(databaseName); + } + + public abstract List GetDatabases(); + + public bool DatabaseExists(string name) + { + return GetDatabases().Any(c => string.Equals(name, c, StringComparison.OrdinalIgnoreCase)); + } + + public virtual void CreateDatabases(string databaseName) + { + ExecuteNonQuery(string.Format("CREATE DATABASE {0}", databaseName)); + } + + public virtual void KillDatabaseConnections(string databaseName) + { + //todo, implement this for each DB, no default implementation possible!!! + } + + public virtual void DropDatabases(string databaseName) + { + ExecuteNonQuery(string.Format("DROP DATABASE {0}", databaseName)); + } + + public virtual void AddColumn(string table, string column, DbType type) + { + AddColumn(table, new Column(column, type)); + } + + public virtual void AddColumn(string table, string column, MigratorDbType type) + { + AddColumn(table, new Column(column, type)); + } + + public virtual void AddColumn(string table, string column, DbType type, int size) + { + AddColumn(table, new Column(column, type, size)); + } + + public virtual void AddColumn(string table, string column, MigratorDbType type, int size) + { + AddColumn(table, new Column(column, type, size)); + } + + public virtual void AddColumn(string table, string column, DbType type, object defaultValue) + { + AddColumn(table, column, (MigratorDbType)type, defaultValue); + } + + public virtual void AddColumn(string table, string column, MigratorDbType type, object defaultValue) + { + var mapper = + _dialect.GetAndMapColumnProperties(new Column(column, type, defaultValue)); + + AddColumn(table, mapper.ColumnSql); + } + + /// + /// Append a primary key to a table. + /// + /// Constraint name + /// Table name + /// Primary column names + public virtual void AddPrimaryKey(string name, string table, params string[] columns) + { + table = QuoteTableNameIfRequired(table); + + ExecuteNonQuery( + string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} PRIMARY KEY ({2}) ", table, QuoteConstraintNameIfRequired(name), + string.Join(",", QuoteColumnNamesIfRequired(columns)))); + } + public virtual void AddPrimaryKeyNonClustered(string name, string table, params string[] columns) + { + this.AddPrimaryKey(name, table, columns); + } + public virtual void AddUniqueConstraint(string name, string table, params string[] columns) + { + table = QuoteTableNameIfRequired(table); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE({2}) ", table, QuoteConstraintNameIfRequired(name), + string.Join(", ", QuoteColumnNamesIfRequired(columns)))); + } + + public virtual void AddCheckConstraint(string name, string table, string checkSql) + { + table = QuoteTableNameIfRequired(table); + + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} CHECK ({2}) ", table, QuoteConstraintNameIfRequired(name), checkSql)); + } + + /// + /// Guesses the name of the foreign key and adds it + /// + public virtual void GenerateForeignKey(string childTable, string childColumn, string parentTable, string parentColumn) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn); + } + + /// + /// Guesses the name of the foreign key and adds it + /// + /// + public virtual void GenerateForeignKey( + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns); + } + + /// + /// Guesses the name of the foreign key and adds it + /// + public virtual void GenerateForeignKey( + string childTable, + string childColumn, + string parentTable, + string parentColumn, + ForeignKeyConstraintType constraint) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumn, parentTable, parentColumn, constraint); + } + + /// + /// Guesses the name of the foreign key and add it + /// + /// + public virtual void GenerateForeignKey( + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns, + ForeignKeyConstraintType constraint) + { + AddForeignKey("FK_" + childTable + "_" + parentTable, childTable, childColumns, parentTable, parentColumns, constraint); + } + + public virtual void AddForeignKey(string table, ForeignKeyConstraint fk) + { + if (string.IsNullOrWhiteSpace(fk.OnDelete) && string.IsNullOrWhiteSpace(fk.OnUpdate)) + AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone()); + else + AddForeignKey(fk.Name, table, (string[])fk.ChildColumns.Clone(), fk.ParentTable, (string[])fk.ParentColumns.Clone(), ParseAction(fk.OnDelete), ParseAction(fk.OnUpdate)); + + static ForeignKeyConstraintType ParseAction(string action) + { + if (string.IsNullOrWhiteSpace(action)) return ForeignKeyConstraintType.NoAction; + return Enum.TryParse(action.Replace(" ", ""), true, out var parsed) && Enum.IsDefined(parsed) + ? parsed : throw new ArgumentException("Unsupported foreign-key action.", nameof(fk)); + } + } + + public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn) + { + try + { + AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn]); + } + catch (Exception ex) + { + throw new Exception(string.Format("Error occured while adding foreign key: \"{0}\" between table: \"{1}\" and table: \"{2}\" - see inner exception for details", name, parentTable, childTable), ex); + } + } + + public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns) + { + AddForeignKey(name, childTable, childColumns, parentTable, parentColumns, ForeignKeyConstraintType.NoAction); + } + + public virtual void AddForeignKey(string name, string childTable, string childColumn, string parentTable, string parentColumn, ForeignKeyConstraintType constraint) + { + AddForeignKey(name, childTable, [childColumn], parentTable, [parentColumn], constraint); + } + + public virtual void AddForeignKey( + string name, + string childTable, + string[] childColumns, + string parentTable, + string[] parentColumns, + ForeignKeyConstraintType constraint) + { + childTable = QuoteTableNameIfRequired(childTable); + parentTable = QuoteTableNameIfRequired(parentTable); + parentColumns = QuoteColumnNamesIfRequired(parentColumns); + childColumns = QuoteColumnNamesIfRequired(childColumns); + name = QuoteConstraintNameIfRequired(name); + + var constraintResolved = constraintMapper.SqlForConstraint(constraint); + + // Legacy overload preserves one action for both clauses; IForeignKeyActions provides independent actions. + var childColumnsString = string.Join(", ", childColumns); + var parentColumnsString = string.Join(", ", parentColumns); + + var stringBuilder = new StringBuilder(); + stringBuilder.Append($"ALTER TABLE {childTable} ADD CONSTRAINT {name} FOREIGN KEY ({childColumnsString}) REFERENCES {parentTable} ({parentColumnsString})"); + stringBuilder.Append($"ON DELETE {constraintResolved} ON UPDATE {constraintResolved}"); + + ExecuteNonQuery(stringBuilder.ToString()); + } + + /// + /// Determines if a constraint exists. + /// + /// Constraint name + /// Table owning the constraint + /// true if the constraint exists. + public abstract bool ConstraintExists(string table, string name); + + public virtual bool PrimaryKeyExists(string table, string name) + { + return ConstraintExists(table, name); + } + + public virtual int ExecuteNonQuery(string sql) + { + return ExecuteNonQuery(sql, CommandTimeout ?? 30); + } + + public virtual int ExecuteNonQuery(string sql, int timeout) + { + return ExecuteNonQuery(sql, timeout, null); + } + + public virtual int ExecuteNonQuery(string sql, int timeout, params object[] args) + { + if (args == null) + { + Logger.Trace(sql); + Logger.ApplyingDBChange(sql); + } + else + { + Logger.Trace(string.Format(sql, args)); + Logger.ApplyingDBChange(string.Format(sql, args)); + } + + using var cmd = BuildCommand(sql); + + try + { + cmd.CommandTimeout = timeout; + + if (args != null) + { + var index = 0; + + foreach (var obj in args) + { + var parameter = cmd.CreateParameter(); + ConfigureParameterWithValue(parameter, index, obj); + parameter.ParameterName = GenerateParameterNameParameter(index); + cmd.Parameters.Add(parameter); + ++index; + } + } + + Logger.Trace(cmd.CommandText); + return cmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + Logger.Warn(ex.Message); + throw new MigrationException(string.Format("Error occured executing sql: {0}, see inner exception for details, error: " + ex, sql), ex); + } + } + + public List ExecuteStringQuery(string sql, params object[] args) + { + var values = new List(); + + using (var cmd = CreateCommand()) + { + using var reader = ExecuteQuery(cmd, string.Format(sql, args)); + while (reader.Read()) + { + var value = reader[0]; + + if (value == null || value == DBNull.Value) + { + values.Add(null); + } + else + { + values.Add(value.ToString()); + } + } + } + + return values; + } + + public virtual void ExecuteScript(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("A script path is required.", nameof(fileName)); + var root = CurrentMigration == null ? AppContext.BaseDirectory : Path.GetDirectoryName(CurrentMigration.GetType().Assembly.Location); + var path = Path.IsPathRooted(fileName) ? fileName : Path.Combine(root ?? AppContext.BaseDirectory, fileName); + this.ExecuteSqlScript(File.ReadAllText(path)); + } + + public virtual void ExecuteResourceScript(System.Reflection.Assembly assembly, string resourceName) + { + using var stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new FileNotFoundException("Embedded SQL resource not found.", resourceName); + using var reader = new StreamReader(stream); + this.ExecuteSqlScript(reader.ReadToEnd()); + } + + /// + /// Execute an SQL query returning results. + /// + /// The SQL text. + /// The IDbCommand. + /// A data iterator, IDataReader. + public virtual IDataReader ExecuteQuery(IDbCommand cmd, string sql) + { + Logger.Trace(sql); + cmd.CommandText = sql; + try + { + return cmd.ExecuteReader(); + } + catch (Exception ex) + { + Logger.Warn("query failed: {0}", cmd.CommandText); + throw new Exception("Failed to execute sql statement: " + sql, ex); + } + } + + public virtual object ExecuteScalar(string sql) + { + Logger.Trace(sql); + using var cmd = BuildCommand(sql); + try + { + return cmd.ExecuteScalar(); + } + catch + { + Logger.Warn("Query failed: {0}", cmd.CommandText); + throw; + } + } + + public virtual IDataReader Select(IDbCommand cmd, string what, string from) + { + return Select(cmd, what, from, "1=1"); + } + + public virtual IDataReader Select(IDbCommand cmd, string what, string from, string where) + { + return ExecuteQuery(cmd, string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); + } + + public virtual IDataReader Select(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, object[] whereValues = null) + { + return SelectComplex(cmd, table, columns, whereColumns, whereValues); + } + + public virtual IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string[] whereColumns = null, + object[] whereValues = null, string[] nullWhereColumns = null, string[] notNullWhereColumns = null) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + table = QuoteTableNameIfRequired(table); + + var builder = new StringBuilder(); + for (var i = 0; i < columns.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(QuoteColumnNameIfRequired(columns[i])); + } + + + cmd.Transaction = _transaction; + + var query = string.Format("SELECT {0} FROM {1}", builder.ToString(), table); + + if (whereColumns != null || nullWhereColumns != null || notNullWhereColumns != null) + { + query = string.Format("SELECT {0} FROM {1} WHERE ", builder.ToString(), table); + } + + var andNeeded = false; + if (whereColumns != null) + { + query += GetWhereString(whereColumns, whereValues); + andNeeded = true; + } + if (nullWhereColumns != null) + { + if (andNeeded) + { + query += " AND "; + } + + query += GetWhereStringIsNull(nullWhereColumns); + andNeeded = true; + } + if (notNullWhereColumns != null) + { + if (andNeeded) + { + query += " AND "; + } + + query += GetWhereStringIsNotNull(notNullWhereColumns); + andNeeded = true; + } + + cmd.CommandText = query; + cmd.CommandType = CommandType.Text; + + var paramCount = 0; + + if (whereColumns != null) + { + foreach (var value in whereValues) + { + var parameter = cmd.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + cmd.Parameters.Add(parameter); + + paramCount++; + } + } + + Logger.Trace(cmd.CommandText); + return cmd.ExecuteReader(); + + } + + public object SelectScalar(string what, string from) + { + return SelectScalar(what, from, "1=1"); + } + + public virtual object SelectScalar(string what, string from, string where) + { + return ExecuteScalar(string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); + } + + public virtual object SelectScalar(string what, string from, string[] whereColumns, object[] whereValues) + { + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + var query = string.Format("SELECT {0} FROM {1} WHERE {2}", what, from, GetWhereString(whereColumns, whereValues)); + + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + + foreach (var value in whereValues) + { + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + Logger.Trace(command.CommandText); + return command.ExecuteScalar(); + } + + public virtual int Update(string table, string[] columns, object[] values) + { + return Update(table, columns, values, null); + } + + public virtual int Update(string table, string[] columns, object[] values, string where) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (columns.Length != values.Length) + { + throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); + } + + table = QuoteTableNameIfRequired(table); + + var builder = new StringBuilder(); + for (var i = 0; i < values.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(QuoteColumnNameIfRequired(columns[i])); + builder.Append(" = "); + // A literal NULL has no driver-dependent inferred type (notably for + // nullable LOBs). Non-null values remain fully parameterized. + builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); + } + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + var query = string.Format("UPDATE {0} SET {1}", table, builder.ToString()); + if (!string.IsNullOrEmpty(where)) + { + query += " WHERE " + where; + } + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + + foreach (var value in values) { - if (!ColumnExists(_schemaInfotable, "Scope")) - { - AddColumn(_schemaInfotable, new Column("Scope", DbType.String, 50) { IsNullable = false, DefaultValue = "default" }); - RemoveAllConstraints(_schemaInfotable); - AddPrimaryKey("PK_SchemaInfo", _schemaInfotable, ["Version", "Scope"]); - } - - if (!ColumnExists(_schemaInfotable, "TimeStamp")) + if (value == null || value == DBNull.Value) { - AddColumn(_schemaInfotable, "TimeStamp", DbType.DateTime); + paramCount++; + continue; } - } - } - - public virtual string QuoteValues(string values) - { - return QuoteValues([values])[0]; - } - - public virtual string[] QuoteValues(string[] values) - { - return values.Select(val => + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + Logger.Trace(command.CommandText); + return command.ExecuteNonQuery(); + } + + public virtual int Update(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (columns.Length != values.Length) + { + throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); + } + + if (whereColumns.Length != whereValues.Length) + { + throw new Exception(string.Format("The number of whereColumns: {0} does not match the number of supplied whereValues: {1}", whereColumns.Length, whereValues.Length)); + } + + table = QuoteTableNameIfRequired(table); + + var builder = new StringBuilder(); + + for (var i = 0; i < values.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(QuoteColumnNameIfRequired(columns[i])); + builder.Append(" = "); + builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); + } + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + var query = string.Format("UPDATE {0} SET {1} WHERE {2}", table, builder.ToString(), GetWhereStringWithNullCheck(whereColumns, whereValues, values.Length)); + + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + + foreach (var value in values) { - if (null == val) + if (value == null || value == DBNull.Value) { - return "null"; + paramCount++; + continue; } - else + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + foreach (var value in whereValues) + { + if (value == null || value == DBNull.Value) + { + continue; + } + + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + + Logger.Trace(command.CommandText); + return command.ExecuteNonQuery(); + } + + public virtual void UpdateTargetFromSource(string tableSourceNotQuoted, string tableTargetNotQuoted, ColumnPair[] fromSourceToTargetColumnPairs, ColumnPair[] conditionColumnPairs) + { + throw new NotImplementedException(); + } + + public virtual int Insert(string table, string[] columns, object[] values) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (columns.Length != values.Length) + { + throw new MigrationException(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); + } + + table = QuoteTableNameIfRequired(table); + + var columnNames = string.Join(", ", columns.Select(col => QuoteColumnNameIfRequired(col)).ToArray()); + + var builder = new StringBuilder(); + + for (var i = 0; i < values.Length; i++) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append(values[i] == null || values[i] == DBNull.Value ? "NULL" : GenerateParameterName(i)); + } + + var parameterNames = builder.ToString(); + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + command.CommandText = string.Format("INSERT INTO {0} ({1}) VALUES ({2})", table, columnNames, parameterNames); + command.CommandType = CommandType.Text; + + var paramCount = 0; + + foreach (var value in values) + { + if (value == null || value == DBNull.Value) { - return string.Format("'{0}'", val.Replace("'", "''")); + paramCount++; + continue; } - }).ToArray(); - } - - public virtual string JoinColumnsAndValues(string[] columns, string[] values) - { - return JoinColumnsAndValues(columns, values, ", "); - } - - public virtual string JoinColumnsAndValues(string[] columns, string[] values, string joinSeperator) + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + return command.ExecuteNonQuery(); + } + + protected virtual string GetWhereStringWithNullCheck(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) + { + var builder2 = new StringBuilder(); + var parCnt = 0; + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + var val = whereValues[i]; + if (val == null || val == DBNull.Value) + { + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" is null "); + } + else + { + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" = "); + builder2.Append(GenerateParameterName(parCnt + parameterStartIndex)); + parCnt++; + } + } + + return builder2.ToString(); + } + + protected virtual string GetWhereString(string[] whereColumns, object[] whereValues, int parameterStartIndex = 0) + { + var builder2 = new StringBuilder(); + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" = "); + builder2.Append(GenerateParameterName(i + parameterStartIndex)); + } + + return builder2.ToString(); + } + + protected virtual string GetWhereStringIsNull(string[] whereColumns) + { + var builder2 = new StringBuilder(); + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" IS NULL"); + } + + return builder2.ToString(); + } + + protected virtual string GetWhereStringIsNotNull(string[] whereColumns) + { + var builder2 = new StringBuilder(); + for (var i = 0; i < whereColumns.Length; i++) + { + if (builder2.Length > 0) + { + builder2.Append(" AND "); + } + + builder2.Append(QuoteColumnNameIfRequired(whereColumns[i])); + builder2.Append(" IS NOT NULL"); + } + + return builder2.ToString(); + } + + public virtual int InsertIfNotExists(string table, string[] columns, object[] values, string[] whereColumns, object[] whereValues) + { + using var cmd = CreateCommand(); + using var reader = this.Select(cmd, table, [whereColumns[0]], whereColumns, whereValues); + if (!reader.Read()) + { + reader.Close(); + return this.Insert(table, columns, values); + } + else + { + reader.Close(); + return 0; + } + } + + public virtual int Delete(string table, string[] whereColumns = null, object[] whereValues = null) + { + if (string.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + + if (whereColumns == null && whereValues == null) + { + return ExecuteNonQuery(string.Format("DELETE FROM {0}", table)); + } + else + { + ArgumentNullException.ThrowIfNull(whereColumns); + ArgumentNullException.ThrowIfNull(whereValues); + if (whereColumns.Length == 0 || whereColumns.Length != whereValues.Length) + throw new ArgumentException("Delete predicates need matching, non-empty column and value arrays."); + table = QuoteTableNameIfRequired(table); + + using var command = CreateCommand(); + if (CommandTimeout.HasValue) + { + command.CommandTimeout = CommandTimeout.Value; + } + + command.Transaction = _transaction; + + var query = string.Format("DELETE FROM {0} WHERE ({1})", table, + GetWhereStringWithNullCheck(whereColumns, whereValues)); + + command.CommandText = query; + command.CommandType = CommandType.Text; + + var paramCount = 0; + + foreach (var value in whereValues) + { + if (value == null || value == DBNull.Value) continue; + var parameter = command.CreateParameter(); + + ConfigureParameterWithValue(parameter, paramCount, value); + + parameter.ParameterName = GenerateParameterNameParameter(paramCount); + + command.Parameters.Add(parameter); + + paramCount++; + } + + Logger.Trace(command.CommandText); + return command.ExecuteNonQuery(); + } + } + + public virtual int Delete(string table, string wherecolumn, string wherevalue) + { + if (string.IsNullOrEmpty(wherecolumn) && string.IsNullOrEmpty(wherevalue)) + { + return Delete(table, (string[])null, null); + } + + return ExecuteNonQuery(string.Format("DELETE FROM {0} WHERE {1} = {2}", table, wherecolumn, QuoteValues(wherevalue))); + } + + public virtual int TruncateTable(string table) + { + return ExecuteNonQuery(string.Format("TRUNCATE TABLE {0} ", table)); + } + + public virtual void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, + ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) + { + var deleteAction = constraintMapper.SqlForConstraint(onDelete); + var updateAction = constraintMapper.SqlForConstraint(onUpdate); + var oracle = _dialect is DotNetProjects.Migrator.Providers.Impl.Oracle.OracleDialect; + if (oracle && onUpdate != ForeignKeyConstraintType.NoAction) + throw new NotSupportedException("Oracle does not support ON UPDATE foreign key actions."); + if (oracle && onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict or ForeignKeyConstraintType.Cascade or ForeignKeyConstraintType.SetNull)) + throw new NotSupportedException("Oracle supports default restrictive, CASCADE or SET NULL deletion actions."); + var sql = $"ALTER TABLE {QuoteTableNameIfRequired(childTable)} ADD CONSTRAINT {QuoteConstraintNameIfRequired(name)} FOREIGN KEY ({string.Join(", ", QuoteColumnNamesIfRequired(childColumns))}) REFERENCES {QuoteTableNameIfRequired(parentTable)} ({string.Join(", ", QuoteColumnNamesIfRequired(parentColumns))})"; + if (!oracle || onDelete is not (ForeignKeyConstraintType.NoAction or ForeignKeyConstraintType.Restrict)) sql += $" ON DELETE {deleteAction}"; + if (!oracle) sql += $" ON UPDATE {updateAction}"; + ExecuteNonQuery(sql); + } + + /// + /// Starts a transaction. Called by the migration mediator. + /// + public virtual void BeginTransaction() + { + if (_transaction == null && _connection != null) + { + EnsureHasConnection(); + _transaction = _connection.BeginTransaction(_dialect is DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect ? IsolationLevel.Serializable : IsolationLevel.ReadCommitted); + } + } + + /// + /// Rollback the current migration. Called by the migration mediator. + /// + public virtual void Rollback() => CompleteTransaction(false); + + public virtual void Commit() => CompleteTransaction(true); + + public bool HasActiveTransaction => _transaction != null; + public string Scope => _scope; + public void InvalidateHistory() => _appliedMigrations = null; + + private void CompleteTransaction(bool commit) + { + var transaction = _transaction; + try + { + if (transaction != null) + { + if (commit) transaction.Commit(); + else transaction.Rollback(); + _transaction = null; + transaction.Dispose(); + } + } + finally { InvalidateHistory(); } + } + + /// Reads existing history without creating or upgrading its table. + public virtual IReadOnlyList ReadAppliedMigrations() + { + var versions = new List(); + if (!TableExists(_schemaInfotable)) return versions; + var hasScope = ColumnExists(_schemaInfotable, "Scope"); + if (!hasScope && _scope != "default") return versions; + using var cmd = CreateCommand(); + var predicate = "1=1"; + if (hasScope) + { + var parameter = cmd.CreateParameter(); + parameter.ParameterName = GenerateParameterNameParameter(0); + parameter.Value = _scope; + cmd.Parameters.Add(parameter); + predicate = QuoteColumnNameIfRequired("Scope") + " = " + GenerateParameterName(0); + } + using var reader = Select(cmd, QuoteColumnNameIfRequired("Version"), QuoteTableNameIfRequired(_schemaInfotable), predicate); + while (reader.Read()) versions.Add(Convert.ToInt64(reader.GetValue(0))); + versions.Sort(); + return versions; + } + + public virtual List AppliedMigrations + { + get + { + if (_appliedMigrations == null) + { + CreateSchemaInfoTable(); // Preserve the legacy property contract. + _appliedMigrations = new List(ReadAppliedMigrations()); + } + return _appliedMigrations; + } + } + + public virtual bool IsMigrationApplied(long version, string scope) + { + var value = SelectScalar("Version", _schemaInfotable, ["Scope", "Version"], [scope, version]); + return Convert.ToInt64(value) == version; + } + + /// + /// Marks a Migration version number as having been applied + /// + /// The version number of the migration that was applied + public virtual void MigrationApplied(long version, string scope) + { + CreateSchemaInfoTable(); + Insert(_schemaInfotable, ["Scope", "Version", "TimeStamp"], [scope ?? _scope, version, DateTime.UtcNow]); + InvalidateHistory(); + } + + /// + /// Marks a Migration version number as having been rolled back from the database + /// + /// The version number of the migration that was removed + public virtual void MigrationUnApplied(long version, string scope) + { + CreateSchemaInfoTable(); + Delete(_schemaInfotable, ["Scope", "Version"], [scope ?? _scope, version]); + InvalidateHistory(); + } + + public virtual void AddColumn(string table, Column column) + { + AddColumn(table, _dialect.GetAndMapColumnProperties(column.CopyDefinition()).ColumnSql); + } + + public virtual void AddColumn(string table, Column column, PrimaryKeyConstraint primaryKey) { - var quotedValues = QuoteValues(values); - var namesAndValues = new string[columns.Length]; - for (var i = 0; i < columns.Length; i++) - { - namesAndValues[i] = string.Format("{0}={1}", columns[i], quotedValues[i]); - } - - return string.Join(joinSeperator, namesAndValues); + var definition = PrepareColumnWithPrimaryKey(table, column, primaryKey); + AddColumn(table, definition); + if (primaryKey.NonClustered) AddPrimaryKeyNonClustered(primaryKey.Name, table, primaryKey.KeyColumns); + else AddPrimaryKey(primaryKey.Name, table, primaryKey.KeyColumns); } - public virtual string GenerateParameterNameParameter(int index) + protected Column PrepareColumnWithPrimaryKey(string table, Column column, PrimaryKeyConstraint primaryKey) { - return "@p" + index; + ArgumentException.ThrowIfNullOrWhiteSpace(table); + ArgumentNullException.ThrowIfNull(column); + ArgumentNullException.ThrowIfNull(primaryKey); + ArgumentException.ThrowIfNullOrWhiteSpace(primaryKey.Name); + if (!TableExists(table)) throw new MigrationException("Table does not exist."); + var columns = GetColumns(table); + if (columns.Any(existing => existing.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase))) throw new MigrationException("Column already exists."); + if (GetTableConstraints(table).OfType().Any()) throw new MigrationException("The table already has a primary key."); + ValidateKeyColumns(primaryKey.Name, primaryKey.KeyColumns, columns.Append(column).ToArray()); + // Validate provider-specific key options before any DDL. + _dialect.GetTableConstraintSql(primaryKey); + var definition = column.CopyDefinition(); + if (primaryKey.KeyColumns.Contains(column.Name, StringComparer.OrdinalIgnoreCase)) definition.IsNullable = false; + return definition; } - public virtual string GenerateParameterName(int index) + public virtual void RemoveUniqueConstraint(string table, UniqueConstraint constraint) { - return GenerateParameterNameParameter(index); + ArgumentException.ThrowIfNullOrWhiteSpace(table); + ArgumentNullException.ThrowIfNull(constraint); + var matches = GetTableConstraints(table).OfType().Where(candidate => + string.Equals(candidate.Name, constraint.Name, StringComparison.OrdinalIgnoreCase) && + candidate.KeyColumns.SequenceEqual(constraint.KeyColumns, StringComparer.OrdinalIgnoreCase)).ToArray(); + if (matches.Length != 1) throw new MigrationException("Unique constraint selection must match exactly one definition."); + if (string.IsNullOrWhiteSpace(matches[0].Name)) throw new NotSupportedException("This provider cannot remove an unnamed unique constraint."); + RemoveConstraint(table, matches[0].Name); } - protected virtual void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) - { - if (value == null || value == DBNull.Value) - { - parameter.Value = DBNull.Value; - } - else if (value is Guid || value is Guid?) - { - parameter.DbType = DbType.Guid; - parameter.Value = (Guid)value; - } - else if (value is byte[] bytes) - { - parameter.DbType = DbType.Binary; - parameter.Value = bytes; - } - else if (value is byte) - { - parameter.DbType = DbType.Byte; - parameter.Value = value; - } + public virtual void GenerateForeignKey(string primaryTable, string refTable) + { + GenerateForeignKey(primaryTable, refTable, ForeignKeyConstraintType.NoAction); + } + + public virtual void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraintType constraint) + { + GenerateForeignKey(primaryTable, refTable + "Id", refTable, "Id", constraint); + } + + public virtual IDbCommand GetCommand() + { + return BuildCommand(null); + } + + public void Dispose() + { + try { if (_transaction != null) Rollback(); } + finally + { + if (!_outsideConnection) _connection?.Dispose(); + _connection = null; + InvalidateHistory(); + } + } + + public virtual string QuoteColumnNameIfRequired(string name) + { + return _dialect.QuoteColumnNameIfRequired(name); + } + + public virtual string QuoteTableNameIfRequired(string name) + { + if (!string.IsNullOrWhiteSpace(_defaultSchema) && SqlIdentifier.Parse(name).Length == 1) + name = _defaultSchema + "." + name; + return _dialect.QuoteTableNameIfRequired(name); + } + + public virtual string Encode(Guid guid) + { + return guid.ToString(); + } + + public virtual string[] QuoteColumnNamesIfRequired(params string[] columnNames) + { + var quotedColumns = new string[columnNames.Length]; + + for (var i = 0; i < columnNames.Length; i++) + { + quotedColumns[i] = QuoteColumnNameIfRequired(columnNames[i]); + } + + return quotedColumns; + } + + public virtual bool IsThisProvider(string provider) + { + // XXX: This might need to be more sophisticated. Currently just a convention + return GetType().Name.ToLower().StartsWith(provider.ToLower()); + } + + public virtual void RemoveAllForeignKeys(string tableName, string columnName) + { } + + public virtual void AddTable(string table, string engine, string columns) + { + table = QuoteTableNameIfRequired(table); + var sqlCreate = string.Format("CREATE TABLE {0} ({1})", table, columns); + + ExecuteNonQuery(sqlCreate); + } + + + + public virtual void AddColumnDefaultValue(string table, string column, object defaultValue) + { + if (defaultValue is DateTime defaultValueDateTime) + { + if (defaultValueDateTime.Kind != DateTimeKind.Utc) + { + throw new Exception("Only UTC values are accepted as default DateTime values."); + } + } + + table = QuoteTableNameIfRequired(table); + column = QuoteColumnNameIfRequired(column); + var def = Dialect.Default(defaultValue); + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD DEFAULT('{1}') FOR {2}", table, def, column)); + } + + public virtual void AddColumn(string table, string sqlColumn) + { + table = QuoteTableNameIfRequired(table); + ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD COLUMN {1}", table, sqlColumn)); + } + + public virtual void ChangeColumn(string table, string sqlColumn) + { + table = QuoteTableNameIfRequired(table); + ExecuteNonQuery(string.Format("ALTER TABLE {0} ALTER COLUMN {1}", table, sqlColumn)); + } + + protected virtual string JoinColumns(IEnumerable columns) + { + var columnStrings = new List(); + + foreach (var column in columns) + { + columnStrings.Add(column.ColumnSql); + } + + return string.Join(", ", columnStrings.ToArray()); + } + + public IDbCommand CreateCommand() + { + EnsureHasConnection(); + var cmd = _connection.CreateCommand(); + + if (CommandTimeout.HasValue) + { + cmd.CommandTimeout = CommandTimeout.Value; + } + + cmd.CommandType = CommandType.Text; + + if (_transaction != null) + { + cmd.Transaction = _transaction; + } + + if (CommandTimeout.HasValue) + { + cmd.CommandTimeout = CommandTimeout.Value; + } + return cmd; + } + + protected IDbCommand BuildCommand(string sql) + { + var cmd = CreateCommand(); + cmd.CommandText = sql; + return cmd; + } + + public virtual int Delete(string table) + { + return Delete(table, null, (string[])null); + } + + protected void EnsureHasConnection() + { + if (_connection.State != ConnectionState.Open) + { + _connection.Open(); + } + } + + protected virtual void CreateSchemaInfoTable() + { + EnsureHasConnection(); + if (!TableExists(_schemaInfotable)) + { + AddTable(_schemaInfotable, + new Column("Version",DbType.Int64){IsNullable = false}, + new Column("Scope",DbType.String,50,"default"){IsNullable = false}, + new Column("TimeStamp", DbType.DateTime)); + } + else + { + if (!ColumnExists(_schemaInfotable, "Scope")) + { + AddColumn(_schemaInfotable, new Column("Scope", DbType.String, 50) { IsNullable = false, DefaultValue = "default" }); + RemoveAllConstraints(_schemaInfotable); + AddPrimaryKey("PK_SchemaInfo", _schemaInfotable, ["Version", "Scope"]); + } + + if (!ColumnExists(_schemaInfotable, "TimeStamp")) + { + AddColumn(_schemaInfotable, "TimeStamp", DbType.DateTime); + } + } + } + + public virtual string QuoteValues(string values) + { + return QuoteValues([values])[0]; + } + + public virtual string[] QuoteValues(string[] values) + { + return values.Select(val => + { + if (null == val) + { + return "null"; + } + else + { + return string.Format("'{0}'", val.Replace("'", "''")); + } + }).ToArray(); + } + + public virtual string JoinColumnsAndValues(string[] columns, string[] values) + { + return JoinColumnsAndValues(columns, values, ", "); + } + + public virtual string JoinColumnsAndValues(string[] columns, string[] values, string joinSeperator) + { + var quotedValues = QuoteValues(values); + var namesAndValues = new string[columns.Length]; + for (var i = 0; i < columns.Length; i++) + { + namesAndValues[i] = string.Format("{0}={1}", columns[i], quotedValues[i]); + } + + return string.Join(joinSeperator, namesAndValues); + } + + public virtual string GenerateParameterNameParameter(int index) + { + return "@p" + index; + } + + public virtual string GenerateParameterName(int index) + { + return GenerateParameterNameParameter(index); + } + + protected virtual void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) + { + if (value == null || value == DBNull.Value) + { + parameter.Value = DBNull.Value; + } + else if (value is Guid || value is Guid?) + { + parameter.DbType = DbType.Guid; + parameter.Value = (Guid)value; + } + else if (value is byte[] bytes) + { + parameter.DbType = DbType.Binary; + parameter.Value = bytes; + } + else if (value is byte) + { + parameter.DbType = DbType.Byte; + parameter.Value = value; + } else if (value is sbyte signedByte) { parameter.DbType = DbType.Int16; parameter.Value = (short)signedByte; } else if (value is short) - { - parameter.DbType = DbType.Int16; - parameter.Value = value; - } - else if (value is int) - { - parameter.DbType = DbType.Int32; - parameter.Value = value; - } - else if (value is long) - { - parameter.DbType = DbType.Int64; - parameter.Value = value; - } - else if (value is ushort) - { - parameter.DbType = DbType.UInt16; - parameter.Value = value; - } - else if (value is uint) - { - parameter.DbType = DbType.UInt32; - parameter.Value = value; - } - else if (value is ulong) - { - parameter.DbType = DbType.UInt64; - parameter.Value = value; - } + { + parameter.DbType = DbType.Int16; + parameter.Value = value; + } + else if (value is int) + { + parameter.DbType = DbType.Int32; + parameter.Value = value; + } + else if (value is long) + { + parameter.DbType = DbType.Int64; + parameter.Value = value; + } + else if (value is ushort) + { + parameter.DbType = DbType.UInt16; + parameter.Value = value; + } + else if (value is uint) + { + parameter.DbType = DbType.UInt32; + parameter.Value = value; + } + else if (value is ulong) + { + parameter.DbType = DbType.UInt64; + parameter.Value = value; + } else if (value is float) { parameter.DbType = DbType.Single; parameter.Value = value; } else if (value is double) - { - parameter.DbType = DbType.Double; - parameter.Value = value; - } - else if (value is decimal) - { - parameter.DbType = DbType.Decimal; - parameter.Value = value; - } - else if (value is string) - { - parameter.DbType = DbType.String; - parameter.Value = value; - } - else if (value is DateTime || value is DateTime?) - { - parameter.DbType = DbType.DateTime; - parameter.Value = value; - } - else if (value is TimeOnly time) - { - parameter.DbType = DbType.Time; - parameter.Value = time.ToTimeSpan(); // ADO.NET drivers commonly carry SQL TIME as TimeSpan. - } - else if (value is TimeSpan interval) - { - var type = _dialect.GetTypeName((DbType)MigratorDbType.Interval); - if (type is not ("BIGINT" or "INTEGER")) throw new NotSupportedException("This provider requires native interval parameter handling."); - parameter.DbType = DbType.Int64; - parameter.Value = interval.Ticks; - } - else if (value is DateTimeOffset dateTimeOffset) - { - parameter.DbType = DbType.DateTimeOffset; - parameter.Value = dateTimeOffset.ToUniversalTime(); - } - else if (value is DateTimeOffset?) - { - parameter.DbType = DbType.DateTimeOffset; - parameter.Value = value == null ? null : ((DateTimeOffset?)value).Value.ToUniversalTime(); - } - else if (value is bool || value is bool?) - { - parameter.DbType = DbType.Boolean; - parameter.Value = value; - } - else - { - throw new NotSupportedException(string.Format("TransformationProvider does not support value: {0} of type: {1}", value, value.GetType())); - } - } - - private string FormatValue(object value) - { - if (value == null) - { - return null; - } - - if (value is DateTime) - { - return ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss:fff"); - } - - return value.ToString(); - } - - private void QuoteColumnNames(string[] primaryColumns) - { - for (var i = 0; i < primaryColumns.Length; i++) - { - primaryColumns[i] = QuoteColumnNameIfRequired(primaryColumns[i]); - } - } - - public virtual void RemoveIndex(string table, string name) - { - if (TableExists(table) && IndexExists(table, name)) - { - name = QuoteConstraintNameIfRequired(name); - ExecuteNonQuery(string.Format("DROP INDEX {0}", name)); - } - } - - public virtual string AddIndex(string table, Index index) - { - throw new NotImplementedException($"{nameof(AddIndex)} is not overridden for the provider."); - } - - public virtual string AddIndex(string name, string table, params string[] columns) - { - var index = new Index { Name = name, KeyColumns = columns }; - - return AddIndex(table, index); - } - - protected string QuoteConstraintNameIfRequired(string name) - { - if (!_dialect.ConstraintNameNeedsQuote && !_dialect.IsReservedWord(name) - && System.Text.RegularExpressions.Regex.IsMatch(name, @"^[A-Za-z_][A-Za-z0-9_$#]*$")) return name; - var template = _dialect.QuoteTemplate; - var closing = template[^1].ToString(); - return string.Format(template, name.Replace(closing, closing + closing)); - } - - public abstract bool IndexExists(string table, string name); - - protected virtual string GetPrimaryKeyConstraintName(string table) - { - return null; - } - - public virtual void RemovePrimaryKey(string table) - { - if (!TableExists(table)) - { - return; - } - - var primaryKeyConstraintName = GetPrimaryKeyConstraintName(table); - - if (primaryKeyConstraintName == null || !ConstraintExists(table, primaryKeyConstraintName)) - { - return; - } - - RemoveConstraint(table, primaryKeyConstraintName); - } - - public virtual void RemoveAllIndexes(string table) - { - if (!TableExists(table)) - { - return; - } - - var indexes = GetIndexes(table); - - foreach (var index in indexes) - { - if (index.Name == null || !IndexExists(table, index.Name)) - { - continue; - } - - if (index.PrimaryKey || index.UniqueConstraint) - { - RemoveConstraint(table, index.Name); - } - else - { - RemoveIndex(table, index.Name); - } - } - } - - public virtual string Concatenate(params string[] strings) - { - return string.Join(" || ", strings); - } - - public IDbConnection Connection - { - get { return _connection; } - } - - public IEnumerable GetTables(string schema) - { - var tableRestrictions = new string[4]; - tableRestrictions[1] = schema; - - var c = _connection as DbConnection; - var tables = c.GetSchema("Tables", tableRestrictions); - return from DataRow row in tables.Rows select (row["TABLE_NAME"] as string); - } - - public IEnumerable GetColumns(string schema, string table) - { - var tableRestrictions = new string[4]; - tableRestrictions[1] = schema; - tableRestrictions[2] = table; - - var c = _connection as DbConnection; - var tables = c.GetSchema("Columns", tableRestrictions); + { + parameter.DbType = DbType.Double; + parameter.Value = value; + } + else if (value is decimal) + { + parameter.DbType = DbType.Decimal; + parameter.Value = value; + } + else if (value is string) + { + parameter.DbType = DbType.String; + parameter.Value = value; + } + else if (value is DateTime || value is DateTime?) + { + parameter.DbType = DbType.DateTime; + parameter.Value = value; + } + else if (value is TimeOnly time) + { + parameter.DbType = DbType.Time; + parameter.Value = time.ToTimeSpan(); // ADO.NET drivers commonly carry SQL TIME as TimeSpan. + } + else if (value is TimeSpan interval) + { + var type = _dialect.GetTypeName((DbType)MigratorDbType.Interval); + if (type is not ("BIGINT" or "INTEGER")) throw new NotSupportedException("This provider requires native interval parameter handling."); + parameter.DbType = DbType.Int64; + parameter.Value = interval.Ticks; + } + else if (value is DateTimeOffset dateTimeOffset) + { + parameter.DbType = DbType.DateTimeOffset; + parameter.Value = dateTimeOffset.ToUniversalTime(); + } + else if (value is DateTimeOffset?) + { + parameter.DbType = DbType.DateTimeOffset; + parameter.Value = value == null ? null : ((DateTimeOffset?)value).Value.ToUniversalTime(); + } + else if (value is bool || value is bool?) + { + parameter.DbType = DbType.Boolean; + parameter.Value = value; + } + else + { + throw new NotSupportedException(string.Format("TransformationProvider does not support value: {0} of type: {1}", value, value.GetType())); + } + } + + private string FormatValue(object value) + { + if (value == null) + { + return null; + } + + if (value is DateTime) + { + return ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss:fff"); + } + + return value.ToString(); + } + + private void QuoteColumnNames(string[] primaryColumns) + { + for (var i = 0; i < primaryColumns.Length; i++) + { + primaryColumns[i] = QuoteColumnNameIfRequired(primaryColumns[i]); + } + } + + public virtual void RemoveIndex(string table, string name) + { + if (TableExists(table) && IndexExists(table, name)) + { + name = QuoteConstraintNameIfRequired(name); + ExecuteNonQuery(string.Format("DROP INDEX {0}", name)); + } + } + + public virtual string AddIndex(string table, Index index) + { + throw new NotImplementedException($"{nameof(AddIndex)} is not overridden for the provider."); + } + + public virtual string AddIndex(string name, string table, params string[] columns) + { + var index = new Index { Name = name, KeyColumns = columns }; + + return AddIndex(table, index); + } + + protected string QuoteConstraintNameIfRequired(string name) + { + if (!_dialect.ConstraintNameNeedsQuote && !_dialect.IsReservedWord(name) + && System.Text.RegularExpressions.Regex.IsMatch(name, @"^[A-Za-z_][A-Za-z0-9_$#]*$")) return name; + var template = _dialect.QuoteTemplate; + var closing = template[^1].ToString(); + return string.Format(template, name.Replace(closing, closing + closing)); + } + + public abstract bool IndexExists(string table, string name); + + protected virtual string GetPrimaryKeyConstraintName(string table) + { + return null; + } + + public virtual void RemovePrimaryKey(string table) + { + if (!TableExists(table)) + { + return; + } + + var primaryKeyConstraintName = GetPrimaryKeyConstraintName(table); + + if (primaryKeyConstraintName == null || !ConstraintExists(table, primaryKeyConstraintName)) + { + return; + } + + RemoveConstraint(table, primaryKeyConstraintName); + } + + public virtual void RemoveAllIndexes(string table) + { + if (!TableExists(table)) + { + return; + } + + var indexes = GetIndexes(table); + + foreach (var index in indexes) + { + if (index.Name == null || !IndexExists(table, index.Name)) + { + continue; + } + + if (index.PrimaryKey || index.UniqueConstraint) + { + RemoveConstraint(table, index.Name); + } + else + { + RemoveIndex(table, index.Name); + } + } + } + + public virtual string Concatenate(params string[] strings) + { + return string.Join(" || ", strings); + } + + public IDbConnection Connection + { + get { return _connection; } + } + + public IEnumerable GetTables(string schema) + { + var tableRestrictions = new string[4]; + tableRestrictions[1] = schema; + + var c = _connection as DbConnection; + var tables = c.GetSchema("Tables", tableRestrictions); + return from DataRow row in tables.Rows select (row["TABLE_NAME"] as string); + } + + public IEnumerable GetColumns(string schema, string table) + { + var tableRestrictions = new string[4]; + tableRestrictions[1] = schema; + tableRestrictions[2] = table; + + var c = _connection as DbConnection; + var tables = c.GetSchema("Columns", tableRestrictions); return from DataRow row in tables.Rows select (row["COLUMN_NAME"] as string); - } - - protected void ValidateIndex(string tableName, Index index) - { - var hasFilterItems = index.FilterItems != null && index.FilterItems.Count > 0; - var columns = GetColumns(table: tableName); - - if (!TableExists(tableName)) - { - throw new MigrationException($"Table '{tableName}' does not exist."); - } - - foreach (var keyColumn in index.KeyColumns) - { - if (!index.KeyColumns.All(x => columns.Any(y => y.Name.Equals(x, StringComparison.OrdinalIgnoreCase)))) - { - throw new MigrationException($"Column '{keyColumn}' does not exist."); - } - } - - if (hasFilterItems) - { - if (!index.FilterItems.All(x => index.KeyColumns.Any(y => x.ColumnName.Equals(y, StringComparison.OrdinalIgnoreCase)))) - { - throw new MigrationException($"All columns in the {nameof(index.FilterItems)} should exist in the {nameof(index.KeyColumns)}."); - } - } - - if (IndexExists(tableName, index.Name)) - { - throw new MigrationException($"Index '{index.Name}' in table {tableName} already exists."); - } - - if (index.IncludeColumns != null && index.IncludeColumns.Length > 0) - { - if (index.IncludeColumns.Any(x => index.KeyColumns.Any(y => x.Equals(y, StringComparison.OrdinalIgnoreCase)))) - { - throw new MigrationException($"It is not allowed to use a column in {nameof(index.IncludeColumns)} that exist in {nameof(index.KeyColumns)}."); - } - } - } - - public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) - { - throw new NotImplementedException(); - } -} - + } + + protected void ValidateIndex(string tableName, Index index) + { + var hasFilterItems = index.FilterItems != null && index.FilterItems.Count > 0; + var columns = GetColumns(table: tableName); + + if (!TableExists(tableName)) + { + throw new MigrationException($"Table '{tableName}' does not exist."); + } + + foreach (var keyColumn in index.KeyColumns) + { + if (!index.KeyColumns.All(x => columns.Any(y => y.Name.Equals(x, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"Column '{keyColumn}' does not exist."); + } + } + + if (hasFilterItems) + { + if (!index.FilterItems.All(x => index.KeyColumns.Any(y => x.ColumnName.Equals(y, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"All columns in the {nameof(index.FilterItems)} should exist in the {nameof(index.KeyColumns)}."); + } + } + + if (IndexExists(tableName, index.Name)) + { + throw new MigrationException($"Index '{index.Name}' in table {tableName} already exists."); + } + + if (index.IncludeColumns != null && index.IncludeColumns.Length > 0) + { + if (index.IncludeColumns.Any(x => index.KeyColumns.Any(y => x.Equals(y, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"It is not allowed to use a column in {nameof(index.IncludeColumns)} that exist in {nameof(index.KeyColumns)}."); + } + } + } + + public virtual void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) + { + throw new NotImplementedException(); + } +} + From 05fadaa1846815ebf8eaee55aa053bb7265a7e1f Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Thu, 24 Sep 2026 11:05:16 +0200 Subject: [PATCH 3/3] Document direct schema API mappings in the operation coverage manifest --- docs/fluent-operation-coverage.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/fluent-operation-coverage.json b/docs/fluent-operation-coverage.json index d510cf1b..6f5fed38 100644 --- a/docs/fluent-operation-coverage.json +++ b/docs/fluent-operation-coverage.json @@ -1,6 +1,7 @@ { "AddTable": "Create.Table(...).WithFields(...) / WithEngine(...)", - "AddColumn": "Create.Column(name).OnTable(table).OfType(...) / Create.Column(definition).OnTable(table)", + "AddColumn": "Create.Column(name).OnTable(table).OfType(...) / Create.Column(definition).OnTable(table); Database.AddColumn(table, column, primaryKey) for the combined operation", + "RemoveUniqueConstraint": "Database.RemoveUniqueConstraint(table, definition) for exact metadata-based selection, including unnamed SQLite constraints", "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(...)",