diff --git a/.gitignore b/.gitignore index 8a30d25..a99142a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,13 @@ ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files +.idea/ *.rsuser *.suo *.user *.userosscache *.sln.docstates +__delete/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/DapperMatic.sln b/DapperMatic.sln index 0369a65..7d8cc5c 100644 --- a/DapperMatic.sln +++ b/DapperMatic.sln @@ -11,6 +11,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C1CEAB9E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperMatic.Tests", "tests\DapperMatic.Tests\DapperMatic.Tests.csproj", "{9FF5A12B-6617-4492-9BD0-434C58051054}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution Files", "{93285C01-B819-4C94-8A18-0CCC817EB3EE}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/README.md b/README.md index a47ee72..e67118e 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,615 @@ [![.github/workflows/release.yml](https://github.com/mjczone/DapperMatic/actions/workflows/release.yml/badge.svg)](https://github.com/mjczone/DapperMatic/actions/workflows/release.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + Additional extensions leveraging Dapper + +- [DapperMatic](#dappermatic) + - [Supported Providers](#supported-providers) + - [Models](#models) + - [Model related factory methods](#model-related-factory-methods) + - [`IDbConnection` CRUD extension methods](#idbconnection-crud-extension-methods) + - [General methods](#general-methods) + - [Schema methods](#schema-methods) + - [Table methods](#table-methods) + - [Column methods](#column-methods) + - [Check constraint methods](#check-constraint-methods) + - [Default constraint methods](#default-constraint-methods) + - [Foreign Key constraint methods](#foreign-key-constraint-methods) + - [UniqueConstraint constraint methods](#uniqueconstraint-constraint-methods) + - [Index constraint methods](#index-constraint-methods) + - [PrimaryKeyConstraint constraint methods](#primarykeyconstraint-constraint-methods) + - [View methods](#view-methods) + - [Testing](#testing) + - [Reference](#reference) + - [Provider documentation links](#provider-documentation-links) + +## Supported Providers + +Unit tests against versions in parenthesis. + +- [x] SQLite (v3) +- [x] MySQL (v5.7, 8.4) +- [x] MariaDB (v10.11) +- [x] PostgreSQL (v15, v16) +- [x] SQL Server (v2017, v2019, v2022) +- [ ] Oracle +- [ ] IBM DB2 + +## Models + +- [DxCheckConstraint](src/DapperMatic/Models/DxCheckConstraint.cs) +- [DxColumn](src/DapperMatic/Models/DxColumn.cs) +- [DxColumnOrder](src/DapperMatic/Models/DxColumnOrder.cs) +- [DxConstraint](src/DapperMatic/Models/DxConstraint.cs) +- [DxConstraintType](src/DapperMatic/Models/DxConstraintType.cs) +- [DxDefaultConstraint](src/DapperMatic/Models/DxDefaultConstraint.cs) +- [DxForeignKeyAction](src/DapperMatic/Models/DxForeignKeyAction.cs) +- [DxForeignKeyContraint](src/DapperMatic/Models/DxForeignKeyContraint.cs) +- [DxIndex](src/DapperMatic/Models/DxIndex.cs) +- [DxOrderedColumn](src/DapperMatic/Models/DxOrderedColumn.cs) +- [DxPrimaryKeyConstraint](src/DapperMatic/Models/DxPrimaryKeyConstraint.cs) +- [DxTable](src/DapperMatic/Models/DxTable.cs) +- [DxUniqueConstraint](src/DapperMatic/Models/DxUniqueConstraint.cs) +- [DxView](src/DapperMatic/Models/DxView.cs) + +### Model related factory methods + +- [DxTableFactory](src/DapperMatic/Models/DxTableFactory.cs) + +```cs +DxTable table = DxTableFactory.GetTable(typeof(app_employees)) +``` + +- [DxViewFactory](src/DapperMatic/Models/DxViewFactory.cs) + +```cs +DxView view = DxViewFactory.GetView(typeof(vw_onboarded_employees)) +``` + +## `IDbConnection` CRUD extension methods + +All methods are async and support an optional transaction (recommended), and cancellation token. + +The schema name is nullable on all methods, as many database providers don't support schemas (e.g., SQLite and MySql). If a database supports schemas, and the schema name passed in is `null` or an empty string, then a default schema name is used for that database provider. + +The following default schemas apply: + +- SqLite: "" (empty string) +- MySql: "" (empty string) +- PostgreSql: "public" +- SqlServer: "dbo" + +### General methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +Version version = await db.GetDatabaseVersionAsync(tx, cancellationToken); + +// Check to see if the database supports schemas +var supportsSchemas = db.SupportsSchemas(); + +// Get the mapped .NET type matching a specific provider sql data type (e.g., varchar(255), decimal(15,4)) +var (/* Type */ dotnetType, /* int? */ length, /* int? */ precision, /* int? */ scale) = db.GetDotnetTypeFromSqlType(string sqlType); + +// Normalize a database name identifier to some idiomatic standard, namely alpha numeric with underscores and without spaces +var normalizedName = db.NormalizeName(name); + +// Get the last sql executed inside DapperMatic +var lastSql = db.GetLastSql(); +(string sql, object? parameters) lastSqlWithParams = db.GetLastSqlWithParms(); +``` + +### Schema methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +// EXISTS: Check to see if a database schema exists +bool exists = await db.DoesSchemaExistAsync("app", tx, cancellationToken); + +// CREATE: Create a database schema +bool created = await db.CreateSchemaIfNotExistsAsync("app", ...); + +// GET: Retrieve database schema names +List names = await db.GetSchemaNamesAsync("*ap*", ...); + +// DROP: Drop a database schema +bool dropped = await db.DropSchemaIfExistsAsync("app", ...) +``` + +### Table methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +// EXISTS: Check to see if a database table exists +bool exists = await db.DoesTableExistAsync("app","app_employees", tx, cancellationToken); + +// CREATE: Create a database table +bool created = await db.CreateTableIfNotExistsAsync("app", /* DxTable */ table); +// or + created = await db.CreateTableIfNotExistsAsync( + "app", + "app_employees", + // DxColumn[] columns, + columns, + // DxPrimaryKeyConstraint? primaryKey = null, + primaryKey, + // DxCheckConstraint[]? checkConstraints = null, + checkConstraints, + // DxDefaultConstraint[]? defaultConstraints = null, + defaultConstraints, + // DxUniqueConstraint[]? uniqueConstraints = null, + uniqueConstraints, + // DxForeignKeyConstraint[]? foreignKeyConstraints = null, + foreignKeyConstraints, + // DxIndex[]? indexes = null, + indexes, + ... + ); + +// GET: Retrieve table names +List names = await db.GetTableNamesAsync("app", "app_*", ...); + +// GET: Retrieve tables +List tables = await db.GetTablesAsync("app", "app_*", ...); + +// GET: Retrieve single table +DxTable? table = await db.GetTableAsync("app", "app_employees", ...); + +// DROP: Drop a database table +bool dropped = await db.DropTableIfExistsAsync("app", "app_employees", ...); + +// RENAME: Rename a database table +bool renamed = await db.RenameTableIfExistsAsync("app", "app_employees", /* new name */ "app_staff", ...); + +// TRUNCATE: Drop a database table +bool truncated = await db.TruncateTableIfExistsAsync("app", "app_employees", ...); +``` + +### Column methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +// EXISTS: Check to see if a table column exists +bool exists = await db.DoesColumnExistAsync("app", "app_employees", "title", tx, cancellationToken); + +// CREATE: Create a table column +bool created = await db.CreateColumnIfNotExistsAsync("app", /* DxColumn */ column); +// or + created = await db.CreateColumnIfNotExistsAsync( + "app", + "app_employees", + // string columnName, + "manager_id" + // Type dotnetType, + typeof(Guid), + // optional parameters (the actual sql data type is derived from the dotnet type, but can be specified if desired) + providerDataType: (string?) null, + length: (int?) null, + precision: (int?) null, + scale: (int?) null, + checkExpression: (string?)null, + defaultExpression: (string?)null, + isNullable: false /* default */, + isPrimaryKey: false /* default */, + isAutoIncrement: false /* default */, + isUnique: false /* default */, + isIndexed: false /* default */, + isForeignKey: true, + referencedTableName: (string?) "app_managers", + referencedColumnName: (string?) "id", + onDelete: (DxForeignKeyAction?) DxForeignKeyAction.Cascade, + onUpdate: (DxForeignKeyAction?) DxForeignKeyAction.NoAction, + ... + ); + +// GET: Retrieve table column names +List names = await db.GetColumnNamesAsync("app", "app_employees", "*title*", ...); + +// GET: Retrieve table columns +List tables = await db.GetColumnsAsync("app", "app_employees", "*title*", ...); + +// GET: Retrieve single table column +DxColumn? column = await db.GetColumnAsync("app", "app_employees", "title", ...); + +// DROP: Drop a table column +bool dropped = await db.DropColumnIfExistsAsync("app", "app_employees", "title", ...); + +// RENAME: Rename a table column +bool renamed = await db.RenameColumnIfExistsAsync("app", "app_employees", "title", /* new name */ "job_title", ...); +``` + +### Check constraint methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +var constraintName = ProviderUtils.GenerateCheckConstraintName("app_employees", "age"); + +// EXISTS: Check to see if a check constraint exists +bool exists = await db.DoesCheckConstraintExistAsync("app","app_employees", constraintName, tx, cancellationToken); + +// EXISTS: Check to see if a check constraint exists on a column +exists = await db.DoesCheckConstraintExistOnColumnAsync("app","app_employees", "age", tx, cancellationToken); + +// CREATE: Create a check constraint +bool created = await db.CreateCheckConstraintIfNotExistsAsync("app", /* DxCheckConstraint */ checkConstraint); +// or + created = await db.CreateCheckConstraintIfNotExistsAsync( + "app", + "app_employees", + // string? columnName, + "age", + // string constraintName, + constraintName, + // string expression, + "age > 21", + ... + ); + +// GET: Retrieve check constraints +List names = await db.GetCheckConstraintNamesAsync("app", "app_employees", "ck_*", ...); + +string name = await db.GetCheckConstraintNameOnColumnAsync("app", "app_employees", "age", ...); + +// GET: Retrieve check constraints +List checkConstraints = await db.GetCheckConstraintsAsync("app", "app_employees", "ck_*", ...); + +// GET: Retrieve single check constraint +DxCheckConstraint? checkConstraint = await db.GetCheckConstraintAsync("app", "app_employees", constraintName, ...); + +// GET: Retrieve single check constraint on column +checkConstraint = await db.GetCheckConstraintOnColumnAsync("app", "app_employees", "age", ...); + +// DROP: Drop a check constraint +bool dropped = await db.DropCheckConstraintIfExistsAsync("app", "app_employees", constraintName, ...); + +// DROP: Drop a check constraint on column +dropped = await db.DropCheckConstraintOnColumnIfExistsAsync("app", "app_employees", "age", ...); +``` + +### Default constraint methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +var constraintName = ProviderUtils.GenerateDefaultConstraintName("app_employees", "age"); + +// EXISTS: Check to see if a default constraint exists +bool exists = await db.DoesDefaultConstraintExistAsync("app","app_employees", constraintName, tx, cancellationToken); + +// EXISTS: Check to see if a default constraint exists on a column +exists = await db.DoesDefaultConstraintExistOnColumnAsync("app","app_employees", "age", tx, cancellationToken); + +// CREATE: Create a default constraint +bool created = await db.CreateDefaultConstraintIfNotExistsAsync("app", /* DxDefaultConstraint */ defaultConstraint); +// or + created = await db.CreateDefaultConstraintIfNotExistsAsync( + "app", + "app_employees", + // string? columnName, + "age", + // string constraintName, + constraintName, + // string expression, + "-1", + ... + ); + +// GET: Retrieve default constraints +List names = await db.GetDefaultConstraintNamesAsync("app", "app_employees", "df_*", ...); + +string name = await db.GetDefaultConstraintNameOnColumnAsync("app", "app_employees", "age", ...); + +// GET: Retrieve default constraints +List defaultConstraints = await db.GetDefaultConstraintsAsync("app", "app_employees", "df*", ...); + +// GET: Retrieve single default constraint +DxDefaultConstraint? defaultConstraint = await db.GetDefaultConstraintAsync("app", "app_employees", constraintName, ...); + +// GET: Retrieve single default constraint on column +defaultConstraint = await db.GetDefaultConstraintOnColumnAsync("app", "app_employees", "age", ...); + +// DROP: Drop a default constraint +bool dropped = await db.DropDefaultConstraintIfExistsAsync("app", "app_employees", constraintName, ...); + +// DROP: Drop a default constraint on column +dropped = await db.DropDefaultConstraintOnColumnIfExistsAsync("app", "app_employees", "age", ...); +``` + +### Foreign Key constraint methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +var constraintName = ProviderUtils.GenerateForeignKeyConstraintName("app_employees", "manager_id", "app_managers", "id"); + +// EXISTS: Check to see if a foreign key exists +bool exists = await db.DoesForeignKeyConstraintExistAsync("app","app_employees", constraintName, tx, cancellationToken); + +// EXISTS: Check to see if a foreign key exists on a column +exists = await db.DoesForeignKeyConstraintExistOnColumnAsync("app","app_employees", "manager_id", tx, cancellationToken); + +// CREATE: Create a foreign key +bool created = await db.CreateForeignKeyConstraintIfNotExistsAsync("app", /* DxForeignKeyConstraint */ foreignKeyConstraint); +// or + created = await db.CreateForeignKeyConstraintIfNotExistsAsync( + "app", + "app_employees", + // string constraintName, + constraintName, + // DxOrderedColumn[] sourceColumns, + [ new DxOrderedColumn("manager_id") ] + // string referencedTableName, + "app_managers", + // DxOrderedColumn[] referencedColumns, + [ new DxOrderedColumn("id") ], + onDelete: DxForeignKeyAction.Cascade, + onUpdate: DxForeignKeyAction.NoAction, + ... + ); + +// GET: Retrieve foreign key names +List names = await db.GetForeignKeyConstraintNamesAsync("app", "app_employees", "fk_*", ...); + +// GET: Retrieve foreign key name on column +string name = await db.GetForeignKeyConstraintNameOnColumnAsync("app", "app_employees", "manager_id", ...); + +// GET: Retrieve foreign keys +List foreignKeyConstraints = await db.GetForeignKeyConstraintsAsync("app", "app_employees", "fk_*", ...); + +// GET: Retrieve single foreign key +DxForeignKeyConstraint? foreignKeyConstraint = await db.GetForeignKeyConstraintAsync("app", "app_employees", constraintName, ...); + +// GET: Retrieve single foreign key on column +foreignKeyConstraint = await db.GetForeignKeyConstraintOnColumnAsync("app", "app_employees", "manager_id", ...); + +// DROP: Drop a foreign key +bool dropped = await db.DropForeignKeyConstraintIfExistsAsync("app", "app_employees", constraintName, ...); + +// DROP: Drop a foreign key on column +dropped = await db.DropForeignKeyConstraintOnColumnIfExistsAsync("app", "app_employees", "age", ...); +``` + +### UniqueConstraint constraint methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +var uniqueConstraintName = ProviderUtils.GenerateUniqueConstraintName("app_employees", "email"); + +// EXISTS: Check to see if a unique constraint exists +bool exists = await db.DoesUniqueConstraintExistAsync("app","app_employees", uniqueConstraintName, tx, cancellationToken); + +// EXISTS: Check to see if a unique constraint exists on a column +exists = await db.DoesUniqueConstraintExistOnColumnAsync("app","app_employees", "email", tx, cancellationToken); + +// CREATE: Create a unique constraint +bool created = await db.CreateUniqueConstraintIfNotExistsAsync("app", /* DxUniqueConstraint */ uniqueConstraint, ...); +// or + created = await db.CreateUniqueConstraintIfNotExistsAsync( + "app", + "app_employees", + // string uniqueConstraintName, + uniqueConstraintName, + // DxOrderedColumn[] columns, + [ new DxOrderedColumn("email", DxColumnOrder.Descending) ], + ... + ); + +// GET: Retrieve unique constraint names +List names = await db.GetUniqueConstraintNamesAsync("app", "app_employees", "uc_*", ...); + +// GET: Retrieve uniqueConstraint names on column +names = await db.GetUniqueConstraintNamesOnColumnAsync("app", "app_employees", "email", ...); + +// GET: Retrieve uniqueConstraints +List uniqueConstraints = await db.GetUniqueConstraintsAsync("app", "app_employees", "uc_*", ...); + +// GET: Retrieve single unique constraint +DxUniqueConstraint? uniqueConstraint = await db.GetUniqueConstraintAsync("app", "app_employees", uniqueConstraintName, ...); + +// GET: Retrieve single unique constraint on column +uniqueConstraint = await db.GetUniqueConstraintOnColumnAsync("app", "app_employees", "email", ...); + +// DROP: Drop an unique constraint +bool dropped = await db.DropUniqueConstraintIfExistsAsync("app", "app_employees", uniqueConstraintName, ...); + +// DROP: Drop unique constraints on column +dropped = await db.DropUniqueConstraintsOnColumnIfExistsAsync("app", "app_employees", "email", ...); +``` + +### Index constraint methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +var indexName = ProviderUtils.GenerateIndexName("app_employees", "is_onboarded"); + +// EXISTS: Check to see if a index exists +bool exists = await db.DoesIndexExistAsync("app","app_employees", indexName, tx, cancellationToken); + +// EXISTS: Check to see if a index exists on a column +exists = await db.DoesIndexExistOnColumnAsync("app","app_employees", "is_onboarded", tx, cancellationToken); + +// CREATE: Create a index +bool created = await db.CreateIndexIfNotExistsAsync("app", /* DxIndex */ index); +// or + created = await db.CreateIndexIfNotExistsAsync( + "app", + "app_employees", + // string indexName, + indexName, + // DxOrderedColumn[] columns, + [ new DxOrderedColumn("is_onboarded", DxColumnOrder.Descending) ], + isUnique: false, + ... + ); + +// GET: Retrieve index names +List names = await db.GetIndexNamesAsync("app", "app_employees", "ix_*", ...); + +// GET: Retrieve index names on column +names = await db.GetIndexNamesOnColumnAsync("app", "app_employees", "is_onboarded", ...); + +// GET: Retrieve indexs +List indexes = await db.GetIndexesAsync("app", "app_employees", "ix_*", ...); + +// GET: Retrieve single index +DxIndex? index = await db.GetIndexAsync("app", "app_employees", indexName, ...); + +// GET: Retrieve single index on column +index = await db.GetIndexOnColumnAsync("app", "app_employees", "is_onboarded", ...); + +// DROP: Drop an index +bool dropped = await db.DropIndexIfExistsAsync("app", "app_employees", indexName, ...); + +// DROP: Drop indexes on column +dropped = await db.DropIndexesOnColumnIfExistsAsync("app", "app_employees", "is_onboarded", ...); +``` + +### PrimaryKeyConstraint constraint methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +var primaryKeyConstraintName = ProviderUtils.GeneratePrimaryKeyConstraintName("app_employees", "email"); + +// EXISTS: Check to see if a primary key constraint exists +bool exists = await db.DoesPrimaryKeyConstraintExistAsync("app","app_employees", tx, cancellationToken); + +// CREATE: Create a primary key constraint +bool created = await db.CreatePrimaryKeyConstraintIfNotExistsAsync("app", /* DxPrimaryKeyConstraint */ primaryKeyConstraint, ...); +// or + created = await db.CreatePrimaryKeyConstraintIfNotExistsAsync( + "app", + "app_employees", + // string primaryKeyConstraintName, + primaryKeyConstraintName, + // DxOrderedColumn[] columns, + [ new DxOrderedColumn("email", DxColumnOrder.Descending) ], + ... + ); + +// GET: Retrieve single primary key constraint +DxPrimaryKeyConstraint? primaryKeyConstraint = await db.GetPrimaryKeyConstraintAsync("app", "app_employees", ...); + +// DROP: Drop a primary key constraint +bool dropped = await db.DropPrimaryKeyConstraintIfExistsAsync("app", "app_employees", primaryKeyConstraintName, ...); +``` + +### View methods + +```cs +using var db = await connectionFactory.OpenConnectionAsync(); +using var tx = db.BeginTransaction(); + +var viewName = "vw_employees_not_yet_onboarded"; + +// EXISTS: Check to see if a view exists +bool exists = await db.DoesViewExistAsync("app", viewName, tx, cancellationToken); + +// CREATE: Create a view +bool created = await db.CreateViewIfNotExistsAsync("app", /* DxView */ view, ...); +// or + created = await db.CreateViewIfNotExistsAsync( + "app", + // string viewName, + viewName, + // string viewDefinition, + "SELECT * FROM app_employees WHERE is_onboarded = 0", + ... + ); + +// UPDATE: Update a view +bool updated = await db.CreateViewIfNotExistsAsync( + "app", + // string viewName, + viewName, + // string viewDefinition, + "SELECT * FROM app_employees WHERE is_onboarded = 0 and employment_date_ended is null", + ... +); + +// GET: Retrieve view names +List viewNames = await db.GetViewNames("app", "vw_*", ...); + +// GET: Retrieve single view +List views = await db.GetViewsAsync("app", "vw_*", ...); + +// GET: Retrieve single view +DxView? view = await db.GetViewAsync("app", viewName, ...); + +// DROP: Drop a view +bool dropped = await db.DropViewIfExistsAsync("app", viewName, ...); + +// RENAME: Rename a view +bool dropped = await db.RenameViewIfExistsAsync( + "app", + // string viewName, + "vw_employees_not_yet_onboarded", + // string newViewName, + "vw_current_employees_not_yet_onboarded", + ... +); +``` + +## Testing + +The testing methodology consists of using the following very handy `Testcontainers.*` nuget library packages. +Tests are executed on Linux, and can be run on WSL during development. + +```xml + + + + +``` + +The exact same tests are run for each database provider, ensuring consistent behavior across all providers. + +The tests leverage docker containers for each supported database version (created and disposed of automatically at runtime). +The local file system is used for SQLite. + +## Reference + +### Provider documentation links + +The extension methods and operation implementations are derived from the SQL documentation residing at the following links: + +- MySQL + - MySQL 8.4: + - MySQL 5.7: +- MariaDB + - MariaDB 10.11: +- PostgreSQL + - PostgreSQL 16: + - PostgreSQL 15: +- SQLite + - SQLite (v3): +- SQL Server + - SQL Server 2022: + - SQL Server 2019: + - SQL Server 2017: + +### Future plans (t.b.d.) + +- Add per-method provider `Option` parameters for greater coverage of provider-specific capabilities and nuances. +- Improve on some of the convention-based design decisions, and support a fluent configuration syntax for handling types and data type mappings. +- Add database CRUD methods +- Add account management CRUD methods (for users and roles) diff --git a/ref/Database Types.xlsx b/ref/Database Types.xlsx new file mode 100644 index 0000000..64a3b28 Binary files /dev/null and b/ref/Database Types.xlsx differ diff --git a/ref/database_instances.md b/ref/database_instances.md deleted file mode 100644 index c889c08..0000000 --- a/ref/database_instances.md +++ /dev/null @@ -1,82 +0,0 @@ -# Docker - -## Unit testing with `Testcontainers` - -The tests in this project use [testcontainers](https://testcontainers.com/guides/getting-started-with-test-containers-for-dotnet). - -## Local/Manual testing - -To play around locally with a variety of databases to test SQL statements on, you can use Docker. - -Here are some shortcut one-liners to get some databases up and running for your favorite IDE (e.g., DBeaver, DataGrip, etc...). - -### PostgreSQL - -Start a container, and persist the volume: - -```sh -docker run --rm --name test_postgres15 \ - -e PGDATA=/var/lib/postgresql/data/pgdata \ - -e POSTGRES_PASSWORD=Pa33w0rd! \ - -p 2432:5432 \ - -d \ - -v test_postgres_data:/var/lib/postgresql/data \ - postgis/postgis:15-3.4 -``` - -Stop the container (also deletes it if it was started with `--rm`): - -```sh -docker stop test_postgres -``` - -### MySQL - -Start a container, and persist the volume: - -```sh -docker run --rm --name test_mysql84 \ - -e MYSQL_DATABASE=testdb \ - -e MYSQL_ROOT_PASSWORD=Pa33w0rd! \ - -p 2306:3306 \ - -d \ - -v test_mysql_data:/var/lib/mysql \ - mysql:8.4 -``` - -Stop the container (also deletes it if it was started with `--rm`): - -```sh -docker stop test_mysql84 -``` - -### SQL Server - -Start a container, and persist the volume: - -```sh -docker run --rm --name test_mssql19 \ - --user root \ - -e ACCEPT_EULA=Y \ - -e MSSQL_SA_PASSWORD=Pa33w0rd! \ - -p 2433:1433 \ - -d \ - -v test_mssql_data:/var/opt/mssql/data \ - mcr.microsoft.com/mssql/server:2019-latest -``` - -Stop it with - -```sh -docker stop test_mssql19 -``` - -### SQLite - -No containers necessary for SQLite, just connect to a file on your local filesystem. - -### Sample Data - -You can use the SQL scripts in the [cristiscu/employees-test-database](https://github.com/cristiscu/employees-test-database/tree/master/scripts) repository to setup a common database in each provider. - - Copies of these scripts are also provided in this `ref` folder. diff --git a/ref/employees-test-database/LICENSE.md b/ref/employees-test-database/LICENSE.md deleted file mode 100644 index 5dea104..0000000 --- a/ref/employees-test-database/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -# Employees Database - -**Copyright (c) 2009-2019 XtractPro Software** - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ref/employees-test-database/README.md b/ref/employees-test-database/README.md deleted file mode 100644 index bd6d905..0000000 --- a/ref/employees-test-database/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Employees Test Database - - Adapted from the repository hosted at . - -***Employees*** (or ***EmployeesQX***, to avoid a name conflict with other samples) is a very simple database to be used for testing in relational database systems. It is inspired from similar small databases included in Oracle, MySQL or other database systems over the years. - -This shared repository makes it easy to download ready-to-use database files, when supported. Or SQL scripts, to be run in a database admin tool, to create and populate the few tables with data. - -## Supported Databases - -Before loading and running the SQL scripts with a specific tool, you may already have to create manually a database, user or schema, with proper access rights, and log in with the new credentials. If that's the case, comment-out the first few lines related to these database objects. Checking and dropping existing tables can be also ignored, for a brand new database. - -- **Microsoft SQL Server** - since version 2008 to 2017. Can use the free SSMS (SQL Server Management Studio) to manually create an EmployeesQX database first, with appropriate access rights. -- **PostgreSQL** - since version 8.4 to 11. Can use the free pgAdmin application to create an EmployeesQX database and run the creation script. There is a separate script for v8.4, as it has some other specific old data types. -- **MySQL** - since version 5.5 to 8.0. Can use the free MySQL Workbench to load and run the script locally, or phpMyAdmin for remote databases. Connect with the root admin username or create your own. -- **SQLite** - should support all version 3 releases. Rather that running the SQL script, use directly the DB file included in this repository. All Data Xtractor applications ship with this ready-to-use database sample. - -***Please let me know when/if you create a similar SQL script for a new database type, to add it to the list and share it for free with the community.*** - -## Data Model - -The database is also used for automated tests in my [**Data Xtractor applications**](https://data-xtractor.com/knowledgebase/employees-database-sample/), including **Model Xtractor**, **Query Xtractor** and **Visual Xtractor**. The apps also include the SQL creation scripts in the setup. Employees ready-to-use database files are used as SQLite and Microsoft SQL CE samples. - -Following database diagram is from Data Xtractor. It shows all tables and views of an Employees Oracle 11g database, with lookup data on the emp (Employees) table. - -![enter image description here](./employees-test-database.png) - -## Tables and Views - -The database exposes a one-to-many relationship between two main tables: **dept** (Departments) and **emp** (Employees). These tables have been used way back by Oracle, in its original *scott/tiger* sample (*scott* username/schema, with *tiger* password). - -Each employee - except for KING, the president - has one manager. This is implemented as a reflexive one-to-one/zero relationship. The **Managers** view shows all pairs of Manager-Employee names. - -A small **proj** (Projects) table has been added for time-series and Gantt diagram testing. One employee may be involved in one or more projects over time. - -Finally, one **datatypes** table (displayed collapsed here before) is specific to each database system, as it includes particular data types. One single record allows us to check if database types are processed correctly. - -## Table Data - -INSERT SQL statements to populate tables with data are all included in the creation scripts. We kept it small and simple, so there is only one single script per any database. Each table was populated with just a few rows, just to allow some simple or complex SQL queries to be run against: - -- **dept** - 4 entries, with departments 10, 20, 30 and 40. -- **emp** - 14 employee entries, as in the previous image, with lookup data. -- **proj** - 15 project entries, for dates between June-July 2005. -- **datatypes** - one single entry, to test database-specific data type values. diff --git a/ref/employees-test-database/employees-test-database.png b/ref/employees-test-database/employees-test-database.png deleted file mode 100644 index b785e32..0000000 Binary files a/ref/employees-test-database/employees-test-database.png and /dev/null differ diff --git a/ref/employees-test-database/scripts/sample_mssql_database.sql b/ref/employees-test-database/scripts/sample_mssql_database.sql deleted file mode 100644 index 7047139..0000000 --- a/ref/employees-test-database/scripts/sample_mssql_database.sql +++ /dev/null @@ -1,250 +0,0 @@ --- EmployeesQX - Microsoft SQL Server.sql --- Use SQL Server Management Studio or another SQL Server administrator. --- Create an EmployeesQX database, then run this script. - -IF NOT EXISTS (SELECT * -FROM sys.databases -WHERE name = 'EmployeesQX') -BEGIN - CREATE DATABASE EmployeesQX; -END -GO - -USE EmployeesQX -GO - -DROP TABLE IF EXISTS [datatypes]; -DROP VIEW IF EXISTS [Managers]; -DROP VIEW IF EXISTS [Materialized]; -DROP TABLE IF EXISTS [proj]; -DROP TABLE IF EXISTS [emp]; -DROP TABLE IF EXISTS [dept]; -GO - -CREATE TABLE [dept] -( - [DEPTNO] integer NOT NULL, - [DNAME] varchar(20) NOT NULL, - [LOC] varchar(20) NOT NULL, - - PRIMARY KEY ([DEPTNO]) -); -GO - -INSERT INTO [dept] -VALUES - (10, 'ACCOUNTING', 'NEW YORK'); -INSERT INTO [dept] -VALUES - (20, 'RESEARCH', 'DALLAS'); -INSERT INTO [dept] -VALUES - (30, 'SALES', 'CHICAGO'); -INSERT INTO [dept] -VALUES - (40, 'OPERATIONS', 'BOSTON'); -GO - - -CREATE TABLE [emp] -( - [EMPNO] integer NOT NULL, - [ENAME] varchar(20) NOT NULL, - [JOB] varchar(20) NOT NULL, - [MGR] integer, - [HIREDATE] date NOT NULL, - [SAL] integer NOT NULL, - [COMM] integer, - [DEPTNO] integer NOT NULL, - - PRIMARY KEY ([EMPNO]), - CONSTRAINT [fk_MGR] FOREIGN KEY ([MGR]) REFERENCES [emp] ([EMPNO]) - ON DELETE NO ACTION - ON UPDATE NO ACTION, - CONSTRAINT [fk_DEPTNO] FOREIGN KEY ([DEPTNO]) REFERENCES [dept] ([DEPTNO]) - ON DELETE CASCADE - ON UPDATE NO ACTION -); -GO - -INSERT INTO [emp] -VALUES - (7839, 'KING', 'PRESIDENT', NULL, '1981-11-17', 5000, NULL, 10); -INSERT INTO [emp] -VALUES - (7698, 'BLAKE', 'MANAGER', 7839, '1981-05-01', 2850, NULL, 30); -INSERT INTO [emp] -VALUES - (7654, 'MARTIN', 'SALESMAN', 7698, '1981-09-28', 1250, 1400, 30); -INSERT INTO [emp] -VALUES - (7499, 'ALLEN', 'SALESMAN', 7698, '1981-02-20', 1600, 300, 30); -INSERT INTO [emp] -VALUES - (7521, 'WARD', 'SALESMAN', 7698, '1981-02-22', 1250, 500, 30); -INSERT INTO [emp] -VALUES - (7900, 'JAMES', 'CLERK', 7698, '1981-12-03', 950, NULL, 30); -INSERT INTO [emp] -VALUES - (7844, 'TURNER', 'SALESMAN', 7698, '1981-09-08', 1500, 0, 30); -INSERT INTO [emp] -VALUES - (7782, 'CLARK', 'MANAGER', 7839, '1981-06-09', 2450, NULL, 10); -INSERT INTO [emp] -VALUES - (7934, 'MILLER', 'CLERK', 7782, '1982-01-23', 1300, NULL, 10); -INSERT INTO [emp] -VALUES - (7566, 'JONES', 'MANAGER', 7839, '1981-04-02', 2975, NULL, 20); -INSERT INTO [emp] -VALUES - (7788, 'SCOTT', 'ANALYST', 7566, '1982-12-09', 3000, NULL, 20); -INSERT INTO [emp] -VALUES - (7876, 'ADAMS', 'CLERK', 7788, '1983-01-12', 1100, NULL, 20); -INSERT INTO [emp] -VALUES - (7902, 'FORD', 'ANALYST', 7566, '1981-12-03', 3000, NULL, 20); -INSERT INTO [emp] -VALUES - (7369, 'SMITH', 'CLERK', 7902, '1980-12-17', 800, NULL, 20); -GO - - -CREATE TABLE [proj] -( - [PROJID] integer NOT NULL, - [EMPNO] integer NOT NULL, - [STARTDATE] date NOT NULL, - [ENDDATE] date NOT NULL, - - PRIMARY KEY ([PROJID]), - CONSTRAINT [fk_PROJ] FOREIGN KEY ([EMPNO]) REFERENCES [emp] ([EMPNO]) - ON DELETE NO ACTION - ON UPDATE CASCADE -); -GO - -INSERT INTO [proj] -VALUES - (1, 7782, '2005-06-16', '2005-06-18'); -INSERT INTO [proj] -VALUES - (4, 7782, '2005-06-19', '2005-06-24'); -INSERT INTO [proj] -VALUES - (7, 7782, '2005-06-22', '2005-06-25'); -INSERT INTO [proj] -VALUES - (10, 7782, '2005-06-25', '2005-06-28'); -INSERT INTO [proj] -VALUES - (13, 7782, '2005-06-28', '2005-07-02'); -INSERT INTO [proj] -VALUES - (2, 7839, '2005-06-17', '2005-06-21'); -INSERT INTO [proj] -VALUES - (8, 7839, '2005-06-23', '2005-06-25'); -INSERT INTO [proj] -VALUES - (14, 7839, '2005-06-29', '2005-06-30'); -INSERT INTO [proj] -VALUES - (11, 7839, '2005-06-26', '2005-06-27'); -INSERT INTO [proj] -VALUES - (5, 7839, '2005-06-20', '2005-06-24'); -INSERT INTO [proj] -VALUES - (3, 7934, '2005-06-18', '2005-06-22'); -INSERT INTO [proj] -VALUES - (12, 7934, '2005-06-27', '2005-06-28'); -INSERT INTO [proj] -VALUES - (15, 7934, '2005-06-30', '2005-07-03'); -INSERT INTO [proj] -VALUES - (9, 7934, '2005-06-24', '2005-06-27'); -INSERT INTO [proj] -VALUES - (6, 7934, '2005-06-21', '2005-06-23'); -GO - - -CREATE VIEW [Managers] -AS - SELECT m.[ENAME] AS [Manager], e.[ENAME] AS [Employee] - FROM [dbo].[emp] AS e LEFT JOIN [dbo].[emp] AS m ON e.[MGR] = m.[EMPNO]; -GO - -CREATE VIEW [Materialized] -WITH - SCHEMABINDING -AS - SELECT m.[ENAME] AS [Manager] - FROM [dbo].[emp] AS m; -GO -CREATE UNIQUE CLUSTERED INDEX [IDX_Materialized] - ON [dbo].[Materialized] ([Manager]); -GO - - -CREATE TABLE [datatypes] -( - - [INT_] int NOT NULL DEFAULT 2312, - - [TINY_INT] tinyint DEFAULT 3, - [SMALL_INT] smallint DEFAULT 232, - [BIG_INT] bigint, - [BIT_] bit DEFAULT 0, - [DECIMAL_] decimal(18, 0), - [NUMERIC_] numeric(18, 2) DEFAULT 234234, - [FLOAT_] float DEFAULT 444.44, - [REAL_] real, - [SMALL_MONEY] smallmoney DEFAULT 11.23, - [MONEY_] money DEFAULT 22.33, - - [CHAR_] char(10) DEFAULT 'D', - [NCHAR_] nchar(10) DEFAULT N'D', - [VAR_CHAR] varchar(50) DEFAULT 'zxcxcvxvcxvxv', - [VAR_CHAR_MAX] varchar(max), - [NVARCHAR_] nvarchar(50) DEFAULT N'ASDASD', - [NVARCHAR_MAX] nvarchar(max), - [TEXT_] text, - [NTEXT_] ntext, - - [DATE_] date, - [SMALL_DT] smalldatetime, - [TIME_] time(7), - [TIMESTAMP_] timestamp, - [DATETIME_] datetime, - [DATETIME2_] datetime2(7) DEFAULT '2012-03-04', - [DATETIME_OFF] datetimeoffset(7), - - [BINARY_] binary(50), - [VARBIN] varbinary(50), - [VARBIN_MAX] varbinary(max), - [IMAGE_] image, - - [GEOGRAPHY_] geography, - [GEOMETRY_] geometry, - [HIER_ID] hierarchyid, - [SQL_VAR] sql_variant, - [UID_] uniqueidentifier, - [XML_] xml, - - PRIMARY KEY ([INT_]) -); -GO - -INSERT INTO [datatypes] - ([BIG_INT], [BIT_], [CHAR_], [DATE_], [DECIMAL_], [FLOAT_], [INT_], - [MONEY_], [NCHAR_], [REAL_], [SMALL_INT], [SMALL_MONEY], [TINY_INT], [VAR_CHAR], [VAR_CHAR_MAX]) -VALUES - (234, 0, 'abcdefghij', '2012-04-04', 12.34, 44.55, 12345, - 44.56, N'1234567890', 9999, 2, 22.50, 2, 'var char', 'var char max'); -GO diff --git a/ref/employees-test-database/scripts/sample_mysql_database.sql b/ref/employees-test-database/scripts/sample_mysql_database.sql deleted file mode 100644 index 75f7702..0000000 --- a/ref/employees-test-database/scripts/sample_mysql_database.sql +++ /dev/null @@ -1,158 +0,0 @@ --- EmployeesQX - MySQL.sql --- Use MySQL Workbench or another MySQL administrator. Run this script. --- It should create an EmployeesQX database schema. - -DROP SCHEMA IF EXISTS `EmployeesQX`; -CREATE SCHEMA `EmployeesQX`; -USE `EmployeesQX`; - -CREATE TABLE `dept` ( - `DEPTNO` integer NOT NULL COMMENT 'Department\'s identification number', - `DNAME` varchar(20) NOT NULL COMMENT 'Name of the current department', - `LOC` varchar(20) NOT NULL COMMENT 'Location of the current department', - - PRIMARY KEY (`DEPTNO`) -) COMMENT 'Company departments, with employees'; - -ALTER TABLE `dept` COMMENT 'All company\'s departments, with employees'; - -INSERT INTO `dept` VALUES (10, 'ACCOUNTING', 'NEW YORK'); -INSERT INTO `dept` VALUES (20, 'RESEARCH', 'DALLAS'); -INSERT INTO `dept` VALUES (30, 'SALES', 'CHICAGO'); -INSERT INTO `dept` VALUES (40, 'OPERATIONS', 'BOSTON'); - - -CREATE TABLE `emp` ( - `EMPNO` integer NOT NULL, - `ENAME` varchar(20) NOT NULL, - `JOB` varchar(20) NOT NULL, - `MGR` integer, - `HIREDATE` date NOT NULL, - `SAL` integer NOT NULL, - `COMM` integer, - `DEPTNO` integer NOT NULL, - - PRIMARY KEY (`EMPNO`), - CONSTRAINT `fk_MGR` FOREIGN KEY (`MGR`) REFERENCES `emp` (`EMPNO`) - ON DELETE SET NULL - ON UPDATE CASCADE, - CONSTRAINT `fk_DEPTNO` FOREIGN KEY (`DEPTNO`) REFERENCES `dept` (`DEPTNO`) - ON DELETE RESTRICT - ON UPDATE NO ACTION -); - -INSERT INTO `emp` VALUES (7839, 'KING', 'PRESIDENT', NULL, '1981-11-17', 5000, NULL, 10); -INSERT INTO `emp` VALUES (7698, 'BLAKE', 'MANAGER', 7839, '1981-05-01', 2850, NULL, 30); -INSERT INTO `emp` VALUES (7654, 'MARTIN', 'SALESMAN', 7698, '1981-09-28', 1250, 1400, 30); -INSERT INTO `emp` VALUES (7499, 'ALLEN', 'SALESMAN', 7698, '1981-02-20', 1600, 300, 30); -INSERT INTO `emp` VALUES (7521, 'WARD', 'SALESMAN', 7698, '1981-02-22', 1250, 500, 30); -INSERT INTO `emp` VALUES (7900, 'JAMES', 'CLERK', 7698, '1981-12-03', 950, NULL, 30); -INSERT INTO `emp` VALUES (7844, 'TURNER', 'SALESMAN', 7698, '1981-09-08', 1500, 0, 30); -INSERT INTO `emp` VALUES (7782, 'CLARK', 'MANAGER', 7839, '1981-06-09', 2450, NULL, 10); -INSERT INTO `emp` VALUES (7934, 'MILLER', 'CLERK', 7782, '1982-01-23', 1300, NULL, 10); -INSERT INTO `emp` VALUES (7566, 'JONES', 'MANAGER', 7839, '1981-04-02', 2975, NULL, 20); -INSERT INTO `emp` VALUES (7788, 'SCOTT', 'ANALYST', 7566, '1982-12-09', 3000, NULL, 20); -INSERT INTO `emp` VALUES (7876, 'ADAMS', 'CLERK', 7788, '1983-01-12', 1100, NULL, 20); -INSERT INTO `emp` VALUES (7902, 'FORD', 'ANALYST', 7566, '1981-12-03', 3000, NULL, 20); -INSERT INTO `emp` VALUES (7369, 'SMITH', 'CLERK', 7902, '1980-12-17', 800, NULL, 20); - - -CREATE TABLE `proj` ( - `PROJID` integer NOT NULL, - `EMPNO` integer NOT NULL, - `STARTDATE` date NOT NULL, - `ENDDATE` date NOT NULL, - - PRIMARY KEY (`PROJID`), - CONSTRAINT `fk_PROJ` FOREIGN KEY (`EMPNO`) REFERENCES `emp` (`EMPNO`) - ON DELETE NO ACTION - ON UPDATE CASCADE -); - -INSERT INTO `proj` VALUES (1, 7782, '2005-06-16', '2005-06-18'); -INSERT INTO `proj` VALUES (4, 7782, '2005-06-19', '2005-06-24'); -INSERT INTO `proj` VALUES (7, 7782, '2005-06-22', '2005-06-25'); -INSERT INTO `proj` VALUES (10, 7782, '2005-06-25', '2005-06-28'); -INSERT INTO `proj` VALUES (13, 7782, '2005-06-28', '2005-07-02'); -INSERT INTO `proj` VALUES (2, 7839, '2005-06-17', '2005-06-21'); -INSERT INTO `proj` VALUES (8, 7839, '2005-06-23', '2005-06-25'); -INSERT INTO `proj` VALUES (14, 7839, '2005-06-29', '2005-06-30'); -INSERT INTO `proj` VALUES (11, 7839, '2005-06-26', '2005-06-27'); -INSERT INTO `proj` VALUES (5, 7839, '2005-06-20', '2005-06-24'); -INSERT INTO `proj` VALUES (3, 7934, '2005-06-18', '2005-06-22'); -INSERT INTO `proj` VALUES (12, 7934, '2005-06-27', '2005-06-28'); -INSERT INTO `proj` VALUES (15, 7934, '2005-06-30', '2005-07-03'); -INSERT INTO `proj` VALUES (9, 7934, '2005-06-24', '2005-06-27'); -INSERT INTO `proj` VALUES (6, 7934, '2005-06-21', '2005-06-23'); - - -CREATE VIEW `Managers` AS -SELECT m.`ENAME` AS `Manager`, e.`ENAME` AS `Employee` -FROM `emp` AS e LEFT JOIN `emp` AS m ON e.`MGR` = m.`EMPNO` -ORDER BY m.`ENAME`, e.`ENAME`; - - -CREATE TABLE `datatypes` ( - - `INT_` int(11) NOT NULL DEFAULT 12, - `TINY_INT` tinyint DEFAULT 1, - `SMALL_INT` smallint DEFAULT 12, - `MEDIUM_INT` mediumint DEFAULT 123, - `BIG_INT` bigint DEFAULT 123456, - - `BIT_` bit, - `BOOL_` bool, - `BOOLEAN_` boolean, - - `DECIMAL_` decimal(10,2) DEFAULT 14.55, - `DEC_` dec(10,2) zerofill, - `FIXED_` fixed(10,2) unsigned, - `NUMERIC_` numeric(10,2) unsigned zerofill, - - `FLOAT_` float DEFAULT 0.5, - `DOUBLE_` double DEFAULT 0.3, - `DOUBLE_PRECISION` double precision(14, 5), - `REAL_` real(11, 3), - - `CHAR_` char(5) DEFAULT '12345', - `NATIONAL_CHAR` national char(5), - `N_CHAR` nchar(5), - `VAR_CHAR` varchar(50) DEFAULT 'abc', - `NATIONAL_VARCHAR` national varchar(50), - `N_VAR_CHAR` nvarchar(50), - - `TEXT_` text, - `TINY_TEXT` tinytext, - `MEDIUM_TEXT` mediumtext, - `LONG_TEXT` longtext, - - `DATE_` date DEFAULT NULL, - `TIME_` time DEFAULT NULL, - `DATE_TIME` datetime DEFAULT NULL, - `TIME_STAMP` timestamp NULL DEFAULT NULL, - `YEAR_4` year(4), - - `BINARY_` binary(10) DEFAULT NULL, - `VAR_BINARY` varbinary(20) DEFAULT '4564', - `TINY_BLOB` tinyblob, - `LONG_BLOB` longblob, - `MEDIUM_BLOB` mediumblob, - - `ENUM_` enum('aa','bb','cc') DEFAULT 'cc', - `SET_` set('a','b','c') DEFAULT 'b', - - `GEOMETRY_` geometry, - `POINT_` point DEFAULT NULL, - `LINE_STRING` linestring, - `POLYGON_` polygon DEFAULT NULL, - `GEOMETRY_COLLECTION` geometrycollection, - `MULTI_POINT` multipoint DEFAULT NULL, - `MULTI_LINE_STRING` multilinestring, - `MULTI_POLYGON` multipolygon DEFAULT NULL, - - PRIMARY KEY (`INT_`) -); - -INSERT INTO `datatypes` (`INT_`, `VAR_CHAR`, `DECIMAL_`, `BIG_INT`, `TINY_TEXT`, `MEDIUM_TEXT`, - `LONG_TEXT`, `CHAR_`, `DOUBLE_`, `FLOAT_`, `MEDIUM_INT`, `SMALL_INT`, `TINY_INT`) - VALUES (1, 'fssdf', 12.33, 34234234, '', '', '', 'sdsdf', 33.44, 22.33, 444, 33, 3); diff --git a/ref/employees-test-database/scripts/sample_postgresql_database.sql b/ref/employees-test-database/scripts/sample_postgresql_database.sql deleted file mode 100644 index dc052f3..0000000 --- a/ref/employees-test-database/scripts/sample_postgresql_database.sql +++ /dev/null @@ -1,207 +0,0 @@ --- EmployeesQX - PostgreSQL.sql, for v9.5+ --- Use pgAdmin or another PostgreSQL administrator. --- Create a public EmployeesQX database, then run this script. - --- Run the following to create the database, then --- manually reconnect to that new database to run this script ---DROP DATABASE IF EXISTS EmployeesQX; ---CREATE DATABASE EmployeesQX; - -DROP TABLE IF EXISTS "datatypes"; ---DROP TYPE "mood"; -DROP VIEW IF EXISTS "Managers"; -DROP MATERIALIZED VIEW IF EXISTS "Materialized"; -DROP TABLE IF EXISTS "proj"; -DROP TABLE IF EXISTS "emp"; -DROP TABLE IF EXISTS "dept"; - -CREATE TABLE "dept" ( - "DEPTNO" integer NOT NULL, - "DNAME" varchar(20) NOT NULL, - "LOC" varchar(20) NOT NULL, - - PRIMARY KEY ("DEPTNO") -); - -COMMENT ON TABLE "dept" IS 'All company''s departments, with employees'; -COMMENT ON COLUMN "dept"."DEPTNO" IS 'Department''s identification number'; -COMMENT ON COLUMN "dept"."DNAME" IS 'Name of the current department'; -COMMENT ON COLUMN "dept"."LOC" IS 'Location of the current department'; - -INSERT INTO "dept" VALUES (10, 'ACCOUNTING', 'NEW YORK'); -INSERT INTO "dept" VALUES (20, 'RESEARCH', 'DALLAS'); -INSERT INTO "dept" VALUES (30, 'SALES', 'CHICAGO'); -INSERT INTO "dept" VALUES (40, 'OPERATIONS', 'BOSTON'); - - -CREATE TABLE "emp" ( - "EMPNO" integer NOT NULL, - "ENAME" varchar(20) NOT NULL, - "JOB" varchar(20) NOT NULL, - "MGR" integer, - "HIREDATE" date NOT NULL, - "SAL" integer NOT NULL, - "COMM" integer, - "DEPTNO" integer NOT NULL, - - PRIMARY KEY ("EMPNO"), - CONSTRAINT "fk_MGR" FOREIGN KEY ("MGR") REFERENCES "emp" ("EMPNO") - ON DELETE SET NULL - ON UPDATE CASCADE, - CONSTRAINT "fk_DEPTNO" FOREIGN KEY ("DEPTNO") REFERENCES "dept" ("DEPTNO") - ON DELETE RESTRICT - ON UPDATE NO ACTION -); - -INSERT INTO "emp" VALUES (7839, 'KING', 'PRESIDENT', NULL, '1981-11-17', 5000, NULL, 10); -INSERT INTO "emp" VALUES (7698, 'BLAKE', 'MANAGER', 7839, '1981-05-01', 2850, NULL, 30); -INSERT INTO "emp" VALUES (7654, 'MARTIN', 'SALESMAN', 7698, '1981-09-28', 1250, 1400, 30); -INSERT INTO "emp" VALUES (7499, 'ALLEN', 'SALESMAN', 7698, '1981-02-20', 1600, 300, 30); -INSERT INTO "emp" VALUES (7521, 'WARD', 'SALESMAN', 7698, '1981-02-22', 1250, 500, 30); -INSERT INTO "emp" VALUES (7900, 'JAMES', 'CLERK', 7698, '1981-12-03', 950, NULL, 30); -INSERT INTO "emp" VALUES (7844, 'TURNER', 'SALESMAN', 7698, '1981-09-08', 1500, 0, 30); -INSERT INTO "emp" VALUES (7782, 'CLARK', 'MANAGER', 7839, '1981-06-09', 2450, NULL, 10); -INSERT INTO "emp" VALUES (7934, 'MILLER', 'CLERK', 7782, '1982-01-23', 1300, NULL, 10); -INSERT INTO "emp" VALUES (7566, 'JONES', 'MANAGER', 7839, '1981-04-02', 2975, NULL, 20); -INSERT INTO "emp" VALUES (7788, 'SCOTT', 'ANALYST', 7566, '1982-12-09', 3000, NULL, 20); -INSERT INTO "emp" VALUES (7876, 'ADAMS', 'CLERK', 7788, '1983-01-12', 1100, NULL, 20); -INSERT INTO "emp" VALUES (7902, 'FORD', 'ANALYST', 7566, '1981-12-03', 3000, NULL, 20); -INSERT INTO "emp" VALUES (7369, 'SMITH', 'CLERK', 7902, '1980-12-17', 800, NULL, 20); - - -CREATE TABLE "proj" ( - "PROJID" integer NOT NULL, - "EMPNO" integer NOT NULL, - "STARTDATE" date NOT NULL, - "ENDDATE" date NOT NULL, - - PRIMARY KEY ("PROJID"), - CONSTRAINT "fk_PROJ" FOREIGN KEY ("EMPNO") REFERENCES "emp" ("EMPNO") - ON DELETE NO ACTION - ON UPDATE CASCADE -); - -INSERT INTO "proj" VALUES (1, 7782, '2005-06-16', '2005-06-18'); -INSERT INTO "proj" VALUES (4, 7782, '2005-06-19', '2005-06-24'); -INSERT INTO "proj" VALUES (7, 7782, '2005-06-22', '2005-06-25'); -INSERT INTO "proj" VALUES (10, 7782, '2005-06-25', '2005-06-28'); -INSERT INTO "proj" VALUES (13, 7782, '2005-06-28', '2005-07-02'); -INSERT INTO "proj" VALUES (2, 7839, '2005-06-17', '2005-06-21'); -INSERT INTO "proj" VALUES (8, 7839, '2005-06-23', '2005-06-25'); -INSERT INTO "proj" VALUES (14, 7839, '2005-06-29', '2005-06-30'); -INSERT INTO "proj" VALUES (11, 7839, '2005-06-26', '2005-06-27'); -INSERT INTO "proj" VALUES (5, 7839, '2005-06-20', '2005-06-24'); -INSERT INTO "proj" VALUES (3, 7934, '2005-06-18', '2005-06-22'); -INSERT INTO "proj" VALUES (12, 7934, '2005-06-27', '2005-06-28'); -INSERT INTO "proj" VALUES (15, 7934, '2005-06-30', '2005-07-03'); -INSERT INTO "proj" VALUES (9, 7934, '2005-06-24', '2005-06-27'); -INSERT INTO "proj" VALUES (6, 7934, '2005-06-21', '2005-06-23'); - - -CREATE VIEW "Managers" - AS SELECT m."ENAME" AS "Manager", e."ENAME" AS "Employee" - FROM "emp" AS e LEFT JOIN "emp" AS m ON e."MGR" = m."EMPNO" - ORDER BY m."ENAME", e."ENAME"; - -COMMENT ON VIEW "Managers" IS 'Pairs of manager-subordinate names'; - -CREATE MATERIALIZED VIEW "Materialized" - AS SELECT m."ENAME" AS "Manager", e."ENAME" AS "Employee" - FROM "emp" AS e LEFT JOIN "emp" AS m ON e."MGR" = m."EMPNO" - ORDER BY m."ENAME", e."ENAME"; - - -CREATE TYPE "mood" AS ENUM ('sad', 'ok', 'happy'); -CREATE TABLE "datatypes" ( - "INTEGER_" integer NOT NULL, - "INT_" int, - "INT_4" int4, - "SMALL_INT" smallint DEFAULT 23, - "INT_2" int2, - "BIG_INT" bigint, - "INT_8" int8, - "SERIAL_" serial, - "SMALL_SERIAL" smallserial, - "SERIAL_2" serial2, - "BIG_SERIAL" bigserial, - "SERIAL_8" serial8, - "BIT_" bit(1), - "BIT_VARYING" bit varying(5), - "VAR_BIT" varbit(5), - "BOOLEAN_" boolean, - "BOOL_" bool, - "MONEY_" money, - "REAL_" real DEFAULT 45.55, - "FLOAT_4" float4, - "DOUBLE_PRECISION" double precision, - "FLOAT_8" float8, - "DECIMAL_" decimal(9,2), - "NUMERIC_" numeric(7,2), - "ENUM_" mood DEFAULT 'ok', - - "CHARACTER_" character(1), - "CHAR_" char, - "VARCHAR_" varchar(20), - "CHARACTER_VARYING" character varying(20), - "TEXT_" text, - - "DATE_" date, - "TIME_" time, - "TIME_WOTZ" time(6) without time zone, - "TIME_WTZ" time(6) with time zone, - "TIME_TZ" timetz, - "TIMESTAMP_" timestamp, - "TIMESTAMP_WTZ" timestamp(6) with time zone, - "TIMESTAMP_TZ" timestamptz, - --"ABS_TIME" abstime, - --"REL_TIME" reltime, - "INTERVAL_" interval(6), - - "INT4_RANGE" int4range, - "INT8_RANGE" int8range , - "NUM_RANGE" numrange, - "TS_RANGE" tsrange , - "TSTZ_RANGE" tstzrange , - "DATE_RANGE" daterange, - - "BYTEA_" bytea, - "TXID_SNAPSHOT_" txid_snapshot, - - "POINT_" point, - "LINE_" line, - "LSEG_" lseg, - "BOX_" box, - "PATH_" path, - "POLYGON_" polygon, - "CIRCLE_" circle, - - "TS_VECTOR" tsvector, - "TS_QUERY" tsquery, - - "CIDR_" cidr, - "INET_" inet, - "MACADDR_" macaddr, - - "UUID_" uuid, - "OID_" oid, - "CID_" cid, - "REG_PROC" regproc, - "REG_PROCEDURE" regprocedure, - "REG_OPER" regoper, - "REG_OPERATOR" regoperator, - "REG_CLASS" regclass, - "REG_TYPE" regtype, - - "JSON_" json, - "XML_" xml, - - "INT_ARRAY" int[], - "CHAR_ARRAY" char(1)[], - "VARCHAR_ARRAY" varchar(10)[], - - PRIMARY KEY ("INTEGER_") -); - -INSERT INTO "datatypes" ("INTEGER_", "CHAR_", "BIG_INT", "CHARACTER_", "CHARACTER_VARYING", - "DATE_", "DOUBLE_PRECISION", "MONEY_", "NUMERIC_", "REAL_", "SMALL_INT", "INT_ARRAY") - VALUES(55, 'A', 43543, 'c', 'asdas', '2016-01-02', 33.444, 33.44, 3587, 22.01, 12, array[3,7,8,11]); diff --git a/ref/employees-test-database/scripts/sample_sqlite_database.sql b/ref/employees-test-database/scripts/sample_sqlite_database.sql deleted file mode 100644 index b4d35cb..0000000 --- a/ref/employees-test-database/scripts/sample_sqlite_database.sql +++ /dev/null @@ -1,204 +0,0 @@ --- EmployeesQX - SQLite.sql --- Use DBeaver or another SQLite administrator. --- Create an EmployeesQX database, then run this script. --- You can also connect to the ready-to-use EmployeesQX.db file. - --- DROP TABLE "datatypes"; --- DROP VIEW "Managers"; --- DROP TABLE "proj"; --- DROP TABLE "emp"; --- DROP TABLE "dept"; - -CREATE TABLE "dept" -( - "DEPTNO" integer NOT NULL, - "DNAME" varchar(20) NOT NULL, - "LOC" varchar(20) NOT NULL, - - PRIMARY KEY ("DEPTNO") -); - -INSERT INTO "dept" -VALUES - (10, 'ACCOUNTING', 'NEW YORK'); -INSERT INTO "dept" -VALUES - (20, 'RESEARCH', 'DALLAS'); -INSERT INTO "dept" -VALUES - (30, 'SALES', 'CHICAGO'); -INSERT INTO "dept" -VALUES - (40, 'OPERATIONS', 'BOSTON'); - - -CREATE TABLE "emp" ( - "EMPNO" integer NOT NULL, - "ENAME" varchar(20) NOT NULL, - "JOB" varchar(20) NOT NULL, - "MGR" integer, - "HIREDATE" date NOT NULL, - "SAL" integer NOT NULL, - "COMM" integer, - "DEPTNO" integer NOT NULL, - - PRIMARY KEY ("EMPNO"), - CONSTRAINT "fk_MGR" FOREIGN KEY ("MGR") REFERENCES "emp" ("EMPNO") - ON DELETE SET NULL - ON UPDATE CASCADE, - CONSTRAINT "fk_DEPTNO" FOREIGN KEY ("DEPTNO") REFERENCES "dept" ("DEPTNO") - ON DELETE RESTRICT - ON -UPDATE NO ACTION -); - -INSERT INTO "emp" -VALUES - (7839, 'KING', 'PRESIDENT', NULL, '1981-11-17', 5000, NULL, 10); -INSERT INTO "emp" -VALUES - (7698, 'BLAKE', 'MANAGER', 7839, '1981-05-01', 2850, NULL, 30); -INSERT INTO "emp" -VALUES - (7654, 'MARTIN', 'SALESMAN', 7698, '1981-09-28', 1250, 1400, 30); -INSERT INTO "emp" -VALUES - (7499, 'ALLEN', 'SALESMAN', 7698, '1981-02-20', 1600, 300, 30); -INSERT INTO "emp" -VALUES - (7521, 'WARD', 'SALESMAN', 7698, '1981-02-22', 1250, 500, 30); -INSERT INTO "emp" -VALUES - (7900, 'JAMES', 'CLERK', 7698, '1981-12-03', 950, NULL, 30); -INSERT INTO "emp" -VALUES - (7844, 'TURNER', 'SALESMAN', 7698, '1981-09-08', 1500, 0, 30); -INSERT INTO "emp" -VALUES - (7782, 'CLARK', 'MANAGER', 7839, '1981-06-09', 2450, NULL, 10); -INSERT INTO "emp" -VALUES - (7934, 'MILLER', 'CLERK', 7782, '1982-01-23', 1300, NULL, 10); -INSERT INTO "emp" -VALUES - (7566, 'JONES', 'MANAGER', 7839, '1981-04-02', 2975, NULL, 20); -INSERT INTO "emp" -VALUES - (7788, 'SCOTT', 'ANALYST', 7566, '1982-12-09', 3000, NULL, 20); -INSERT INTO "emp" -VALUES - (7876, 'ADAMS', 'CLERK', 7788, '1983-01-12', 1100, NULL, 20); -INSERT INTO "emp" -VALUES - (7902, 'FORD', 'ANALYST', 7566, '1981-12-03', 3000, NULL, 20); -INSERT INTO "emp" -VALUES - (7369, 'SMITH', 'CLERK', 7902, '1980-12-17', 800, NULL, 20); - - -CREATE TABLE "proj" -( - "PROJID" integer NOT NULL, - "EMPNO" integer NOT NULL, - "STARTDATE" date NOT NULL, - "ENDDATE" date NOT NULL, - - PRIMARY KEY ("PROJID"), - CONSTRAINT "fk_PROJ" FOREIGN KEY ("EMPNO") REFERENCES "emp" ("EMPNO") - ON DELETE NO ACTION - ON UPDATE CASCADE -); - -INSERT INTO "proj" -VALUES - (1, 7782, '2005-06-16', '2005-06-18'); -INSERT INTO "proj" -VALUES - (4, 7782, '2005-06-19', '2005-06-24'); -INSERT INTO "proj" -VALUES - (7, 7782, '2005-06-22', '2005-06-25'); -INSERT INTO "proj" -VALUES - (10, 7782, '2005-06-25', '2005-06-28'); -INSERT INTO "proj" -VALUES - (13, 7782, '2005-06-28', '2005-07-02'); -INSERT INTO "proj" -VALUES - (2, 7839, '2005-06-17', '2005-06-21'); -INSERT INTO "proj" -VALUES - (8, 7839, '2005-06-23', '2005-06-25'); -INSERT INTO "proj" -VALUES - (14, 7839, '2005-06-29', '2005-06-30'); -INSERT INTO "proj" -VALUES - (11, 7839, '2005-06-26', '2005-06-27'); -INSERT INTO "proj" -VALUES - (5, 7839, '2005-06-20', '2005-06-24'); -INSERT INTO "proj" -VALUES - (3, 7934, '2005-06-18', '2005-06-22'); -INSERT INTO "proj" -VALUES - (12, 7934, '2005-06-27', '2005-06-28'); -INSERT INTO "proj" -VALUES - (15, 7934, '2005-06-30', '2005-07-03'); -INSERT INTO "proj" -VALUES - (9, 7934, '2005-06-24', '2005-06-27'); -INSERT INTO "proj" -VALUES - (6, 7934, '2005-06-21', '2005-06-23'); - - -CREATE VIEW "Managers" -AS - SELECT m."ENAME" AS "Manager", e."ENAME" AS "Employee" - FROM "emp" AS e LEFT JOIN "emp" AS m ON e."MGR" = m."EMPNO" - ORDER BY m."ENAME", e."ENAME"; - - -CREATE TABLE "datatypes" -( - - "INTEGER_" INTEGER NOT NULL DEFAULT 44, - "TINY_INT" TINYINT, - "MEDIUM_INT" MEDIUMINT, - "BIG_INT" BIGINT, - "U_BIG_INT" UNSIGNED - BIG INT, - "BOOLEAN_" BOOLEAN, - - "FLOAT_" FLOAT DEFAULT 44.55, - "REAL_" REAL DEFAULT 55.67, - "DOUBLE_PRECISION" DOUBLE PRECISION, - - "CHAR_" CHAR - (5) DEFAULT 'QWERT', - "NCHAR_" NCHAR - (5) DEFAULT 'QWERT', - "VARCHAR_" VARCHAR - (50) DEFAULT 'abc', - "NVARCHAR_" NVARCHAR - (50) DEFAULT 'abc', - "TEXT_" TEXT, - - "DATETIME_" DATETIME DEFAULT '2016-01-02', - "DATE_" DATE, - - "BLOB_" BLOB, - "CLOB_" CLOB, - - PRIMARY KEY - ("INTEGER_") -); - - INSERT INTO "datatypes" - ("CHAR_", "DATETIME_", "FLOAT_", "INTEGER_", "VARCHAR_", "REAL_") - VALUES - ('abcde', '2015-10-15', 22.33, 1234, 'abcdefg', 567.788); diff --git a/src/DapperMatic/DapperMatic.csproj b/src/DapperMatic/DapperMatic.csproj index e95752e..71fe0e0 100644 --- a/src/DapperMatic/DapperMatic.csproj +++ b/src/DapperMatic/DapperMatic.csproj @@ -19,6 +19,7 @@ + diff --git a/src/DapperMatic/DataAnnotations/DxCheckConstraintAttribute.cs b/src/DapperMatic/DataAnnotations/DxCheckConstraintAttribute.cs new file mode 100644 index 0000000..1be7d2a --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxCheckConstraintAttribute.cs @@ -0,0 +1,36 @@ +namespace DapperMatic.DataAnnotations; + +/// +/// Check Constraint Attribute +/// +/// +/// [DxCheckConstraint("Age > 18")] +/// public int Age { get; set; } +/// +[AttributeUsage( + AttributeTargets.Property | AttributeTargets.Class)] +public class DxCheckConstraintAttribute : Attribute +{ + public DxCheckConstraintAttribute(string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required", nameof(expression)); + + Expression = expression; + } + + public DxCheckConstraintAttribute(string constraintName, string expression) + { + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required", nameof(constraintName)); + + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required", nameof(expression)); + + ConstraintName = constraintName; + Expression = expression; + } + + public string? ConstraintName { get; } + public string Expression { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxColumnAttribute.cs b/src/DapperMatic/DataAnnotations/DxColumnAttribute.cs new file mode 100644 index 0000000..f60af8b --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxColumnAttribute.cs @@ -0,0 +1,68 @@ +using DapperMatic.Models; + +namespace DapperMatic.DataAnnotations; + +[AttributeUsage(AttributeTargets.Property)] +public class DxColumnAttribute : Attribute +{ + public DxColumnAttribute( + string columnName, + string? providerDataType = null, + int? length = null, + int? precision = null, + int? scale = null, + string? checkExpression = null, + string? defaultExpression = null, + bool isNullable = true, + bool isPrimaryKey = false, + bool isAutoIncrement = false, + bool isUnique = false, + bool isIndexed = false, + bool isForeignKey = false, + string? referencedTableName = null, + string? referencedColumnName = null, + DxForeignKeyAction? onDelete = null, + DxForeignKeyAction? onUpdate = null + ) + { + ColumnName = columnName; + ProviderDataType = providerDataType; + Length = length; + Precision = precision; + Scale = scale; + CheckExpression = checkExpression; + DefaultExpression = defaultExpression; + IsNullable = isNullable; + IsPrimaryKey = isPrimaryKey; + IsAutoIncrement = isAutoIncrement; + IsUnique = isUnique; + IsIndexed = isIndexed; + IsForeignKey = isForeignKey; + ReferencedTableName = referencedTableName; + ReferencedColumnName = referencedColumnName; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + public string? ColumnName { get; } + + /// + /// /// Format of provider data types: {mysql:varchar(255),sqlserver:nvarchar(255)} + /// + public string? ProviderDataType { get; } + public int? Length { get; } + public int? Precision { get; } + public int? Scale { get; } + public string? CheckExpression { get; } + public string? DefaultExpression { get; } + public bool IsNullable { get; } + public bool IsPrimaryKey { get; } + public bool IsAutoIncrement { get; } + public bool IsUnique { get; } + public bool IsIndexed { get; } + public bool IsForeignKey { get; } + public string? ReferencedTableName { get; } + public string? ReferencedColumnName { get; } + public DxForeignKeyAction? OnDelete { get; } + public DxForeignKeyAction? OnUpdate { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxDefaultConstraintAttribute.cs b/src/DapperMatic/DataAnnotations/DxDefaultConstraintAttribute.cs new file mode 100644 index 0000000..ed7ff3a --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxDefaultConstraintAttribute.cs @@ -0,0 +1,35 @@ +namespace DapperMatic.DataAnnotations; + +/// +/// Check Constraint Attribute +/// +/// +/// [DxDefaultConstraint("0")] +/// public int Age { get; set; } +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] +public class DxDefaultConstraintAttribute : Attribute +{ + public DxDefaultConstraintAttribute(string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required", nameof(expression)); + + Expression = expression; + } + + public DxDefaultConstraintAttribute(string constraintName, string expression) + { + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required", nameof(constraintName)); + + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required", nameof(expression)); + + ConstraintName = constraintName; + Expression = expression; + } + + public string? ConstraintName { get; } + public string Expression { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxForeignKeyConstraintAttribute.cs b/src/DapperMatic/DataAnnotations/DxForeignKeyConstraintAttribute.cs new file mode 100644 index 0000000..ef777a7 --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxForeignKeyConstraintAttribute.cs @@ -0,0 +1,143 @@ +using DapperMatic.Models; + +namespace DapperMatic.DataAnnotations; + +[AttributeUsage( + AttributeTargets.Property | AttributeTargets.Class, + AllowMultiple = true +)] +public class DxForeignKeyConstraintAttribute : Attribute +{ + /// + /// Use this on a class to define a foreign key constraint. Constructor for single-column foreign key constraint + /// + public DxForeignKeyConstraintAttribute( + string sourceColumnName, + Type referencedType, + string? referencedColumnName = null, + string? constraintName = null, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction + ) + { + ConstraintName = constraintName; + SourceColumnNames = [sourceColumnName]; + ReferencedType = referencedType; + ReferencedColumnNames = string.IsNullOrWhiteSpace(referencedColumnName) + ? null + : [referencedColumnName]; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + /// + /// Use this on a class to define a foreign key constraint. Constructor for multi-column foreign key constraint (composite keys) + /// + public DxForeignKeyConstraintAttribute( + string[] sourceColumnNames, + Type referencedType, + string[]? referencedColumnNames = null, + string? constraintName = null, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction + ) + { + ConstraintName = constraintName; + SourceColumnNames = sourceColumnNames; + ReferencedType = referencedType; + ReferencedColumnNames = + referencedColumnNames == null || referencedColumnNames.Length == 0 + ? null + : referencedColumnNames; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + /// + /// Use this on a class to define a foreign key constraint. Constructor for single-column foreign key constraint + /// + public DxForeignKeyConstraintAttribute( + string sourceColumnName, + string referencedTableName, + string referencedColumnName, + string? constraintName = null, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction + ) + { + ConstraintName = constraintName; + SourceColumnNames = [sourceColumnName]; + ReferencedTableName = referencedTableName; + ReferencedColumnNames = [referencedColumnName]; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + /// + /// Use this on a class to define a foreign key constraint. Constructor for multi-column foreign key constraint (composite keys) + /// + public DxForeignKeyConstraintAttribute( + string[] sourceColumnNames, + string referencedTableName, + string[] referencedColumnNames, + string? constraintName = null, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction + ) + { + ConstraintName = constraintName; + SourceColumnNames = sourceColumnNames; + ReferencedTableName = referencedTableName; + ReferencedColumnNames = referencedColumnNames; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + /// + /// Use this on a property to define a foreign key constraint + /// + public DxForeignKeyConstraintAttribute( + Type referencedType, + string? referencedColumnName = null, + string? constraintName = null, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction + ) + { + ConstraintName = constraintName; + ReferencedType = referencedType; + ReferencedColumnNames = string.IsNullOrWhiteSpace(referencedColumnName) + ? null + : [referencedColumnName]; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + /// + /// Use this on a property to define a foreign key constraint + /// + public DxForeignKeyConstraintAttribute( + string referencedTableName, + string? referencedColumnName = null, + string? constraintName = null, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction + ) + { + ConstraintName = constraintName; + ReferencedTableName = referencedTableName; + ReferencedColumnNames = string.IsNullOrWhiteSpace(referencedColumnName) + ? null + : [referencedColumnName]; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + public string? ConstraintName { get; } + public string[]? SourceColumnNames { get; } + public Type? ReferencedType { get; } + public string? ReferencedTableName { get; } + public string[]? ReferencedColumnNames { get; } + public DxForeignKeyAction? OnDelete { get; } + public DxForeignKeyAction? OnUpdate { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxIgnoreAttribute.cs b/src/DapperMatic/DataAnnotations/DxIgnoreAttribute.cs new file mode 100644 index 0000000..4401cde --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxIgnoreAttribute.cs @@ -0,0 +1,4 @@ +namespace DapperMatic.DataAnnotations; + +[AttributeUsage(AttributeTargets.Property)] +public class DxIgnoreAttribute : Attribute { } diff --git a/src/DapperMatic/DataAnnotations/DxIndexAttribute.cs b/src/DapperMatic/DataAnnotations/DxIndexAttribute.cs new file mode 100644 index 0000000..dcd04b4 --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxIndexAttribute.cs @@ -0,0 +1,40 @@ +using DapperMatic.Models; + +namespace DapperMatic.DataAnnotations; + +[AttributeUsage( + AttributeTargets.Property | AttributeTargets.Class, + AllowMultiple = true +)] +public class DxIndexAttribute : Attribute +{ + public DxIndexAttribute(string constraintName, bool isUnique, params string[] columnNames) + { + IndexName = constraintName; + IsUnique = isUnique; + Columns = columnNames.Select(columnName => new DxOrderedColumn(columnName)).ToArray(); + } + + public DxIndexAttribute(bool isUnique, params string[] columnNames) + { + IsUnique = isUnique; + Columns = columnNames.Select(columnName => new DxOrderedColumn(columnName)).ToArray(); + } + + public DxIndexAttribute(string constraintName, bool isUnique, params DxOrderedColumn[] columns) + { + IndexName = constraintName; + IsUnique = isUnique; + Columns = columns; + } + + public DxIndexAttribute(bool isUnique, params DxOrderedColumn[] columns) + { + IsUnique = isUnique; + Columns = columns; + } + + public string? IndexName { get; } + public bool IsUnique { get; } + public DxOrderedColumn[]? Columns { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxPrimaryKeyConstraintAttribute.cs b/src/DapperMatic/DataAnnotations/DxPrimaryKeyConstraintAttribute.cs new file mode 100644 index 0000000..ccf81cb --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxPrimaryKeyConstraintAttribute.cs @@ -0,0 +1,28 @@ +using DapperMatic.Models; + +namespace DapperMatic.DataAnnotations; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = true)] +public class DxPrimaryKeyConstraintAttribute : Attribute +{ + public DxPrimaryKeyConstraintAttribute() { } + + public DxPrimaryKeyConstraintAttribute(string constraintName) + { + ConstraintName = constraintName; + } + + public DxPrimaryKeyConstraintAttribute(string constraintName, params string[] columnNames) + { + ConstraintName = constraintName; + Columns = columnNames.Select(columnName => new DxOrderedColumn(columnName)).ToArray(); + } + + public DxPrimaryKeyConstraintAttribute(string[] columnNames) + { + Columns = columnNames.Select(columnName => new DxOrderedColumn(columnName)).ToArray(); + } + + public string? ConstraintName { get; } + public DxOrderedColumn[]? Columns { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxTableAttribute.cs b/src/DapperMatic/DataAnnotations/DxTableAttribute.cs new file mode 100644 index 0000000..775a363 --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxTableAttribute.cs @@ -0,0 +1,21 @@ +namespace DapperMatic.DataAnnotations; + +[AttributeUsage(AttributeTargets.Class)] +public class DxTableAttribute : Attribute +{ + public DxTableAttribute() { } + + public DxTableAttribute(string? tableName) + { + TableName = tableName; + } + + public DxTableAttribute(string? schemaName, string? tableName) + { + SchemaName = schemaName; + TableName = tableName; + } + + public string? SchemaName { get; } + public string? TableName { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxUniqueConstraintAttribute.cs b/src/DapperMatic/DataAnnotations/DxUniqueConstraintAttribute.cs new file mode 100644 index 0000000..1a1ae03 --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxUniqueConstraintAttribute.cs @@ -0,0 +1,35 @@ +using DapperMatic.Models; + +namespace DapperMatic.DataAnnotations; + +[AttributeUsage( + AttributeTargets.Property | AttributeTargets.Class, + AllowMultiple = true +)] +public class DxUniqueConstraintAttribute : Attribute +{ + public DxUniqueConstraintAttribute(string constraintName, params string[] columnNames) + { + ConstraintName = constraintName; + Columns = columnNames.Select(columnName => new DxOrderedColumn(columnName)).ToArray(); + } + + public DxUniqueConstraintAttribute(params string[] columnNames) + { + Columns = columnNames.Select(columnName => new DxOrderedColumn(columnName)).ToArray(); + } + + public DxUniqueConstraintAttribute(string constraintName, params DxOrderedColumn[] columns) + { + ConstraintName = constraintName; + Columns = columns; + } + + public DxUniqueConstraintAttribute(params DxOrderedColumn[] columns) + { + Columns = columns; + } + + public string? ConstraintName { get; } + public DxOrderedColumn[]? Columns { get; } +} diff --git a/src/DapperMatic/DataAnnotations/DxViewAttribute.cs b/src/DapperMatic/DataAnnotations/DxViewAttribute.cs new file mode 100644 index 0000000..99ef3d8 --- /dev/null +++ b/src/DapperMatic/DataAnnotations/DxViewAttribute.cs @@ -0,0 +1,33 @@ +namespace DapperMatic.DataAnnotations; + +[AttributeUsage(AttributeTargets.Class)] +public class DxViewAttribute : Attribute +{ + public DxViewAttribute() { } + + /// + /// A view definition as an attribute. + /// + /// The SQL definition for the view. Use '{0}' to represent the schema name. + public DxViewAttribute(string definition) + { + Definition = definition; + } + + /// + /// A view definition as an attribute. + /// + /// + /// + /// The SQL definition for the view. Use '{0}' to represent the schema name. + public DxViewAttribute(string? schemaName, string? viewName, string definition) + { + SchemaName = schemaName; + ViewName = viewName; + Definition = definition; + } + + public string? SchemaName { get; } + public string? ViewName { get; } + public string? Definition { get; } +} diff --git a/src/DapperMatic/DataTypeMap.cs b/src/DapperMatic/DataTypeMap.cs deleted file mode 100644 index 7abc919..0000000 --- a/src/DapperMatic/DataTypeMap.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DapperMatic; - -public class DataTypeMap -{ - public Type DotnetType { get; set; } = null!; - public string SqlType { get; set; } = null!; - public string? SqlTypeWithLength { get; set; } - public string? SqlTypeWithMaxLength { get; set; } - public string? SqlTypeWithPrecisionAndScale { get; set; } -} diff --git a/src/DapperMatic/DataTypeMapFactory.cs b/src/DapperMatic/DataTypeMapFactory.cs deleted file mode 100644 index 3327718..0000000 --- a/src/DapperMatic/DataTypeMapFactory.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Collections.Concurrent; - -namespace DapperMatic; - -public static class DataTypeMapFactory -{ - private static ConcurrentDictionary< - DatabaseTypes, - List - > _databaseTypeDataTypeMappings = new(); - - public static List GetDefaultDatabaseTypeDataTypeMap(DatabaseTypes databaseType) - { - return _databaseTypeDataTypeMappings.GetOrAdd( - databaseType, - dbt => - { - return dbt switch - { - DatabaseTypes.SqlServer => GetSqlServerDataTypeMap(), - DatabaseTypes.PostgreSql => GetPostgresqlDataTypeMap(), - DatabaseTypes.MySql => GetMySqlDataTypeMap(), - DatabaseTypes.Sqlite => GetSqliteDataTypeMap(), - _ => throw new NotSupportedException($"Database type {dbt} is not supported.") - }; - } - ); - } - - private static List GetSqliteDataTypeMap() - { - var types = new List - { - new DataTypeMap { DotnetType = typeof(string), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(Guid), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(int), SqlType = "INTEGER" }, - new DataTypeMap { DotnetType = typeof(long), SqlType = "INTEGER" }, - new DataTypeMap { DotnetType = typeof(float), SqlType = "REAL" }, - new DataTypeMap { DotnetType = typeof(double), SqlType = "REAL" }, - new DataTypeMap { DotnetType = typeof(decimal), SqlType = "NUMERIC" }, - new DataTypeMap { DotnetType = typeof(bool), SqlType = "INTEGER" }, - new DataTypeMap { DotnetType = typeof(DateTime), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(DateTimeOffset), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(byte[]), SqlType = "BLOB" }, - new DataTypeMap { DotnetType = typeof(Guid[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(int[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(long[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(double[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(decimal[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(string[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "TEXT" } - }; - - return UnionNullableValueTypes(types); - } - - private static List GetMySqlDataTypeMap() - { - // get the type map for MySql - var types = new List - { - new DataTypeMap - { - DotnetType = typeof(string), - SqlType = "VARCHAR(255)", - SqlTypeWithLength = "VARCHAR({0})", - SqlTypeWithMaxLength = "TEXT" - }, - new DataTypeMap { DotnetType = typeof(Guid), SqlType = "CHAR(36)" }, - new DataTypeMap { DotnetType = typeof(int), SqlType = "INT" }, - new DataTypeMap { DotnetType = typeof(long), SqlType = "BIGINT" }, - new DataTypeMap { DotnetType = typeof(float), SqlType = "FLOAT" }, - new DataTypeMap { DotnetType = typeof(double), SqlType = "DOUBLE" }, - new DataTypeMap { DotnetType = typeof(decimal), SqlType = "DECIMAL" }, - new DataTypeMap { DotnetType = typeof(bool), SqlType = "TINYINT" }, - new DataTypeMap { DotnetType = typeof(DateTime), SqlType = "DATETIME" }, - new DataTypeMap { DotnetType = typeof(DateTimeOffset), SqlType = "DATETIME" }, - new DataTypeMap { DotnetType = typeof(byte[]), SqlType = "BLOB" }, - new DataTypeMap { DotnetType = typeof(Guid[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(int[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(long[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(double[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(decimal[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(string[]), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "TEXT" } - }; - - return UnionNullableValueTypes(types); - } - - private static List GetPostgresqlDataTypeMap() - { - // get the type map for Postgresql - var types = new List - { - new DataTypeMap - { - DotnetType = typeof(string), - SqlType = "CHARACTER VARYING", - SqlTypeWithLength = "CHARACTER VARYING({0})", - SqlTypeWithMaxLength = "TEXT" - }, - new DataTypeMap { DotnetType = typeof(Guid), SqlType = "UUID" }, - new DataTypeMap { DotnetType = typeof(int), SqlType = "INTEGER" }, - new DataTypeMap { DotnetType = typeof(long), SqlType = "BIGINT" }, - new DataTypeMap { DotnetType = typeof(float), SqlType = "REAL" }, - new DataTypeMap { DotnetType = typeof(double), SqlType = "DOUBLE PRECISION" }, - new DataTypeMap { DotnetType = typeof(decimal), SqlType = "DECIMAL" }, - new DataTypeMap { DotnetType = typeof(bool), SqlType = "BOOLEAN" }, - new DataTypeMap { DotnetType = typeof(DateTime), SqlType = "TIMESTAMP" }, - new DataTypeMap - { - DotnetType = typeof(DateTimeOffset), - SqlType = "TIMESTAMP WITH TIME ZONE" - }, - new DataTypeMap { DotnetType = typeof(byte[]), SqlType = "BYTEA" }, - // new DataTypeMap { DotnetType = typeof(int[]), SqlType = "TEXT" }, - // new DataTypeMap { DotnetType = typeof(long[]), SqlType = "TEXT" }, - // new DataTypeMap { DotnetType = typeof(double[]), SqlType = "TEXT" }, - // new DataTypeMap { DotnetType = typeof(decimal[]), SqlType = "TEXT" }, - // new DataTypeMap { DotnetType = typeof(string[]), SqlType = "TEXT" }, - // new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "TEXT" }, - // new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "TEXT" }, - new DataTypeMap { DotnetType = typeof(Guid[]), SqlType = "UUID[]" }, - new DataTypeMap { DotnetType = typeof(int[]), SqlType = "INTEGER[]" }, - new DataTypeMap { DotnetType = typeof(long[]), SqlType = "BIGINT[]" }, - new DataTypeMap { DotnetType = typeof(double[]), SqlType = "DOUBLE PRECISION[]" }, - new DataTypeMap { DotnetType = typeof(decimal[]), SqlType = "DECIMAL[]" }, - new DataTypeMap { DotnetType = typeof(string[]), SqlType = "CHARACTER VARYING[]" }, - new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "JSONB" }, - new DataTypeMap { DotnetType = typeof(Dictionary), SqlType = "JSONB" } - }; - - return UnionNullableValueTypes(types); - } - - private static List GetSqlServerDataTypeMap() - { - // get the type map for SqlServer - var types = new List - { - new DataTypeMap - { - DotnetType = typeof(string), - SqlType = "NVARCHAR", - SqlTypeWithLength = "NVARCHAR({0})", - SqlTypeWithMaxLength = "NVARCHAR(MAX)" - }, - new DataTypeMap { DotnetType = typeof(Guid), SqlType = "UNIQUEIDENTIFIER" }, - new DataTypeMap { DotnetType = typeof(int), SqlType = "INT" }, - new DataTypeMap { DotnetType = typeof(long), SqlType = "BIGINT" }, - new DataTypeMap { DotnetType = typeof(float), SqlType = "REAL" }, - new DataTypeMap { DotnetType = typeof(double), SqlType = "FLOAT" }, - new DataTypeMap { DotnetType = typeof(decimal), SqlType = "DECIMAL" }, - new DataTypeMap { DotnetType = typeof(bool), SqlType = "BIT" }, - new DataTypeMap { DotnetType = typeof(DateTime), SqlType = "DATETIME2" }, - new DataTypeMap { DotnetType = typeof(DateTimeOffset), SqlType = "DATETIMEOFFSET" }, - new DataTypeMap { DotnetType = typeof(byte[]), SqlType = "VARBINARY" }, - new DataTypeMap { DotnetType = typeof(Guid[]), SqlType = "NVARCHAR(MAX)" }, - new DataTypeMap { DotnetType = typeof(int[]), SqlType = "NVARCHAR(MAX)" }, - new DataTypeMap { DotnetType = typeof(long[]), SqlType = "NVARCHAR(MAX)" }, - new DataTypeMap { DotnetType = typeof(double[]), SqlType = "NVARCHAR(MAX)" }, - new DataTypeMap { DotnetType = typeof(decimal[]), SqlType = "NVARCHAR(MAX)" }, - new DataTypeMap { DotnetType = typeof(string[]), SqlType = "NVARCHAR(MAX)" }, - new DataTypeMap - { - DotnetType = typeof(Dictionary), - SqlType = "NVARCHAR(MAX)" - }, - new DataTypeMap - { - DotnetType = typeof(Dictionary), - SqlType = "NVARCHAR(MAX)" - } - }; - - return UnionNullableValueTypes(types); - } - - private static List UnionNullableValueTypes(List types) - { - // add nullable version of all the value types - foreach (var type in types.ToArray()) - { - if (type.DotnetType.IsValueType) - { - types.Add( - new DataTypeMap - { - DotnetType = typeof(Nullable<>).MakeGenericType(type.DotnetType), - SqlType = type.SqlType, - SqlTypeWithLength = type.SqlTypeWithLength, - SqlTypeWithMaxLength = type.SqlTypeWithMaxLength, - SqlTypeWithPrecisionAndScale = type.SqlTypeWithPrecisionAndScale - } - ); - } - } - - return types; - } -} diff --git a/src/DapperMatic/DatabaseExtensionMethods.cs b/src/DapperMatic/DatabaseExtensionMethods.cs deleted file mode 100644 index 7859622..0000000 --- a/src/DapperMatic/DatabaseExtensionMethods.cs +++ /dev/null @@ -1,556 +0,0 @@ -using System.Collections.Concurrent; -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic; - -public static partial class DatabaseExtensionMethods -{ - public static string GetLastSql(this IDbConnection db) - { - return Database(db).GetLastSql(db); - } - - public static (string sql, object? parameters) GetLastSqlWithParams(this IDbConnection db) - { - return Database(db).GetLastSqlWithParams(db); - } - - public static async Task GetDatabaseVersionAsync( - this IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db).GetDatabaseVersionAsync(db, tx, cancellationToken); - } - - #region schemaName methods - public static async Task SupportsSchemasAsync( - this IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db).SupportsSchemasAsync(db, tx, cancellationToken); - } - - public static async Task SchemaExistsAsync( - this IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (!await Database(db).SupportsSchemasAsync(db, tx, cancellationToken)) - { - return false; - } - - return await Database(db).SchemaExistsAsync(db, schemaName, tx, cancellationToken); - } - - public static async Task> GetSchemaNamesAsync( - this IDbConnection db, - string? nameFilter = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (!await Database(db).SupportsSchemasAsync(db, tx, cancellationToken)) - { - return []; - } - - return await Database(db).GetSchemaNamesAsync(db, nameFilter, tx, cancellationToken); - } - - public static async Task CreateSchemaIfNotExistsAsync( - this IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (!await Database(db).SupportsSchemasAsync(db, tx, cancellationToken)) - { - return false; - } - - return await Database(db) - .CreateSchemaIfNotExistsAsync(db, schemaName, tx, cancellationToken); - } - - public static async Task DropSchemaIfExistsAsync( - this IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (!await Database(db).SupportsSchemasAsync(db, tx, cancellationToken)) - { - return false; - } - - return await Database(db).DropSchemaIfExistsAsync(db, schemaName, tx, cancellationToken); - } - - #endregion // schemaName methods - - #region table methods - - public static async Task TableExistsAsync( - this IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .TableExistsAsync(db, tableName, schemaName, tx, cancellationToken); - } - - public static async Task> GetTableNamesAsync( - this IDbConnection db, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db).GetTableNamesAsync(db, nameFilter, schemaName, tx, cancellationToken); - } - - public static async Task CreateTableIfNotExistsAsync( - this IDbConnection db, - string tableName, - string? schemaName = null, - string[]? primaryKeyColumnNames = null, - Type[]? primaryKeyDotnetTypes = null, - int?[]? primaryKeyColumnLengths = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .CreateTableIfNotExistsAsync( - db, - tableName, - schemaName, - primaryKeyColumnNames, - primaryKeyDotnetTypes, - primaryKeyColumnLengths, - tx, - cancellationToken - ); - } - - public static async Task DropTableIfExistsAsync( - this IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .DropTableIfExistsAsync(db, tableName, schemaName, tx, cancellationToken); - } - - #endregion // table methods - - #region columnName methods - - public static async Task ColumnExistsAsync( - this IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken); - } - - public static async Task> GetColumnNamesAsync( - this IDbConnection db, - string tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .GetColumnNamesAsync(db, tableName, nameFilter, schemaName, tx, cancellationToken); - } - - public static async Task CreateColumnIfNotExistsAsync( - this IDbConnection db, - string tableName, - string columnName, - Type dotnetType, - string? type = null, - int? length = null, - int? precision = null, - int? scale = null, - string? schemaName = null, - string? defaultValue = null, - bool nullable = true, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .CreateColumnIfNotExistsAsync( - db, - tableName, - columnName, - dotnetType, - type, - length, - precision, - scale, - schemaName, - defaultValue, - nullable, - unique, - tx, - cancellationToken - ); - } - - public static async Task DropColumnIfExistsAsync( - this IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .DropColumnIfExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken); - } - - #endregion // columnName methods - - #region indexName methods - - public static async Task IndexExistsAsync( - this IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken); - } - - public static async Task> GetIndexesAsync( - this IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .GetIndexesAsync(db, tableName, nameFilter, schemaName, tx, cancellationToken); - } - - public static async Task> GetIndexNamesAsync( - this IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .GetIndexNamesAsync(db, tableName, nameFilter, schemaName, tx, cancellationToken); - } - - public static async Task CreateIndexIfNotExistsAsync( - this IDbConnection db, - string tableName, - string indexName, - string[] columnNames, - string? schemaName = null, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .CreateIndexIfNotExistsAsync( - db, - tableName, - indexName, - columnNames, - schemaName, - unique, - tx, - cancellationToken - ); - } - - public static async Task DropIndexIfExistsAsync( - this IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .DropIndexIfExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken); - } - - #endregion // index methods - - #region foreign key methods - public static async Task SupportsNamedForeignKeysAsync( - this IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db).SupportsNamedForeignKeysAsync(db, tx, cancellationToken); - } - - public static async Task ForeignKeyExistsAsync( - this IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ); - } - - public static async Task> GetForeignKeysAsync( - this IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .GetForeignKeysAsync(db, tableName, nameFilter, schemaName, tx, cancellationToken); - } - - public static async Task> GetForeignKeyNamesAsync( - this IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .GetForeignKeyNamesAsync(db, tableName, nameFilter, schemaName, tx, cancellationToken); - } - - public static async Task CreateForeignKeyIfNotExistsAsync( - this IDbConnection db, - string tableName, - string columnName, - string foreignKey, - string referenceTable, - string referenceColumn, - string? schemaName = null, - string onDelete = "NO ACTION", - string onUpdate = "NO ACTION", - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .CreateForeignKeyIfNotExistsAsync( - db, - tableName, - columnName, - foreignKey, - referenceTable, - referenceColumn, - schemaName, - onDelete, - onUpdate, - tx, - cancellationToken - ); - } - - public static async Task DropForeignKeyIfExistsAsync( - this IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .DropForeignKeyIfExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ); - } - - #endregion // foreign key methods - - #region unique constraint methods - - public static async Task UniqueConstraintExistsAsync( - this IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ); - } - - public static async Task> GetUniqueConstraintNamesAsync( - this IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .GetUniqueConstraintNamesAsync( - db, - tableName, - nameFilter, - schemaName, - tx, - cancellationToken - ); - } - - public static async Task CreateUniqueConstraintIfNotExistsAsync( - this IDbConnection db, - string tableName, - string uniqueConstraintName, - string[] columnNames, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .CreateUniqueConstraintIfNotExistsAsync( - db, - tableName, - uniqueConstraintName, - columnNames, - schemaName, - tx, - cancellationToken - ); - } - - public static async Task DropUniqueConstraintIfExistsAsync( - this IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await Database(db) - .DropUniqueConstraintIfExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ); - } - - #endregion // unique constraint methods - - #region Private static methods - private static readonly ConcurrentDictionary _providerTypes = new(); - private static readonly ConcurrentDictionary _extensions = - new(); - - private static IDatabaseExtensions Database(IDbConnection db) - { - return GetDatabaseExtensions(GetDatabaseType(db)); - } - - public static DatabaseTypes GetDatabaseType(this IDbConnection db) - { - var dbType = db.GetType(); - if (_providerTypes.TryGetValue(dbType, out var provider)) - { - return provider; - } - return dbType.FullName!.ToDatabaseType(); - } - - private static IDatabaseExtensions GetDatabaseExtensions(DatabaseTypes provider) - { - return _extensions.GetOrAdd( - provider, - provider switch - { - DatabaseTypes.Sqlite => new Providers.Sqlite.SqliteExtensions(), - DatabaseTypes.SqlServer => new Providers.SqlServer.SqlServerExtensions(), - DatabaseTypes.MySql => new Providers.MySql.MySqlExtensions(), - DatabaseTypes.PostgreSql => new Providers.PostgreSql.PostgreSqlExtensions(), - _ => throw new NotSupportedException($"Provider {provider} is not supported.") - } - ); - } - #endregion // Private static methods -} diff --git a/src/DapperMatic/DatabaseTypes.cs b/src/DapperMatic/DatabaseTypes.cs deleted file mode 100644 index dd9be0d..0000000 --- a/src/DapperMatic/DatabaseTypes.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace DapperMatic; - -public enum DatabaseTypes -{ - Sqlite, - SqlServer, - MySql, - PostgreSql, -} - -public static class DatabaseTypeExtensions -{ - public static DatabaseTypes ToDatabaseType(this string provider) - { - if ( - string.IsNullOrWhiteSpace(provider) - || provider.Contains("sqlite", StringComparison.OrdinalIgnoreCase) - ) - return DatabaseTypes.Sqlite; - - if ( - provider.Contains("mysql", StringComparison.OrdinalIgnoreCase) - || provider.Contains("mariadb", StringComparison.OrdinalIgnoreCase) - ) - return DatabaseTypes.MySql; - - if ( - provider.Contains("postgres", StringComparison.OrdinalIgnoreCase) - || provider.Contains("npgsql", StringComparison.OrdinalIgnoreCase) - || provider.Contains("pg", StringComparison.OrdinalIgnoreCase) - ) - return DatabaseTypes.PostgreSql; - - if ( - provider.Contains("sqlserver", StringComparison.OrdinalIgnoreCase) - || provider.Contains("mssql", StringComparison.OrdinalIgnoreCase) - || provider.Contains("localdb", StringComparison.OrdinalIgnoreCase) - || provider.Contains("sqlclient", StringComparison.OrdinalIgnoreCase) - ) - return DatabaseTypes.SqlServer; - - throw new NotSupportedException($"Cache type {provider} is not supported."); - } -} diff --git a/src/DapperMatic/DbConnectionExtensions.cs b/src/DapperMatic/DbConnectionExtensions.cs new file mode 100644 index 0000000..5a37cdc --- /dev/null +++ b/src/DapperMatic/DbConnectionExtensions.cs @@ -0,0 +1,1777 @@ +using System.Data; +using System.Diagnostics.CodeAnalysis; +using DapperMatic.Interfaces; +using DapperMatic.Models; +using DapperMatic.Providers; + +namespace DapperMatic; + +[SuppressMessage("ReSharper", "UnusedMember.Global")] +[SuppressMessage("ReSharper", "UnusedMethodReturnValue.Global")] +public static class DbConnectionExtensions +{ + #region IDatabaseMethods + public static string GetLastSql(this IDbConnection db) + { + return Database(db).GetLastSql(db); + } + + public static (string sql, object? parameters) GetLastSqlWithParams(this IDbConnection db) + { + return Database(db).GetLastSqlWithParams(db); + } + + public static async Task GetDatabaseVersionAsync( + this IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db).GetDatabaseVersionAsync(db, tx, cancellationToken); + } + + public static IDbProviderTypeMap GetProviderTypeMap(this IDbConnection db) + { + return Database(db).ProviderTypeMap; + } + + public static DbProviderDotnetTypeDescriptor GetDotnetTypeFromSqlType( + this IDbConnection db, + string sqlType + ) + { + return Database(db).GetDotnetTypeFromSqlType(sqlType); + } + + public static string NormalizeName(this IDbConnection db, string name) + { + return Database(db).NormalizeName(name); + } + #endregion // IDatabaseMethods + + #region Private static methods + private static IDatabaseMethods Database(this IDbConnection db) + { + return DatabaseMethodsFactory.GetDatabaseMethods(db); + } + #endregion // Private static methods + + #region IDatabaseSchemaMethods + + public static bool SupportsSchemas(this IDbConnection db) + { + return Database(db).SupportsSchemas; + } + + public static string GetSchemaQualifiedTableName( + this IDbConnection db, + string? schemaName, + string tableName + ) + { + return Database(db).GetSchemaQualifiedIdentifierName(schemaName, tableName); + } + + public static async Task SupportsCheckConstraintsAsync( + this IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .SupportsCheckConstraintsAsync(db, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task SupportsOrderedKeysInConstraintsAsync( + this IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .SupportsOrderedKeysInConstraintsAsync(db, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateSchemaIfNotExistsAsync( + this IDbConnection db, + string schemaName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateSchemaIfNotExistsAsync(db, schemaName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task DoesSchemaExistAsync( + this IDbConnection db, + string schemaName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesSchemaExistAsync(db, schemaName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetSchemaNamesAsync( + this IDbConnection db, + string? schemaNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetSchemaNamesAsync(db, schemaNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task DropSchemaIfExistsAsync( + this IDbConnection db, + string schemaName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropSchemaIfExistsAsync(db, schemaName, tx, cancellationToken) + .ConfigureAwait(false); + } + #endregion // IDatabaseSchemaMethods + + #region IDatabaseTableMethods + + public static async Task DoesTableExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesTableExistAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateTableIfNotExistsAsync( + this IDbConnection db, + DxTable table, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateTableIfNotExistsAsync(db, table, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateTableIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + DxColumn[] columns, + DxPrimaryKeyConstraint? primaryKey = null, + DxCheckConstraint[]? checkConstraints = null, + DxDefaultConstraint[]? defaultConstraints = null, + DxUniqueConstraint[]? uniqueConstraints = null, + DxForeignKeyConstraint[]? foreignKeyConstraints = null, + DxIndex[]? indexes = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateTableIfNotExistsAsync( + db, + schemaName, + tableName, + columns, + primaryKey, + checkConstraints, + defaultConstraints, + uniqueConstraints, + foreignKeyConstraints, + indexes, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetTableAsync( + this IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetTablesAsync( + this IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetTablesAsync(db, schemaName, tableNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetTableNamesAsync( + this IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetTableNamesAsync(db, schemaName, tableNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task DropTableIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropTableIfExistsAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task RenameTableIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string newTableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .RenameTableIfExistsAsync( + db, + schemaName, + tableName, + newTableName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task TruncateTableIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .TruncateTableIfExistsAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + } + #endregion // IDatabaseTableMethods + + #region IDatabaseColumnMethods + + public static async Task DoesColumnExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesColumnExistAsync(db, schemaName, tableName, columnName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task GetColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetColumnAsync(db, schemaName, tableName, columnName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetColumnNamesAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? columnNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetColumnNamesAsync(db, schemaName, tableName, columnNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetColumnsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? columnNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetColumnsAsync(db, schemaName, tableName, columnNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateColumnIfNotExistsAsync( + this IDbConnection db, + DxColumn column, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateColumnIfNotExistsAsync(db, column, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateColumnIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + Type dotnetType, + string? providerDataType = null, + int? length = null, + int? precision = null, + int? scale = null, + string? checkExpression = null, + string? defaultExpression = null, + bool isNullable = true, + bool isPrimaryKey = false, + bool isAutoIncrement = false, + bool isUnique = false, + bool isIndexed = false, + bool isForeignKey = false, + string? referencedTableName = null, + string? referencedColumnName = null, + DxForeignKeyAction? onDelete = null, + DxForeignKeyAction? onUpdate = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateColumnIfNotExistsAsync( + db, + schemaName, + tableName, + columnName, + dotnetType, + providerDataType, + length, + precision, + scale, + checkExpression, + defaultExpression, + isNullable, + isPrimaryKey, + isAutoIncrement, + isUnique, + isIndexed, + isForeignKey, + referencedTableName, + referencedColumnName, + onDelete, + onUpdate, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropColumnIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropColumnIfExistsAsync(db, schemaName, tableName, columnName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task RenameColumnIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + string newColumnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .RenameColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + newColumnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + #endregion // IDatabaseColumnMethods + + #region IDatabaseCheckConstraintMethods + public static async Task DoesCheckConstraintExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesCheckConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DoesCheckConstraintExistOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesCheckConstraintExistOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task CreateCheckConstraintIfNotExistsAsync( + this IDbConnection db, + DxCheckConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateCheckConstraintIfNotExistsAsync(db, constraint, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateCheckConstraintIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateCheckConstraintIfNotExistsAsync( + db, + schemaName, + tableName, + columnName, + constraintName, + expression, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetCheckConstraintAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetCheckConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetCheckConstraintNameOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetCheckConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetCheckConstraintNamesAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetCheckConstraintNamesAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetCheckConstraintOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetCheckConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetCheckConstraintsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetCheckConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropCheckConstraintIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropCheckConstraintIfExistsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropCheckConstraintOnColumnIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropCheckConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + #endregion // IDatabaseCheckConstraintMethods + + #region IDatabaseDefaultConstraintMethods + + public static async Task DoesDefaultConstraintExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesDefaultConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DoesDefaultConstraintExistOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesDefaultConstraintExistOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task CreateDefaultConstraintIfNotExistsAsync( + this IDbConnection db, + DxDefaultConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateDefaultConstraintIfNotExistsAsync(db, constraint, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateDefaultConstraintIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateDefaultConstraintIfNotExistsAsync( + db, + schemaName, + tableName, + columnName, + constraintName, + expression, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetDefaultConstraintAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetDefaultConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetDefaultConstraintNameOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetDefaultConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetDefaultConstraintNamesAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetDefaultConstraintNamesAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetDefaultConstraintOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetDefaultConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetDefaultConstraintsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetDefaultConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropDefaultConstraintIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropDefaultConstraintIfExistsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropDefaultConstraintOnColumnIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropDefaultConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + #endregion // IDatabaseDefaultConstraintMethods + + #region IDatabaseForeignKeyConstraintMethods + + public static async Task DoesForeignKeyConstraintExistOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesForeignKeyConstraintExistOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DoesForeignKeyConstraintExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesForeignKeyConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task CreateForeignKeyConstraintIfNotExistsAsync( + this IDbConnection db, + DxForeignKeyConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateForeignKeyConstraintIfNotExistsAsync(db, constraint, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateForeignKeyConstraintIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] sourceColumns, + string referencedTableName, + DxOrderedColumn[] referencedColumns, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateForeignKeyConstraintIfNotExistsAsync( + db, + schemaName, + tableName, + constraintName, + sourceColumns, + referencedTableName, + referencedColumns, + onDelete, + onUpdate, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetForeignKeyConstraintOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetForeignKeyConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetForeignKeyConstraintAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetForeignKeyConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetForeignKeyConstraintsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetForeignKeyConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetForeignKeyConstraintNameOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetForeignKeyConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetForeignKeyConstraintNamesAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetForeignKeyConstraintNamesAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropForeignKeyConstraintOnColumnIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropForeignKeyConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropForeignKeyConstraintIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropForeignKeyConstraintIfExistsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + #endregion // IDatabaseForeignKeyConstraintMethods + + #region IDatabaseIndexMethods + + public static async Task DoesIndexExistOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesIndexExistOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DoesIndexExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesIndexExistAsync(db, schemaName, tableName, indexName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateIndexIfNotExistsAsync( + this IDbConnection db, + DxIndex constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateIndexIfNotExistsAsync(db, constraint, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateIndexIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string indexName, + DxOrderedColumn[] columns, + bool isUnique = false, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateIndexIfNotExistsAsync( + db, + schemaName, + tableName, + indexName, + columns, + isUnique, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetIndexesOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetIndexesOnColumnAsync(db, schemaName, tableName, columnName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task GetIndexAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetIndexAsync(db, schemaName, tableName, indexName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetIndexesAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetIndexesAsync(db, schemaName, tableName, indexNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetIndexNamesOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetIndexNamesOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetIndexNamesAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetIndexNamesAsync(db, schemaName, tableName, indexNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task DropIndexesOnColumnIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropIndexesOnColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropIndexIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropIndexIfExistsAsync(db, schemaName, tableName, indexName, tx, cancellationToken) + .ConfigureAwait(false); + } + #endregion // IDatabaseIndexMethods + + #region IDatabaseUniqueConstraintMethods + + public static async Task DoesUniqueConstraintExistOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesUniqueConstraintExistOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DoesUniqueConstraintExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesUniqueConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task CreateUniqueConstraintIfNotExistsAsync( + this IDbConnection db, + DxUniqueConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateUniqueConstraintIfNotExistsAsync(db, constraint, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateUniqueConstraintIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateUniqueConstraintIfNotExistsAsync( + db, + schemaName, + tableName, + constraintName, + columns, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetUniqueConstraintOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetUniqueConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetUniqueConstraintAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetUniqueConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetUniqueConstraintsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetUniqueConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetUniqueConstraintNameOnColumnAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetUniqueConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task> GetUniqueConstraintNamesAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetUniqueConstraintNamesAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropUniqueConstraintOnColumnIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropUniqueConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task DropUniqueConstraintIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropUniqueConstraintIfExistsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + #endregion // IDatabaseUniqueConstraintMethods + + #region IDatabasePrimaryKeyConstraintMethods + + public static async Task DoesPrimaryKeyConstraintExistAsync( + this IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesPrimaryKeyConstraintExistAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreatePrimaryKeyConstraintIfNotExistsAsync( + this IDbConnection db, + DxPrimaryKeyConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreatePrimaryKeyConstraintIfNotExistsAsync(db, constraint, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreatePrimaryKeyConstraintIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreatePrimaryKeyConstraintIfNotExistsAsync( + db, + schemaName, + tableName, + constraintName, + columns, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetPrimaryKeyConstraintAsync( + this IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetPrimaryKeyConstraintAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task DropPrimaryKeyConstraintIfExistsAsync( + this IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropPrimaryKeyConstraintIfExistsAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + } + #endregion // IDatabasePrimaryKeyConstraintMethods + + #region IDatabaseViewMethods + public static async Task DoesViewExistAsync( + this IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DoesViewExistAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateViewIfNotExistsAsync( + this IDbConnection db, + DxView view, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateViewIfNotExistsAsync(db, view, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task CreateViewIfNotExistsAsync( + this IDbConnection db, + string? schemaName, + string viewName, + string viewDefinition, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .CreateViewIfNotExistsAsync( + db, + schemaName, + viewName, + viewDefinition, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task UpdateViewIfExistsAsync( + this IDbConnection db, + string? schemaName, + string viewName, + string viewDefinition, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await db.DropViewIfExistsAsync(schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + return await db.CreateViewIfNotExistsAsync( + schemaName, + viewName, + viewDefinition, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public static async Task GetViewAsync( + this IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetViewAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetViewsAsync( + this IDbConnection db, + string? schemaName, + string? viewNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetViewsAsync(db, schemaName, viewNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task> GetViewNamesAsync( + this IDbConnection db, + string? schemaName, + string? viewNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .GetViewNamesAsync(db, schemaName, viewNameFilter, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task DropViewIfExistsAsync( + this IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .DropViewIfExistsAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false); + } + + public static async Task RenameViewIfExistsAsync( + this IDbConnection db, + string? schemaName, + string viewName, + string newViewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await Database(db) + .RenameViewIfExistsAsync(db, schemaName, viewName, newViewName, tx, cancellationToken) + .ConfigureAwait(false); + } + #endregion // IDatabaseViewMethods +} diff --git a/src/DapperMatic/DbProviderSqlType.cs b/src/DapperMatic/DbProviderSqlType.cs new file mode 100644 index 0000000..78575fe --- /dev/null +++ b/src/DapperMatic/DbProviderSqlType.cs @@ -0,0 +1,74 @@ +namespace DapperMatic.Providers; + +/// +/// The provider SQL type. +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +public class DbProviderSqlType( + DbProviderSqlTypeAffinity affinity, + string name, + Type? recommendedDotnetType = null, + string? aliasOf = null, + string? formatWithLength = null, + string? formatWithPrecision = null, + string? formatWithPrecisionAndScale = null, + int? defaultLength = null, + int? defaultPrecision = null, + int? defaultScale = null, + bool canUseToAutoIncrement = false, + bool autoIncrementsAutomatically = false, + double? minValue = null, + double? maxValue = null, + bool includesTimeZone = false, + bool isDateOnly = false, + bool isTimeOnly = false, + bool isYearOnly = false, + bool isFixedLength = false, + bool isGuidOnly = false, + bool isUnicode = false +) +{ + public DbProviderSqlTypeAffinity Affinity { get; init; } = affinity; + public string Name { get; init; } = name; + public Type? RecommendedDotnetType { get; init; } = recommendedDotnetType; + public string? AliasOf { get; set; } = aliasOf; + public string? FormatWithLength { get; init; } = formatWithLength; + public string? FormatWithPrecision { get; init; } = formatWithPrecision; + public string? FormatWithPrecisionAndScale { get; init; } = formatWithPrecisionAndScale; + public int? DefaultLength { get; set; } = defaultLength; + public int? DefaultPrecision { get; set; } = defaultPrecision; + public int? DefaultScale { get; set; } = defaultScale; + public bool CanUseToAutoIncrement { get; init; } = canUseToAutoIncrement; + public bool AutoIncrementsAutomatically { get; init; } = autoIncrementsAutomatically; + public double? MinValue { get; init; } = minValue; + public double? MaxValue { get; init; } = maxValue; + public bool IncludesTimeZone { get; init; } = includesTimeZone; + public bool IsDateOnly { get; init; } = isDateOnly; + public bool IsTimeOnly { get; init; } = isTimeOnly; + public bool IsYearOnly { get; init; } = isYearOnly; + public bool IsFixedLength { get; init; } = isFixedLength; + public bool IsGuidOnly { get; init; } = isGuidOnly; + public bool IsUnicode { get; set; } = isUnicode; +} + +public static class DbProviderSqlTypeExtensions +{ + public static bool SupportsLength(this DbProviderSqlType providerSqlType) => + !string.IsNullOrWhiteSpace(providerSqlType.FormatWithLength); + + public static bool SupportsPrecision(this DbProviderSqlType providerSqlType) => + !string.IsNullOrWhiteSpace(providerSqlType.FormatWithPrecision); + + public static bool SupportsPrecisionAndScale(this DbProviderSqlType providerSqlType) => + !string.IsNullOrWhiteSpace(providerSqlType.FormatWithPrecisionAndScale); +} diff --git a/src/DapperMatic/DbProviderSqlTypeAffinity.cs b/src/DapperMatic/DbProviderSqlTypeAffinity.cs new file mode 100644 index 0000000..d4c9802 --- /dev/null +++ b/src/DapperMatic/DbProviderSqlTypeAffinity.cs @@ -0,0 +1,14 @@ +namespace DapperMatic; + +public enum DbProviderSqlTypeAffinity +{ + Integer, + Real, + Boolean, + DateTime, + Text, + Binary, + Geometry, + RangeType, + Other +} diff --git a/src/DapperMatic/DbProviderType.cs b/src/DapperMatic/DbProviderType.cs new file mode 100644 index 0000000..8f41292 --- /dev/null +++ b/src/DapperMatic/DbProviderType.cs @@ -0,0 +1,60 @@ +using System.Collections.Concurrent; +using System.Data; + +namespace DapperMatic; + +public enum DbProviderType +{ + Sqlite, + SqlServer, + MySql, + PostgreSql +} + +public static class DbProviderTypeExtensions +{ + private static readonly ConcurrentDictionary ProviderTypes = new(); + + public static DbProviderType GetDbProviderType(this IDbConnection db) + { + var type = db.GetType(); + if (ProviderTypes.TryGetValue(type, out var dbType)) + { + return dbType; + } + + dbType = ToDbProviderType(type.FullName!); + ProviderTypes.TryAdd(type, dbType); + + return dbType; + } + + private static DbProviderType ToDbProviderType(string provider) + { + if (provider.Contains("sqlite", StringComparison.OrdinalIgnoreCase)) + return DbProviderType.Sqlite; + + if ( + provider.Contains("mysql", StringComparison.OrdinalIgnoreCase) + || provider.Contains("maria", StringComparison.OrdinalIgnoreCase) + ) + return DbProviderType.MySql; + + if ( + provider.Contains("postgres", StringComparison.OrdinalIgnoreCase) + || provider.Contains("npgsql", StringComparison.OrdinalIgnoreCase) + || provider.Contains("pg", StringComparison.OrdinalIgnoreCase) + ) + return DbProviderType.PostgreSql; + + if ( + provider.Contains("sqlserver", StringComparison.OrdinalIgnoreCase) + || provider.Contains("mssql", StringComparison.OrdinalIgnoreCase) + || provider.Contains("localdb", StringComparison.OrdinalIgnoreCase) + || provider.Contains("sqlclient", StringComparison.OrdinalIgnoreCase) + ) + return DbProviderType.SqlServer; + + throw new NotSupportedException($"Db type {provider} is not supported."); + } +} diff --git a/src/DapperMatic/ExtensionMethods.cs b/src/DapperMatic/ExtensionMethods.cs new file mode 100644 index 0000000..b5afe8f --- /dev/null +++ b/src/DapperMatic/ExtensionMethods.cs @@ -0,0 +1,308 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace DapperMatic; + +[SuppressMessage("ReSharper", "UnusedMember.Global")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public static partial class ExtensionMethods +{ + public static string GetFriendlyName(this Type type) + { + if (type == null) + return "(Unknown Type)"; + + if (!type.IsGenericType) + return type.Name; + + var genericTypeName = type.GetGenericTypeDefinition().Name; + var friendlyGenericTypeName = genericTypeName[..genericTypeName.LastIndexOf("`")]; + + var genericArguments = type.GetGenericArguments(); + var genericArgumentNames = genericArguments.Select(GetFriendlyName).ToArray(); + var genericTypeArgumentsString = string.Join(", ", genericArgumentNames); + + return $"{friendlyGenericTypeName}<{genericTypeArgumentsString}>"; + } + + public static TValue? GetFieldValue(this object instance, string name) + { + var type = instance.GetType(); + var field = type.GetFields( + BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance + ) + .FirstOrDefault(e => + typeof(TValue).IsAssignableFrom(e.FieldType) + && e.Name.Equals(name, StringComparison.OrdinalIgnoreCase) + ); + return (TValue?)field?.GetValue(instance) ?? default; + } + + public static TValue? GetPropertyValue(this object instance, string name) + { + var type = instance.GetType(); + var property = type.GetProperties( + BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance + ) + .FirstOrDefault(e => + typeof(TValue).IsAssignableFrom(e.PropertyType) + && e.Name.Equals(name, StringComparison.OrdinalIgnoreCase) + ); + return (TValue?)property?.GetValue(instance); + } + + public static bool TryGetFieldValue( + this object instance, + string name, + out TValue? value + ) + { + value = instance.GetFieldValue(name); + return value != null; + } + + public static bool TryGetPropertyValue( + this object instance, + string name, + out TValue? value + ) + { + value = instance.GetPropertyValue(name); + return value != null; + } + + [GeneratedRegex(@"\d+")] + private static partial Regex ExtractNumbersRegex(); + + public static int[] ExtractNumbers(this string input) + { + MatchCollection matches = ExtractNumbersRegex().Matches(input); + + var numbers = new List(); + foreach (Match match in matches) + { + if (int.TryParse(match.Value, out var number)) + numbers.Add(number); + } + + return [.. numbers]; + } + + public static string DiscardLengthPrecisionAndScaleFromSqlTypeName(this string sqlTypeName) + { + // extract the type name from the sql type name where a sqlTypeName might be "time(5, 2) without time zone" and the return value would be "time without time zone", + // it could also be "time ( 122, 2 ) without time zone" and the return value would be "time without time zone + var openIndex = sqlTypeName.IndexOf('('); + var closeIndex = sqlTypeName.IndexOf(')'); + var txt = ( + openIndex > 0 && closeIndex > 0 + ? sqlTypeName.Remove(openIndex, closeIndex - openIndex + 1) + : sqlTypeName + ).Trim(); + while (txt.Contains(" ")) + txt = txt.Replace(" ", " "); + return txt; + } + + public static string ToQuotedIdentifier( + this string prefix, + char[] quoteChar, + params string[] identifierSegments + ) + { + return quoteChar.Length switch + { + 0 => prefix.ToRawIdentifier(identifierSegments), + 1 => quoteChar[0] + prefix.ToRawIdentifier(identifierSegments) + quoteChar[0], + _ => quoteChar[0] + prefix.ToRawIdentifier(identifierSegments) + quoteChar[1] + }; + } + + /// + /// Returns a string as a valid unquoted raw SQL identifier. + /// All non-alphanumeric characters are removed from segment string values. + /// The segment string values are joined using an underscore. + /// + public static string ToRawIdentifier(this string prefix, params string[] identifierSegments) + { + var sb = new StringBuilder(prefix.ToAlphaNumeric("_")); + foreach (var segment in identifierSegments) + { + if (string.IsNullOrWhiteSpace(segment)) + continue; + + sb.Append('_'); + sb.Append(segment.ToAlphaNumeric("_")); + } + return sb.ToString().Trim('_'); + } + + public static bool IsAlphaNumeric(this char c) + { + return c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9'; + } + + public static bool IsAlpha(this char c) + { + return c is >= 'A' and <= 'Z' or >= 'a' and <= 'z'; + } + + public static string ToAlphaNumeric(this string text, string additionalAllowedCharacters = "") + { + // using Regex + // var rgx = new Regex("[^a-zA-Z0-9_.]"); + // return rgx.Replace(text, ""); + + // using IsLetterOrDigit (faster, BUT allows non-ASCII letters and digits) + // char[] allowed = additionalAllowedCharacters.ToCharArray(); + // char[] arr = text.Where(c => + // char.IsLetterOrDigit(c) || char.IsWhiteSpace(c) || allowed.Contains(c) + // ) + // .ToArray(); + // return new string(arr); + + return string.Concat( + Array.FindAll( + text.ToCharArray(), + c => c.IsAlphaNumeric() || additionalAllowedCharacters.Contains(c) + ) + ); + } + + public static string ToAlpha(this string text, string additionalAllowedCharacters = "") + { + return string.Concat( + Array.FindAll( + text.ToCharArray(), + c => c.IsAlpha() || additionalAllowedCharacters.Contains(c) + ) + ); + } + + public static bool EqualsAlpha( + this string text, + string textToDetermineMatch, + bool ignoreCase = true, + string additionalAllowedCharacters = "" + ) + { + return text.ToAlpha(additionalAllowedCharacters) + .Equals( + textToDetermineMatch.ToAlpha(additionalAllowedCharacters), + ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal + ); + } + + public static bool EqualsAlphaNumeric( + this string text, + string textToDetermineMatch, + bool ignoreCase = true, + string additionalAllowedCharacters = "" + ) + { + return text.ToAlphaNumeric(additionalAllowedCharacters) + .Equals( + textToDetermineMatch.ToAlphaNumeric(additionalAllowedCharacters), + ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal + ); + } + + /// + /// Converts a string to snake case, e.g. "MyProperty" becomes "my_property", and "IOas_d_DEfH" becomes "i_oas_d_d_ef_h". + /// + public static string ToSnakeCase(this string str) + { + str = str.Trim(); + var sb = new StringBuilder(); + for (var i = 0; i < str.Length; i++) + { + var c = str[i]; + if ( + i > 0 + && char.IsUpper(c) + && (char.IsLower(str[i - 1]) || (i < str.Length - 1 && char.IsLower(str[i + 1]))) + ) + { + sb.Append('_'); + } + sb.Append(char.ToLowerInvariant(c)); + } + return sb.ToString(); + } + + // create a wildcard pattern matching algorithm that accepts wildcards (*) and questions (?) + // for example: + // *abc* should match abc, abcd, abcdabc, etc. + // a?c should match ac, abc, abcc, etc. + // the method should take in a string and a wildcard pattern and return true/false whether the string + // matches the wildcard pattern. + /// + /// Wildcard pattern matching algorithm. Accepts wildcards (*) and question marks (?) + /// + /// A string + /// Wildcard pattern string + /// Ignore the case of the string when evaluating a match + /// bool + public static bool IsWildcardPatternMatch( + this string text, + string wildcardPattern, + bool ignoreCase = true + ) + { + if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(wildcardPattern)) + return false; + + if (ignoreCase) + { + text = text.ToLowerInvariant(); + wildcardPattern = wildcardPattern.ToLowerInvariant(); + } + + var inputIndex = 0; + var patternIndex = 0; + var inputLength = text.Length; + var patternLength = wildcardPattern.Length; + var lastWildcardIndex = -1; + var lastInputIndex = -1; + + while (inputIndex < inputLength) + { + if ( + patternIndex < patternLength + && ( + wildcardPattern[patternIndex] == '?' + || wildcardPattern[patternIndex] == text[inputIndex] + ) + ) + { + patternIndex++; + inputIndex++; + } + else if (patternIndex < patternLength && wildcardPattern[patternIndex] == '*') + { + lastWildcardIndex = patternIndex; + lastInputIndex = inputIndex; + patternIndex++; + } + else if (lastWildcardIndex != -1) + { + patternIndex = lastWildcardIndex + 1; + lastInputIndex++; + inputIndex = lastInputIndex; + } + else + { + return false; + } + } + + while (patternIndex < patternLength && wildcardPattern[patternIndex] == '*') + { + patternIndex++; + } + + return patternIndex == patternLength; + } +} diff --git a/src/DapperMatic/Interfaces/IDatabaseCheckConstraintMethods.cs b/src/DapperMatic/Interfaces/IDatabaseCheckConstraintMethods.cs new file mode 100644 index 0000000..f022035 --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseCheckConstraintMethods.cs @@ -0,0 +1,106 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseCheckConstraintMethods +{ + Task CreateCheckConstraintIfNotExistsAsync( + IDbConnection db, + DxCheckConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateCheckConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesCheckConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesCheckConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetCheckConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetCheckConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetCheckConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetCheckConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetCheckConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropCheckConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropCheckConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseColumnMethods.cs b/src/DapperMatic/Interfaces/IDatabaseColumnMethods.cs new file mode 100644 index 0000000..914f9a1 --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseColumnMethods.cs @@ -0,0 +1,95 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseColumnMethods +{ + Task CreateColumnIfNotExistsAsync( + IDbConnection db, + DxColumn column, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateColumnIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + Type dotnetType, + string? providerDataType = null, + int? length = null, + int? precision = null, + int? scale = null, + string? checkExpression = null, + string? defaultExpression = null, + bool isNullable = true, + bool isPrimaryKey = false, + bool isAutoIncrement = false, + bool isUnique = false, + bool isIndexed = false, + bool isForeignKey = false, + string? referencedTableName = null, + string? referencedColumnName = null, + DxForeignKeyAction? onDelete = null, + DxForeignKeyAction? onUpdate = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesColumnExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetColumnsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? columnNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetColumnNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? columnNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task RenameColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + string newColumnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseDefaultConstraintMethods.cs b/src/DapperMatic/Interfaces/IDatabaseDefaultConstraintMethods.cs new file mode 100644 index 0000000..c850703 --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseDefaultConstraintMethods.cs @@ -0,0 +1,106 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseDefaultConstraintMethods +{ + Task CreateDefaultConstraintIfNotExistsAsync( + IDbConnection db, + DxDefaultConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateDefaultConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesDefaultConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesDefaultConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetDefaultConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetDefaultConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetDefaultConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetDefaultConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetDefaultConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropDefaultConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropDefaultConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseExtensions.ColumnMethods.cs b/src/DapperMatic/Interfaces/IDatabaseExtensions.ColumnMethods.cs deleted file mode 100644 index bcc49e1..0000000 --- a/src/DapperMatic/Interfaces/IDatabaseExtensions.ColumnMethods.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Data; - -namespace DapperMatic; - -public partial interface IDatabaseExtensions -{ - Task ColumnExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task> GetColumnNamesAsync( - IDbConnection db, - string tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task CreateColumnIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - Type dotnetType, - // TPropertyType will determine the columnName type at runtime, - // e.g. string will be NVARCHAR or TEXT depending on length, int will be INTEGER, etc. - // However, the type can be overridden by specifying the type parameter. - string? type = null, - int? length = null, - int? precision = null, - int? scale = null, - string? schemaName = null, - string? defaultValue = null, - bool nullable = true, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task DropColumnIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); -} diff --git a/src/DapperMatic/Interfaces/IDatabaseExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Interfaces/IDatabaseExtensions.ForeignKeyMethods.cs deleted file mode 100644 index 1f47e7a..0000000 --- a/src/DapperMatic/Interfaces/IDatabaseExtensions.ForeignKeyMethods.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic; - -public partial interface IDatabaseExtensions -{ - Task SupportsNamedForeignKeysAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - - Task ForeignKeyExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task> GetForeignKeysAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task> GetForeignKeyNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task CreateForeignKeyIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string foreignKey, - string referenceTable, - string referenceColumn, - string? schemaName = null, - string onDelete = "NO ACTION", - string onUpdate = "NO ACTION", - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task DropForeignKeyIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); -} diff --git a/src/DapperMatic/Interfaces/IDatabaseExtensions.TableMethods.cs b/src/DapperMatic/Interfaces/IDatabaseExtensions.TableMethods.cs deleted file mode 100644 index b1d8edb..0000000 --- a/src/DapperMatic/Interfaces/IDatabaseExtensions.TableMethods.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Data; - -namespace DapperMatic; - -public partial interface IDatabaseExtensions -{ - Task TableExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task> GetTableNamesAsync( - IDbConnection db, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - - /// - /// Creates a table if it does not exist. - /// - /// - /// - /// - /// The type of the primary key columnName (int, short, long, Guid, string are the only valid types available) - /// If the TPrimaryKeyDotnetType is of type string, this represents the length of the primary key columnName - /// - /// - Task CreateTableIfNotExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - string[]? primaryKeyColumnNames = null, - Type[]? primaryKeyDotnetTypes = null, - int?[]? primaryKeyColumnLengths = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task DropTableIfExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); -} diff --git a/src/DapperMatic/Interfaces/IDatabaseExtensions.UniqueConstraintMethods.cs b/src/DapperMatic/Interfaces/IDatabaseExtensions.UniqueConstraintMethods.cs deleted file mode 100644 index 9d99524..0000000 --- a/src/DapperMatic/Interfaces/IDatabaseExtensions.UniqueConstraintMethods.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Data; - -namespace DapperMatic; - -public partial interface IDatabaseExtensions -{ - Task UniqueConstraintExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task> GetUniqueConstraintNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task CreateUniqueConstraintIfNotExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string[] columnNames, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task DropUniqueConstraintIfExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); -} diff --git a/src/DapperMatic/Interfaces/IDatabaseExtensions.cs b/src/DapperMatic/Interfaces/IDatabaseExtensions.cs deleted file mode 100644 index e2a670d..0000000 --- a/src/DapperMatic/Interfaces/IDatabaseExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Data; - -namespace DapperMatic; - -public partial interface IDatabaseExtensions -{ - string GetLastSql(IDbConnection db); - (string sql, object? parameters) GetLastSqlWithParams(IDbConnection db); - Task GetDatabaseVersionAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); -} diff --git a/src/DapperMatic/Interfaces/IDatabaseForeignKeyConstraintMethods.cs b/src/DapperMatic/Interfaces/IDatabaseForeignKeyConstraintMethods.cs new file mode 100644 index 0000000..601cddc --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseForeignKeyConstraintMethods.cs @@ -0,0 +1,109 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseForeignKeyConstraintMethods +{ + Task CreateForeignKeyConstraintIfNotExistsAsync( + IDbConnection db, + DxForeignKeyConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateForeignKeyConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] sourceColumns, + string referencedTableName, + DxOrderedColumn[] referencedColumns, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesForeignKeyConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesForeignKeyConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetForeignKeyConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetForeignKeyConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetForeignKeyConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetForeignKeyConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetForeignKeyConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropForeignKeyConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropForeignKeyConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseIndexMethods.cs b/src/DapperMatic/Interfaces/IDatabaseIndexMethods.cs new file mode 100644 index 0000000..41e6b01 --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseIndexMethods.cs @@ -0,0 +1,106 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseIndexMethods +{ + Task CreateIndexIfNotExistsAsync( + IDbConnection db, + DxIndex constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateIndexIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + DxOrderedColumn[] columns, + bool isUnique = false, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesIndexExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesIndexExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetIndexesOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetIndexAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetIndexesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetIndexNamesOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetIndexNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropIndexesOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropIndexIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseMethods.cs b/src/DapperMatic/Interfaces/IDatabaseMethods.cs new file mode 100644 index 0000000..c7a3154 --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseMethods.cs @@ -0,0 +1,46 @@ +using System.Data; +using DapperMatic.Providers; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseMethods + : IDatabaseTableMethods, + IDatabaseColumnMethods, + IDatabaseIndexMethods, + IDatabaseCheckConstraintMethods, + IDatabaseDefaultConstraintMethods, + IDatabasePrimaryKeyConstraintMethods, + IDatabaseUniqueConstraintMethods, + IDatabaseForeignKeyConstraintMethods, + IDatabaseSchemaMethods, + IDatabaseViewMethods +{ + DbProviderType ProviderType { get; } + IDbProviderTypeMap ProviderTypeMap { get; } + + bool SupportsSchemas { get; } + + Task SupportsCheckConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + Task SupportsOrderedKeysInConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + string GetLastSql(IDbConnection db); + (string sql, object? parameters) GetLastSqlWithParams(IDbConnection db); + Task GetDatabaseVersionAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + DbProviderDotnetTypeDescriptor GetDotnetTypeFromSqlType(string sqlType); + string GetSqlTypeFromDotnetType(DbProviderDotnetTypeDescriptor descriptor); + + string NormalizeName(string name); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseExtensions.IndexMethods.cs b/src/DapperMatic/Interfaces/IDatabasePrimaryKeyConstraintMethods.cs similarity index 50% rename from src/DapperMatic/Interfaces/IDatabaseExtensions.IndexMethods.cs rename to src/DapperMatic/Interfaces/IDatabasePrimaryKeyConstraintMethods.cs index 57fa823..050121b 100644 --- a/src/DapperMatic/Interfaces/IDatabaseExtensions.IndexMethods.cs +++ b/src/DapperMatic/Interfaces/IDatabasePrimaryKeyConstraintMethods.cs @@ -1,51 +1,47 @@ using System.Data; using DapperMatic.Models; -namespace DapperMatic; +namespace DapperMatic.Interfaces; -public partial interface IDatabaseExtensions +public interface IDatabasePrimaryKeyConstraintMethods { - Task IndexExistsAsync( + Task CreatePrimaryKeyConstraintIfNotExistsAsync( IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, + DxPrimaryKeyConstraint constraint, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); - Task> GetIndexesAsync( + Task CreatePrimaryKeyConstraintIfNotExistsAsync( IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); - - Task> GetIndexNamesAsync( + + Task DoesPrimaryKeyConstraintExistAsync( IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, + string? schemaName, + string tableName, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); - Task CreateIndexIfNotExistsAsync( + + Task GetPrimaryKeyConstraintAsync( IDbConnection db, + string? schemaName, string tableName, - string indexName, - string[] columnNames, - string? schemaName = null, - bool unique = false, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); - Task DropIndexIfExistsAsync( + + Task DropPrimaryKeyConstraintIfExistsAsync( IDbConnection db, + string? schemaName, string tableName, - string indexName, - string? schemaName = null, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); diff --git a/src/DapperMatic/Interfaces/IDatabaseExtensions.SchemaMethods.cs b/src/DapperMatic/Interfaces/IDatabaseSchemaMethods.cs similarity index 66% rename from src/DapperMatic/Interfaces/IDatabaseExtensions.SchemaMethods.cs rename to src/DapperMatic/Interfaces/IDatabaseSchemaMethods.cs index 98432ba..f222436 100644 --- a/src/DapperMatic/Interfaces/IDatabaseExtensions.SchemaMethods.cs +++ b/src/DapperMatic/Interfaces/IDatabaseSchemaMethods.cs @@ -1,32 +1,32 @@ using System.Data; -namespace DapperMatic; +namespace DapperMatic.Interfaces; -public partial interface IDatabaseExtensions +public interface IDatabaseSchemaMethods { - Task SupportsSchemasAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ); - Task SchemaExistsAsync( + string GetSchemaQualifiedIdentifierName(string? schemaName, string tableName); + + Task CreateSchemaIfNotExistsAsync( IDbConnection db, string schemaName, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); - Task> GetSchemaNamesAsync( + + Task DoesSchemaExistAsync( IDbConnection db, - string? nameFilter = null, + string schemaName, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); - Task CreateSchemaIfNotExistsAsync( + + Task> GetSchemaNamesAsync( IDbConnection db, - string schemaName, + string? schemaNameFilter = null, IDbTransaction? tx = null, CancellationToken cancellationToken = default ); + Task DropSchemaIfExistsAsync( IDbConnection db, string schemaName, diff --git a/src/DapperMatic/Interfaces/IDatabaseTableMethods.cs b/src/DapperMatic/Interfaces/IDatabaseTableMethods.cs new file mode 100644 index 0000000..d181cac --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseTableMethods.cs @@ -0,0 +1,86 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseTableMethods +{ + Task DoesTableExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateTableIfNotExistsAsync( + IDbConnection db, + DxTable table, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateTableIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + DxColumn[] columns, + DxPrimaryKeyConstraint? primaryKey = null, + DxCheckConstraint[]? checkConstraints = null, + DxDefaultConstraint[]? defaultConstraints = null, + DxUniqueConstraint[]? uniqueConstraints = null, + DxForeignKeyConstraint[]? foreignKeyConstraints = null, + DxIndex[]? indexes = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetTableAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetTablesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetTableNamesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropTableIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task RenameTableIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string newTableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task TruncateTableIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseUniqueConstraintMethods.cs b/src/DapperMatic/Interfaces/IDatabaseUniqueConstraintMethods.cs new file mode 100644 index 0000000..2b8954c --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseUniqueConstraintMethods.cs @@ -0,0 +1,105 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseUniqueConstraintMethods +{ + Task CreateUniqueConstraintIfNotExistsAsync( + IDbConnection db, + DxUniqueConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateUniqueConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesUniqueConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DoesUniqueConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetUniqueConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetUniqueConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetUniqueConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetUniqueConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetUniqueConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropUniqueConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropUniqueConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Interfaces/IDatabaseViewMethods.cs b/src/DapperMatic/Interfaces/IDatabaseViewMethods.cs new file mode 100644 index 0000000..dd2bfb0 --- /dev/null +++ b/src/DapperMatic/Interfaces/IDatabaseViewMethods.cs @@ -0,0 +1,72 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Interfaces; + +public interface IDatabaseViewMethods +{ + Task DoesViewExistAsync( + IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateViewIfNotExistsAsync( + IDbConnection db, + DxView view, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task CreateViewIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string viewName, + string definition, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task GetViewAsync( + IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetViewsAsync( + IDbConnection db, + string? schemaName, + string? viewNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task> GetViewNamesAsync( + IDbConnection db, + string? schemaName, + string? viewNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task DropViewIfExistsAsync( + IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + Task RenameViewIfExistsAsync( + IDbConnection db, + string? schemaName, + string viewName, + string newViewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Logging/DxLogger.cs b/src/DapperMatic/Logging/DxLogger.cs new file mode 100644 index 0000000..4546b91 --- /dev/null +++ b/src/DapperMatic/Logging/DxLogger.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DapperMatic.Logging; + +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public static class DxLogger +{ + private static ILoggerFactory _loggerFactory = new NullLoggerFactory(); + + public static void SetLoggerFactory(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + public static ILogger CreateLogger() + { + return _loggerFactory.CreateLogger(); + } + + public static ILogger CreateLogger(Type type) + { + return _loggerFactory.CreateLogger(type); + } +} diff --git a/src/DapperMatic/Models/Column.cs b/src/DapperMatic/Models/Column.cs deleted file mode 100644 index 1c5ea8e..0000000 --- a/src/DapperMatic/Models/Column.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace DapperMatic.Models; - -public class Column -{ - public Column(string name, Type dotnetType) - { - Name = name; - DotnetType = dotnetType; - } - - public string Name { get; set; } - public Type DotnetType { get; set; } - public int? Length { get; set; } - public int? Precision { get; set; } - public int? Scale { get; set; } - public bool Nullable { get; set; } - public string? DefaultValue { get; set; } - public bool AutoIncrement { get; set; } - public bool PrimaryKey { get; set; } - public bool Unique { get; set; } - public bool Indexed { get; set; } - public bool ForeignKey { get; set; } - public string? ReferenceTable { get; set; } - public string? ReferenceColumn { get; set; } - public ReferentialAction? OnDelete { get; set; } - public ReferentialAction? OnUpdate { get; set; } - public string? Comment { get; set; } -} diff --git a/src/DapperMatic/Models/DxCheckConstraint.cs b/src/DapperMatic/Models/DxCheckConstraint.cs new file mode 100644 index 0000000..f9321b7 --- /dev/null +++ b/src/DapperMatic/Models/DxCheckConstraint.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxCheckConstraint : DxConstraint +{ + /// + /// Used for deserialization + /// + public DxCheckConstraint() + : base("") { } + + [SetsRequiredMembers] + public DxCheckConstraint( + string? schemaName, + string tableName, + string? columnName, + string constraintName, + string expression + ) + : base(constraintName) + { + SchemaName = schemaName; + TableName = string.IsNullOrWhiteSpace(tableName) + ? throw new ArgumentException("Table name is required") + : tableName; + ColumnName = columnName; + Expression = string.IsNullOrWhiteSpace(expression) + ? throw new ArgumentException("Expression is required") + : expression; + } + + public string? SchemaName { get; set; } + public required string TableName { get; init; } + public string? ColumnName { get; set; } + public required string Expression { get; init; } + + public override DxConstraintType ConstraintType => DxConstraintType.Check; + + public override string ToString() + { + if (string.IsNullOrWhiteSpace(ColumnName)) + return $"{ConstraintType} Constraint on {TableName} with expression: {Expression}"; + + return $"{ConstraintType} Constraint on {TableName}.{ColumnName} with expression: {Expression}"; + } +} diff --git a/src/DapperMatic/Models/DxColumn.cs b/src/DapperMatic/Models/DxColumn.cs new file mode 100644 index 0000000..ee47f5d --- /dev/null +++ b/src/DapperMatic/Models/DxColumn.cs @@ -0,0 +1,216 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace DapperMatic.Models; + +[Serializable] +public class DxColumn +{ + /// + /// Used for deserialization + /// + public DxColumn() { } + + [SetsRequiredMembers] + public DxColumn( + string? schemaName, + string tableName, + string columnName, + Type dotnetType, + Dictionary? providerDataTypes = null, + int? length = null, + int? precision = null, + int? scale = null, + string? checkExpression = null, + string? defaultExpression = null, + bool isNullable = false, + bool isPrimaryKey = false, + bool isAutoIncrement = false, + bool isUnique = false, + bool isIndexed = false, + bool isForeignKey = false, + string? referencedTableName = null, + string? referencedColumnName = null, + DxForeignKeyAction? onDelete = null, + DxForeignKeyAction? onUpdate = null + ) + { + SchemaName = schemaName; + TableName = tableName; + ColumnName = columnName; + DotnetType = dotnetType; + ProviderDataTypes = providerDataTypes ?? []; + Length = length; + Precision = precision; + Scale = scale; + CheckExpression = checkExpression; + DefaultExpression = defaultExpression; + IsNullable = isNullable; + IsPrimaryKey = isPrimaryKey; + IsAutoIncrement = isAutoIncrement; + IsUnique = isUnique; + IsIndexed = isIndexed; + IsForeignKey = isForeignKey; + ReferencedTableName = referencedTableName; + ReferencedColumnName = referencedColumnName; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + public string? SchemaName { get; set; } + public required string TableName { get; init; } + public required string ColumnName { get; init; } + public required Type DotnetType { get; init; } + + /// + /// The FULL native provider data type. This is the data type that the provider uses to + /// store the data (e.g. "INTEGER", "DECIMAL(14,3)", "VARCHAR(255)", "TEXT", "BLOB", etc.) + /// + /// + /// The provider data type should include the length, precision, and scale if applicable. + /// + public Dictionary ProviderDataTypes { get; } = new(); + public int? Length { get; set; } + public int? Precision { get; set; } + public int? Scale { get; set; } + public string? CheckExpression { get; set; } + public string? DefaultExpression { get; set; } + public bool IsNullable { get; set; } + public bool IsPrimaryKey { get; set; } + public bool IsAutoIncrement { get; set; } + public bool IsUnicode { get; set; } + + /// + /// Is either part of a single column unique constraint or a single column unique index. + /// + public bool IsUnique { get; set; } + + /// + /// Is part of an index + /// + public bool IsIndexed { get; set; } + + /// + /// Is a foreign key to a another referenced table. This is the MANY side of a ONE-TO-MANY relationship. + /// + public bool IsForeignKey { get; set; } + public string? ReferencedTableName { get; set; } + public string? ReferencedColumnName { get; set; } + public DxForeignKeyAction? OnDelete { get; set; } + public DxForeignKeyAction? OnUpdate { get; set; } + + public bool IsNumeric() + { + return DotnetType == typeof(byte) + || DotnetType == typeof(sbyte) + || DotnetType == typeof(short) + || DotnetType == typeof(ushort) + || DotnetType == typeof(int) + || DotnetType == typeof(uint) + || DotnetType == typeof(long) + || DotnetType == typeof(ulong) + || DotnetType == typeof(float) + || DotnetType == typeof(double) + || DotnetType == typeof(decimal); + } + + public bool IsText() + { + return DotnetType == typeof(string) + || DotnetType == typeof(char) + || DotnetType == typeof(char[]); + } + + public bool IsDateTime() + { + return DotnetType == typeof(DateTime) || DotnetType == typeof(DateTimeOffset); + } + + public bool IsBoolean() + { + return DotnetType == typeof(bool); + } + + public bool IsBinary() + { + return DotnetType == typeof(byte[]); + } + + public bool IsGuid() + { + return DotnetType == typeof(Guid); + } + + public bool IsEnum() + { + return DotnetType.IsEnum; + } + + public bool IsArray() + { + return DotnetType.IsArray; + } + + public bool IsDictionary() + { + return DotnetType.IsGenericType + && DotnetType.GetGenericTypeDefinition() == typeof(Dictionary<,>); + } + + public bool IsEnumerable() + { + return typeof(IEnumerable<>).IsAssignableFrom(DotnetType); + } + + public string GetTypeCategory() + { + if (IsNumeric()) + return "Numeric"; + if (IsText()) + return "Text"; + if (IsDateTime()) + return "DateTime"; + if (IsBoolean()) + return "Boolean"; + if (IsBinary()) + return "Binary"; + if (IsGuid()) + return "Guid"; + if (IsEnum()) + return "Enum"; + if (IsArray()) + return "Array"; + if (IsDictionary()) + return "Dictionary"; + if (IsEnumerable()) + return "Enumerable"; + return "Unknown"; + } + + // ToString override to display column definition + public override string ToString() + { + return $"{ColumnName} ({JsonSerializer.Serialize(ProviderDataTypes)}) {(IsNullable ? "NULL" : "NOT NULL")}" + + $"{(IsPrimaryKey ? " PRIMARY KEY" : "")}" + + $"{(IsUnique ? " UNIQUE" : "")}" + + $"{(IsIndexed ? " INDEXED" : "")}" + + $"{(IsForeignKey ? $" FOREIGN KEY({ReferencedTableName ?? ""}) REFERENCES({ReferencedColumnName ?? ""})" : "")}" + + $"{(IsAutoIncrement ? " AUTOINCREMENT" : "")}" + + $"{(!string.IsNullOrWhiteSpace(CheckExpression) ? $" CHECK ({CheckExpression})" : "")}" + + $"{(!string.IsNullOrWhiteSpace(DefaultExpression) ? $" DEFAULT {(DefaultExpression.Contains(' ') ? $"({DefaultExpression})" : DefaultExpression)}" : "")}"; + } + + public string? GetProviderDataType(DbProviderType providerType) + { + return ProviderDataTypes.TryGetValue(providerType, out var providerDataType) + ? providerDataType + : null; + } + + public DxColumn SetProviderDataType(DbProviderType providerType, string providerDataType) + { + ProviderDataTypes[providerType] = providerDataType; + return this; + } +} diff --git a/src/DapperMatic/Models/DxColumnOrder.cs b/src/DapperMatic/Models/DxColumnOrder.cs new file mode 100644 index 0000000..019030b --- /dev/null +++ b/src/DapperMatic/Models/DxColumnOrder.cs @@ -0,0 +1,8 @@ +namespace DapperMatic.Models; + +[Serializable] +public enum DxColumnOrder +{ + Ascending, + Descending +} diff --git a/src/DapperMatic/Models/DxConstraint.cs b/src/DapperMatic/Models/DxConstraint.cs new file mode 100644 index 0000000..2c1238d --- /dev/null +++ b/src/DapperMatic/Models/DxConstraint.cs @@ -0,0 +1,13 @@ +namespace DapperMatic.Models; + +public abstract class DxConstraint +{ + protected DxConstraint(string constraintName) + { + ConstraintName = constraintName; + } + + public abstract DxConstraintType ConstraintType { get; } + + public string ConstraintName { get; set; } +} diff --git a/src/DapperMatic/Models/DxConstraintType.cs b/src/DapperMatic/Models/DxConstraintType.cs new file mode 100644 index 0000000..ff411ba --- /dev/null +++ b/src/DapperMatic/Models/DxConstraintType.cs @@ -0,0 +1,11 @@ +namespace DapperMatic.Models; + +[Serializable] +public enum DxConstraintType +{ + PrimaryKey, + ForeignKey, + Unique, + Check, + Default +} \ No newline at end of file diff --git a/src/DapperMatic/Models/DxDefaultConstraint.cs b/src/DapperMatic/Models/DxDefaultConstraint.cs new file mode 100644 index 0000000..b741ddf --- /dev/null +++ b/src/DapperMatic/Models/DxDefaultConstraint.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxDefaultConstraint : DxConstraint +{ + /// + /// Used for deserialization + /// + public DxDefaultConstraint() + : base("") { } + + [SetsRequiredMembers] + public DxDefaultConstraint( + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression + ) + : base(constraintName) + { + SchemaName = schemaName; + TableName = string.IsNullOrWhiteSpace(tableName) + ? throw new ArgumentException("Table name is required") + : tableName; + ColumnName = string.IsNullOrWhiteSpace(columnName) + ? throw new ArgumentException("Column name is required") + : columnName; + Expression = string.IsNullOrWhiteSpace(expression) + ? throw new ArgumentException("Expression is required") + : expression; + } + + public string? SchemaName { get; set; } + public required string TableName { get; init; } + public required string ColumnName { get; init; } + public required string Expression { get; init; } + + public override DxConstraintType ConstraintType => DxConstraintType.Default; + + public override string ToString() + { + return $"{ConstraintType} Constraint on {TableName}.{ColumnName} with expression: {Expression}"; + } +} diff --git a/src/DapperMatic/Models/DxForeignKeyAction.cs b/src/DapperMatic/Models/DxForeignKeyAction.cs new file mode 100644 index 0000000..049e556 --- /dev/null +++ b/src/DapperMatic/Models/DxForeignKeyAction.cs @@ -0,0 +1,37 @@ +namespace DapperMatic.Models; + +[Serializable] +public enum DxForeignKeyAction +{ + NoAction, + Cascade, + Restrict, + SetNull +} + +public static class DxForeignKeyActionExtensions +{ + public static string ToSql(this DxForeignKeyAction foreignKeyAction) + { + return foreignKeyAction switch + { + DxForeignKeyAction.NoAction => "NO ACTION", + DxForeignKeyAction.Cascade => "CASCADE", + DxForeignKeyAction.Restrict => "RESTRICT", + DxForeignKeyAction.SetNull => "SET NULL", + _ => "NO ACTION" + }; + } + + public static DxForeignKeyAction ToForeignKeyAction(this string behavior) + { + return behavior.ToAlpha().ToUpperInvariant() switch + { + "NOACTION" => DxForeignKeyAction.NoAction, + "CASCADE" => DxForeignKeyAction.Cascade, + "RESTRICT" => DxForeignKeyAction.Restrict, + "SETNULL" => DxForeignKeyAction.SetNull, + _ => DxForeignKeyAction.NoAction + }; + } +} diff --git a/src/DapperMatic/Models/DxForeignKeyConstraint.cs b/src/DapperMatic/Models/DxForeignKeyConstraint.cs new file mode 100644 index 0000000..812e544 --- /dev/null +++ b/src/DapperMatic/Models/DxForeignKeyConstraint.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxForeignKeyConstraint : DxConstraint +{ + /// + /// Used for deserialization + /// + public DxForeignKeyConstraint() + : base("") { } + + [SetsRequiredMembers] + public DxForeignKeyConstraint( + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] sourceColumns, + string referencedTableName, + DxOrderedColumn[] referencedColumns, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction + ) + : base(constraintName) + { + if (sourceColumns.Length != referencedColumns.Length) + throw new ArgumentException( + "SourceColumns and ReferencedColumns must have the same number of columns." + ); + + SchemaName = schemaName; + TableName = tableName; + SourceColumns = sourceColumns; + ReferencedTableName = referencedTableName; + ReferencedColumns = referencedColumns; + OnDelete = onDelete; + OnUpdate = onUpdate; + } + + public string? SchemaName { get; set; } + public required string TableName { get; set; } + public required DxOrderedColumn[] SourceColumns { get; set; } + public required string ReferencedTableName { get; set; } + public required DxOrderedColumn[] ReferencedColumns { get; set; } + public DxForeignKeyAction OnDelete { get; set; } = DxForeignKeyAction.NoAction; + public DxForeignKeyAction OnUpdate { get; set; } = DxForeignKeyAction.NoAction; + + public override DxConstraintType ConstraintType => DxConstraintType.ForeignKey; +} diff --git a/src/DapperMatic/Models/DxIndex.cs b/src/DapperMatic/Models/DxIndex.cs new file mode 100644 index 0000000..6af3e1a --- /dev/null +++ b/src/DapperMatic/Models/DxIndex.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxIndex +{ + /// + /// Used for deserialization + /// + public DxIndex() { } + + [SetsRequiredMembers] + public DxIndex( + string? schemaName, + string tableName, + string indexName, + DxOrderedColumn[] columns, + bool isUnique = false + ) + { + SchemaName = schemaName; + TableName = tableName; + IndexName = indexName; + Columns = columns; + IsUnique = isUnique; + } + + public string? SchemaName { get; set; } + + public required string TableName { get; set; } + + public required string IndexName { get; set; } + + public required DxOrderedColumn[] Columns { get; set; } + public bool IsUnique { get; set; } +} diff --git a/src/DapperMatic/Models/DxOrderedColumn.cs b/src/DapperMatic/Models/DxOrderedColumn.cs new file mode 100644 index 0000000..aa856d1 --- /dev/null +++ b/src/DapperMatic/Models/DxOrderedColumn.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxOrderedColumn +{ + /// + /// Used for deserialization + /// + public DxOrderedColumn() { } + + [SetsRequiredMembers] + public DxOrderedColumn(string columnName, DxColumnOrder order = DxColumnOrder.Ascending) + { + ColumnName = columnName; + Order = order; + } + + public required string ColumnName { get; set; } + public required DxColumnOrder Order { get; set; } + + public override string ToString() => ToString(true); + + public string ToString(bool includeOrder) => + $"{ColumnName}{(includeOrder ? Order == DxColumnOrder.Descending ? " DESC" : "" : "")}"; +} diff --git a/src/DapperMatic/Models/DxPrimaryKeyConstraint.cs b/src/DapperMatic/Models/DxPrimaryKeyConstraint.cs new file mode 100644 index 0000000..85d92b2 --- /dev/null +++ b/src/DapperMatic/Models/DxPrimaryKeyConstraint.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxPrimaryKeyConstraint : DxConstraint +{ + /// + /// Used for deserialization + /// + public DxPrimaryKeyConstraint() + : base("") { } + + [SetsRequiredMembers] + public DxPrimaryKeyConstraint( + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns + ) + : base(constraintName) + { + SchemaName = schemaName; + TableName = tableName; + Columns = columns; + } + + public string? SchemaName { get; set; } + public required string TableName { get; set; } + public required DxOrderedColumn[] Columns { get; set; } + + public override DxConstraintType ConstraintType => DxConstraintType.PrimaryKey; +} diff --git a/src/DapperMatic/Models/DxTable.cs b/src/DapperMatic/Models/DxTable.cs new file mode 100644 index 0000000..fbfc946 --- /dev/null +++ b/src/DapperMatic/Models/DxTable.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxTable +{ + /// + /// Used for deserialization + /// + public DxTable() { } + + [SetsRequiredMembers] + public DxTable( + string? schemaName, + string tableName, + DxColumn[]? columns = null, + DxPrimaryKeyConstraint? primaryKey = null, + DxCheckConstraint[]? checkConstraints = null, + DxDefaultConstraint[]? defaultConstraints = null, + DxUniqueConstraint[]? uniqueConstraints = null, + DxForeignKeyConstraint[]? foreignKeyConstraints = null, + DxIndex[]? indexes = null + ) + { + SchemaName = schemaName; + TableName = tableName; + Columns = columns == null ? [] : [.. columns]; + PrimaryKeyConstraint = primaryKey; + CheckConstraints = checkConstraints == null ? [] : [.. checkConstraints]; + DefaultConstraints = defaultConstraints == null ? [] : [.. defaultConstraints]; + UniqueConstraints = uniqueConstraints == null ? [] : [.. uniqueConstraints]; + ForeignKeyConstraints = foreignKeyConstraints == null ? [] : [.. foreignKeyConstraints]; + Indexes = indexes == null ? [] : [.. indexes]; + } + + public string? SchemaName { get; set; } + public required string TableName { get; set; } + public List Columns { get; set; } = []; + public DxPrimaryKeyConstraint? PrimaryKeyConstraint { get; set; } + public List CheckConstraints { get; set; } = []; + public List DefaultConstraints { get; set; } = []; + public List UniqueConstraints { get; set; } = []; + public List ForeignKeyConstraints { get; set; } = []; + public List Indexes { get; set; } = []; +} diff --git a/src/DapperMatic/Models/DxTableFactory.cs b/src/DapperMatic/Models/DxTableFactory.cs new file mode 100644 index 0000000..fb4fec7 --- /dev/null +++ b/src/DapperMatic/Models/DxTableFactory.cs @@ -0,0 +1,733 @@ +using System.Collections.Concurrent; +using System.Data; +using System.Reflection; +using DapperMatic.DataAnnotations; +using DapperMatic.Providers; + +namespace DapperMatic.Models; + +public static class DxTableFactory +{ + private static readonly ConcurrentDictionary _cache = new(); + private static readonly ConcurrentDictionary< + Type, + Dictionary + > _propertyCache = new(); + + private static Action? _customMappingAction; + + /// + /// Configure ahead of time any custom configuration for mapping types to DxTable instances. Call this + /// before the application attempts to map types to DxTable instances, as the mappings are cached once generated + /// the very first time. + /// + /// A delegate that receives the Type that is currently being mapped to a DxTable, and an initial DxTable that represents the default mapping before any customizations are applied. The delegate will run when the GetTable method is run for the first time each particular type. + public static void Configure(Action configure) + { + _customMappingAction = configure; + } + + /// + /// Configure a specific type to your liking. This method can be used to customize the behavior of DxTable generation. + /// + /// A delegate that receives an initial DxTable that represents the default mapping before any customizations are applied. The type mapping is created immediately and the delegate is run immediately as well. + /// Type that should be mapped to a DxTable instance. + public static void Configure(Action configure) + { + Configure(typeof(T), configure); + } + + /// + /// Configure a specific type to your liking. This method can be used to customize the behavior of DxTable generation. + /// + /// Type that should be mapped to a DxTable instance. + /// A delegate that receives an initial DxTable that represents the default mapping before any customizations are applied. The type mapping is created immediately and the delegate is run immediately as well. + public static void Configure(Type type, Action configure) + { + var table = GetTable(type); + configure(table); + _cache.AddOrUpdate(type, table, (_, _) => table); + } + + /// + /// Returns an instance of a DxTable for the given type. If the type is not a valid DxTable, + /// denoted by the use of a DxTableAAttribute on the class, this method returns null. + /// + public static DxTable GetTable(Type type) + { + if (_cache.TryGetValue(type, out var table)) + return table; + + var propertyNameToColumnMap = new Dictionary(); + table = GetTableInternal(type, propertyNameToColumnMap); + + _customMappingAction?.Invoke(type, table); + + _cache.TryAdd(type, table); + _propertyCache.TryAdd(type, propertyNameToColumnMap); + return table; + } + + private static DxTable GetTableInternal( + Type type, + Dictionary propertyMappings + ) + { + var classAttributes = type.GetCustomAttributes(); + + var tableAttribute = + type.GetCustomAttribute() ?? new DxTableAttribute(null, type.Name); + + var schemaName = + classAttributes + .Select(ca => + { + var paType = ca.GetType(); + if (ca is DxTableAttribute dca && !string.IsNullOrWhiteSpace(dca.SchemaName)) + return dca.SchemaName; + // EF Core + if ( + ca is System.ComponentModel.DataAnnotations.Schema.TableAttribute ta + && !string.IsNullOrWhiteSpace(ta.Schema) + ) + return ta.Schema; + // ServiceStack.OrmLite + if ( + paType.Name == "SchemaAttribute" + && ca.TryGetPropertyValue("Name", out var name) + ) + return name; + return null; + }) + .FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) + ?? tableAttribute?.SchemaName + ?? null; + + var tableName = + classAttributes + .Select(ca => + { + var paType = ca.GetType(); + if (ca is DxTableAttribute dca && !string.IsNullOrWhiteSpace(dca.TableName)) + return dca.TableName; + // EF Core + if ( + ca is System.ComponentModel.DataAnnotations.Schema.TableAttribute ta + && !string.IsNullOrWhiteSpace(ta.Name) + ) + return ta.Name; + // ServiceStack.OrmLite + if ( + paType.Name == "AliasAttribute" + && ca.TryGetPropertyValue("Name", out var name) + ) + return name; + return null; + }) + .FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) + ?? tableAttribute?.TableName + ?? type.Name; + + // columns must bind to public properties that can be both read and written + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite); + + DxPrimaryKeyConstraint? primaryKey = null; + var columns = new List(); + var checkConstraints = new List(); + var defaultConstraints = new List(); + var uniqueConstraints = new List(); + var foreignKeyConstraints = new List(); + var indexes = new List(); + + foreach (var property in properties) + { + var propertyAttributes = property.GetCustomAttributes(); + + var hasIgnoreAttribute = propertyAttributes.Any(pa => + { + var paType = pa.GetType(); + return pa is DxIgnoreAttribute + // EF Core + || pa is System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute + // ServiceStack.OrmLite + || paType.Name == "IgnoreAttribute"; + }); + + if (hasIgnoreAttribute) + continue; + + var columnAttribute = property.GetCustomAttribute(); + var columnName = + propertyAttributes + .Select(pa => + { + var paType = pa.GetType(); + if (pa is DxColumnAttribute dca) + return dca.ColumnName; + // EF Core + if (pa is System.ComponentModel.DataAnnotations.Schema.ColumnAttribute ca) + return ca.Name; + // ServiceStack.OrmLite + if ( + paType.Name == "AliasAttribute" + && pa.TryGetPropertyValue("Name", out var name) + ) + return name; + return null; + }) + .FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) + ?? columnAttribute?.ColumnName + ?? property.Name; + + var isPrimaryKey = + columnAttribute?.IsPrimaryKey == true + || propertyAttributes.Any(pa => + { + var paType = pa.GetType(); + return pa is DxPrimaryKeyConstraintAttribute + // EF Core + || pa is System.ComponentModel.DataAnnotations.KeyAttribute + // ServiceStack.OrmLite + || paType.Name == "PrimaryKeyAttribute"; + }); + + var isRequired = propertyAttributes.Any(pa => + { + var paType = pa.GetType(); + return + // EF Core + pa is System.ComponentModel.DataAnnotations.RequiredAttribute + // ServiceStack.OrmLite + || paType.Name == "RequiredAttribute"; + }); + + // Format of provider data types: {mysql:varchar(255),sqlserver:nvarchar(255)} + var providerDataTypes = new Dictionary(); + if (!string.IsNullOrWhiteSpace(columnAttribute?.ProviderDataType)) + { + var pdts = columnAttribute + .ProviderDataType.Trim('{', '}', '[', ']', ' ') + .Split([',', ';']); + foreach (var pdt in pdts) + { + var pdtParts = pdt.Split(':'); + if ( + pdtParts.Length == 2 + && !string.IsNullOrWhiteSpace(pdtParts[0]) + && !string.IsNullOrWhiteSpace(pdtParts[1]) + ) + { + if ( + pdtParts[0].Contains("mysql", StringComparison.OrdinalIgnoreCase) + || pdtParts[0].Contains("maria", StringComparison.OrdinalIgnoreCase) + ) + { + providerDataTypes.Add(DbProviderType.MySql, pdtParts[1]); + } + else if ( + pdtParts[0].Contains("pg", StringComparison.OrdinalIgnoreCase) + || pdtParts[0].Contains("postgres", StringComparison.OrdinalIgnoreCase) + ) + { + providerDataTypes.Add(DbProviderType.PostgreSql, pdtParts[1]); + } + else if (pdtParts[0].Contains("sqlite", StringComparison.OrdinalIgnoreCase)) + { + providerDataTypes.Add(DbProviderType.Sqlite, pdtParts[1]); + } + else if ( + pdtParts[0].Contains("sqlserver", StringComparison.OrdinalIgnoreCase) + || pdtParts[0].Contains("mssql", StringComparison.OrdinalIgnoreCase) + ) + { + providerDataTypes.Add(DbProviderType.SqlServer, pdtParts[1]); + } + } + } + } + + var column = new DxColumn( + schemaName, + tableName, + columnName, + property.PropertyType, + providerDataTypes.Count != 0 ? providerDataTypes : null, + columnAttribute?.Length, + columnAttribute?.Precision, + columnAttribute?.Scale, + string.IsNullOrWhiteSpace(columnAttribute?.CheckExpression) + ? null + : columnAttribute.CheckExpression, + string.IsNullOrWhiteSpace(columnAttribute?.DefaultExpression) + ? null + : columnAttribute.DefaultExpression, + !isRequired && (columnAttribute?.IsNullable ?? true), + columnAttribute?.IsPrimaryKey ?? false, + columnAttribute?.IsAutoIncrement ?? false, + columnAttribute?.IsUnique ?? false, + columnAttribute?.IsIndexed ?? false, + columnAttribute?.IsForeignKey ?? false, + string.IsNullOrWhiteSpace(columnAttribute?.ReferencedTableName) + ? null + : columnAttribute.ReferencedTableName, + string.IsNullOrWhiteSpace(columnAttribute?.ReferencedColumnName) + ? null + : columnAttribute.ReferencedColumnName, + columnAttribute?.OnDelete ?? null, + columnAttribute?.OnUpdate ?? null + ); + columns.Add(column); + propertyMappings.Add(property.Name, column); + + if (column.Length == null) + { + var stringLengthAttribute = + property.GetCustomAttribute(); + if (stringLengthAttribute != null) + { + column.Length = stringLengthAttribute.MaximumLength; + } + else + { + var maxLengthAttribute = + property.GetCustomAttribute(); + if (maxLengthAttribute != null) + { + column.Length = maxLengthAttribute.Length; + } + } + } + + // set primary key if present + var columnPrimaryKeyAttribute = + property.GetCustomAttribute(); + if (columnPrimaryKeyAttribute != null) + { + column.IsPrimaryKey = true; + if (primaryKey == null) + { + primaryKey = new DxPrimaryKeyConstraint( + schemaName, + tableName, + !string.IsNullOrWhiteSpace(columnPrimaryKeyAttribute.ConstraintName) + ? columnPrimaryKeyAttribute.ConstraintName + : string.Empty, + [new(columnName)] + ); + } + else + { + primaryKey.Columns = + [ + .. new List(primaryKey.Columns) { new(columnName) } + ]; + if (!string.IsNullOrWhiteSpace(columnPrimaryKeyAttribute.ConstraintName)) + { + primaryKey.ConstraintName = columnPrimaryKeyAttribute.ConstraintName; + } + } + } + else if (isPrimaryKey) + { + column.IsPrimaryKey = true; + if (primaryKey == null) + { + primaryKey = new DxPrimaryKeyConstraint( + schemaName, + tableName, + string.Empty, + [new(columnName)] + ); + } + else + { + primaryKey.Columns = + [ + .. new List(primaryKey.Columns) { new(columnName) } + ]; + } + } + + // set check expression if present + var columnCheckConstraintAttribute = + property.GetCustomAttribute(); + if (columnCheckConstraintAttribute != null) + { + var checkConstraint = new DxCheckConstraint( + schemaName, + tableName, + columnName, + !string.IsNullOrWhiteSpace(columnCheckConstraintAttribute.ConstraintName) + ? columnCheckConstraintAttribute.ConstraintName + : DbProviderUtils.GenerateCheckConstraintName(tableName, columnName), + columnCheckConstraintAttribute.Expression + ); + checkConstraints.Add(checkConstraint); + + column.CheckExpression = columnCheckConstraintAttribute.Expression; + } + + // set default expression if present + var columnDefaultConstraintAttribute = + property.GetCustomAttribute(); + if (columnDefaultConstraintAttribute != null) + { + var defaultConstraint = new DxDefaultConstraint( + schemaName, + tableName, + columnName, + !string.IsNullOrWhiteSpace(columnDefaultConstraintAttribute.ConstraintName) + ? columnDefaultConstraintAttribute.ConstraintName + : DbProviderUtils.GenerateDefaultConstraintName(tableName, columnName), + columnDefaultConstraintAttribute.Expression + ); + defaultConstraints.Add(defaultConstraint); + + column.DefaultExpression = columnDefaultConstraintAttribute.Expression; + } + + // set unique constraint if present + var columnUniqueConstraintAttribute = + property.GetCustomAttribute(); + if (columnUniqueConstraintAttribute != null) + { + var uniqueConstraint = new DxUniqueConstraint( + schemaName, + tableName, + !string.IsNullOrWhiteSpace(columnUniqueConstraintAttribute.ConstraintName) + ? columnUniqueConstraintAttribute.ConstraintName + : DbProviderUtils.GenerateUniqueConstraintName(tableName, columnName), + [new(columnName)] + ); + uniqueConstraints.Add(uniqueConstraint); + + column.IsUnique = true; + } + + // set index if present + var columnIndexAttribute = property.GetCustomAttribute(); + if (columnIndexAttribute != null) + { + var index = new DxIndex( + schemaName, + tableName, + !string.IsNullOrWhiteSpace(columnIndexAttribute.IndexName) + ? columnIndexAttribute.IndexName + : DbProviderUtils.GenerateIndexName(tableName, columnName), + [new(columnName)], + isUnique: columnIndexAttribute.IsUnique + ); + indexes.Add(index); + + column.IsIndexed = true; + if (index.IsUnique) + column.IsUnique = true; + } + else + { + var indexAttribute = propertyAttributes.FirstOrDefault(pa => + pa.GetType().FullName == "Microsoft.EntityFrameworkCore.IndexAttribute" + ); + if (indexAttribute != null) + { + var isUnique = + indexAttribute.TryGetPropertyValue("IsUnique", out var u) && u; + var indexName = + ( + indexAttribute.TryGetPropertyValue("Name", out var n) + && !string.IsNullOrWhiteSpace(n) + ) + ? n + : DbProviderUtils.GenerateIndexName(tableName, columnName); + var index = new DxIndex( + schemaName, + tableName, + indexName, + [new(columnName)], + isUnique + ); + indexes.Add(index); + + column.IsIndexed = true; + if (index.IsUnique) + column.IsUnique = true; + } + } + + // set foreign key constraint if present + var columnForeignKeyConstraintAttribute = + property.GetCustomAttribute(); + if (columnForeignKeyConstraintAttribute != null) + { + var referencedTableName = columnForeignKeyConstraintAttribute.ReferencedTableName; + var referencedColumnNames = + columnForeignKeyConstraintAttribute.ReferencedColumnNames; + var onDelete = columnForeignKeyConstraintAttribute.OnDelete; + var onUpdate = columnForeignKeyConstraintAttribute.OnUpdate; + if ( + !string.IsNullOrWhiteSpace(referencedTableName) + && referencedColumnNames != null + && referencedColumnNames.Length > 0 + && !string.IsNullOrWhiteSpace(referencedColumnNames[0]) + ) + { + var constraintName = !string.IsNullOrWhiteSpace( + columnForeignKeyConstraintAttribute.ConstraintName + ) + ? columnForeignKeyConstraintAttribute.ConstraintName + : DbProviderUtils.GenerateForeignKeyConstraintName( + tableName, + columnName, + referencedTableName, + referencedColumnNames[0] + ); + var foreignKeyConstraint = new DxForeignKeyConstraint( + schemaName, + tableName, + constraintName, + [new(columnName)], + referencedTableName, + [new(referencedColumnNames[0])], + onDelete ?? DxForeignKeyAction.NoAction, + onUpdate ?? DxForeignKeyAction.NoAction + ); + foreignKeyConstraints.Add(foreignKeyConstraint); + + column.IsForeignKey = true; + column.ReferencedTableName = referencedTableName; + column.ReferencedColumnName = referencedColumnNames[0]; + column.OnDelete = onDelete; + column.OnUpdate = onUpdate; + } + } + else + { + var foreignKeyAttribute = + property.GetCustomAttribute(); + if (foreignKeyAttribute != null) + { + var inversePropertyAttribute = + property.GetCustomAttribute(); + var referencedTableName = foreignKeyAttribute.Name; + // TODO: figure out a way to derive the referenced column name + var referencedColumnNames = new[] + { + inversePropertyAttribute?.Property ?? "id" + }; + var onDelete = DxForeignKeyAction.NoAction; + var onUpdate = DxForeignKeyAction.NoAction; + var constraintName = DbProviderUtils.GenerateForeignKeyConstraintName( + tableName, + columnName, + referencedTableName, + referencedColumnNames[0] + ); + var foreignKeyConstraint = new DxForeignKeyConstraint( + schemaName, + tableName, + constraintName, + [new(columnName)], + referencedTableName, + [new(referencedColumnNames[0])], + onDelete, + onUpdate + ); + foreignKeyConstraints.Add(foreignKeyConstraint); + + column.IsForeignKey = true; + column.ReferencedTableName = referencedTableName; + column.ReferencedColumnName = referencedColumnNames[0]; + column.OnDelete = onDelete; + column.OnUpdate = onUpdate; + } + } + + if (columnAttribute == null) + continue; + + columns.Add(column); + } + + // TRUST that the developer knows what they are doing and not creating double the amount of attributes then + // necessary. Class level attributes get used without questioning. + + var cpa = type.GetCustomAttribute(); + if (cpa != null && cpa.Columns != null) + { + var constraintName = !string.IsNullOrWhiteSpace(cpa.ConstraintName) + ? cpa.ConstraintName + : string.Empty; + + primaryKey = new DxPrimaryKeyConstraint( + schemaName, + tableName, + constraintName, + cpa.Columns + ); + + // flag the column as part of the primary key + foreach (var c in cpa.Columns) + { + var column = columns.FirstOrDefault(col => + col.ColumnName.Equals(c.ColumnName, StringComparison.OrdinalIgnoreCase) + ); + if (column != null) + column.IsPrimaryKey = true; + } + } + + if (primaryKey != null && string.IsNullOrWhiteSpace(primaryKey.ConstraintName)) + { + primaryKey.ConstraintName = DbProviderUtils.GeneratePrimaryKeyConstraintName( + tableName, + primaryKey.Columns.Select(c => c.ColumnName).ToArray() + ); + } + + var ccas = type.GetCustomAttributes(); + var ccaId = 1; + foreach (var cca in ccas) + { + if (string.IsNullOrWhiteSpace(cca.Expression)) + continue; + + var constraintName = !string.IsNullOrWhiteSpace(cca.ConstraintName) + ? cca.ConstraintName + : DbProviderUtils.GenerateCheckConstraintName(tableName, $"{ccaId++}"); + + checkConstraints.Add( + new DxCheckConstraint(schemaName, tableName, null, constraintName, cca.Expression) + ); + } + + var ucas = type.GetCustomAttributes(); + foreach (var uca in ucas) + { + if (uca.Columns == null) + continue; + + var constraintName = !string.IsNullOrWhiteSpace(uca.ConstraintName) + ? uca.ConstraintName + : DbProviderUtils.GenerateUniqueConstraintName( + tableName, + uca.Columns.Select(c => c.ColumnName).ToArray() + ); + + uniqueConstraints.Add( + new DxUniqueConstraint(schemaName, tableName, constraintName, uca.Columns) + ); + + if (uca.Columns.Length == 1) + { + var column = columns.FirstOrDefault(c => + c.ColumnName.Equals( + uca.Columns[0].ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ); + if (column != null) + column.IsUnique = true; + } + } + + var cias = type.GetCustomAttributes(); + foreach (var cia in cias) + { + if (cia.Columns == null) + continue; + + var indexName = !string.IsNullOrWhiteSpace(cia.IndexName) + ? cia.IndexName + : DbProviderUtils.GenerateIndexName( + tableName, + cia.Columns.Select(c => c.ColumnName).ToArray() + ); + + indexes.Add( + new DxIndex(schemaName, tableName, indexName, cia.Columns, isUnique: cia.IsUnique) + ); + + if (cia.Columns.Length == 1) + { + var column = columns.FirstOrDefault(c => + c.ColumnName.Equals( + cia.Columns[0].ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ); + if (column != null) + { + column.IsIndexed = true; + if (cia.IsUnique) + column.IsUnique = true; + } + } + } + + var cfkas = type.GetCustomAttributes(); + foreach (var cfk in cfkas) + { + if ( + cfk.SourceColumnNames == null + || cfk.SourceColumnNames.Length == 0 + || string.IsNullOrWhiteSpace(cfk.ReferencedTableName) + || cfk.ReferencedColumnNames == null + || cfk.ReferencedColumnNames.Length == 0 + || cfk.SourceColumnNames.Length != cfk.ReferencedColumnNames.Length + ) + continue; + + var constraintName = !string.IsNullOrWhiteSpace(cfk.ConstraintName) + ? cfk.ConstraintName + : DbProviderUtils.GenerateForeignKeyConstraintName( + tableName, + cfk.SourceColumnNames, + cfk.ReferencedTableName, + cfk.ReferencedColumnNames + ); + + var foreignKeyConstraint = new DxForeignKeyConstraint( + schemaName, + tableName, + constraintName, + [.. cfk.SourceColumnNames.Select(c => new DxOrderedColumn(c))], + cfk.ReferencedTableName, + [.. cfk.ReferencedColumnNames.Select(c => new DxOrderedColumn(c))], + cfk.OnDelete ?? DxForeignKeyAction.NoAction, + cfk.OnUpdate ?? DxForeignKeyAction.NoAction + ); + + foreignKeyConstraints.Add(foreignKeyConstraint); + + for (var i = 0; i < cfk.SourceColumnNames.Length; i++) + { + var sc = cfk.SourceColumnNames[i]; + var column = columns.FirstOrDefault(c => + c.ColumnName.Equals(sc, StringComparison.OrdinalIgnoreCase) + ); + if (column != null) + { + column.IsForeignKey = true; + column.ReferencedTableName = cfk.ReferencedTableName; + column.ReferencedColumnName = cfk.ReferencedColumnNames[i]; + column.OnDelete = cfk.OnDelete; + column.OnUpdate = cfk.OnUpdate; + } + } + } + + var table = new DxTable( + schemaName, + tableName, + [.. columns], + primaryKey, + [.. checkConstraints], + [.. defaultConstraints], + [.. uniqueConstraints], + [.. foreignKeyConstraints], + [.. indexes] + ); + return table; + } +} diff --git a/src/DapperMatic/Models/DxUniqueConstraint.cs b/src/DapperMatic/Models/DxUniqueConstraint.cs new file mode 100644 index 0000000..9709fdc --- /dev/null +++ b/src/DapperMatic/Models/DxUniqueConstraint.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxUniqueConstraint : DxConstraint +{ + /// + /// Used for deserialization + /// + public DxUniqueConstraint() + : base("") { } + + [SetsRequiredMembers] + public DxUniqueConstraint( + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns + ) + : base(constraintName) + { + SchemaName = schemaName; + TableName = tableName; + Columns = columns; + } + + public string? SchemaName { get; set; } + public required string TableName { get; set; } + public required DxOrderedColumn[] Columns { get; set; } + + public override DxConstraintType ConstraintType => DxConstraintType.Unique; +} diff --git a/src/DapperMatic/Models/DxView.cs b/src/DapperMatic/Models/DxView.cs new file mode 100644 index 0000000..d37ec13 --- /dev/null +++ b/src/DapperMatic/Models/DxView.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Models; + +[Serializable] +public class DxView +{ + /// + /// Used for deserialization + /// + public DxView() { } + + [SetsRequiredMembers] + public DxView(string? schemaName, string viewName, string definition) + { + SchemaName = schemaName; + ViewName = viewName; + Definition = definition; + } + + public string? SchemaName { get; set; } + public required string ViewName { get; set; } + public required string Definition { get; set; } +} diff --git a/src/DapperMatic/Models/DxViewFactory.cs b/src/DapperMatic/Models/DxViewFactory.cs new file mode 100644 index 0000000..67f3bf2 --- /dev/null +++ b/src/DapperMatic/Models/DxViewFactory.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using System.Reflection; +using DapperMatic.DataAnnotations; + +namespace DapperMatic.Models; + +public static class DxViewFactory +{ + private static readonly ConcurrentDictionary Cache = new(); + + /// + /// Returns an instance of a DxView for the given type. If the type is not a valid DxView, + /// denoted by the use of a DxViewAAttribute on the class, this method returns null. + /// + public static DxView? GetView(Type type) + { + if (Cache.TryGetValue(type, out var view)) + return view; + + var viewAttribute = type.GetCustomAttribute(); + if (viewAttribute == null) + return null; + + if (string.IsNullOrWhiteSpace(viewAttribute.Definition)) + throw new InvalidOperationException("Type is missing a view definition."); + + view = new DxView( + string.IsNullOrWhiteSpace(viewAttribute.SchemaName) ? null : viewAttribute.SchemaName, + string.IsNullOrWhiteSpace(viewAttribute.ViewName) ? type.Name : viewAttribute.ViewName, + viewAttribute.Definition.Trim() + ); + + Cache.TryAdd(type, view); + return view; + } +} diff --git a/src/DapperMatic/Models/ForeignKey.cs b/src/DapperMatic/Models/ForeignKey.cs deleted file mode 100644 index e903ef7..0000000 --- a/src/DapperMatic/Models/ForeignKey.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace DapperMatic.Models; - -public class ForeignKey -{ - public ForeignKey( - string? schemaName, - string foreignKeyName, - string tableName, - string columnName, - string referenceTable, - string referenceColumn, - ReferentialAction onDelete, - ReferentialAction onUpdate - ) - { - SchemaName = schemaName; - ForeignKeyName = foreignKeyName; - TableName = tableName; - ColumnName = columnName; - ReferenceTableName = referenceTable; - ReferenceColumnName = referenceColumn; - OnDelete = onDelete; - OnUpdate = onUpdate; - } - - public string? SchemaName { get; set; } - public string ForeignKeyName { get; set; } - public string TableName { get; set; } - public string ColumnName { get; set; } - public string ReferenceTableName { get; set; } - public string ReferenceColumnName { get; set; } - public ReferentialAction OnDelete { get; set; } = ReferentialAction.NoAction; - public ReferentialAction OnUpdate { get; set; } = ReferentialAction.NoAction; -} diff --git a/src/DapperMatic/Models/ModelDefinition.cs b/src/DapperMatic/Models/ModelDefinition.cs deleted file mode 100644 index 9c6030f..0000000 --- a/src/DapperMatic/Models/ModelDefinition.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DapperMatic.Models; - -public class ModelDefinition -{ - public Type? Type { get; set; } - public Table? Table { get; set; } -} diff --git a/src/DapperMatic/Models/PrimaryKey.cs b/src/DapperMatic/Models/PrimaryKey.cs deleted file mode 100644 index a5cd79f..0000000 --- a/src/DapperMatic/Models/PrimaryKey.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DapperMatic.Models; - -public class PrimaryKey -{ - public PrimaryKey(string name, string[] columnNames) - { - Name = name; - Columns = columnNames; - } - - public string Name { get; set; } - public string[] Columns { get; set; } -} diff --git a/src/DapperMatic/Models/ReferentialAction.cs b/src/DapperMatic/Models/ReferentialAction.cs deleted file mode 100644 index 0d93bf1..0000000 --- a/src/DapperMatic/Models/ReferentialAction.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace DapperMatic.Models; - -public enum ReferentialAction -{ - NoAction, - Cascade, - SetNull, -} - -public static class ReferentialActionExtensions -{ - public static string ToSql(this ReferentialAction referentialAction) - { - return referentialAction switch - { - ReferentialAction.NoAction => "NO ACTION", - ReferentialAction.Cascade => "CASCADE", - ReferentialAction.SetNull => "SET NULL", - _ - => throw new ArgumentOutOfRangeException( - nameof(referentialAction), - referentialAction, - null - ), - }; - } -} diff --git a/src/DapperMatic/Models/Table.cs b/src/DapperMatic/Models/Table.cs deleted file mode 100644 index 76e588f..0000000 --- a/src/DapperMatic/Models/Table.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace DapperMatic.Models; - -public class Table -{ - public Table(string name, string? schemaName) - { - Name = name; - Schema = schemaName; - } - - public string Name { get; set; } - public string? Schema { get; set; } - public PrimaryKey? PrimaryKey { get; set; } - public Column[] Columns { get; set; } = []; - public UniqueConstraint[] UniqueConstraints { get; set; } = []; - public TableIndex[] Indexes { get; set; } = []; - public ForeignKey[] ForeignKeys { get; set; } = []; -} diff --git a/src/DapperMatic/Models/TableIndex.cs b/src/DapperMatic/Models/TableIndex.cs deleted file mode 100644 index ae99eb8..0000000 --- a/src/DapperMatic/Models/TableIndex.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace DapperMatic.Models; - -public class TableIndex -{ - public TableIndex(string? schemaName, string tableName, string indexName, string[] columnNames, bool unique = false) - { - SchemaName = schemaName; - TableName = tableName; - IndexName = indexName; - ColumnNames = columnNames; - Unique = unique; - } - - public string? SchemaName { get; set; } - - public string TableName { get; set; } - - public string IndexName { get; set; } - - /// - /// Column names (optionally, appended with ` ASC` or ` DESC`) - /// - public string[] ColumnNames { get; set; } - public bool Unique { get; set; } -} diff --git a/src/DapperMatic/Models/UniqueConstraint.cs b/src/DapperMatic/Models/UniqueConstraint.cs deleted file mode 100644 index 8f9be5c..0000000 --- a/src/DapperMatic/Models/UniqueConstraint.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DapperMatic.Models; - -public class UniqueConstraint -{ - public UniqueConstraint(string name, string[] columnNames) - { - Name = name; - Columns = columnNames; - } - - public string Name { get; set; } - public string[] Columns { get; set; } -} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.CheckConstraints.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.CheckConstraints.cs new file mode 100644 index 0000000..a01e3c9 --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.CheckConstraints.cs @@ -0,0 +1,335 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesCheckConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (!await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + return false; + + return await GetCheckConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task DoesCheckConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (!await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + return false; + + return await GetCheckConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task CreateCheckConstraintIfNotExistsAsync( + IDbConnection db, + DxCheckConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (!await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + return false; + + return await CreateCheckConstraintIfNotExistsAsync( + db, + constraint.SchemaName, + constraint.TableName, + constraint.ColumnName, + constraint.ConstraintName, + constraint.Expression, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task CreateCheckConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required.", nameof(expression)); + + if (!await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + return false; + + if ( + await DoesCheckConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlAlterTableAddCheckConstraint( + schemaName, + tableName, + constraintName, + expression + ); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task GetCheckConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + var checkConstraints = await GetCheckConstraintsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + return checkConstraints.SingleOrDefault(); + } + + public virtual async Task GetCheckConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + var checkConstraints = await GetCheckConstraintsAsync( + db, + schemaName, + tableName, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + return checkConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ) + ?.ConstraintName; + } + + public virtual async Task> GetCheckConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var checkConstraints = await GetCheckConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + return checkConstraints.Select(c => c.ConstraintName).ToList(); + } + + public virtual async Task GetCheckConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + var checkConstraints = await GetCheckConstraintsAsync( + db, + schemaName, + tableName, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + return checkConstraints.FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ); + } + + public virtual async Task> GetCheckConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (!await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + return []; + + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + if (table == null) + return []; + + var filter = string.IsNullOrWhiteSpace(constraintNameFilter) + ? null + : ToSafeString(constraintNameFilter); + + return string.IsNullOrWhiteSpace(filter) + ? table.CheckConstraints + : table + .CheckConstraints.Where(c => c.ConstraintName.IsWildcardPatternMatch(filter)) + .ToList(); + } + + public virtual async Task DropCheckConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + if (!await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + return false; + + var constraintName = await GetCheckConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ); + if (string.IsNullOrWhiteSpace(constraintName)) + return false; + + var sql = SqlDropCheckConstraint(schemaName, tableName, constraintName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task DropCheckConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (!await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + return false; + + if ( + !await DoesCheckConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlDropCheckConstraint(schemaName, tableName, constraintName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Columns.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Columns.cs new file mode 100644 index 0000000..c8e1a39 --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Columns.cs @@ -0,0 +1,426 @@ +using System.Data; +using System.Text; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesColumnExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetColumnAsync(db, schemaName, tableName, columnName, tx, cancellationToken) + .ConfigureAwait(false) != null; + } + + public virtual async Task CreateColumnIfNotExistsAsync( + IDbConnection db, + DxColumn column, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(column.TableName)) + throw new ArgumentException("Table name is required", nameof(column.TableName)); + + if (string.IsNullOrWhiteSpace(column.ColumnName)) + throw new ArgumentException("Column name is required", nameof(column.ColumnName)); + + var table = await GetTableAsync( + db, + column.SchemaName, + column.TableName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(table?.TableName)) + return false; + + if ( + table.Columns.Any(c => + c.ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + ) + return false; + + var dbVersion = await GetDatabaseVersionAsync(db, tx, cancellationToken) + .ConfigureAwait(false); + + var tableConstraints = new DxTable(table.SchemaName, table.TableName); + + // attach the existing primary key constraint if it exists to ensure that it doesn't get recreated + if (table.PrimaryKeyConstraint != null) + tableConstraints.PrimaryKeyConstraint = table.PrimaryKeyConstraint; + + var columnDefinitionSql = SqlInlineColumnDefinition( + table, + column, + tableConstraints, + dbVersion + ); + + var sql = new StringBuilder(); + sql.Append( + $"ALTER TABLE {GetSchemaQualifiedIdentifierName(column.SchemaName, column.TableName)} ADD {columnDefinitionSql}" + ); + + await ExecuteAsync(db, sql.ToString(), tx).ConfigureAwait(false); + + // ONLY add the primary key constraint if it didn't exist before and if it wasn't + // already added as part of the column definition (in which case that tableConstraints.PrimaryKeyConstraint will be null) + // will be null. + if (tableConstraints.PrimaryKeyConstraint != null) + { + await CreatePrimaryKeyConstraintIfNotExistsAsync( + db, + tableConstraints.PrimaryKeyConstraint, + tx: tx, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + } + + foreach (var checkConstraint in tableConstraints.CheckConstraints) + { + await CreateCheckConstraintIfNotExistsAsync( + db, + checkConstraint, + tx: tx, + cancellationToken: cancellationToken + ); + } + + foreach (var defaultConstraint in tableConstraints.DefaultConstraints) + { + await CreateDefaultConstraintIfNotExistsAsync( + db, + defaultConstraint, + tx: tx, + cancellationToken: cancellationToken + ); + } + + foreach (var uniqueConstraint in tableConstraints.UniqueConstraints) + { + await CreateUniqueConstraintIfNotExistsAsync( + db, + uniqueConstraint, + tx: tx, + cancellationToken: cancellationToken + ); + } + + foreach (var foreignKeyConstraint in tableConstraints.ForeignKeyConstraints) + { + await CreateForeignKeyConstraintIfNotExistsAsync( + db, + foreignKeyConstraint, + tx: tx, + cancellationToken: cancellationToken + ); + } + + foreach (var index in tableConstraints.Indexes) + { + await CreateIndexIfNotExistsAsync( + db, + index, + tx: tx, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + } + + return true; + } + + public virtual async Task CreateColumnIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + Type dotnetType, + string? providerDataType = null, + int? length = null, + int? precision = null, + int? scale = null, + string? checkExpression = null, + string? defaultExpression = null, + bool isNullable = true, + bool isPrimaryKey = false, + bool isAutoIncrement = false, + bool isUnique = false, + bool isIndexed = false, + bool isForeignKey = false, + string? referencedTableName = null, + string? referencedColumnName = null, + DxForeignKeyAction? onDelete = null, + DxForeignKeyAction? onUpdate = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreateColumnIfNotExistsAsync( + db, + new DxColumn( + schemaName, + tableName, + columnName, + dotnetType, + providerDataType == null + ? null + : new Dictionary + { + { ProviderType, providerDataType } + }, + length, + precision, + scale, + checkExpression, + defaultExpression, + isNullable, + isPrimaryKey, + isAutoIncrement, + isUnique, + isIndexed, + isForeignKey, + referencedTableName, + referencedColumnName, + onDelete, + onUpdate + ), + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task GetColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return ( + await GetColumnsAsync(db, schemaName, tableName, columnName, tx, cancellationToken) + .ConfigureAwait(false) + ).FirstOrDefault(); + } + + public virtual async Task> GetColumnNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? columnNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var columns = await GetColumnsAsync( + db, + schemaName, + tableName, + columnNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return columns.Select(x => x.ColumnName).ToList(); + } + + public virtual async Task> GetColumnsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? columnNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + if (table == null) + return []; + + var filter = string.IsNullOrWhiteSpace(columnNameFilter) + ? null + : ToSafeString(columnNameFilter); + + return string.IsNullOrWhiteSpace(filter) + ? table.Columns + : table.Columns.Where(c => c.ColumnName.IsWildcardPatternMatch(filter)).ToList(); + } + + public virtual async Task DropColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + if (table == null) + return false; + + (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); + + var column = table.Columns.FirstOrDefault(c => + c.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ); + + if (column == null) + return false; + + // drop any related constraints + if (column.IsPrimaryKey) + { + await DropPrimaryKeyConstraintIfExistsAsync( + db, + schemaName, + tableName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + if (column.IsForeignKey) + { + await DropForeignKeyConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + column.ColumnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + if (column.IsUnique) + { + await DropUniqueConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + column.ColumnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + if (column.IsIndexed) + { + await DropIndexesOnColumnIfExistsAsync( + db, + schemaName, + tableName, + column.ColumnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + await DropCheckConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + await DropDefaultConstraintOnColumnIfExistsAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + var sql = SqlDropColumn(schemaName, tableName, columnName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task RenameColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + string newColumnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await DoesColumnExistAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + if ( + await DoesColumnExistAsync( + db, + schemaName, + tableName, + newColumnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); + + var schemaQualifiedTableName = GetSchemaQualifiedIdentifierName(schemaName, tableName); + + // As of version 3.25.0 released September 2018, SQLite supports renaming columns + await ExecuteAsync( + db, + $""" + ALTER TABLE {schemaQualifiedTableName} + RENAME COLUMN {columnName} + TO {newColumnName} + """, + tx: tx + ) + .ConfigureAwait(false); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.DefaultConstraints.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.DefaultConstraints.cs new file mode 100644 index 0000000..88494ec --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.DefaultConstraints.cs @@ -0,0 +1,308 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesDefaultConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetDefaultConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task DoesDefaultConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetDefaultConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task CreateDefaultConstraintIfNotExistsAsync( + IDbConnection db, + DxDefaultConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreateDefaultConstraintIfNotExistsAsync( + db, + constraint.SchemaName, + constraint.TableName, + constraint.ColumnName, + constraint.ConstraintName, + constraint.Expression, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task CreateDefaultConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required.", nameof(expression)); + + if ( + await DoesDefaultConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlAlterTableAddDefaultConstraint( + schemaName, + tableName, + columnName, + constraintName, + expression + ); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task GetDefaultConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + var defaultConstraints = await GetDefaultConstraintsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + return defaultConstraints.SingleOrDefault(); + } + + public virtual async Task GetDefaultConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + var defaultConstraints = await GetDefaultConstraintsAsync( + db, + schemaName, + tableName, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + return defaultConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ) + ?.ConstraintName; + } + + public virtual async Task> GetDefaultConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var checkConstraints = await GetDefaultConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return checkConstraints.Select(c => c.ConstraintName).ToList(); + } + + public virtual async Task GetDefaultConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + var defaultConstraints = await GetDefaultConstraintsAsync( + db, + schemaName, + tableName, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + return defaultConstraints.FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ); + } + + public virtual async Task> GetDefaultConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + if (table == null) + return []; + + var filter = string.IsNullOrWhiteSpace(constraintNameFilter) + ? null + : ToSafeString(constraintNameFilter); + + return string.IsNullOrWhiteSpace(filter) + ? table.DefaultConstraints + : table + .DefaultConstraints.Where(c => c.ConstraintName.IsWildcardPatternMatch(filter)) + .ToList(); + } + + public virtual async Task DropDefaultConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var constraintName = await GetDefaultConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ); + + if (string.IsNullOrWhiteSpace(constraintName)) + return false; + + var sql = SqlDropDefaultConstraint(schemaName, tableName, columnName, constraintName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task DropDefaultConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var defaultConstraint = await GetDefaultConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(defaultConstraint?.ColumnName)) + return false; + + var sql = SqlDropDefaultConstraint( + schemaName, + tableName, + defaultConstraint.ColumnName, + constraintName + ); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.ForeignKeyConstraints.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.ForeignKeyConstraints.cs new file mode 100644 index 0000000..d18c88c --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.ForeignKeyConstraints.cs @@ -0,0 +1,326 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesForeignKeyConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetForeignKeyConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task DoesForeignKeyConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetForeignKeyConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task CreateForeignKeyConstraintIfNotExistsAsync( + IDbConnection db, + DxForeignKeyConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreateForeignKeyConstraintIfNotExistsAsync( + db, + constraint.SchemaName, + constraint.TableName, + constraint.ConstraintName, + constraint.SourceColumns, + constraint.ReferencedTableName, + constraint.ReferencedColumns, + constraint.OnDelete, + constraint.OnUpdate, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task CreateForeignKeyConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] sourceColumns, + string referencedTableName, + DxOrderedColumn[] referencedColumns, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (sourceColumns.Length == 0) + throw new ArgumentException( + "At least one column must be specified.", + nameof(sourceColumns) + ); + + if (string.IsNullOrWhiteSpace(referencedTableName)) + throw new ArgumentException( + "Referenced table name is required.", + nameof(referencedTableName) + ); + + if (referencedColumns.Length == 0) + throw new ArgumentException( + "At least one column must be specified.", + nameof(referencedColumns) + ); + + if (sourceColumns.Length != referencedColumns.Length) + throw new ArgumentException( + "The number of source columns must match the number of referenced columns.", + nameof(referencedColumns) + ); + + if ( + await DoesForeignKeyConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlAlterTableAddForeignKeyConstraint( + schemaName, + constraintName, + tableName, + sourceColumns, + referencedTableName, + referencedColumns, + onDelete, + onUpdate + ); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task GetForeignKeyConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + var foreignKeyConstraints = await GetForeignKeyConstraintsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return foreignKeyConstraints.SingleOrDefault(); + } + + public virtual async Task GetForeignKeyConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return ( + await GetForeignKeyConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + )?.ConstraintName; + } + + public virtual async Task> GetForeignKeyConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var foreignKeyConstraints = await GetForeignKeyConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return foreignKeyConstraints.Select(c => c.ConstraintName).ToList(); + } + + public virtual async Task GetForeignKeyConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + var foreignKeyConstraints = await GetForeignKeyConstraintsAsync( + db, + schemaName, + tableName, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return foreignKeyConstraints.FirstOrDefault(c => + c.SourceColumns.Any(sc => + sc.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ) + ); + } + + public virtual async Task> GetForeignKeyConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + if (table == null) + return new List(); + + var filter = string.IsNullOrWhiteSpace(constraintNameFilter) + ? null + : ToSafeString(constraintNameFilter); + + return string.IsNullOrWhiteSpace(filter) + ? table.ForeignKeyConstraints + : table + .ForeignKeyConstraints.Where(c => c.ConstraintName.IsWildcardPatternMatch(filter)) + .ToList(); + } + + public virtual async Task DropForeignKeyConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var constraintName = await GetForeignKeyConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return constraintName != null + && await DropForeignKeyConstraintIfExistsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task DropForeignKeyConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await DoesForeignKeyConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlDropForeignKeyConstraint(schemaName, tableName, constraintName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Indexes.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Indexes.cs new file mode 100644 index 0000000..ac12d0d --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Indexes.cs @@ -0,0 +1,259 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesIndexExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetIndexAsync(db, schemaName, tableName, indexName, tx, cancellationToken) + .ConfigureAwait(false) != null; + } + + public virtual async Task DoesIndexExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return ( + await GetIndexesOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ).Count > 0; + } + + public virtual async Task CreateIndexIfNotExistsAsync( + IDbConnection db, + DxIndex index, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreateIndexIfNotExistsAsync( + db, + index.SchemaName, + index.TableName, + index.IndexName, + index.Columns, + index.IsUnique, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task CreateIndexIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + DxOrderedColumn[] columns, + bool isUnique = false, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(indexName)) + { + throw new ArgumentException("Index name is required.", nameof(indexName)); + } + + if ( + await DoesIndexExistAsync(db, schemaName, tableName, indexName, tx, cancellationToken) + .ConfigureAwait(false) + ) + { + return false; + } + + var sql = SqlCreateIndex(schemaName, tableName, indexName, columns, isUnique); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task GetIndexAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(indexName)) + throw new ArgumentException("Index name is required.", nameof(indexName)); + + var indexes = await GetIndexesAsync( + db, + schemaName, + tableName, + indexName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return indexes.SingleOrDefault(); + } + + public virtual async Task> GetIndexesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); + + return await GetIndexesInternalAsync( + db, + schemaName, + tableName, + string.IsNullOrWhiteSpace(indexNameFilter) ? null : indexNameFilter, + tx, + cancellationToken + ); + } + + public virtual async Task> GetIndexNamesOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return ( + await GetIndexesOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + .Select(x => x.IndexName) + .ToList(); + } + + public virtual async Task> GetIndexNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return ( + await GetIndexesAsync(db, schemaName, tableName, indexNameFilter, tx, cancellationToken) + .ConfigureAwait(false) + ) + .Select(x => x.IndexName) + .ToList(); + } + + public virtual async Task> GetIndexesOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + var indexes = await GetIndexesAsync(db, schemaName, tableName, null, tx, cancellationToken) + .ConfigureAwait(false); + + return indexes + .Where(c => + c.Columns.Any(x => + x.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ) + ) + .ToList(); + } + + public virtual async Task DropIndexIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string indexName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await DoesIndexExistAsync(db, schemaName, tableName, indexName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlDropIndex(schemaName, tableName, indexName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task DropIndexesOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var indexNames = await GetIndexNamesOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + if (indexNames.Count == 0) + return false; + + foreach (var indexName in indexNames) + { + var sql = SqlDropIndex(schemaName, tableName, indexName); + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + } + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.PrimaryKeyConstraints.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.PrimaryKeyConstraints.cs new file mode 100644 index 0000000..3229340 --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.PrimaryKeyConstraints.cs @@ -0,0 +1,131 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesPrimaryKeyConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetPrimaryKeyConstraintAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false) != null; + } + + public virtual async Task CreatePrimaryKeyConstraintIfNotExistsAsync( + IDbConnection db, + DxPrimaryKeyConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreatePrimaryKeyConstraintIfNotExistsAsync( + db, + constraint.SchemaName, + constraint.TableName, + constraint.ConstraintName, + constraint.Columns, + tx, + cancellationToken + ); + } + + public virtual async Task CreatePrimaryKeyConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (columns.Length == 0) + throw new ArgumentException("At least one column must be specified.", nameof(columns)); + + if ( + await DoesPrimaryKeyConstraintExistAsync( + db, + schemaName, + tableName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var supportsOrderedKeysInConstraints = await SupportsOrderedKeysInConstraintsAsync( + db, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + var sql = SqlAlterTableAddPrimaryKeyConstraint( + schemaName, + tableName, + constraintName, + columns, + supportsOrderedKeysInConstraints + ); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task GetPrimaryKeyConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + if (table?.PrimaryKeyConstraint is null) + return null; + + return table.PrimaryKeyConstraint; + } + + public virtual async Task DropPrimaryKeyConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var primaryKeyConstraint = await GetPrimaryKeyConstraintAsync( + db, + schemaName, + tableName, + tx, + cancellationToken + ); + + if (string.IsNullOrWhiteSpace(primaryKeyConstraint?.ConstraintName)) + return false; + + var sql = SqlDropPrimaryKeyConstraint(schemaName, tableName, primaryKeyConstraint.ConstraintName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Schemas.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Schemas.cs new file mode 100644 index 0000000..f0e2193 --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Schemas.cs @@ -0,0 +1,87 @@ +using System.Data; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesSchemaExistAsync( + IDbConnection db, + string schemaName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (!SupportsSchemas) + return false; + + if (string.IsNullOrWhiteSpace(schemaName)) + throw new ArgumentException("Schema name is required.", nameof(schemaName)); + + return ( + await GetSchemaNamesAsync(db, schemaName, tx, cancellationToken) + .ConfigureAwait(false) + ).Count() > 0; + } + + public virtual async Task CreateSchemaIfNotExistsAsync( + IDbConnection db, + string schemaName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (!SupportsSchemas) + return false; + + if (string.IsNullOrWhiteSpace(schemaName)) + throw new ArgumentException("Schema name is required.", nameof(schemaName)); + + if (await DoesSchemaExistAsync(db, schemaName, tx, cancellationToken).ConfigureAwait(false)) + return false; + + var sql = SqlCreateSchema(schemaName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task> GetSchemaNamesAsync( + IDbConnection db, + string? schemaNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) { + if (!SupportsSchemas) + return []; + + var (sql, parameters) = SqlGetSchemaNames(schemaNameFilter); + + return await QueryAsync(db, sql, parameters, tx: tx).ConfigureAwait(false); + } + + public virtual async Task DropSchemaIfExistsAsync( + IDbConnection db, + string schemaName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (!SupportsSchemas) + return false; + + if (string.IsNullOrWhiteSpace(schemaName)) + throw new ArgumentException("Schema name is required.", nameof(schemaName)); + + if ( + !await DoesSchemaExistAsync(db, schemaName, tx, cancellationToken).ConfigureAwait(false) + ) + return false; + + var sql = SqlDropSchema(schemaName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Strings.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Strings.cs new file mode 100644 index 0000000..a72d65f --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Strings.cs @@ -0,0 +1,813 @@ +using System.Text; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + #region Schema Strings + protected virtual string SqlCreateSchema(string schemaName) + { + return $"CREATE SCHEMA {NormalizeSchemaName(schemaName)}"; + } + + protected virtual (string sql, object parameters) SqlGetSchemaNames( + string? schemaNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(schemaNameFilter) + ? "" + : ToLikeString(schemaNameFilter); + + var sql = $""" + + SELECT SCHEMA_NAME + FROM INFORMATION_SCHEMA.SCHEMATA + {(string.IsNullOrWhiteSpace(where) ? "" : "WHERE SCHEMA_NAME LIKE @where")} + ORDER BY SCHEMA_NAME + """; + + return (sql, new { where }); + } + + protected virtual string SqlDropSchema(string schemaName) + { + return $"DROP SCHEMA {NormalizeSchemaName(schemaName)}"; + } + #endregion // Schema Strings + + #region Table Strings + protected virtual (string sql, object parameters) SqlDoesTableExist( + string? schemaName, + string tableName + ) + { + var sql = $""" + + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.TABLES + WHERE + TABLE_TYPE='BASE TABLE' + {( + string.IsNullOrWhiteSpace(schemaName) ? "" : " AND TABLE_SCHEMA = @schemaName" + )} + AND TABLE_NAME = @tableName + """; + + return ( + sql, + new + { + schemaName = NormalizeSchemaName(schemaName), + tableName = NormalizeName(tableName) + } + ); + } + + protected virtual (string sql, object parameters) SqlGetTableNames( + string? schemaName, + string? tableNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(tableNameFilter) ? "" : ToLikeString(tableNameFilter); + + var sql = $""" + + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE + TABLE_TYPE = 'BASE TABLE' + AND TABLE_SCHEMA = @schemaName + {( + string.IsNullOrWhiteSpace(where) ? null : " AND TABLE_NAME LIKE @where" + )} + ORDER BY TABLE_NAME + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + protected virtual string SqlDropTable(string? schemaName, string tableName) + { + return $"DROP TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)}"; + } + + protected virtual string SqlRenameTable( + string? schemaName, + string tableName, + string newTableName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} RENAME TO {NormalizeName(newTableName)}"; + } + + protected virtual string SqlTruncateTable(string? schemaName, string tableName) + { + return $"TRUNCATE TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)}"; + } + + /// + /// Anything inside of tableConstraints does NOT get added to the column definition. + /// Anything added to the column definition should be added to the tableConstraints object. + /// + /// The existing table WITHOUT the column being added + /// The new column + /// Table constraints that will get added after the column definitions clauses in the CREATE TABLE or ALTER TABLE commands. + /// + /// + protected virtual string SqlInlineColumnDefinition( + DxTable existingTable, + DxColumn column, + DxTable tableConstraints, + Version dbVersion + ) + { + var (schemaName, tableName, columnName) = NormalizeNames( + existingTable.SchemaName, + existingTable.TableName, + column.ColumnName + ); + + var sql = new StringBuilder(); + + sql.Append($"{SqlInlineColumnNameAndType(column, dbVersion)}"); + + sql.Append($" {SqlInlineColumnNullable(column)}"); + + // Only add the primary key here if the primary key is a single column key + // and doesn't already exist in the existing table constraints + var tpkc = tableConstraints.PrimaryKeyConstraint; + if ( + column.IsPrimaryKey + && ( + tpkc == null + || ( + tpkc.Columns.Count() == 1 + && tpkc.Columns[0] + .ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + ) + ) + { + var pkConstraintName = DbProviderUtils.GeneratePrimaryKeyConstraintName( + tableName, + columnName + ); + var pkInlineSql = SqlInlinePrimaryKeyColumnConstraint( + column, + pkConstraintName, + out var useTableConstraint + ); + if (!string.IsNullOrWhiteSpace(pkInlineSql)) + sql.Append($" {pkInlineSql}"); + + if (useTableConstraint) + { + tableConstraints.PrimaryKeyConstraint = new DxPrimaryKeyConstraint( + schemaName, + tableName, + pkConstraintName, + [new DxOrderedColumn(columnName)] + ); + } + else + { // since we added the PK inline, we're going to remove it from the table constraints + tableConstraints.PrimaryKeyConstraint = null; + } + } +#if DEBUG + else if (column.IsPrimaryKey) + { + // PROVIDED FOR BREAKPOINT PURPOSES WHILE DEBUGGING: Primary key will be added as a table constraint + sql.Append(""); + } +#endif + + if ( + !string.IsNullOrWhiteSpace(column.DefaultExpression) + && tableConstraints.DefaultConstraints.All(dc => + !dc.ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + ) + { + var defConstraintName = DbProviderUtils.GenerateDefaultConstraintName( + tableName, + columnName + ); + sql.Append( + $" {SqlInlineDefaultColumnConstraint(defConstraintName, column.DefaultExpression)}" + ); + } + else + { + // DEFAULT EXPRESSIONS ARE A LITTLE DIFFERENT + // In our case, we're always going to add them via the column definition. + // SQLite ONLY allows default expressions to be added via the column definition. + // Other providers also allow it, so let's just do them all here + var defaultConstraint = tableConstraints.DefaultConstraints.FirstOrDefault(dc => + dc.ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ); + if (defaultConstraint != null) + { + sql.Append( + $" {SqlInlineDefaultColumnConstraint(defaultConstraint.ConstraintName, defaultConstraint.Expression)}" + ); + } + } + + if ( + !string.IsNullOrWhiteSpace(column.CheckExpression) + && tableConstraints.CheckConstraints.All(ck => + string.IsNullOrWhiteSpace(ck.ColumnName) + || !ck.ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + ) + { + var ckConstraintName = DbProviderUtils.GenerateCheckConstraintName( + tableName, + columnName + ); + var ckInlineSql = SqlInlineCheckColumnConstraint( + ckConstraintName, + column.CheckExpression, + out var useTableConstraint + ); + + if (!string.IsNullOrWhiteSpace(ckInlineSql)) + sql.Append($" {ckInlineSql}"); + + if (useTableConstraint) + { + tableConstraints.CheckConstraints.Add( + new DxCheckConstraint( + schemaName, + tableName, + columnName, + ckConstraintName, + column.CheckExpression + ) + ); + } + } + + if ( + column.IsUnique + && !column.IsIndexed + && tableConstraints.UniqueConstraints.All(uc => + !uc.Columns.Any(c => + c.ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + ) + ) + { + var ucConstraintName = DbProviderUtils.GenerateUniqueConstraintName( + tableName, + columnName + ); + var ucInlineSql = SqlInlineUniqueColumnConstraint( + ucConstraintName, + out var useTableConstraint + ); + + if (!string.IsNullOrWhiteSpace(ucInlineSql)) + sql.Append($" {ucInlineSql}"); + + if (useTableConstraint) + { + tableConstraints.UniqueConstraints.Add( + new DxUniqueConstraint( + schemaName, + tableName, + ucConstraintName, + [new DxOrderedColumn(columnName)] + ) + ); + } + } + + if ( + column.IsForeignKey + && !string.IsNullOrWhiteSpace(column.ReferencedTableName) + && !string.IsNullOrWhiteSpace(column.ReferencedColumnName) + && tableConstraints.ForeignKeyConstraints.All(fk => + !fk.SourceColumns.Any(c => + c.ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + ) + ) + { + var fkConstraintName = DbProviderUtils.GenerateForeignKeyConstraintName( + tableName, + columnName, + NormalizeName(column.ReferencedTableName), + NormalizeName(column.ReferencedColumnName) + ); + var fkInlineSql = SqlInlineForeignKeyColumnConstraint( + schemaName, + fkConstraintName, + column.ReferencedTableName, + new DxOrderedColumn(column.ReferencedColumnName), + column.OnDelete, + column.OnUpdate, + out var useTableConstraint + ); + + if (!string.IsNullOrWhiteSpace(fkInlineSql)) + sql.Append($" {fkInlineSql}"); + + if (useTableConstraint) + { + tableConstraints.ForeignKeyConstraints.Add( + new DxForeignKeyConstraint( + schemaName, + tableName, + fkConstraintName, + [new DxOrderedColumn(columnName)], + column.ReferencedTableName, + [new DxOrderedColumn(column.ReferencedColumnName)], + column.OnDelete ?? DxForeignKeyAction.NoAction, + column.OnUpdate ?? DxForeignKeyAction.NoAction + ) + ); + } + } + + if ( + column.IsIndexed + && tableConstraints.Indexes.All(i => + !i.Columns.Any(c => + c.ColumnName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + ) + ) + { + var indexName = DbProviderUtils.GenerateIndexName(tableName, columnName); + tableConstraints.Indexes.Add( + new DxIndex( + schemaName, + tableName, + indexName, + [new DxOrderedColumn(columnName)], + column.IsUnique + ) + ); + } + + return sql.ToString(); + } + + protected virtual string SqlInlineColumnNameAndType(DxColumn column, Version dbVersion) + { + var descriptor = new DbProviderDotnetTypeDescriptor( + column.DotnetType, + column.Length, + column.Precision, + column.Scale, + column.IsAutoIncrement, + column.IsUnicode + ); + + var columnType = string.IsNullOrWhiteSpace(column.GetProviderDataType(ProviderType)) + ? GetSqlTypeFromDotnetType(descriptor) + : column.GetProviderDataType(ProviderType); + + if (string.IsNullOrWhiteSpace(columnType)) + throw new InvalidOperationException( + $"Could not determine the SQL type for column {column.ColumnName} of type {column.DotnetType.Name}" + ); + + // set the type on the column so that it can be used in other methods + column.SetProviderDataType(ProviderType, columnType); + + return $"{NormalizeName(column.ColumnName)} {columnType}"; + } + + protected virtual string SqlInlineColumnNullable(DxColumn column) + { + return column.IsNullable && !column.IsUnique && !column.IsPrimaryKey + ? " NULL" + : " NOT NULL"; + } + + protected virtual string SqlInlinePrimaryKeyColumnConstraint( + DxColumn column, + string constraintName, + out bool useTableConstraint + ) + { + useTableConstraint = false; + return $"CONSTRAINT {NormalizeName(constraintName)} PRIMARY KEY {(column.IsAutoIncrement ? SqlInlinePrimaryKeyAutoIncrementColumnConstraint(column) : "")}".Trim(); + } + + protected virtual string SqlInlinePrimaryKeyAutoIncrementColumnConstraint(DxColumn column) + { + return "IDENTITY(1,1)"; + } + + protected virtual string SqlInlineDefaultColumnConstraint( + string constraintName, + string defaultExpression + ) + { + defaultExpression = defaultExpression.Trim(); + var addParentheses = + defaultExpression.Contains(' ') + && !(defaultExpression.StartsWith('(') && defaultExpression.EndsWith(')')) + && !(defaultExpression.StartsWith('"') && defaultExpression.EndsWith('"')) + && !(defaultExpression.StartsWith('\'') && defaultExpression.EndsWith('\'')); + + return $"CONSTRAINT {NormalizeName(constraintName)} DEFAULT {(addParentheses ? $"({defaultExpression})" : defaultExpression)}"; + } + + protected virtual string SqlInlineCheckColumnConstraint( + string constraintName, + string checkExpression, + out bool useTableConstraint + ) + { + useTableConstraint = false; + return $"CONSTRAINT {NormalizeName(constraintName)} CHECK ({checkExpression})"; + } + + protected virtual string SqlInlineUniqueColumnConstraint( + string constraintName, + out bool useTableConstraint + ) + { + useTableConstraint = false; + return $"CONSTRAINT {NormalizeName(constraintName)} UNIQUE"; + } + + protected virtual string SqlInlineForeignKeyColumnConstraint( + string? schemaName, + string constraintName, + string referencedTableName, + DxOrderedColumn referencedColumn, + DxForeignKeyAction? onDelete, + DxForeignKeyAction? onUpdate, + out bool useTableConstraint + ) + { + useTableConstraint = false; + return $"CONSTRAINT {NormalizeName(constraintName)} REFERENCES {GetSchemaQualifiedIdentifierName(schemaName, referencedTableName)} ({NormalizeName(referencedColumn.ColumnName)})" + + (onDelete.HasValue ? $" ON DELETE {onDelete.Value.ToSql()}" : "") + + (onUpdate.HasValue ? $" ON UPDATE {onUpdate.Value.ToSql()}" : ""); + } + + protected virtual string SqlInlinePrimaryKeyTableConstraint( + DxTable table, + DxPrimaryKeyConstraint primaryKeyConstraint + ) + { + var pkColumnNames = primaryKeyConstraint.Columns.Select(c => c.ColumnName).ToArray(); + var pkConstrainName = !string.IsNullOrWhiteSpace(primaryKeyConstraint.ConstraintName) + ? primaryKeyConstraint.ConstraintName + : DbProviderUtils.GeneratePrimaryKeyConstraintName( + table.TableName, + pkColumnNames.ToArray() + ); + var pkColumnsCsv = string.Join(", ", pkColumnNames); + return $"CONSTRAINT {NormalizeName(pkConstrainName)} PRIMARY KEY ({pkColumnsCsv})"; + } + + protected virtual string SqlInlineCheckTableConstraint(DxTable table, DxCheckConstraint check) + { + var ckConstraintName = !string.IsNullOrWhiteSpace(check.ConstraintName) + ? check.ConstraintName + : string.IsNullOrWhiteSpace(check.ColumnName) + ? DbProviderUtils.GenerateCheckConstraintName( + table.TableName, + DateTime.Now.Ticks.ToString() + ) + : DbProviderUtils.GenerateCheckConstraintName(table.TableName, check.ColumnName); + + return $"CONSTRAINT {NormalizeName(ckConstraintName)} CHECK ({check.Expression})"; + } + + // protected virtual string SqlInlineDefaultTableConstraint(DxTable table, DxDefaultConstraint def) + // { + // var defaultExpression = def.Expression.Trim(); + // var addParentheses = + // defaultExpression.Contains(' ') + // && !(defaultExpression.StartsWith('(') && defaultExpression.EndsWith(')')) + // && !(defaultExpression.StartsWith('"') && defaultExpression.EndsWith('"')) + // && !(defaultExpression.StartsWith('\'') && defaultExpression.EndsWith('\'')); + + // var constraintName = !string.IsNullOrWhiteSpace(def.ConstraintName) + // ? def.ConstraintName + // : ProviderUtils.GenerateDefaultConstraintName(table.TableName, def.ColumnName); + + // return $"CONSTRAINT {NormalizeName(constraintName)} DEFAULT {(addParentheses ? $"({defaultExpression})" : defaultExpression)}"; + // } + + protected virtual string SqlInlineUniqueTableConstraint( + DxTable table, + DxUniqueConstraint uc, + bool supportsOrderedKeysInConstraints + ) + { + var ucConstraintName = !string.IsNullOrWhiteSpace(uc.ConstraintName) + ? uc.ConstraintName + : DbProviderUtils.GenerateUniqueConstraintName( + table.TableName, + uc.Columns.Select(c => NormalizeName(c.ColumnName)).ToArray() + ); + + var uniqueColumns = uc.Columns.Select(c => + supportsOrderedKeysInConstraints + ? new DxOrderedColumn(NormalizeName(c.ColumnName), c.Order).ToString() + : new DxOrderedColumn(NormalizeName(c.ColumnName)).ToString() + ); + return $"CONSTRAINT {NormalizeName(ucConstraintName)} UNIQUE ({string.Join(", ", uniqueColumns)})"; + } + + protected virtual string SqlInlineForeignKeyTableConstraint( + DxTable table, + DxForeignKeyConstraint fk + ) + { + return $""" + + CONSTRAINT {NormalizeName(fk.ConstraintName)} + FOREIGN KEY ({string.Join( + ", ", + fk.SourceColumns.Select(c => NormalizeName(c.ColumnName)) + )}) + REFERENCES {GetSchemaQualifiedIdentifierName( + table.SchemaName, + fk.ReferencedTableName + )} ({string.Join(", ", fk.ReferencedColumns.Select(c => NormalizeName(c.ColumnName)))}) + ON DELETE {fk.OnDelete.ToSql()} + ON UPDATE {fk.OnUpdate.ToSql()} + """.Trim(); + } + + #endregion // Table Strings + + #region Column Strings + protected virtual string SqlDropColumn(string? schemaName, string tableName, string columnName) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP COLUMN {NormalizeName(columnName)}"; + } + #endregion // Column Strings + + #region Check Constraint Strings + protected virtual string SqlAlterTableAddCheckConstraint( + string? schemaName, + string tableName, + string constraintName, + string expression + ) + { + if (expression.Trim().StartsWith('(') && expression.Trim().EndsWith(')')) + expression = expression.Trim().Substring(1, expression.Length - 2); + + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} ADD CONSTRAINT {NormalizeName(constraintName)} CHECK ({expression})"; + } + + protected virtual string SqlDropCheckConstraint( + string? schemaName, + string tableName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP CONSTRAINT {NormalizeName(constraintName)}"; + } + #endregion // Check Constraint Strings + + #region Default Constraint Strings + protected virtual string SqlAlterTableAddDefaultConstraint( + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression + ) + { + var schemaQualifiedTableName = GetSchemaQualifiedIdentifierName(schemaName, tableName); + + return $""" + + ALTER TABLE {schemaQualifiedTableName} + ADD CONSTRAINT {NormalizeName( + constraintName + )} DEFAULT {expression} FOR {NormalizeName(columnName)} + + """; + } + + protected virtual string SqlDropDefaultConstraint( + string? schemaName, + string tableName, + string columnName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP CONSTRAINT {NormalizeName(constraintName)}"; + } + #endregion // Default Constraint Strings + + #region Primary Key Strings + protected virtual string SqlAlterTableAddPrimaryKeyConstraint( + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + bool supportsOrderedKeysInConstraints + ) + { + var primaryKeyColumns = string.Join( + ", ", + columns.Select(c => + { + var columnName = NormalizeName(c.ColumnName); + return c.Order == DxColumnOrder.Ascending ? columnName : $"{columnName} DESC"; + }) + ); + return $""" + ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} + ADD CONSTRAINT {NormalizeName(constraintName)} + PRIMARY KEY ({primaryKeyColumns}) + """; + } + + protected virtual string SqlDropPrimaryKeyConstraint( + string? schemaName, + string tableName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP CONSTRAINT {NormalizeName(constraintName)}"; + } + #endregion // Primary Key Strings + + #region Unique Constraint Strings + protected virtual string SqlAlterTableAddUniqueConstraint( + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + bool supportsOrderedKeysInConstraints + ) + { + var uniqueColumns = columns.Select(c => + supportsOrderedKeysInConstraints + ? new DxOrderedColumn(NormalizeName(c.ColumnName), c.Order).ToString() + : new DxOrderedColumn(NormalizeName(c.ColumnName)).ToString() + ); + return $""" + ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} + ADD CONSTRAINT {NormalizeName(constraintName)} UNIQUE ({string.Join( + ", ", + uniqueColumns + )}) + """; + } + + protected virtual string SqlDropUniqueConstraint( + string? schemaName, + string tableName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP CONSTRAINT {NormalizeName(constraintName)}"; + } + #endregion // Unique Constraint Strings + + #region Foreign Key Constraint Strings + protected virtual string SqlAlterTableAddForeignKeyConstraint( + string? schemaName, + string constraintName, + string tableName, + DxOrderedColumn[] columns, + string referencedTableName, + DxOrderedColumn[] referencedColumns, + DxForeignKeyAction onDelete, + DxForeignKeyAction onUpdate + ) + { + var schemaQualifiedTableName = GetSchemaQualifiedIdentifierName(schemaName, tableName); + var schemaQualifiedReferencedTableName = GetSchemaQualifiedIdentifierName( + schemaName, + referencedTableName + ); + var columnNames = columns.Select(c => NormalizeName(c.ColumnName)); + var referencedColumnNames = referencedColumns.Select(c => NormalizeName(c.ColumnName)); + + return $""" + + ALTER TABLE {schemaQualifiedTableName} + ADD CONSTRAINT {NormalizeName(constraintName)} + FOREIGN KEY ({string.Join(", ", columnNames)}) + REFERENCES {schemaQualifiedReferencedTableName} ({string.Join( + ", ", + referencedColumnNames + )}) + ON DELETE {onDelete.ToSql()} + ON UPDATE {onUpdate.ToSql()} + + """; + } + + protected virtual string SqlDropForeignKeyConstraint( + string? schemaName, + string tableName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP CONSTRAINT {NormalizeName(constraintName)}"; + } + #endregion // Foreign Key Constraint Strings + + #region Index Strings + protected virtual string SqlCreateIndex( + string? schemaName, + string tableName, + string indexName, + DxOrderedColumn[] columns, + bool isUnique = false + ) + { + return $"CREATE {(isUnique ? "UNIQUE " : "")}INDEX {NormalizeName(indexName)} ON {GetSchemaQualifiedIdentifierName(schemaName, tableName)} ({string.Join(", ", columns.Select(c => c.ToString()))})"; + } + + protected virtual string SqlDropIndex(string? schemaName, string tableName, string indexName) + { + return $"DROP INDEX {NormalizeName(indexName)} ON {GetSchemaQualifiedIdentifierName(schemaName, tableName)}"; + } + #endregion // Index Strings + + #region View Strings + + protected virtual string SqlCreateView(string? schemaName, string viewName, string definition) + { + return $"CREATE VIEW {GetSchemaQualifiedIdentifierName(schemaName, viewName)} AS {definition}"; + } + + protected virtual (string sql, object parameters) SqlGetViewNames( + string? schemaName, + string? viewNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + SELECT + TABLE_NAME AS ViewName + FROM + INFORMATION_SCHEMA.VIEWS + WHERE + TABLE_NAME IS NOT NULL + {( + string.IsNullOrWhiteSpace(schemaName) ? "" : " AND TABLE_SCHEMA = @schemaName" + )} + {( + string.IsNullOrWhiteSpace(where) ? "" : " AND TABLE_NAME LIKE @where" + )} + ORDER BY + TABLE_NAME + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + protected virtual (string sql, object parameters) SqlGetViews( + string? schemaName, + string? viewNameFilter + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + SELECT + TABLE_SCHEMA AS SchemaName + TABLE_NAME AS ViewName, + VIEW_DEFINITION AS Definition + FROM + INFORMATION_SCHEMA.VIEWS + WHERE + TABLE_NAME IS NOT NULL + {( + string.IsNullOrWhiteSpace(schemaName) ? "" : " AND TABLE_SCHEMA = @schemaName" + )} + {( + string.IsNullOrWhiteSpace(where) ? "" : " AND TABLE_NAME LIKE @where" + )} + ORDER BY + TABLE_NAME + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + protected virtual string NormalizeViewDefinition(string definition) + { + return definition; + } + + protected virtual string SqlDropView(string? schemaName, string viewName) + { + return $"DROP VIEW {GetSchemaQualifiedIdentifierName(schemaName, viewName)}"; + } + #endregion // View Strings +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Tables.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Tables.cs new file mode 100644 index 0000000..490c2c6 --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Tables.cs @@ -0,0 +1,542 @@ +using System.Data; +using System.Text; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesTableExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var (sql, parameters) = SqlDoesTableExist(schemaName, tableName); + + var result = await ExecuteScalarAsync(db, sql, parameters, tx: tx) + .ConfigureAwait(false); + + return result > 0; + } + + public virtual async Task CreateTablesIfNotExistsAsync( + IDbConnection db, + DxTable[] tables, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var afterAllTablesConstraints = new List(); + + foreach (var table in tables) + { + var created = await CreateTableIfNotExistsAsync( + db, + table, + afterAllTablesConstraints, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + if (!created) + return false; + } + + // Add foreign keys AFTER all tables are created + foreach ( + var foreignKeyConstraint in afterAllTablesConstraints.SelectMany(x => + x.ForeignKeyConstraints + ) + ) + { + await CreateForeignKeyConstraintIfNotExistsAsync( + db, + foreignKeyConstraint, + tx: tx, + cancellationToken: cancellationToken + ); + } + + return true; + } + + public virtual async Task CreateTableIfNotExistsAsync( + IDbConnection db, + DxTable table, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreateTableIfNotExistsAsync(db, table, null, tx, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// If NULL, then the foreign keys will get added inline, or as table constraints, otherwise, if a list is passed, they'll get processed outside this function. + /// + /// + /// + protected virtual async Task CreateTableIfNotExistsAsync( + IDbConnection db, + DxTable table, + List? afterAllTablesConstraints, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(table.TableName)) + { + throw new ArgumentException("Table name is required.", nameof(table.TableName)); + } + + if (table.Columns == null || table.Columns.Count == 0) + { + throw new ArgumentException("At least one column is required.", nameof(table.Columns)); + } + + if ( + await DoesTableExistAsync(db, table.SchemaName, table.TableName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + var dbVersion = await GetDatabaseVersionAsync(db, tx, cancellationToken) + .ConfigureAwait(false); + + var supportsOrderedKeysInConstraints = await SupportsOrderedKeysInConstraintsAsync( + db, + tx: tx, + cancellationToken + ) + .ConfigureAwait(false); + + var (schemaName, tableName, _) = NormalizeNames(table.SchemaName, table.TableName); + + var sql = new StringBuilder(); + sql.Append( + $"CREATE TABLE {GetSchemaQualifiedIdentifierName(table.SchemaName, table.TableName)} (" + ); + + var tableConstraints = new DxTable( + schemaName, + tableName, + [], + table.PrimaryKeyConstraint, + [.. table.CheckConstraints], + [.. table.DefaultConstraints], + [.. table.UniqueConstraints], + [.. table.ForeignKeyConstraints], + [.. table.Indexes] + ); + + afterAllTablesConstraints?.Add(tableConstraints); + + // if there are multiple columns with a primary key, + // we need to add the primary key as a constraint, and not inline + // with the column definition. + if (table.PrimaryKeyConstraint == null && table.Columns.Count(c => c.IsPrimaryKey) > 1) + { + var pkColumns = table.Columns.Where(c => c.IsPrimaryKey).ToArray(); + var pkConstraintName = DbProviderUtils.GeneratePrimaryKeyConstraintName( + table.TableName, + pkColumns.Select(c => c.ColumnName).ToArray() + ); + + // The column definition builder will detect the primary key constraint is + // already added and disregard adding it again. + tableConstraints.PrimaryKeyConstraint = new DxPrimaryKeyConstraint( + table.SchemaName, + table.TableName, + pkConstraintName, + [.. pkColumns.Select(c => new DxOrderedColumn(c.ColumnName))] + ); + } + + for (var i = 0; i < table.Columns.Count; i++) + { + sql.AppendLine(); + sql.Append(i == 0 ? " " : " , "); + + var column = table.Columns[i]; + + if (afterAllTablesConstraints != null) + { + // the caller of this function wants to process the foreign keys + // outside this function. + if ( + column.IsForeignKey + && !string.IsNullOrWhiteSpace(column.ReferencedTableName) + && !string.IsNullOrWhiteSpace(column.ReferencedColumnName) + && tableConstraints.ForeignKeyConstraints.All(fk => + !fk.SourceColumns.Any(c => + c.ColumnName.Equals( + column.ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ) + ) + ) + { + var fkConstraintName = DbProviderUtils.GenerateForeignKeyConstraintName( + tableName, + column.ColumnName, + column.ReferencedTableName, + column.ReferencedColumnName + ); + tableConstraints.ForeignKeyConstraints.Add( + new DxForeignKeyConstraint( + table.SchemaName, + table.TableName, + NormalizeName(fkConstraintName), + [new DxOrderedColumn(column.ColumnName)], + column.ReferencedTableName, + [new DxOrderedColumn(column.ReferencedColumnName)] + ) + ); + } + } + + var columnDefinitionSql = SqlInlineColumnDefinition( + table, + column, + tableConstraints, + dbVersion + ); + sql.Append(columnDefinitionSql); + } + + if (tableConstraints.PrimaryKeyConstraint != null) + { + sql.AppendLine(); + sql.Append(" ,"); + sql.Append( + SqlInlinePrimaryKeyTableConstraint(table, tableConstraints.PrimaryKeyConstraint) + ); + } + + foreach (var check in tableConstraints.CheckConstraints) + { + sql.AppendLine(); + sql.Append(" ,"); + sql.Append(SqlInlineCheckTableConstraint(table, check)); + } + + // Default constraints are added inline with the column definition always during CREATE TABLE and ADD COLUMN + // foreach (var def in tableConstraints.DefaultConstraints) + // { + // sql.AppendLine(); + // sql.Append(" ,"); + // sql.Append(SqlInlineDefaultTableConstraint(table, def)); + // } + + foreach (var uc in tableConstraints.UniqueConstraints) + { + sql.AppendLine(); + sql.Append(" ,"); + sql.Append(SqlInlineUniqueTableConstraint(table, uc, supportsOrderedKeysInConstraints)); + } + + // When creating a single table, we can add the foreign keys inline. + // We assume that the referenced table already exists. + if (afterAllTablesConstraints == null && table.ForeignKeyConstraints.Count > 0) + { + foreach (var fk in table.ForeignKeyConstraints) + { + sql.AppendLine(); + sql.Append(" ,"); + sql.Append(SqlInlineForeignKeyTableConstraint(table, fk)); + } + } + + sql.AppendLine(); + sql.Append(")"); + + // TODO: for MySQL, we need to add the ENGINE=InnoDB; at the end of the CREATE TABLE statement + if (ProviderType == DbProviderType.MySql) + { + sql.Append( + " DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB" + ); + } + + sql.Append(';'); + + var sqlStatement = sql.ToString(); + + await ExecuteAsync(db, sqlStatement, tx: tx).ConfigureAwait(false); + + // Add indexes AFTER the table is created + foreach (var index in tableConstraints.Indexes) + { + await CreateIndexIfNotExistsAsync( + db, + index, + tx: tx, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + } + + return true; + } + + public virtual async Task CreateTableIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + DxColumn[] columns, + DxPrimaryKeyConstraint? primaryKey = null, + DxCheckConstraint[]? checkConstraints = null, + DxDefaultConstraint[]? defaultConstraints = null, + DxUniqueConstraint[]? uniqueConstraints = null, + DxForeignKeyConstraint[]? foreignKeyConstraints = null, + DxIndex[]? indexes = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException("Table name is required.", nameof(tableName)); + } + + if (columns == null || columns.Length == 0) + { + throw new ArgumentException("At least one column is required.", nameof(columns)); + } + + return await CreateTableIfNotExistsAsync( + db, + new DxTable( + schemaName, + tableName, + columns, + primaryKey, + checkConstraints, + defaultConstraints, + uniqueConstraints, + foreignKeyConstraints, + indexes + ), + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task GetTableAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrEmpty(tableName)) + { + throw new ArgumentException("Table name is required.", nameof(tableName)); + } + + return ( + await GetTablesAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false) + ).SingleOrDefault(); + } + + public virtual async Task> GetTableNamesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var (sql, parameters) = SqlGetTableNames(schemaName, tableNameFilter); + return await QueryAsync(db, sql, parameters, tx: tx).ConfigureAwait(false); + } + + public abstract Task> GetTablesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + public virtual async Task DropTableIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken); + + if (string.IsNullOrWhiteSpace(table?.TableName)) + return false; + + schemaName = table.SchemaName; + tableName = table.TableName; + + // drop all related objects + foreach (var index in table.Indexes) + { + await DropIndexIfExistsAsync( + db, + schemaName, + tableName, + index.IndexName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + foreach (var fk in table.ForeignKeyConstraints) + { + await DropForeignKeyConstraintIfExistsAsync( + db, + schemaName, + tableName, + fk.ConstraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + foreach (var uc in table.UniqueConstraints) + { + await DropUniqueConstraintIfExistsAsync( + db, + schemaName, + tableName, + uc.ConstraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + foreach (var dc in table.DefaultConstraints) + { + await DropDefaultConstraintIfExistsAsync( + db, + schemaName, + tableName, + dc.ConstraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + // USUALLY, this is done by the database provider, and + // it's not necessary to do it here. + // foreach (var cc in table.CheckConstraints) + // { + // await DropCheckConstraintIfExistsAsync( + // db, + // schemaName, + // tableName, + // cc.ConstraintName, + // tx, + // cancellationToken + // ) + // .ConfigureAwait(false); + // } + + // USUALLY, this is done by the database provider, and + // it's not necessary to do it here. + // await DropPrimaryKeyConstraintIfExistsAsync( + // db, + // schemaName, + // tableName, + // tx, + // cancellationToken + // ) + // .ConfigureAwait(false); + + var sql = SqlDropTable(schemaName, tableName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task RenameTableIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string newTableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException("Table name is required.", nameof(tableName)); + } + + if (string.IsNullOrWhiteSpace(newTableName)) + { + throw new ArgumentException("New table name is required.", nameof(newTableName)); + } + + if ( + !await DoesTableExistAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlRenameTable(schemaName, tableName, newTableName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task TruncateTableIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException("Table name is required.", nameof(tableName)); + } + + if ( + !await DoesTableExistAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlTruncateTable(schemaName, tableName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + protected abstract Task> GetIndexesInternalAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.UniqueConstraints.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.UniqueConstraints.cs new file mode 100644 index 0000000..f0287af --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.UniqueConstraints.cs @@ -0,0 +1,296 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesUniqueConstraintExistAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetUniqueConstraintAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task DoesUniqueConstraintExistOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await GetUniqueConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) != null; + } + + public virtual async Task CreateUniqueConstraintIfNotExistsAsync( + IDbConnection db, + DxUniqueConstraint constraint, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreateUniqueConstraintIfNotExistsAsync( + db, + constraint.SchemaName, + constraint.TableName, + constraint.ConstraintName, + constraint.Columns, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task CreateUniqueConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (columns.Length == 0) + throw new ArgumentException("At least one column must be specified.", nameof(columns)); + + if ( + await DoesUniqueConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var supportsOrderedKeysInConstraints = await SupportsOrderedKeysInConstraintsAsync( + db, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + var sql = SqlAlterTableAddUniqueConstraint( + schemaName, + tableName, + constraintName, + columns, + supportsOrderedKeysInConstraints + ); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task GetUniqueConstraintAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + var uniqueConstraints = await GetUniqueConstraintsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return uniqueConstraints.SingleOrDefault(); + } + + public virtual async Task GetUniqueConstraintNameOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return ( + await GetUniqueConstraintOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + )?.ConstraintName; + } + + public virtual async Task> GetUniqueConstraintNamesAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var uniqueConstraints = await GetUniqueConstraintsAsync( + db, + schemaName, + tableName, + constraintNameFilter, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return uniqueConstraints.Select(c => c.ConstraintName).ToList(); + } + + public virtual async Task GetUniqueConstraintOnColumnAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + var uniqueConstraints = await GetUniqueConstraintsAsync( + db, + schemaName, + tableName, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + return uniqueConstraints.FirstOrDefault(c => + c.Columns.Any(sc => + sc.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ) + ); + } + + public virtual async Task> GetUniqueConstraintsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? constraintNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + if (table == null) + return []; + + var filter = string.IsNullOrWhiteSpace(constraintNameFilter) + ? null + : ToSafeString(constraintNameFilter); + + return string.IsNullOrWhiteSpace(filter) + ? table.UniqueConstraints + : table + .UniqueConstraints.Where(c => c.ConstraintName.IsWildcardPatternMatch(filter)) + .ToList(); + } + + public virtual async Task DropUniqueConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await DoesUniqueConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlDropUniqueConstraint(schemaName, tableName, constraintName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task DropUniqueConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var constraintName = await GetUniqueConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(constraintName)) + return false; + + var sql = SqlDropUniqueConstraint(schemaName, tableName, constraintName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Views.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Views.cs new file mode 100644 index 0000000..f732bae --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.Views.cs @@ -0,0 +1,165 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase +{ + public virtual async Task DoesViewExistAsync( + IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return ( + await GetViewNamesAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false) + ).Count == 1; + } + + public virtual async Task CreateViewIfNotExistsAsync( + IDbConnection db, + DxView view, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await CreateViewIfNotExistsAsync( + db, + view.SchemaName, + view.ViewName, + view.Definition, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public virtual async Task CreateViewIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string viewName, + string definition, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrEmpty(definition)) + { + throw new ArgumentException("View definition is required.", nameof(definition)); + } + + if ( + await DoesViewExistAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlCreateView(schemaName, viewName, definition); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task GetViewAsync( + IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrEmpty(viewName)) + { + throw new ArgumentException("View name is required.", nameof(viewName)); + } + + return ( + await GetViewsAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false) + ).SingleOrDefault(); + } + + public virtual async Task> GetViewNamesAsync( + IDbConnection db, + string? schemaName, + string? viewNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var (sql, parameters) = SqlGetViewNames(schemaName, viewNameFilter); + return await QueryAsync(db, sql, parameters, tx: tx).ConfigureAwait(false); + } + + public virtual async Task> GetViewsAsync( + IDbConnection db, + string? schemaName, + string? viewNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var (sql, parameters) = SqlGetViews(schemaName, viewNameFilter); + var views = await QueryAsync(db, sql, parameters, tx: tx).ConfigureAwait(false); + foreach (var view in views) + { + view.Definition = NormalizeViewDefinition(view.Definition); + } + return views; + } + + public virtual async Task DropViewIfExistsAsync( + IDbConnection db, + string? schemaName, + string viewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await DoesViewExistAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + var sql = SqlDropView(schemaName, viewName); + + await ExecuteAsync(db, sql, tx: tx).ConfigureAwait(false); + + return true; + } + + public virtual async Task RenameViewIfExistsAsync( + IDbConnection db, + string? schemaName, + string viewName, + string newViewName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var view = await GetViewAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false); + + if (view == null || string.IsNullOrWhiteSpace(view.Definition)) + return false; + + await DropViewIfExistsAsync(db, schemaName, viewName, tx, cancellationToken) + .ConfigureAwait(false); + + await CreateViewIfNotExistsAsync( + db, + schemaName, + newViewName, + view.Definition, + tx, + cancellationToken + ); + + return true; + } +} diff --git a/src/DapperMatic/Providers/Base/DatabaseMethodsBase.cs b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.cs new file mode 100644 index 0000000..23234e2 --- /dev/null +++ b/src/DapperMatic/Providers/Base/DatabaseMethodsBase.cs @@ -0,0 +1,395 @@ +using System.Collections.Concurrent; +using System.Data; +using System.Text.Json; +using Dapper; +using DapperMatic.Interfaces; +using DapperMatic.Logging; +using Microsoft.Extensions.Logging; + +namespace DapperMatic.Providers.Base; + +public abstract partial class DatabaseMethodsBase : IDatabaseMethods +{ + public abstract DbProviderType ProviderType { get; } + + public abstract IDbProviderTypeMap ProviderTypeMap { get; } + + protected abstract string DefaultSchema { get; } + + public virtual bool SupportsSchemas => !string.IsNullOrWhiteSpace(DefaultSchema); + + public virtual Task SupportsCheckConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) => Task.FromResult(true); + + public virtual Task SupportsOrderedKeysInConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) => Task.FromResult(true); + + public virtual Task SupportsDefaultConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) => Task.FromResult(true); + + private ILogger Logger => DxLogger.CreateLogger(GetType()); + + public virtual DbProviderDotnetTypeDescriptor GetDotnetTypeFromSqlType(string sqlType) + { + if ( + !ProviderTypeMap.TryGetDotnetTypeDescriptorMatchingFullSqlTypeName( + sqlType, + out var dotnetTypeDescriptor + ) + || dotnetTypeDescriptor == null + ) + throw new NotSupportedException($"SQL type {sqlType} is not supported."); + + return dotnetTypeDescriptor; + } + + public string GetSqlTypeFromDotnetType(DbProviderDotnetTypeDescriptor descriptor) + { + var tmb = ProviderTypeMap as DbProviderTypeMapBase; + + if ( + !ProviderTypeMap.TryGetProviderSqlTypeMatchingDotnetType( + descriptor, + out var providerDataType + ) + || providerDataType == null + ) + { + if (tmb != null) + return tmb.SqTypeForUnknownDotnetType; + + throw new NotSupportedException( + $"No provider data type found for .NET type {descriptor}." + ); + } + + var length = descriptor.Length; + var precision = descriptor.Precision; + var scale = descriptor.Scale; + + if (providerDataType.SupportsLength()) + { + if (!length.HasValue && descriptor.DotnetType == typeof(Guid)) + length = 36; + if (!length.HasValue && descriptor.DotnetType == typeof(char)) + length = 1; + if (!length.HasValue) + length = providerDataType.DefaultLength; + + if (length.HasValue) + { + if ( + tmb != null + && length >= 8000 + && providerDataType.Affinity == DbProviderSqlTypeAffinity.Text + ) + return tmb.SqTypeForStringLengthMax; + + if ( + tmb != null + && length >= 8000 + && providerDataType.Affinity == DbProviderSqlTypeAffinity.Binary + ) + return tmb.SqTypeForBinaryLengthMax; + + if (!string.IsNullOrWhiteSpace(providerDataType.FormatWithLength)) + return string.Format(providerDataType.FormatWithLength, length); + } + } + + if (providerDataType.SupportsPrecision()) + { + precision ??= providerDataType.DefaultPrecision; + scale ??= providerDataType.DefaultScale; + + if ( + scale.HasValue + && !string.IsNullOrWhiteSpace(providerDataType.FormatWithPrecisionAndScale) + ) + return string.Format( + providerDataType.FormatWithPrecisionAndScale, + precision, + scale + ); + + if (!string.IsNullOrWhiteSpace(providerDataType.FormatWithPrecision)) + return string.Format(providerDataType.FormatWithPrecision, precision); + } + + return providerDataType.Name; + } + + internal readonly ConcurrentDictionary LastSqls = + new(); + + public abstract Task GetDatabaseVersionAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ); + + public string GetLastSql(IDbConnection db) + { + return LastSqls.TryGetValue(db.ConnectionString, out var sql) ? sql.sql : ""; + } + + public (string sql, object? parameters) GetLastSqlWithParams(IDbConnection db) + { + return LastSqls.TryGetValue(db.ConnectionString, out var sql) ? sql : ("", null); + } + + private void SetLastSql(IDbConnection db, string sql, object? param = null) + { + LastSqls.AddOrUpdate(db.ConnectionString, (sql, param), (_, _) => (sql, param)); + } + + protected virtual async Task> QueryAsync( + IDbConnection db, + string sql, + object? param = null, + IDbTransaction? tx = null, + int? commandTimeout = null, + CommandType? commandType = null + ) + { + try + { + Log( + LogLevel.Debug, + "[{provider}] Executing SQL query: {sql}, with parameters {parameters}", + ProviderType, + sql, + param == null ? "{}" : JsonSerializer.Serialize(param) + ); + + SetLastSql(db, sql, param); + return ( + await db.QueryAsync(sql, param, tx, commandTimeout, commandType) + .ConfigureAwait(false) + ).AsList(); + } + catch (Exception ex) + { + Log( + LogLevel.Error, + ex, + "An error occurred while executing {provider} SQL query with map {providerMap}: \n{message}\n{sql}, with parameters {parameters}.", + ProviderType, + ProviderTypeMap.GetType().Name, + ex.Message, + sql, + param == null ? "{}" : JsonSerializer.Serialize(param) + ); + throw; + } + } + + protected virtual async Task ExecuteScalarAsync( + IDbConnection db, + string sql, + object? param = null, + IDbTransaction? tx = null, + int? commandTimeout = null, + CommandType? commandType = null + ) + { + try + { + Log( + LogLevel.Debug, + "[{provider}] Executing SQL scalar: {sql}, with parameters {parameters}", + ProviderType, + sql, + param == null ? "{}" : JsonSerializer.Serialize(param) + ); + + SetLastSql(db, sql, param); + return await db.ExecuteScalarAsync( + sql, + param, + tx, + commandTimeout, + commandType + ); + } + catch (Exception ex) + { + Log( + LogLevel.Error, + ex, + "An error occurred while executing {provider} SQL scalar query with map {providerMap}: \n{message}\n{sql}, with parameters {parameters}.", + ProviderType, + ProviderTypeMap.GetType().Name, + ex.Message, + sql, + param == null ? "{}" : JsonSerializer.Serialize(param) + ); + throw; + } + } + + protected virtual async Task ExecuteAsync( + IDbConnection db, + string sql, + object? param = null, + IDbTransaction? tx = null, + int? commandTimeout = null, + CommandType? commandType = null + ) + { + try + { + Log( + LogLevel.Debug, + "[{provider}] Executing SQL statement: {sql}, with parameters {parameters}", + ProviderType, + sql, + param == null ? "{}" : JsonSerializer.Serialize(param) + ); + + SetLastSql(db, sql, param); + return await db.ExecuteAsync(sql, param, tx, commandTimeout, commandType); + } + catch (Exception ex) + { + Log( + LogLevel.Error, + ex, + "An error occurred while executing {provider} SQL statement with map {providerMap}: \n{message}\n{sql}, with parameters {parameters}.", + ProviderType, + ProviderTypeMap.GetType().Name, + ex.Message, + sql, + param == null ? "{}" : JsonSerializer.Serialize(param) + ); + throw; + } + } + + public abstract char[] QuoteChars { get; } + + protected virtual string GetQuotedIdentifier(string identifier) + { + return "".ToQuotedIdentifier(QuoteChars, identifier); + } + + protected virtual string GetQuotedCompoundIdentifier(string[] identifiers, string union = ".") + { + return string.Join(union, identifiers.Select(x => "".ToQuotedIdentifier(QuoteChars, x))); + } + + protected virtual string ToSafeString(string text, string allowedSpecialChars = "-_.*") + { + return text.ToAlphaNumeric(allowedSpecialChars); + } + + protected virtual string ToLikeString(string text, string allowedSpecialChars = "-_.*") + { + return text.ToAlphaNumeric(allowedSpecialChars).Replace("*", "%"); //.Replace("?", "_"); + } + + public virtual string GetSchemaQualifiedIdentifierName(string? schemaName, string tableName) + { + schemaName = NormalizeSchemaName(schemaName); + tableName = NormalizeName(tableName); + + return SupportsSchemas && !string.IsNullOrWhiteSpace(schemaName) + ? $"{schemaName.ToQuotedIdentifier(QuoteChars)}.{tableName.ToQuotedIdentifier(QuoteChars)}" + : tableName.ToQuotedIdentifier(QuoteChars); + } + + /// + /// The default implementation simply removes all non-alphanumeric characters from the provided name identifier, replacing them with underscores. + /// + public virtual string NormalizeName(string name) + { + return name.ToAlphaNumeric("_"); + } + + /// + /// The schema name is normalized to the default schema if it is null or empty. + /// If the default schema is null or empty, + /// the implementation simply removes all non-alphanumeric characters from the provided name, replacing them with underscores. + /// + /// + /// + protected virtual string NormalizeSchemaName(string? schemaName) + { + if (!SupportsSchemas) + return string.Empty; + + return string.IsNullOrWhiteSpace(schemaName) ? DefaultSchema : NormalizeName(schemaName); + } + + /// + /// The default implementation simply removes all non-alphanumeric characters from the schema, table, and identifier names, replacing them with underscores. + /// The schema name is normalized to the default schema if it is null or empty. + /// If the default schema is null or empty, the schema name is normalized as the other names. + /// + /// + /// + /// + /// + protected virtual (string schemaName, string tableName, string identifierName) NormalizeNames( + string? schemaName = null, + string? tableName = null, + string? identifierName = null + ) + { + schemaName = NormalizeSchemaName(schemaName); + + if (!string.IsNullOrWhiteSpace(tableName)) + tableName = NormalizeName(tableName); + + if (!string.IsNullOrWhiteSpace(identifierName)) + identifierName = NormalizeName(identifierName); + + return (schemaName, tableName ?? "", identifierName ?? ""); + } + + // ReSharper disable once MemberCanBePrivate.Global + protected void Log(LogLevel logLevel, string message, params object?[] args) + { + if (!Logger.IsEnabled(logLevel)) + return; + + try + { + Logger.Log(logLevel, message, args); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + // ReSharper disable once MemberCanBePrivate.Global + protected void Log( + LogLevel logLevel, + Exception exception, + string message, + params object?[] args + ) + { + if (!Logger.IsEnabled(logLevel)) + return; + + try + { + Logger.Log(logLevel, exception, message, args); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } +} diff --git a/src/DapperMatic/Providers/DatabaseExtensionsBase.cs b/src/DapperMatic/Providers/DatabaseExtensionsBase.cs deleted file mode 100644 index c067550..0000000 --- a/src/DapperMatic/Providers/DatabaseExtensionsBase.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System.Collections.Concurrent; -using System.Data; -using Dapper; - -namespace DapperMatic.Providers; - -public abstract class DatabaseExtensionsBase -{ - protected abstract string DefaultSchema { get; } - - protected abstract List DataTypes { get; } - - protected DataTypeMap? GetDbType(Type type) - { - var dotnetType = Nullable.GetUnderlyingType(type) ?? type; - return DataTypes.FirstOrDefault(x => x.DotnetType == type); - } - - protected string GetSqlTypeString( - Type type, - int? length = null, - int? precision = null, - int? scale = null - ) - { - var dotnetType = Nullable.GetUnderlyingType(type) ?? type; - var dataType = GetDbType(dotnetType); - - if (dataType == null) - { - throw new NotSupportedException($"Type {type} is not supported."); - } - - if (length != null && length > 0) - { - if (length == int.MaxValue) - { - return string.Format(dataType.SqlTypeWithMaxLength ?? dataType.SqlType, length); - } - else - { - return string.Format(dataType.SqlTypeWithLength ?? dataType.SqlType, length); - } - } - else if (precision != null) - { - return string.Format( - dataType.SqlTypeWithPrecisionAndScale ?? dataType.SqlType, - precision, - scale ?? 0 - ); - } - - return dataType.SqlType; - } - - protected virtual string NormalizeName(string name) - { - return ToAlphaNumericString(name); - } - - protected virtual string NormalizeSchemaName(string? schemaName) - { - if (string.IsNullOrWhiteSpace(schemaName)) - schemaName = DefaultSchema; - else - schemaName = NormalizeName(schemaName); - - return schemaName; - } - - protected virtual (string schemaName, string tableName, string identifierName) NormalizeNames( - string? schemaName = null, - string? tableName = null, - string? identifierName = null - ) - { - schemaName = NormalizeSchemaName(schemaName); - - if (!string.IsNullOrWhiteSpace(tableName)) - tableName = NormalizeName(tableName); - - if (!string.IsNullOrWhiteSpace(identifierName)) - identifierName = NormalizeName(identifierName); - - return (schemaName ?? "", tableName ?? "", identifierName ?? ""); - } - - protected virtual string ToAlphaNumericString(string text) - { - // var rgx = new Regex("[^a-zA-Z0-9_.]"); - // return rgx.Replace(text, ""); - - char[] arr = text.Where(c => - char.IsLetterOrDigit(c) || char.IsWhiteSpace(c) || c == '-' || c == '_' || c == '.' - ) - .ToArray(); - - return new string(arr); - } - - public virtual Task SupportsSchemasAsync( - IDbConnection connection, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(true); - } - - public virtual Task SupportsNamedForeignKeysAsync( - IDbConnection connection, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(true); - } - - internal static readonly ConcurrentDictionary< - string, - (string sql, object? parameters) - > _lastSqls = new(); - - public string GetLastSql(IDbConnection connection) - { - return _lastSqls.TryGetValue(connection.ConnectionString, out var sql) ? sql.sql : ""; - } - - public (string sql, object? parameters) GetLastSqlWithParams(IDbConnection connection) - { - return _lastSqls.TryGetValue(connection.ConnectionString, out var sql) ? sql : ("", null); - } - - private static void SetLastSql(IDbConnection connection, string sql, object? param = null) - { - _lastSqls.AddOrUpdate( - connection.ConnectionString, - (sql, param), - (key, oldValue) => (sql, param) - ); - } - - protected virtual async Task> QueryAsync( - IDbConnection connection, - string sql, - object? param = null, - IDbTransaction? transaction = null, - int? commandTimeout = null, - CommandType? commandType = null - ) - { - try - { - SetLastSql(connection, sql, param); - return await connection.QueryAsync( - sql, - param, - transaction, - commandTimeout, - commandType - ); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - Console.WriteLine("SQL: " + sql); - throw; - } - } - - protected virtual async Task ExecuteScalarAsync( - IDbConnection connection, - string sql, - object? param = null, - IDbTransaction? transaction = null, - int? commandTimeout = null, - CommandType? commandType = null - ) - { - try - { - SetLastSql(connection, sql, param); - return await connection.ExecuteScalarAsync( - sql, - param, - transaction, - commandTimeout, - commandType - ); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - Console.WriteLine("SQL: " + sql); - throw; - } - } - - protected virtual async Task ExecuteAsync( - IDbConnection connection, - string sql, - object? param = null, - IDbTransaction? transaction = null, - int? commandTimeout = null, - CommandType? commandType = null - ) - { - try - { - SetLastSql(connection, sql, param); - return await connection.ExecuteAsync( - sql, - param, - transaction, - commandTimeout, - commandType - ); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - Console.WriteLine("SQL: " + sql); - throw; - } - } -} diff --git a/src/DapperMatic/Providers/DatabaseMethodsFactory.cs b/src/DapperMatic/Providers/DatabaseMethodsFactory.cs new file mode 100644 index 0000000..a02babf --- /dev/null +++ b/src/DapperMatic/Providers/DatabaseMethodsFactory.cs @@ -0,0 +1,42 @@ +using System.Collections.Concurrent; +using System.Data; +using DapperMatic.Interfaces; +using DapperMatic.Providers.MySql; +using DapperMatic.Providers.PostgreSql; +using DapperMatic.Providers.Sqlite; +using DapperMatic.Providers.SqlServer; + +namespace DapperMatic.Providers; + +internal static class DatabaseMethodsFactory +{ + private static readonly ConcurrentDictionary MethodsCache = + new(); + + public static IDatabaseMethods GetDatabaseMethods(IDbConnection db) + { + return GetDatabaseMethods(db.GetDbProviderType()); + } + + private static IDatabaseMethods GetDatabaseMethods(DbProviderType providerType) + { + // Try to get the DxTable from the cache + if (MethodsCache.TryGetValue(providerType, out var databaseMethods)) + { + return databaseMethods; + } + + databaseMethods = providerType switch + { + DbProviderType.Sqlite => new SqliteMethods(), + DbProviderType.SqlServer => new SqlServerMethods(), + DbProviderType.MySql => new MySqlMethods(), + DbProviderType.PostgreSql => new PostgreSqlMethods(), + _ => throw new NotSupportedException($"Provider {providerType} is not supported.") + }; + + MethodsCache.TryAdd(providerType, databaseMethods); + + return databaseMethods; + } +} diff --git a/src/DapperMatic/Providers/DbProviderDotnetTypeDescriptor.cs b/src/DapperMatic/Providers/DbProviderDotnetTypeDescriptor.cs new file mode 100644 index 0000000..514bf99 --- /dev/null +++ b/src/DapperMatic/Providers/DbProviderDotnetTypeDescriptor.cs @@ -0,0 +1,72 @@ +using System.Text; + +namespace DapperMatic.Providers; + +// The struct to store the parameters: +public class DbProviderDotnetTypeDescriptor +{ + public DbProviderDotnetTypeDescriptor( + Type dotnetType, + int? length = null, + int? precision = null, + int? scale = null, + bool? autoIncrement = false, + bool? unicode = false, + Type[]? otherSupportedTypes = null + ) + { + DotnetType = + ( + dotnetType.IsGenericType + && dotnetType.GetGenericTypeDefinition() == typeof(Nullable<>) + ) + ? Nullable.GetUnderlyingType(dotnetType)! + : dotnetType; + Length = length; + Precision = precision; + Scale = scale; + AutoIncrement = autoIncrement; + Unicode = unicode; + this.otherSupportedTypes = otherSupportedTypes ?? []; + } + + /// + /// Non-nullable type used to determine or map to a recommended sql type + /// + public Type DotnetType { get; init; } + public int? Length { get; init; } + public int? Precision { get; init; } + public int? Scale { get; init; } + public bool? AutoIncrement { get; init; } + public bool? Unicode { get; init; } + + private Type[] otherSupportedTypes = []; + public Type[] AllSupportedTypes + { + get => [DotnetType, .. otherSupportedTypes.Where(t => t != DotnetType).Distinct()]; + set => otherSupportedTypes = value; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(DotnetType.GetFriendlyName()); + if (Length.HasValue) + { + sb.Append($" LENGTH({Length})"); + } + if (Precision.HasValue) + { + sb.Append($" PRECISION({Precision})"); + } + if (AutoIncrement.HasValue) + { + sb.Append(" AUTO_INCREMENT"); + } + if (Unicode.HasValue) + { + sb.Append(" UNICODE"); + } + return sb.ToString(); + } +} diff --git a/src/DapperMatic/Providers/DbProviderTypeMapBase.cs b/src/DapperMatic/Providers/DbProviderTypeMapBase.cs new file mode 100644 index 0000000..06b14ed --- /dev/null +++ b/src/DapperMatic/Providers/DbProviderTypeMapBase.cs @@ -0,0 +1,363 @@ +using System.Collections.Concurrent; + +namespace DapperMatic.Providers; + +public abstract class DbProviderTypeMapBase : IDbProviderTypeMap +{ + // ReSharper disable once MemberCanBePrivate.Global + // ReSharper disable once CollectionNeverUpdated.Global + public static readonly ConcurrentDictionary> TypeMaps = + new(); + + protected abstract DbProviderType ProviderType { get; } + protected abstract DbProviderSqlType[] ProviderSqlTypes { get; } + + private Dictionary? _lookup = null; + protected virtual Dictionary ProviderSqlTypeLookup => + _lookup ??= ProviderSqlTypes.ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase); + + public abstract string SqTypeForStringLengthMax { get; } + public abstract string SqTypeForBinaryLengthMax { get; } + public abstract string SqlTypeForJson { get; } + public virtual string SqTypeForUnknownDotnetType => SqTypeForStringLengthMax; + protected virtual int DefaultLength { get; set; } = 255; + + public virtual bool UseIntegersForEnumTypes { get; set; } = false; + + public virtual bool TryGetDotnetTypeDescriptorMatchingFullSqlTypeName( + string fullSqlType, + out DbProviderDotnetTypeDescriptor? descriptor + ) + { + descriptor = new DbProviderDotnetTypeDescriptor(typeof(string)); + + // Prioritize any custom mappings + if (TypeMaps.TryGetValue(ProviderType, out var additionalTypeMaps)) + { + foreach (var typeMap in additionalTypeMaps) + { + if ( + typeMap.TryGetDotnetTypeDescriptorMatchingFullSqlTypeName( + fullSqlType, + out var rdt + ) + ) + { + descriptor = rdt; + return true; + } + } + } + + if ( + !TryGetProviderSqlTypeFromFullSqlTypeName(fullSqlType, out var providerSqlType) + || providerSqlType == null + ) + return false; + + // perform some detective reasoning to pinpoint a recommended type + var numbers = fullSqlType.ExtractNumbers(); + var isAutoIncrementing = providerSqlType.AutoIncrementsAutomatically; + var unicode = providerSqlType.IsUnicode; + + switch (providerSqlType.Affinity) + { + case DbProviderSqlTypeAffinity.Binary: + descriptor = new(typeof(byte[])); + break; + case DbProviderSqlTypeAffinity.Boolean: + descriptor = new( + typeof(bool), + otherSupportedTypes: + [ + typeof(short), + typeof(int), + typeof(long), + typeof(ushort), + typeof(uint), + typeof(ulong), + typeof(string) + ] + ); + break; + case DbProviderSqlTypeAffinity.DateTime: + if (providerSqlType.IsDateOnly == true) + descriptor = new( + typeof(DateOnly), + otherSupportedTypes: [typeof(DateOnly), typeof(DateTime), typeof(string)] + ); + else if (providerSqlType.IsTimeOnly == true) + descriptor = new( + typeof(TimeOnly), + otherSupportedTypes: [typeof(TimeOnly), typeof(DateTime), typeof(string)] + ); + else if (providerSqlType.IsYearOnly == true) + descriptor = new( + typeof(int), + otherSupportedTypes: + [ + typeof(short), + typeof(long), + typeof(ushort), + typeof(uint), + typeof(ulong), + typeof(string) + ] + ); + else if (providerSqlType.IncludesTimeZone == true) + descriptor = new DbProviderDotnetTypeDescriptor( + typeof(DateTimeOffset), + otherSupportedTypes: [typeof(DateTime), typeof(string)] + ); + else + descriptor = new( + typeof(DateTime), + otherSupportedTypes: [typeof(DateTimeOffset), typeof(string)] + ); + break; + case DbProviderSqlTypeAffinity.Integer: + int? intPrecision = numbers.Length > 0 ? numbers[0] : null; + if (providerSqlType.MinValue.HasValue && providerSqlType.MinValue == 0) + { + if (providerSqlType.MaxValue.HasValue) + { + if (providerSqlType.MaxValue.Value <= ushort.MaxValue) + descriptor = new( + typeof(ushort), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: + [ + typeof(short), + typeof(int), + typeof(long), + typeof(uint), + typeof(ulong), + typeof(string) + ] + ); + else if (providerSqlType.MaxValue.Value <= uint.MaxValue) + descriptor = new( + typeof(uint), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: + [ + typeof(int), + typeof(long), + typeof(ulong), + typeof(string) + ] + ); + else if (providerSqlType.MaxValue.Value <= ulong.MaxValue) + descriptor = new( + typeof(ulong), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: [typeof(long), typeof(string)] + ); + } + descriptor ??= new( + typeof(uint), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: + [ + typeof(int), + typeof(long), + typeof(ulong), + typeof(string) + ] + ); + } + if (descriptor == null) + { + if (providerSqlType.MaxValue.HasValue) + { + if (providerSqlType.MaxValue.Value <= short.MaxValue) + descriptor = new( + typeof(short), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: [typeof(int), typeof(long), typeof(string)] + ); + else if (providerSqlType.MaxValue.Value <= int.MaxValue) + descriptor = new( + typeof(int), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: [typeof(long), typeof(string)] + ); + else if (providerSqlType.MaxValue.Value <= long.MaxValue) + descriptor = new( + typeof(long), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: [typeof(string)] + ); + } + descriptor ??= new( + typeof(int), + precision: intPrecision, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: [typeof(long), typeof(string)] + ); + } + break; + case DbProviderSqlTypeAffinity.Real: + int? precision = numbers.Length > 0 ? numbers[0] : null; + int? scale = numbers.Length > 1 ? numbers[1] : null; + descriptor = new( + typeof(decimal), + precision: precision, + scale: scale, + autoIncrement: isAutoIncrementing, + otherSupportedTypes: + [ + typeof(decimal), + typeof(float), + typeof(double), + typeof(string) + ] + ); + break; + case DbProviderSqlTypeAffinity.Text: + int? length = numbers.Length > 0 ? numbers[0] : null; + if (length >= 8000) + length = int.MaxValue; + descriptor = new( + typeof(string), + length: length, + unicode: unicode, + otherSupportedTypes: [typeof(string)] + ); + break; + case DbProviderSqlTypeAffinity.Geometry: + case DbProviderSqlTypeAffinity.RangeType: + case DbProviderSqlTypeAffinity.Other: + default: + if ( + providerSqlType.Name.Contains("json", StringComparison.OrdinalIgnoreCase) + || providerSqlType.Name.Contains("xml", StringComparison.OrdinalIgnoreCase) + ) + descriptor = new( + typeof(string), + length: int.MaxValue, + unicode: unicode, + otherSupportedTypes: [typeof(string)] + ); + else + descriptor = new( + typeof(object), + otherSupportedTypes: [typeof(object), typeof(string)] + ); + break; + } + + return descriptor != null; + } + + public virtual bool TryGetProviderSqlTypeMatchingDotnetType( + DbProviderDotnetTypeDescriptor descriptor, + out DbProviderSqlType? providerSqlType + ) + { + providerSqlType = null; + + // Prioritize any custom mappings + if (TypeMaps.TryGetValue(ProviderType, out var additionalTypeMaps)) + { + foreach (var typeMap in additionalTypeMaps) + { + if (typeMap.TryGetProviderSqlTypeMatchingDotnetType(descriptor, out var rdt)) + { + providerSqlType = rdt; + return true; + } + } + } + + var dotnetType = descriptor.DotnetType; + + // Enums become strings, or integers if UseIntegersForEnumTypes is true + if (dotnetType.IsEnum) + { + return TryGetProviderSqlTypeMatchingEnumType( + dotnetType, + descriptor.Length, + ref providerSqlType + ); + } + + // char becomes string(1) + if (dotnetType == typeof(char) && (descriptor.Length == null || descriptor.Length == 1)) + { + return TryGetProviderSqlTypeMatchingDotnetType( + new DbProviderDotnetTypeDescriptor(typeof(string), 1, unicode: descriptor.Unicode), + out providerSqlType + ); + } + + if (TryGetProviderSqlTypeMatchingDotnetTypeInternal(descriptor, out providerSqlType)) + return true; + + return providerSqlType != null; + } + + protected abstract bool TryGetProviderSqlTypeMatchingDotnetTypeInternal( + DbProviderDotnetTypeDescriptor descriptor, + out DbProviderSqlType? providerSqlType + ); + + protected virtual bool TryGetProviderSqlTypeFromFullSqlTypeName( + string fullSqlType, + out DbProviderSqlType? providerSqlType + ) + { + // perform some detective reasoning to pinpoint a recommended type + var numbers = fullSqlType.ExtractNumbers(); + + // try to find a sql provider type match by removing the length, precision, and scale + // from the sql type name and converting it to an alpha only representation of the type + var fullSqlTypeAlpha = fullSqlType + .DiscardLengthPrecisionAndScaleFromSqlTypeName() + .ToAlpha("[]"); + + providerSqlType = ProviderSqlTypes.FirstOrDefault(t => + t.Name.DiscardLengthPrecisionAndScaleFromSqlTypeName() + .ToAlpha("[]") + .Equals(fullSqlTypeAlpha, StringComparison.OrdinalIgnoreCase) + ); + + return providerSqlType != null; + } + + protected virtual bool TryGetProviderSqlTypeMatchingEnumType( + Type dotnetType, + int? length, + ref DbProviderSqlType? providerSqlType + ) + { + if (UseIntegersForEnumTypes) + { + return TryGetProviderSqlTypeMatchingDotnetType( + new DbProviderDotnetTypeDescriptor(typeof(int), length), + out providerSqlType + ); + } + + if (length == null) + { + var maxEnumNameLength = Enum.GetNames(dotnetType).Max(m => m.Length); + var x = 64; + while (x < maxEnumNameLength) + x *= 2; + length = x; + } + + return TryGetProviderSqlTypeMatchingDotnetType( + new DbProviderDotnetTypeDescriptor(typeof(string), length), + out providerSqlType + ); + } +} diff --git a/src/DapperMatic/Providers/DbProviderUtils.cs b/src/DapperMatic/Providers/DbProviderUtils.cs new file mode 100644 index 0000000..04b776e --- /dev/null +++ b/src/DapperMatic/Providers/DbProviderUtils.cs @@ -0,0 +1,71 @@ +using System.Text.RegularExpressions; + +namespace DapperMatic.Providers; + +public static partial class DbProviderUtils +{ + public static string GenerateCheckConstraintName(string tableName, string columnName) + { + return "ck".ToRawIdentifier(tableName, columnName); + } + + public static string GenerateDefaultConstraintName(string tableName, string columnName) + { + return "df".ToRawIdentifier(tableName, columnName); + } + + public static string GenerateUniqueConstraintName(string tableName, params string[] columnNames) + { + return "uc".ToRawIdentifier([tableName, .. columnNames]); + } + + public static string GeneratePrimaryKeyConstraintName( + string tableName, + params string[] columnNames + ) + { + return "pk".ToRawIdentifier([tableName, .. columnNames]); + } + + public static string GenerateIndexName(string tableName, params string[] columnNames) + { + return "ix".ToRawIdentifier([tableName, .. columnNames]); + } + + public static string GenerateForeignKeyConstraintName( + string tableName, + string columnName, + string refTableName, + string refColumnName + ) + { + return "fk".ToRawIdentifier(tableName, columnName, refTableName, refColumnName); + } + + public static string GenerateForeignKeyConstraintName( + string tableName, + string[] columnNames, + string refTableName, + string[] refColumnNames + ) + { + return "fk".ToRawIdentifier([tableName, .. columnNames, refTableName, .. refColumnNames]); + } + + [GeneratedRegex(@"\d+(\.\d+)+")] + private static partial Regex VersionPatternRegex(); + + private static readonly Regex VersionPattern = VersionPatternRegex(); + + internal static Version ExtractVersionFromVersionString(string versionString) + { + var m = VersionPattern.Match(versionString); + var version = m.Value; + return Version.TryParse(version, out var vs) + ? vs + : throw new ArgumentException( + $"Could not extract version from: {versionString}", + nameof(versionString) + ); + } +} diff --git a/src/DapperMatic/Providers/IDbProviderTypeMap.cs b/src/DapperMatic/Providers/IDbProviderTypeMap.cs new file mode 100644 index 0000000..b555beb --- /dev/null +++ b/src/DapperMatic/Providers/IDbProviderTypeMap.cs @@ -0,0 +1,14 @@ +namespace DapperMatic.Providers; + +public interface IDbProviderTypeMap +{ + bool TryGetDotnetTypeDescriptorMatchingFullSqlTypeName( + string fullSqlType, + out DbProviderDotnetTypeDescriptor? descriptor + ); + + bool TryGetProviderSqlTypeMatchingDotnetType( + DbProviderDotnetTypeDescriptor descriptor, + out DbProviderSqlType? providerSqlType + ); +} diff --git a/src/DapperMatic/Providers/MySql/MySqlExtensions.ColumnMethods.cs b/src/DapperMatic/Providers/MySql/MySqlExtensions.ColumnMethods.cs deleted file mode 100644 index 34e89af..0000000 --- a/src/DapperMatic/Providers/MySql/MySqlExtensions.ColumnMethods.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.MySql; - -public partial class MySqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task ColumnExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = @tableName AND COLUMN_NAME = @columnName", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateColumnIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - Type dotnetType, - string? type = null, - int? length = null, - int? precision = null, - int? scale = null, - string? schemaName = null, - string? defaultValue = null, - bool nullable = true, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var sqlType = type ?? GetSqlTypeString(dotnetType, length, precision, scale); - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - // create MySql columnName - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` - ADD COLUMN `{columnName}` {sqlType} {(nullable ? "NULL" : "NOT NULL")} {(!string.IsNullOrWhiteSpace(defaultValue) ? $"DEFAULT {defaultValue}" : "")} {(unique ? "UNIQUE" : "")}", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetColumnNamesAsync( - IDbConnection db, - string tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return await QueryAsync( - db, - $@"SELECT COLUMN_NAME FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = @tableName - ORDER BY ORDINAL_POSITION", - new { tableName }, - tx - ) - .ConfigureAwait(false); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - $@"SELECT COLUMN_NAME FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = @tableName - AND COLUMN_NAME LIKE @where - ORDER BY ORDINAL_POSITION", - new { tableName, where }, - tx - ) - .ConfigureAwait(false); - } - } - - public async Task DropColumnIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` DROP COLUMN `{columnName}`", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/MySql/MySqlExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/MySql/MySqlExtensions.ForeignKeyMethods.cs deleted file mode 100644 index 8701fc0..0000000 --- a/src/DapperMatic/Providers/MySql/MySqlExtensions.ForeignKeyMethods.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic.Providers.MySql; - -public partial class MySqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task ForeignKeyExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - if (!string.IsNullOrWhiteSpace(foreignKey)) - { - var foreignKeyName = NormalizeName(foreignKey); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM information_schema.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - CONSTRAINT_NAME = @foreignKeyName AND - CONSTRAINT_TYPE = 'FOREIGN KEY'", - new { tableName, foreignKeyName }, - tx - ) - .ConfigureAwait(false); - } - else - { - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - COLUMN_NAME = @columnName AND - REFERENCED_TABLE_NAME IS NOT NULL", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - } - } - - public async Task CreateForeignKeyIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string foreignKey, - string referenceTable, - string referenceColumn, - string? schemaName = null, - string onDelete = "NO ACTION", - string onUpdate = "NO ACTION", - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(foreignKey)) - throw new ArgumentException("Foreign key name must be specified.", nameof(foreignKey)); - if (string.IsNullOrWhiteSpace(referenceTable)) - throw new ArgumentException( - "Reference tableName name must be specified.", - nameof(referenceTable) - ); - if (string.IsNullOrWhiteSpace(referenceColumn)) - throw new ArgumentException( - "Reference columnName name must be specified.", - nameof(referenceColumn) - ); - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - - if ( - await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - var (_, referenceTableName, referenceColumnName) = NormalizeNames( - schemaName, - referenceTable, - referenceColumn - ); - - var foreignKeyName = NormalizeName(foreignKey); - - // add the foreign key to the MySql database tableName - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` - ADD CONSTRAINT `{foreignKeyName}` - FOREIGN KEY (`{columnName}`) - REFERENCES `{referenceTableName}` (`{referenceColumnName}`) - ON DELETE {onDelete} - ON UPDATE {onUpdate}", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetForeignKeysAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? "" - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"SELECT - kcu.CONSTRAINT_NAME as constraint_name, - kcu.TABLE_NAME as table_name, - kcu.COLUMN_NAME as column_name, - kcu.REFERENCED_TABLE_NAME as referenced_table_name, - kcu.REFERENCED_COLUMN_NAME as referenced_column_name, - rc.DELETE_RULE as delete_rule, - rc.UPDATE_RULE as update_rule - FROM information_schema.KEY_COLUMN_USAGE kcu - INNER JOIN information_schema.referential_constraints rc ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME - WHERE kcu.TABLE_SCHEMA = DATABASE() AND kcu.REFERENCED_TABLE_NAME IS NOT NULL"; - if (!string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND kcu.TABLE_NAME = @tableName"; - if (!string.IsNullOrWhiteSpace(where)) - sql += $@" AND kcu.CONSTRAINT_NAME LIKE @where"; - sql += " ORDER BY kcu.TABLE_NAME, kcu.CONSTRAINT_NAME"; - - var results = await QueryAsync<( - string constraint_name, - string table_name, - string column_name, - string referenced_table_name, - string referenced_column_name, - string delete_rule, - string update_rule - )>(db, sql, new { tableName, where }, tx) - .ConfigureAwait(false); - - return results.Select(r => - { - var deleteRule = r.delete_rule switch - { - "NO ACTION" => ReferentialAction.NoAction, - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - _ => ReferentialAction.NoAction - }; - var updateRule = r.update_rule switch - { - "NO ACTION" => ReferentialAction.NoAction, - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - _ => ReferentialAction.NoAction - }; - return new ForeignKey( - null, - r.constraint_name, - r.table_name, - r.column_name, - r.referenced_table_name, - r.referenced_column_name, - deleteRule, - updateRule - ); - }); - } - - public async Task> GetForeignKeyNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"SELECT CONSTRAINT_NAME - FROM information_schema.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = DATABASE() AND - CONSTRAINT_TYPE = 'FOREIGN KEY'" - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND TABLE_NAME = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND CONSTRAINT_NAME LIKE @where") - + @" ORDER BY CONSTRAINT_NAME"; - - return await QueryAsync(db, sql, new { tableName, where }, tx) - .ConfigureAwait(false); - } - - /// - /// In SQLite, to drop a foreign key, you must re-create the tableName without the foreign key, - /// and then re-insert the data. It's a costly operation. - /// - /// - /// Example: https://www.techonthenet.com/sqlite/foreign_keys/drop.php - /// - public async Task DropForeignKeyIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - var fkExists = await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false); - if (!fkExists) - return false; - - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - if (!string.IsNullOrWhiteSpace(foreignKey)) - { - var foreignKeyName = NormalizeName(foreignKey); - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` DROP FOREIGN KEY `{foreignKeyName}`", - transaction: tx - ) - .ConfigureAwait(false); - } - else - { - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - // get the name of the foreign key for the columnName - var foreignKeyName = await ExecuteScalarAsync( - db, - $@"SELECT CONSTRAINT_NAME - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - COLUMN_NAME = @columnName AND - REFERENCED_TABLE_NAME IS NOT NULL", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(foreignKeyName)) - { - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` DROP FOREIGN KEY `{foreignKeyName}`", - transaction: tx - ) - .ConfigureAwait(false); - } - } - - return true; - } -} diff --git a/src/DapperMatic/Providers/MySql/MySqlExtensions.IndexMethods.cs b/src/DapperMatic/Providers/MySql/MySqlExtensions.IndexMethods.cs deleted file mode 100644 index 3237324..0000000 --- a/src/DapperMatic/Providers/MySql/MySqlExtensions.IndexMethods.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic.Providers.MySql; - -public partial class MySqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task IndexExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - // does indexName exist in MySql tableName - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - INDEX_NAME = @indexName", - new { tableName, indexName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateIndexIfNotExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string[] columnNames, - string? schemaName = null, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var uniqueString = unique ? "UNIQUE" : ""; - var columnList = string.Join(", ", columnNames); - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` - ADD {uniqueString} INDEX `{indexName}` ({columnList})", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetIndexesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql1 = - $@"SELECT * FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE()" - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND TABLE_NAME = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND INDEX_NAME LIKE @where") - + " ORDER BY TABLE_NAME, INDEX_NAME"; - var results1 = await QueryAsync(db, sql1, new { tableName, where }, tx) - .ConfigureAwait(false); - - var sql = - $@"SELECT - TABLE_NAME AS table_name, - INDEX_NAME AS index_name, - COLUMN_NAME AS column_name, - NON_UNIQUE AS non_unique, - INDEX_TYPE AS index_type, - SEQ_IN_INDEX AS seq_in_index, - COLLATION AS collation - FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = DATABASE()" - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND TABLE_NAME = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND INDEX_NAME LIKE @where") - + @" ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX"; - - var results = - await QueryAsync<(string table_name, string index_name, string column_name, int non_unique, string index_type, int seq_in_index, string collation)>( - db, - sql, - new { tableName, where }, - tx - ) - .ConfigureAwait(false); - - var grouped = results.GroupBy( - r => (r.table_name, r.index_name), - r => (r.non_unique, r.column_name, r.index_type, r.seq_in_index, r.collation) - ); - - var indexes = new List(); - foreach (var group in grouped) - { - var (table_name, index_name) = group.Key; - var (non_unique, column_name, index_type, seq_in_index, collation) = group.First(); - var columnNames = group - .Select( - g => - { - var col = g.column_name; - var direction = g.collation.Equals( - "D", - StringComparison.OrdinalIgnoreCase - ) - ? "DESC" - : "ASC"; - return $"{col} {direction}"; - } - ) - .ToArray(); - var index = new TableIndex( - null, - table_name, - index_name, - columnNames, - non_unique != 1 - ); - indexes.Add(index); - } - - return indexes; - } - - public async Task> GetIndexNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - @$"SELECT INDEX_NAME FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = DATABASE()" - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND TABLE_NAME = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND INDEX_NAME LIKE @where") - + @" ORDER BY INDEX_NAME"; - - return await QueryAsync(db, sql, new { tableName, where }, tx) - .ConfigureAwait(false); - } - - public async Task DropIndexIfExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if ( - !await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` - DROP INDEX `{indexName}`", - transaction: tx - ); - - return true; - } -} diff --git a/src/DapperMatic/Providers/MySql/MySqlExtensions.TableMethods.cs b/src/DapperMatic/Providers/MySql/MySqlExtensions.TableMethods.cs deleted file mode 100644 index 35d6510..0000000 --- a/src/DapperMatic/Providers/MySql/MySqlExtensions.TableMethods.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Data; -using System.Text; - -namespace DapperMatic.Providers.MySql; - -public partial class MySqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task TableExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - return 0 - < await ExecuteScalarAsync( - db, - "SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = @tableName AND TABLE_SCHEMA = DATABASE()", - new { tableName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateTableIfNotExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - string[]? primaryKeyColumnNames = null, - Type[]? primaryKeyDotnetTypes = null, - int?[]? primaryKeyColumnLengths = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if (primaryKeyColumnNames == null || primaryKeyColumnNames.Length == 0) - { - await ExecuteAsync( - db, - @$"CREATE TABLE `{tableName}` ( - `id` INT(11) AUTO_INCREMENT, - CONSTRAINT `pk_{tableName}_id` PRIMARY KEY (`id`) - ) - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - var sql = new StringBuilder(); - sql.AppendLine(@$"CREATE TABLE `{tableName}` ("); - var columnNamesWithOrder = new List(); - for (var i = 0; i < primaryKeyColumnNames.Length; i++) - { - var columnArr = primaryKeyColumnNames[i].Split(' '); - var (_, _, columnName) = NormalizeNames(schemaName, tableName, columnArr[0]); - if (string.IsNullOrWhiteSpace(columnName)) - continue; - - columnNamesWithOrder.Add( - columnName + (columnArr.Length > 1 ? $" {columnArr[1]}" : " ASC") - ); - - if (primaryKeyDotnetTypes != null && primaryKeyDotnetTypes.Length > i) - { - sql.AppendLine( - $"{columnName} {GetSqlTypeString(primaryKeyDotnetTypes[i], (primaryKeyColumnLengths != null && primaryKeyColumnLengths.Length > i) ? primaryKeyColumnLengths[i] : null)} NOT NULL," - ); - } - else - { - sql.AppendLine($"`{columnName}` INT(11) AUTO_INCREMENT,"); - } - } - sql.AppendLine( - $"CONSTRAINT `pk_{tableName}_id` PRIMARY KEY ({string.Join(", ", columnNamesWithOrder)})" - ); - sql.AppendLine(")"); - - await ExecuteAsync(db, sql.ToString(), transaction: tx).ConfigureAwait(false); - - return true; - } - - public async Task> GetTableNamesAsync( - IDbConnection db, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return await QueryAsync( - db, - "SELECT DISTINCT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME", - transaction: tx - ) - .ConfigureAwait(false); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - "SELECT DISTINCT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE @where ORDER BY TABLE_NAME", - new { where }, - transaction: tx - ) - .ConfigureAwait(false); - } - } - - public async Task DropTableIfExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - await ExecuteAsync(db, @$"DROP TABLE `{tableName}` CASCADE", transaction: tx) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/MySql/MySqlExtensions.UniqueConstraintMethods.cs b/src/DapperMatic/Providers/MySql/MySqlExtensions.UniqueConstraintMethods.cs deleted file mode 100644 index 541f159..0000000 --- a/src/DapperMatic/Providers/MySql/MySqlExtensions.UniqueConstraintMethods.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.MySql; - -public partial class MySqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task UniqueConstraintExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM information_schema.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - CONSTRAINT_NAME = @uniqueConstraintName AND - CONSTRAINT_TYPE = 'UNIQUE'", - new { tableName, uniqueConstraintName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateUniqueConstraintIfNotExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string[] columnNames, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - var columnList = string.Join(", ", columnNames); - - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` - ADD CONSTRAINT `{uniqueConstraintName}` UNIQUE ({columnList})", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public Task> GetUniqueConstraintNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return QueryAsync( - db, - $@" - SELECT CONSTRAINT_NAME - FROM information_schema.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - CONSTRAINT_TYPE = 'UNIQUE'", - new { tableName }, - tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return QueryAsync( - db, - $@" - SELECT CONSTRAINT_NAME - FROM information_schema.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - CONSTRAINT_TYPE = 'UNIQUE' AND - CONSTRAINT_NAME LIKE @where", - new { tableName, where }, - tx - ); - } - } - - public async Task DropUniqueConstraintIfExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - // drop unique constraint in MySql 5.7 - await ExecuteAsync( - db, - $@"ALTER TABLE `{tableName}` - DROP INDEX `{uniqueConstraintName}`", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/MySql/MySqlExtensions.cs b/src/DapperMatic/Providers/MySql/MySqlExtensions.cs deleted file mode 100644 index 1190242..0000000 --- a/src/DapperMatic/Providers/MySql/MySqlExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.MySql; - -public partial class MySqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - protected override string DefaultSchema => ""; - - protected override List DataTypes => - DataTypeMapFactory.GetDefaultDatabaseTypeDataTypeMap(DatabaseTypes.MySql); - - internal MySqlExtensions() { } - - public async Task GetDatabaseVersionAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await ExecuteScalarAsync(db, $@"SELECT VERSION()", transaction: tx) - .ConfigureAwait(false) ?? ""; - } -} diff --git a/src/DapperMatic/Providers/MySql/MySqlExtenssions.SchemaMethods.cs b/src/DapperMatic/Providers/MySql/MySqlExtenssions.SchemaMethods.cs deleted file mode 100644 index a0c0d09..0000000 --- a/src/DapperMatic/Providers/MySql/MySqlExtenssions.SchemaMethods.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.MySql; - -public partial class MySqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public override Task SupportsSchemasAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } - - public Task SchemaExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } - - public Task CreateSchemaIfNotExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } - - public Task> GetSchemaNamesAsync( - IDbConnection db, - string? nameFilter = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - // does not support schemas, so we return an empty list - return Task.FromResult(Enumerable.Empty()); - } - - public Task DropSchemaIfExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } -} diff --git a/src/DapperMatic/Providers/MySql/MySqlMethods.Strings.cs b/src/DapperMatic/Providers/MySql/MySqlMethods.Strings.cs new file mode 100644 index 0000000..4d507d6 --- /dev/null +++ b/src/DapperMatic/Providers/MySql/MySqlMethods.Strings.cs @@ -0,0 +1,295 @@ +using System.Diagnostics.CodeAnalysis; +using DapperMatic.Models; + +namespace DapperMatic.Providers.MySql; + +public partial class MySqlMethods +{ + #region Schema Strings + #endregion // Schema Strings + + #region Table Strings + + protected override string SqlInlineColumnNameAndType(DxColumn column, Version dbVersion) + { + var nameAndType = base.SqlInlineColumnNameAndType(column, dbVersion); + + if ( + !nameAndType.Contains(" varchar", StringComparison.OrdinalIgnoreCase) + && !nameAndType.Contains(" text", StringComparison.OrdinalIgnoreCase) + ) + return nameAndType; + + var doNotAddUtf8Mb4 = + dbVersion < new Version(5, 5, 3) + // do not include MariaDb here + || dbVersion.Major == 10 + || dbVersion.Major == 11; + // || (dbVersion.Major == 10 && dbVersion < new Version(10, 5, 25)); + + if (!doNotAddUtf8Mb4 && column.IsUnicode) + { + // make it unicode by default + nameAndType += " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"; + } + + return nameAndType; + } + + // MySQL requires the AUTO_INCREMENT keyword to appear in the column definition, also + // MySQL DOES NOT ALLOW a named constraint in the column definition, so we HAVE to create + // the primary key constraint in the table constraints section + protected override string SqlInlinePrimaryKeyColumnConstraint( + DxColumn column, + string constraintName, + out bool useTableConstraint + ) + { + useTableConstraint = true; + return column.IsAutoIncrement ? "AUTO_INCREMENT" : ""; + + // the following code doesn't work because MySQL doesn't allow named constraints in the column definition + // return $"CONSTRAINT {NormalizeName(constraintName)} {(column.IsAutoIncrement ? $"{SqlInlinePrimaryKeyAutoIncrementColumnConstraint(column)} " : "")}PRIMARY KEY".Trim(); + } + + protected override string SqlInlinePrimaryKeyAutoIncrementColumnConstraint(DxColumn column) + { + return "AUTO_INCREMENT"; + } + + // MySQL doesn't allow default constraints to be named, so we just set the default without a name + [SuppressMessage("Performance", "CA1866:Use char overload")] + protected override string SqlInlineDefaultColumnConstraint( + string constraintName, + string defaultExpression + ) + { + defaultExpression = defaultExpression.Trim(); + var addParentheses = + defaultExpression.Contains(' ') + && !(defaultExpression.StartsWith('(') && defaultExpression.EndsWith(')')) + && !(defaultExpression.StartsWith('"') && defaultExpression.EndsWith('"')) + && !(defaultExpression.StartsWith('\'') && defaultExpression.EndsWith('\'')); + + return $"DEFAULT {(addParentheses ? $"({defaultExpression})" : defaultExpression)}"; + } + + // MySQL DOES NOT ALLOW a named constraint in the column definition, so we HAVE to create + // the check constraint in the table constraints section + protected override string SqlInlineCheckColumnConstraint( + string constraintName, + string checkExpression, + out bool useTableConstraint + ) + { + useTableConstraint = true; + return ""; + } + + // MySQL DOES NOT ALLOW a named constraint in the column definition, so we HAVE to create + // the unique constraint in the table constraints section + protected override string SqlInlineUniqueColumnConstraint( + string constraintName, + out bool useTableConstraint + ) + { + useTableConstraint = true; + return ""; + } + + // MySQL DOES NOT ALLOW a named constraint in the column definition, so we HAVE to create + // the foreign key constraint in the table constraints section + protected override string SqlInlineForeignKeyColumnConstraint( + string? schemaName, + string constraintName, + string referencedTableName, + DxOrderedColumn referencedColumn, + DxForeignKeyAction? onDelete, + DxForeignKeyAction? onUpdate, + out bool useTableConstraint + ) + { + useTableConstraint = true; + return ""; + } + + protected override (string sql, object parameters) SqlDoesTableExist( + string? schemaName, + string tableName + ) + { + const string sql = """ + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE' + and TABLE_SCHEMA = DATABASE() + and TABLE_NAME = @tableName + """; + + return ( + sql, + new + { + schemaName = NormalizeSchemaName(schemaName), + tableName = NormalizeName(tableName) + } + ); + } + + protected override (string sql, object parameters) SqlGetTableNames( + string? schemaName, + string? tableNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(tableNameFilter) ? "" : ToLikeString(tableNameFilter); + + var sql = $""" + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE + TABLE_TYPE = 'BASE TABLE' + AND TABLE_SCHEMA = DATABASE() + {(string.IsNullOrWhiteSpace(where) ? null : " AND TABLE_NAME LIKE @where")} + ORDER BY TABLE_NAME + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + #endregion // Table Strings + + #region Column Strings + #endregion // Column Strings + + #region Check Constraint Strings + #endregion // Check Constraint Strings + + #region Default Constraint Strings + protected override string SqlAlterTableAddDefaultConstraint( + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression + ) + { + var schemaQualifiedTableName = GetSchemaQualifiedIdentifierName(schemaName, tableName); + + var defaultExpression = expression.Trim(); + var addParentheses = + defaultExpression.Contains(' ') + && !(defaultExpression.StartsWith('(') && defaultExpression.EndsWith(')')) + && !(defaultExpression.StartsWith('"') && defaultExpression.EndsWith('"')) + && !(defaultExpression.StartsWith('\'') && defaultExpression.EndsWith('\'')); + + return $""" + + ALTER TABLE {schemaQualifiedTableName} + ALTER COLUMN {NormalizeName(columnName)} SET DEFAULT {( + addParentheses ? $"({defaultExpression})" : defaultExpression + )} + + """; + } + + protected override string SqlDropDefaultConstraint( + string? schemaName, + string tableName, + string columnName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} ALTER COLUMN {NormalizeName(columnName)} DROP DEFAULT"; + } + #endregion // Default Constraint Strings + + #region Primary Key Strings + protected override string SqlDropPrimaryKeyConstraint( + string? schemaName, + string tableName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP PRIMARY KEY"; + } + #endregion // Primary Key Strings + + #region Unique Constraint Strings + protected override string SqlDropUniqueConstraint( + string? schemaName, + string tableName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP INDEX {NormalizeName(constraintName)}"; + } + #endregion // Unique Constraint Strings + + #region Foreign Key Constraint Strings + protected override string SqlDropForeignKeyConstraint( + string? schemaName, + string tableName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} DROP FOREIGN KEY {NormalizeName(constraintName)}"; + } + #endregion // Foreign Key Constraint Strings + + #region Index Strings + #endregion // Index Strings + + #region View Strings + + protected override (string sql, object parameters) SqlGetViewNames( + string? schemaName, + string? viewNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + SELECT + TABLE_NAME AS ViewName + FROM + INFORMATION_SCHEMA.VIEWS + WHERE + VIEW_DEFINITION IS NOT NULL + AND TABLE_SCHEMA = DATABASE() + {( + string.IsNullOrWhiteSpace(where) ? "" : " AND TABLE_NAME LIKE @where" + )} + ORDER BY + TABLE_SCHEMA, TABLE_NAME + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + protected override (string sql, object parameters) SqlGetViews( + string? schemaName, + string? viewNameFilter + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + SELECT + NULL AS SchemaName, + TABLE_NAME AS ViewName, + VIEW_DEFINITION AS Definition + FROM + INFORMATION_SCHEMA.VIEWS + WHERE + VIEW_DEFINITION IS NOT NULL + AND TABLE_SCHEMA = DATABASE() + {( + string.IsNullOrWhiteSpace(where) ? "" : "AND TABLE_NAME LIKE @where" + )} + ORDER BY + TABLE_SCHEMA, TABLE_NAME + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + #endregion // View Strings +} diff --git a/src/DapperMatic/Providers/MySql/MySqlMethods.Tables.cs b/src/DapperMatic/Providers/MySql/MySqlMethods.Tables.cs new file mode 100644 index 0000000..8a3a8c1 --- /dev/null +++ b/src/DapperMatic/Providers/MySql/MySqlMethods.Tables.cs @@ -0,0 +1,643 @@ +using System.Data; +using System.Text.RegularExpressions; +using DapperMatic.Models; + +namespace DapperMatic.Providers.MySql; + +public partial class MySqlMethods +{ + public override async Task> GetTablesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + schemaName = NormalizeSchemaName(schemaName); + + var where = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + + // columns + var columnsSql = $""" + SELECT + t.TABLE_SCHEMA AS schema_name, + t.TABLE_NAME AS table_name, + c.COLUMN_NAME AS column_name, + t.TABLE_COLLATION AS table_collation, + c.ORDINAL_POSITION AS column_ordinal, + c.COLUMN_DEFAULT AS column_default, + case when (c.COLUMN_KEY = 'PRI') then 1 else 0 end AS is_primary_key, + case + when (c.COLUMN_KEY = 'UNI') then 1 else 0 end AS is_unique, + case + when (c.COLUMN_KEY = 'UNI') then 1 + when (c.COLUMN_KEY = 'MUL') then 1 + else 0 + end AS is_indexed, + case when (c.IS_NULLABLE = 'YES') then 1 else 0 end AS is_nullable, + c.DATA_TYPE AS data_type, + c.COLUMN_TYPE AS data_type_complete, + c.CHARACTER_MAXIMUM_LENGTH AS max_length, + c.NUMERIC_PRECISION AS numeric_precision, + c.NUMERIC_SCALE AS numeric_scale, + c.EXTRA as extra + FROM INFORMATION_SCHEMA.TABLES t + LEFT OUTER JOIN INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_SCHEMA = c.TABLE_SCHEMA and t.TABLE_NAME = c.TABLE_NAME + WHERE t.TABLE_TYPE = 'BASE TABLE' + AND t.TABLE_SCHEMA = DATABASE() + {( + string.IsNullOrWhiteSpace(where) ? null : " AND t.TABLE_NAME LIKE @where" + )} + ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME, c.ORDINAL_POSITION + + """; + List<( + string schema_name, + string table_name, + string column_name, + string table_collation, + int column_ordinal, + string column_default, + bool is_primary_key, + bool is_unique, + bool is_indexed, + bool is_nullable, + string data_type, + string data_type_complete, + long? max_length, + int? numeric_precision, + int? numeric_scale, + string? extra + )> columnResults = []; + try + { + columnResults = await QueryAsync<( + string schema_name, + string table_name, + string column_name, + string table_collation, + int column_ordinal, + string column_default, + bool is_primary_key, + bool is_unique, + bool is_indexed, + bool is_nullable, + string data_type, + string data_type_complete, + long? max_length, + int? numeric_precision, + int? numeric_scale, + string? extra + )>(db, columnsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + } + catch (Exception e) + { + var rows = await QueryAsync(db, columnsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + Console.WriteLine(e.Message); + throw; + } + + // get primary key, unique key in a single query + var constraintsSql = $""" + SELECT + tc.table_schema AS schema_name, + tc.table_name AS table_name, + tc.constraint_type AS constraint_type, + tc.constraint_name AS constraint_name, + GROUP_CONCAT(kcu.column_name ORDER BY kcu.ordinal_position ASC SEPARATOR ', ') AS columns_csv, + GROUP_CONCAT(CASE isc.collation + WHEN 'A' THEN 'ASC' + WHEN 'D' THEN 'DESC' + ELSE 'ASC' + END ORDER BY kcu.ordinal_position ASC SEPARATOR ', ') AS columns_desc_csv + FROM + information_schema.table_constraints tc + JOIN + information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + AND tc.table_name = kcu.table_name + LEFT JOIN + information_schema.statistics isc + ON kcu.table_schema = isc.table_schema + AND kcu.table_name = isc.table_name + AND kcu.column_name = isc.column_name + AND kcu.constraint_name = isc.index_name + WHERE + tc.table_schema = DATABASE() + and tc.constraint_type in ('UNIQUE', 'PRIMARY KEY') + {( + string.IsNullOrWhiteSpace(where) ? null : " AND tc.table_name LIKE @where" + )} + GROUP BY + tc.table_name, + tc.constraint_type, + tc.constraint_name + ORDER BY + tc.table_name, + tc.constraint_type, + tc.constraint_name + + """; + var constraintResults = await QueryAsync<( + string schema_name, + string table_name, + string constraint_type, + string constraint_name, + string columns_csv, + string columns_desc_csv + )>(db, constraintsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + + var allDefaultConstraints = columnResults + .Where(t => + !string.IsNullOrWhiteSpace(t.column_default) + && + // MariaDB adds NULL as a default constraint, let's ignore it + !t.column_default.Equals("NULL", StringComparison.OrdinalIgnoreCase) + ) + .Select(c => + { + return new DxDefaultConstraint( + DefaultSchema, + c.table_name, + c.column_name, + DbProviderUtils.GenerateDefaultConstraintName(c.table_name, c.column_name), + c.column_default.Trim('(', ')') + ); + }) + .ToArray(); + + var allPrimaryKeyConstraints = constraintResults + .Where(t => t.constraint_type == "PRIMARY KEY") + .Select(t => + { + var columnNames = t.columns_csv.Split(", "); + var columnDescs = t.columns_desc_csv.Split(", "); + return new DxPrimaryKeyConstraint( + DefaultSchema, + t.table_name, + DbProviderUtils.GeneratePrimaryKeyConstraintName(t.table_name, columnNames), + columnNames + .Select( + (c, i) => + new DxOrderedColumn( + c, + columnDescs[i] + .Equals("DESC", StringComparison.OrdinalIgnoreCase) + ? DxColumnOrder.Descending + : DxColumnOrder.Ascending + ) + ) + .ToArray() + ); + }) + .ToArray(); + var allUniqueConstraints = constraintResults + .Where(t => t.constraint_type == "UNIQUE") + .Select(t => + { + var columnNames = t.columns_csv.Split(", "); + var columnDescs = t.columns_desc_csv.Split(", "); + return new DxUniqueConstraint( + DefaultSchema, + t.table_name, + t.constraint_name, + columnNames + .Select( + (c, i) => + new DxOrderedColumn( + c, + columnDescs[i] + .Equals("DESC", StringComparison.OrdinalIgnoreCase) + ? DxColumnOrder.Descending + : DxColumnOrder.Ascending + ) + ) + .ToArray() + ); + }) + .ToArray(); + + var foreignKeysSql = $""" + + select distinct + kcu.TABLE_SCHEMA as schema_name, + kcu.TABLE_NAME as table_name, + kcu.CONSTRAINT_NAME as constraint_name, + kcu.REFERENCED_TABLE_SCHEMA as referenced_schema_name, + kcu.REFERENCED_TABLE_NAME as referenced_table_name, + rc.DELETE_RULE as delete_rule, + rc.UPDATE_RULE as update_rule, + kcu.ORDINAL_POSITION as key_ordinal, + kcu.COLUMN_NAME as column_name, + kcu.REFERENCED_COLUMN_NAME as referenced_column_name + from INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc on kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc on kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + where kcu.CONSTRAINT_SCHEMA = DATABASE() + and tc.CONSTRAINT_SCHEMA = DATABASE() + and tc.CONSTRAINT_TYPE = 'FOREIGN KEY' + {( + string.IsNullOrWhiteSpace(where) ? null : " AND kcu.TABLE_NAME LIKE @where" + )} + order by schema_name, table_name, key_ordinal + + """; + var foreignKeyResults = await QueryAsync<( + string schema_name, + string table_name, + string constraint_name, + string referenced_schema_name, + string referenced_table_name, + string delete_rule, + string update_rule, + string key_ordinal, + string column_name, + string referenced_column_name + )>(db, foreignKeysSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + var allForeignKeyConstraints = foreignKeyResults + .GroupBy(t => new + { + t.schema_name, + t.table_name, + t.constraint_name, + t.referenced_schema_name, + t.referenced_table_name, + t.update_rule, + t.delete_rule + }) + .Select(gb => + { + return new DxForeignKeyConstraint( + DefaultSchema, + gb.Key.table_name, + gb.Key.constraint_name, + gb.Select(c => new DxOrderedColumn(c.column_name)).ToArray(), + gb.Key.referenced_table_name, + gb.Select(c => new DxOrderedColumn(c.referenced_column_name)).ToArray(), + gb.Key.delete_rule.ToForeignKeyAction(), + gb.Key.update_rule.ToForeignKeyAction() + ); + }) + .ToArray(); + + // the table CHECK_CONSTRAINTS only exists starting MySQL 8.0.16 and MariaDB 10.2.1 + // resolve issue for MySQL 5.0.12+ + DxCheckConstraint[] allCheckConstraints = []; + if (await SupportsCheckConstraintsAsync(db, tx, cancellationToken).ConfigureAwait(false)) + { + var checkConstraintsSql = $""" + + SELECT + tc.TABLE_SCHEMA as schema_name, + tc.TABLE_NAME as table_name, + kcu.COLUMN_NAME as column_name, + tc.CONSTRAINT_NAME as constraint_name, + cc.CHECK_CLAUSE AS check_expression + FROM + INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc + JOIN + INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS cc + ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME + LEFT JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu + ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + WHERE + tc.TABLE_SCHEMA = DATABASE() + and tc.CONSTRAINT_TYPE = 'CHECK' + {( + string.IsNullOrWhiteSpace(where) ? null : " AND tc.TABLE_NAME LIKE @where" + )} + order by schema_name, table_name, column_name, constraint_name + + """; + + var checkConstraintResults = await QueryAsync<( + string schema_name, + string table_name, + string? column_name, + string constraint_name, + string check_expression + )>(db, checkConstraintsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + allCheckConstraints = checkConstraintResults + .Select(t => + { + if (string.IsNullOrWhiteSpace(t.column_name)) + { + // try to associate the check constraint with a column + var columnCount = 0; + var columnName = ""; + foreach (var column in columnResults) + { + var pattern = $@"\b{Regex.Escape(column.column_name)}\b"; + if ( + column.table_name.Equals( + t.table_name, + StringComparison.OrdinalIgnoreCase + ) + && Regex.IsMatch( + t.check_expression, + pattern, + RegexOptions.IgnoreCase + ) + ) + { + columnName = column.column_name; + columnCount++; + } + } + if (columnCount == 1) + { + t.column_name = columnName; + } + } + return new DxCheckConstraint( + DefaultSchema, + t.table_name, + t.column_name, + t.constraint_name, + t.check_expression + ); + }) + .ToArray(); + } + + var allIndexes = await GetIndexesInternalAsync( + db, + schemaName, + tableNameFilter, + tx: tx, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + var tables = new List(); + + foreach ( + var tableColumns in columnResults.GroupBy(r => new { r.schema_name, r.table_name }) + ) + { + var tableName = tableColumns.Key.table_name; + + var foreignKeyConstraints = allForeignKeyConstraints + .Where(t => t.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + var checkConstraints = allCheckConstraints + .Where(t => t.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + var defaultConstraints = allDefaultConstraints + .Where(t => t.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + var uniqueConstraints = allUniqueConstraints + .Where(t => t.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + var primaryKeyConstraint = allPrimaryKeyConstraints.SingleOrDefault(t => + t.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) + ); + var indexes = allIndexes + .Where(t => t.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + var columns = new List(); + foreach (var tableColumn in tableColumns) + { + var columnIsUniqueViaUniqueConstraintOrIndex = + uniqueConstraints.Any(c => + c.Columns.Length == 1 + && c.Columns.Any(col => + col.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ) + || indexes.Any(i => + i is { IsUnique: true, Columns.Length: 1 } + && i.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var columnIsPartOfIndex = indexes.Any(i => + i.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var foreignKeyConstraint = foreignKeyConstraints.FirstOrDefault(c => + c.SourceColumns.Any(scol => + scol.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var foreignKeyColumnIndex = foreignKeyConstraint + ?.SourceColumns.Select((scol, i) => new { c = scol, i }) + .FirstOrDefault(c => + c.c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.i; + + var dotnetTypeDescriptor = GetDotnetTypeFromSqlType(tableColumn.data_type_complete); + + var column = new DxColumn( + tableColumn.schema_name, + tableColumn.table_name, + tableColumn.column_name, + dotnetTypeDescriptor.DotnetType, + new Dictionary + { + { ProviderType, tableColumn.data_type_complete } + }, + tableColumn.max_length.HasValue + ? ( + tableColumn.max_length.Value > int.MaxValue + ? int.MaxValue + : (int)tableColumn.max_length.Value + ) + : null, + tableColumn.numeric_precision, + tableColumn.numeric_scale, + checkConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.Expression, + defaultConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.Expression, + tableColumn.is_nullable, + primaryKeyConstraint?.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) == true, + tableColumn.extra?.Contains( + "auto_increment", + StringComparison.OrdinalIgnoreCase + ) == true, + columnIsUniqueViaUniqueConstraintOrIndex, + columnIsPartOfIndex, + foreignKeyConstraint != null, + foreignKeyConstraint?.ReferencedTableName, + foreignKeyConstraint + ?.ReferencedColumns.ElementAtOrDefault(foreignKeyColumnIndex ?? 0) + ?.ColumnName, + foreignKeyConstraint?.OnDelete, + foreignKeyConstraint?.OnUpdate + ); + + columns.Add(column); + } + + var table = new DxTable( + schemaName, + tableName, + [.. columns], + primaryKeyConstraint, + checkConstraints, + defaultConstraints, + uniqueConstraints, + foreignKeyConstraints, + indexes + ); + tables.Add(table); + } + + return tables; + } + + protected override async Task> GetIndexesInternalAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var whereTableLike = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + + var whereIndexLike = string.IsNullOrWhiteSpace(indexNameFilter) + ? null + : ToLikeString(indexNameFilter); + + var sql = $""" + + SELECT + TABLE_SCHEMA as schema_name, + TABLE_NAME as table_name, + INDEX_NAME as index_name, + IF(NON_UNIQUE = 1, 0, 1) AS is_unique, + GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX ASC) AS columns_csv, + GROUP_CONCAT(CASE + WHEN COLLATION = 'A' THEN 'ASC' + WHEN COLLATION = 'D' THEN 'DESC' + ELSE 'N/A' + END ORDER BY SEQ_IN_INDEX ASC) AS columns_desc_csv + FROM + INFORMATION_SCHEMA.STATISTICS stats + WHERE + TABLE_SCHEMA = DATABASE() + and INDEX_NAME != 'PRIMARY' + and INDEX_NAME NOT IN (select CONSTRAINT_NAME from INFORMATION_SCHEMA.TABLE_CONSTRAINTS + where TABLE_SCHEMA = DATABASE() and + TABLE_NAME = stats.TABLE_NAME and + CONSTRAINT_TYPE in ('PRIMARY KEY', 'FOREIGN KEY', 'CHECK')) + {( + !string.IsNullOrWhiteSpace(whereTableLike) + ? "and TABLE_NAME LIKE @whereTableLike" + : "" + )} + {( + !string.IsNullOrWhiteSpace(whereIndexLike) + ? "and INDEX_NAME LIKE @whereIndexLike" + : "" + )} + GROUP BY + TABLE_NAME, INDEX_NAME, NON_UNIQUE + order by schema_name, table_name, index_name + + """; + + var indexResults = await QueryAsync<( + string schema_name, + string table_name, + string index_name, + bool is_unique, + string columns_csv, + string columns_desc_csv + )>(db, sql, new { whereTableLike, whereIndexLike }, tx) + .ConfigureAwait(false); + + var indexes = new List(); + + foreach (var indexResult in indexResults) + { + var columnNames = indexResult.columns_csv.Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ); + var columnDirections = indexResult.columns_desc_csv.Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ); + + var columns = columnNames + .Select( + (c, i) => + new DxOrderedColumn( + c, + columnDirections[i].Equals("desc", StringComparison.OrdinalIgnoreCase) + ? DxColumnOrder.Descending + : DxColumnOrder.Ascending + ) + ) + .ToArray(); + + indexes.Add( + new DxIndex( + DefaultSchema, + indexResult.table_name, + indexResult.index_name, + columns, + indexResult.is_unique + ) + ); + } + + return indexes; + } +} diff --git a/src/DapperMatic/Providers/MySql/MySqlMethods.cs b/src/DapperMatic/Providers/MySql/MySqlMethods.cs new file mode 100644 index 0000000..bfeb0a0 --- /dev/null +++ b/src/DapperMatic/Providers/MySql/MySqlMethods.cs @@ -0,0 +1,57 @@ +using System.Data; +using DapperMatic.Interfaces; +using DapperMatic.Providers.Base; + +namespace DapperMatic.Providers.MySql; + +public partial class MySqlMethods : DatabaseMethodsBase, IDatabaseMethods +{ + public override DbProviderType ProviderType => DbProviderType.MySql; + + public override IDbProviderTypeMap ProviderTypeMap => MySqlProviderTypeMap.Instance.Value; + + protected override string DefaultSchema => ""; + + public override async Task SupportsCheckConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var versionStr = + await ExecuteScalarAsync(db, "SELECT VERSION()", tx: tx).ConfigureAwait(false) + ?? ""; + var version = DbProviderUtils.ExtractVersionFromVersionString(versionStr); + return ( + versionStr.Contains("MariaDB", StringComparison.OrdinalIgnoreCase) + && version > new Version(10, 2, 1) + ) + || version >= new Version(8, 0, 16); + } + + public override Task SupportsOrderedKeysInConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return Task.FromResult(false); + } + + internal MySqlMethods() { } + + public override async Task GetDatabaseVersionAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + // sample output: 8.0.27, 8.4.2 + var sql = @"SELECT VERSION()"; + var versionString = + await ExecuteScalarAsync(db, sql, tx: tx).ConfigureAwait(false) ?? ""; + return DbProviderUtils.ExtractVersionFromVersionString(versionString); + } + + public override char[] QuoteChars => ['`']; +} diff --git a/src/DapperMatic/Providers/MySql/MySqlProviderTypeMap.cs b/src/DapperMatic/Providers/MySql/MySqlProviderTypeMap.cs new file mode 100644 index 0000000..80fd4df --- /dev/null +++ b/src/DapperMatic/Providers/MySql/MySqlProviderTypeMap.cs @@ -0,0 +1,435 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DapperMatic.Providers.MySql; + +public sealed class MySqlProviderTypeMap : DbProviderTypeMapBase +{ + internal static readonly Lazy Instance = + new(() => new MySqlProviderTypeMap()); + + private MySqlProviderTypeMap() + : base() { } + + protected override DbProviderType ProviderType => DbProviderType.MySql; + + public override string SqTypeForStringLengthMax => "text(65535)"; + + public override string SqTypeForBinaryLengthMax => "blob(65535)"; + + public override string SqlTypeForJson => "text(65535)"; + + /// + /// IMPORTANT!! The order within an affinity group matters, as the first possible match will be used as the recommended sql type for a dotnet type + /// + protected override DbProviderSqlType[] ProviderSqlTypes => + [ + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_tinyint, + formatWithPrecision: "tinyint({0})", + defaultPrecision: 4, + canUseToAutoIncrement: true, + minValue: -128, + maxValue: 128 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_tinyint_unsigned, + formatWithPrecision: "tinyint({0}) unsigned", + defaultPrecision: 4, + canUseToAutoIncrement: true, + minValue: 0, + maxValue: 255 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_smallint, + formatWithPrecision: "smallint({0})", + defaultPrecision: 5, + canUseToAutoIncrement: true, + minValue: -32768, + maxValue: 32767 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_smallint_unsigned, + formatWithPrecision: "smallint({0}) unsigned", + defaultPrecision: 5, + canUseToAutoIncrement: true, + minValue: 0, + maxValue: 65535 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_mediumint, + formatWithPrecision: "mediumint({0})", + defaultPrecision: 7, + canUseToAutoIncrement: true, + minValue: -8388608, + maxValue: 8388607 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_mediumint_unsigned, + formatWithPrecision: "mediumint({0}) unsigned", + defaultPrecision: 7, + canUseToAutoIncrement: true, + minValue: 0, + maxValue: 16777215 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_integer, + formatWithPrecision: "integer({0})", + defaultPrecision: 11, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_integer_unsigned, + formatWithPrecision: "integer({0}) unsigned", + defaultPrecision: 11, + canUseToAutoIncrement: true, + minValue: 0, + maxValue: 4294967295 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_int, + aliasOf: "integer", + formatWithPrecision: "int({0})", + defaultPrecision: 11, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_int_unsigned, + formatWithPrecision: "int({0}) unsigned", + defaultPrecision: 11, + canUseToAutoIncrement: true, + minValue: 0, + maxValue: 4294967295 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_bigint, + formatWithPrecision: "bigint({0})", + defaultPrecision: 19, + canUseToAutoIncrement: true, + minValue: -Math.Pow(2, 63), + maxValue: Math.Pow(2, 63) - 1 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_bigint_unsigned, + formatWithPrecision: "bigint({0}) unsigned", + defaultPrecision: 19, + canUseToAutoIncrement: true, + minValue: 0, + maxValue: Math.Pow(2, 64) - 1 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_serial, + aliasOf: "bigint unsigned", + canUseToAutoIncrement: true, + autoIncrementsAutomatically: true, + minValue: 0, + maxValue: Math.Pow(2, 64) - 1 + ), + new( + DbProviderSqlTypeAffinity.Integer, + MySqlTypes.sql_bit, + formatWithPrecision: "bit({0})", + defaultPrecision: 1, + minValue: 0, + maxValue: long.MaxValue + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_decimal, + formatWithPrecision: "decimal({0})", + formatWithPrecisionAndScale: "decimal({0},{1})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_dec, + aliasOf: "decimal", + formatWithPrecision: "dec({0})", + formatWithPrecisionAndScale: "dec({0},{1})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_numeric, + formatWithPrecision: "numeric({0})", + formatWithPrecisionAndScale: "numeric({0},{1})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_fixed, + aliasOf: "decimal", + formatWithPrecision: "fixed({0})", + formatWithPrecisionAndScale: "fixed({0},{1})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_float + // formatWithPrecision: "float({0})", + // formatWithPrecisionAndScale: "float({0},{1})", + // defaultPrecision: 12, + // defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_real + // aliasOf: "double", + // formatWithPrecisionAndScale: "real({0},{1})", + // defaultPrecision: 12, + // defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_double_precision + // aliasOf: "double", + // formatWithPrecisionAndScale: "double precision({0},{1})", + // defaultPrecision: 12, + // defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_double_precision_unsigned, + aliasOf: "double unsigned" + // formatWithPrecisionAndScale: "double precision({0},{1}) unsigned", + // defaultPrecision: 12, + // defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_double + // formatWithPrecisionAndScale: "double({0},{1})", + // defaultPrecision: 12, + // defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + MySqlTypes.sql_double_unsigned + // formatWithPrecisionAndScale: "double({0},{1}) unsigned", + // defaultPrecision: 12, + // defaultScale: 2 + ), + new(DbProviderSqlTypeAffinity.Boolean, MySqlTypes.sql_bool, aliasOf: "tinyint(1)"), + new(DbProviderSqlTypeAffinity.Boolean, MySqlTypes.sql_boolean, aliasOf: "tinyint(1)"), + new(DbProviderSqlTypeAffinity.DateTime, MySqlTypes.sql_datetime), + new(DbProviderSqlTypeAffinity.DateTime, MySqlTypes.sql_timestamp), + new( + DbProviderSqlTypeAffinity.DateTime, + MySqlTypes.sql_time, + formatWithPrecision: "time({0})", + defaultPrecision: 6, + isTimeOnly: true + ), + new(DbProviderSqlTypeAffinity.DateTime, MySqlTypes.sql_date, isDateOnly: true), + new(DbProviderSqlTypeAffinity.DateTime, MySqlTypes.sql_year, isYearOnly: true), + new( + DbProviderSqlTypeAffinity.Text, + MySqlTypes.sql_char, + formatWithLength: "char({0})", + defaultLength: DefaultLength, + isFixedLength: true + ), + new( + DbProviderSqlTypeAffinity.Text, + MySqlTypes.sql_varchar, + formatWithLength: "varchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + MySqlTypes.sql_text, + formatWithLength: "text({0})", + defaultLength: 65535 + ), + new(DbProviderSqlTypeAffinity.Text, MySqlTypes.sql_long_varchar, aliasOf: "mediumtext"), + new(DbProviderSqlTypeAffinity.Text, MySqlTypes.sql_tinytext), + new(DbProviderSqlTypeAffinity.Text, MySqlTypes.sql_mediumtext), + new(DbProviderSqlTypeAffinity.Text, MySqlTypes.sql_longtext), + new(DbProviderSqlTypeAffinity.Text, MySqlTypes.sql_enum), + new(DbProviderSqlTypeAffinity.Text, MySqlTypes.sql_set), + new(DbProviderSqlTypeAffinity.Text, MySqlTypes.sql_json), + new( + DbProviderSqlTypeAffinity.Binary, + MySqlTypes.sql_blob, + formatWithLength: "blob({0})", + defaultLength: 65535 + ), + new(DbProviderSqlTypeAffinity.Binary, MySqlTypes.sql_tinyblob), + new(DbProviderSqlTypeAffinity.Binary, MySqlTypes.sql_mediumblob), + new(DbProviderSqlTypeAffinity.Binary, MySqlTypes.sql_longblob), + new( + DbProviderSqlTypeAffinity.Binary, + MySqlTypes.sql_varbinary, + formatWithLength: "varbinary({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Binary, + MySqlTypes.sql_binary, + formatWithLength: "binary({0})", + defaultLength: DefaultLength, + isFixedLength: true + ), + new( + DbProviderSqlTypeAffinity.Binary, + MySqlTypes.sql_long_varbinary, + aliasOf: "mediumblob" + ), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_geometry), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_point), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_linestring), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_polygon), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_multipoint), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_multilinestring), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_multipolygon), + new(DbProviderSqlTypeAffinity.Geometry, MySqlTypes.sql_geomcollection), + new( + DbProviderSqlTypeAffinity.Geometry, + MySqlTypes.sql_geometrycollection, + aliasOf: "geomcollection" + ) + ]; + + protected override bool TryGetProviderSqlTypeMatchingDotnetTypeInternal( + DbProviderDotnetTypeDescriptor descriptor, + out DbProviderSqlType? providerSqlType + ) + { + providerSqlType = null; + + var dotnetType = descriptor.DotnetType; + + // handle well-known types first + providerSqlType = dotnetType.IsGenericType + ? null + : dotnetType switch + { + Type t when t == typeof(bool) => ProviderSqlTypeLookup[MySqlTypes.sql_boolean], + Type t when t == typeof(byte) + => ProviderSqlTypeLookup[MySqlTypes.sql_smallint_unsigned], + Type t when t == typeof(ReadOnlyMemory) + => ProviderSqlTypeLookup[MySqlTypes.sql_smallint], + Type t when t == typeof(sbyte) => ProviderSqlTypeLookup[MySqlTypes.sql_smallint], + Type t when t == typeof(short) => ProviderSqlTypeLookup[MySqlTypes.sql_smallint], + Type t when t == typeof(ushort) + => ProviderSqlTypeLookup[MySqlTypes.sql_smallint_unsigned], + Type t when t == typeof(int) => ProviderSqlTypeLookup[MySqlTypes.sql_int], + Type t when t == typeof(uint) => ProviderSqlTypeLookup[MySqlTypes.sql_int_unsigned], + Type t when t == typeof(long) => ProviderSqlTypeLookup[MySqlTypes.sql_bigint], + Type t when t == typeof(ulong) + => ProviderSqlTypeLookup[MySqlTypes.sql_bigint_unsigned], + Type t when t == typeof(float) => ProviderSqlTypeLookup[MySqlTypes.sql_float], + Type t when t == typeof(double) => ProviderSqlTypeLookup[MySqlTypes.sql_double], + Type t when t == typeof(decimal) => ProviderSqlTypeLookup[MySqlTypes.sql_decimal], + Type t when t == typeof(char) => ProviderSqlTypeLookup[MySqlTypes.sql_varchar], + Type t when t == typeof(string) + => descriptor.Length.GetValueOrDefault(255) < 8000 + ? ProviderSqlTypeLookup[MySqlTypes.sql_varchar] + : ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t when t == typeof(char[]) + => descriptor.Length.GetValueOrDefault(255) < 8000 + ? ProviderSqlTypeLookup[MySqlTypes.sql_varchar] + : ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t when t == typeof(ReadOnlyMemory[]) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[MySqlTypes.sql_varchar] + : ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t when t == typeof(Stream) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[MySqlTypes.sql_varchar] + : ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t when t == typeof(TextReader) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[MySqlTypes.sql_varchar] + : ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t when t == typeof(byte[]) => ProviderSqlTypeLookup[MySqlTypes.sql_blob], + Type t when t == typeof(object) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[MySqlTypes.sql_varchar] + : ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t when t == typeof(object[]) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[MySqlTypes.sql_varchar] + : ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t when t == typeof(Guid) => ProviderSqlTypeLookup[MySqlTypes.sql_varchar], + Type t when t == typeof(DateTime) => ProviderSqlTypeLookup[MySqlTypes.sql_datetime], + Type t when t == typeof(DateTimeOffset) + => ProviderSqlTypeLookup[MySqlTypes.sql_timestamp], + Type t when t == typeof(TimeSpan) => ProviderSqlTypeLookup[MySqlTypes.sql_bigint], + Type t when t == typeof(DateOnly) => ProviderSqlTypeLookup[MySqlTypes.sql_date], + Type t when t == typeof(TimeOnly) => ProviderSqlTypeLookup[MySqlTypes.sql_time], + Type t when t == typeof(BitArray) || t == typeof(BitVector32) + => ProviderSqlTypeLookup[MySqlTypes.sql_varbinary], + Type t + when t == typeof(ImmutableDictionary) + || t == typeof(Dictionary) + || t == typeof(IDictionary) + => ProviderSqlTypeLookup[MySqlTypes.sql_text], + Type t + when t == typeof(JsonNode) + || t == typeof(JsonObject) + || t == typeof(JsonArray) + || t == typeof(JsonValue) + || t == typeof(JsonDocument) + || t == typeof(JsonElement) + => ProviderSqlTypeLookup[MySqlTypes.sql_text], + _ => null + }; + + if (providerSqlType != null) + return true; + + // handle generic types + providerSqlType = !dotnetType.IsGenericType + ? null + : dotnetType.GetGenericTypeDefinition() switch + { + Type t + when t == typeof(Dictionary<,>) + || t == typeof(IDictionary<,>) + || t == typeof(List<>) + || t == typeof(IList<>) + || t == typeof(Collection<>) + || t == typeof(ICollection<>) + || t == typeof(IEnumerable<>) + => ProviderSqlTypeLookup[MySqlTypes.sql_text], + _ => null + }; + + if (providerSqlType != null) + return true; + + // handle POCO type + if (dotnetType.IsClass || dotnetType.IsInterface) + { + providerSqlType = ProviderSqlTypeLookup[MySqlTypes.sql_text]; + } + + return providerSqlType != null; + } +} diff --git a/src/DapperMatic/Providers/MySql/MySqlTypes.cs b/src/DapperMatic/Providers/MySql/MySqlTypes.cs new file mode 100644 index 0000000..245977e --- /dev/null +++ b/src/DapperMatic/Providers/MySql/MySqlTypes.cs @@ -0,0 +1,78 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Providers.MySql; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static class MySqlTypes +{ + // integers + public const string sql_tinyint = "tinyint"; + public const string sql_tinyint_unsigned = "tinyint unsigned"; + public const string sql_smallint = "smallint"; + public const string sql_smallint_unsigned = "smallint unsigned"; + public const string sql_mediumint = "mediumint"; + public const string sql_mediumint_unsigned = "mediumint unsigned"; + public const string sql_integer = "integer"; + public const string sql_integer_unsigned = "integer unsigned"; + public const string sql_int = "int"; + public const string sql_int_unsigned = "int unsigned"; + public const string sql_bigint = "bigint"; + public const string sql_bigint_unsigned = "bigint unsigned"; + public const string sql_serial = "serial"; + + // real + public const string sql_decimal = "decimal"; + public const string sql_dec = "dec"; + public const string sql_fixed = "fixed"; + public const string sql_numeric = "numeric"; + public const string sql_float = "float"; + public const string sql_real = "real"; + public const string sql_double_precision = "double precision"; + public const string sql_double_precision_unsigned = "double precision unsigned"; + public const string sql_double = "double"; + public const string sql_double_unsigned = "double unsigned"; + public const string sql_bit = "bit"; + + // bool + public const string sql_bool = "bool"; + public const string sql_boolean = "boolean"; + + // datetime + public const string sql_datetime = "datetime"; + public const string sql_timestamp = "timestamp"; + public const string sql_time = "time"; + public const string sql_date = "date"; + public const string sql_year = "year"; + + // text + public const string sql_char = "char"; + public const string sql_varchar = "varchar"; + public const string sql_long_varchar = "long varchar"; + public const string sql_tinytext = "tinytext"; + public const string sql_text = "text"; + public const string sql_mediumtext = "mediumtext"; + public const string sql_longtext = "longtext"; + public const string sql_enum = "enum"; + public const string sql_set = "set"; + public const string sql_json = "json"; + + // binary + public const string sql_binary = "binary"; + public const string sql_varbinary = "varbinary"; + public const string sql_long_varbinary = "long varbinary"; + public const string sql_tinyblob = "tinyblob"; + public const string sql_blob = "blob"; + public const string sql_mediumblob = "mediumblob"; + public const string sql_longblob = "longblob"; + + // geometry + public const string sql_geometry = "geometry"; + public const string sql_point = "point"; + public const string sql_linestring = "linestring"; + public const string sql_polygon = "polygon"; + public const string sql_multipoint = "multipoint"; + public const string sql_multilinestring = "multilinestring"; + public const string sql_multipolygon = "multipolygon"; + public const string sql_geomcollection = "geomcollection"; + public const string sql_geometrycollection = "geometrycollection"; +} \ No newline at end of file diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ColumnMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ColumnMethods.cs deleted file mode 100644 index be3a381..0000000 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ColumnMethods.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Data; -using Dapper; - -namespace DapperMatic.Providers.PostgreSql; - -public partial class PostgreSqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task ColumnExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - return 0 - < await ExecuteScalarAsync( - db, - "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName AND COLUMN_NAME = @columnName", - new { schemaName, tableName, columnName } - ) - .ConfigureAwait(false); - } - - public async Task CreateColumnIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - Type dotnetType, - string? type = null, - int? length = null, - int? precision = null, - int? scale = null, - string? schemaName = null, - string? defaultValue = null, - bool nullable = true, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var sqlType = type ?? GetSqlTypeString(dotnetType, length, precision, scale); - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - var sql = - $@"ALTER TABLE {schemaName}.{tableName} - ADD {columnName} {sqlType} {(nullable ? "NULL" : "NOT NULL")} {(!string.IsNullOrWhiteSpace(defaultValue) ? $"DEFAULT {defaultValue}" : "")} {(unique ? "UNIQUE" : "")} - "; - await ExecuteAsync(db, sql, new { schemaName, tableName, columnName }, tx) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetColumnNamesAsync( - IDbConnection db, - string tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - return await QueryAsync( - db, - $@"SELECT column_name FROM information_schema.columns WHERE table_schema = @schemaName AND table_name = @tableName", - new { schemaName, tableName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task DropColumnIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - await ExecuteAsync( - db, - $@"ALTER TABLE {schemaName}.{tableName} DROP COLUMN {columnName} CASCADE", - new { schemaName, tableName, columnName }, - tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ForeignKeyMethods.cs deleted file mode 100644 index 43ae676..0000000 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ForeignKeyMethods.cs +++ /dev/null @@ -1,388 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic.Providers.PostgreSql; - -public partial class PostgreSqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task ForeignKeyExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - if (!string.IsNullOrWhiteSpace(foreignKey)) - { - var foreignKeyName = NormalizeName(foreignKey); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM information_schema.table_constraints - WHERE table_schema = @schemaName AND - table_name = @tableName AND - constraint_name = @foreignKeyName AND - constraint_type = 'FOREIGN KEY'", - new - { - schemaName, - tableName, - foreignKeyName - }, - tx - ) - .ConfigureAwait(false); - } - else - { - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM information_schema.table_constraints - WHERE table_schema = @schemaName AND - table_name = @tableName AND - constraint_type = 'FOREIGN KEY' AND - constraint_name IN ( - SELECT constraint_name - FROM information_schema.key_column_usage - WHERE table_schema = @schemaName AND - table_name = @tableName AND - column_name = @columnName - )", - new - { - schemaName, - tableName, - columnName - }, - tx - ) - .ConfigureAwait(false); - } - } - - public async Task CreateForeignKeyIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string foreignKey, - string referenceTable, - string referenceColumn, - string? schemaName = null, - string onDelete = "NO ACTION", - string onUpdate = "NO ACTION", - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(foreignKey)) - throw new ArgumentException("Foreign key name must be specified.", nameof(foreignKey)); - if (string.IsNullOrWhiteSpace(referenceTable)) - throw new ArgumentException( - "Reference tableName name must be specified.", - nameof(referenceTable) - ); - if (string.IsNullOrWhiteSpace(referenceColumn)) - throw new ArgumentException( - "Reference columnName name must be specified.", - nameof(referenceColumn) - ); - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - - if ( - await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - var (referenceSchemaName, referenceTableName, referenceColumnName) = NormalizeNames( - schemaName, - referenceTable, - referenceColumn - ); - - var foreignKeyName = NormalizeName(foreignKey); - - await ExecuteAsync( - db, - $@"ALTER TABLE {schemaName}.{tableName} - ADD CONSTRAINT {foreignKeyName} - FOREIGN KEY ({columnName}) - REFERENCES {referenceSchemaName}.{referenceTableName} ({referenceColumnName}) - ON DELETE {onDelete} - ON UPDATE {onUpdate}", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetForeignKeysAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"SELECT c.conname AS constraint_name, - sch.nspname AS schema_name, - tbl.relname AS table_name, - string_agg(col.attname, ',' ORDER BY u.attposition) AS column_names, - f_sch.nspname AS referenced_schema_name, - f_tbl.relname AS referenced_table_name, - string_agg(f_col.attname, ',' ORDER BY f_u.attposition) AS referenced_column_names, - CASE - WHEN c.confdeltype = 'c' THEN 'CASCADE' - WHEN c.confdeltype = 'n' THEN 'SET NULL' - WHEN c.confdeltype = 'r' THEN 'RESTRICT' - ELSE 'NO ACTION' - end as delete_rule, - CASE - WHEN c.confupdtype = 'c' THEN 'CASCADE' - WHEN c.confupdtype = 'n' THEN 'SET NULL' - WHEN c.confupdtype = 'r' THEN 'RESTRICT' - ELSE 'NO ACTION' - end as update_rule, - pg_get_constraintdef(c.oid) AS definition - FROM pg_constraint c - LEFT JOIN LATERAL UNNEST(c.conkey) WITH ORDINALITY AS u(attnum, attposition) ON TRUE - LEFT JOIN LATERAL UNNEST(c.confkey) WITH ORDINALITY AS f_u(attnum, attposition) ON f_u.attposition = u.attposition - JOIN pg_class tbl ON tbl.oid = c.conrelid - JOIN pg_namespace sch ON sch.oid = tbl.relnamespace - LEFT JOIN pg_attribute col ON (col.attrelid = tbl.oid AND col.attnum = u.attnum) - LEFT JOIN pg_class f_tbl ON f_tbl.oid = c.confrelid - LEFT JOIN pg_namespace f_sch ON f_sch.oid = f_tbl.relnamespace - LEFT JOIN pg_attribute f_col ON (f_col.attrelid = f_tbl.oid AND f_col.attnum = f_u.attnum) - where c.contype = 'f'"; - if (!string.IsNullOrWhiteSpace(schemaName)) - sql += $@" AND sch.nspname = @schemaName"; - if (!string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND tbl.relname = @tableName"; - if (!string.IsNullOrWhiteSpace(where)) - sql += $@" AND c.conname LIKE @where"; - sql += - $@" GROUP BY schema_name, table_name, constraint_name, referenced_schema_name, referenced_table_name, definition, delete_rule, update_rule - ORDER BY schema_name, table_name, constraint_name"; - - var results = await QueryAsync<( - string constraint_name, - string schema_name, - string table_name, - string column_names, - string referenced_schema_name, - string referenced_table_name, - string referenced_column_names, - string delete_rule, - string update_rule, - string definition - )>( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - - return results.Select(r => - { - var deleteRule = r.delete_rule switch - { - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - "NO ACTION" => ReferentialAction.NoAction, - _ => ReferentialAction.NoAction - }; - var updateRule = r.update_rule switch - { - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - "NO ACTION" => ReferentialAction.NoAction, - _ => ReferentialAction.NoAction - }; - var columnNames = r.column_names.Split( - ',', - StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries - ); - var referencedColumnNames = r.referenced_column_names.Split( - ',', - StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries - ); - - return new ForeignKey( - r.schema_name, - r.constraint_name, - r.table_name, - columnNames.First(), - r.referenced_table_name, - referencedColumnNames.First(), - deleteRule, - updateRule - ); - }); - } - - public async Task> GetForeignKeyNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"SELECT c.conname - FROM pg_constraint c - JOIN pg_class tbl ON tbl.oid = c.conrelid - JOIN pg_namespace sch ON sch.oid = tbl.relnamespace - where c.contype = 'f'"; - if (!string.IsNullOrWhiteSpace(schemaName)) - sql += $@" AND sch.nspname = @schemaName"; - if (!string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND tbl.relname = @tableName"; - if (!string.IsNullOrWhiteSpace(where)) - sql += $@" AND c.conname LIKE @where"; - sql += @" ORDER BY conname"; - - return await QueryAsync( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - } - - public async Task DropForeignKeyIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - if (!string.IsNullOrWhiteSpace(foreignKey)) - { - var foreignKeyName = NormalizeName(foreignKey); - - await ExecuteAsync( - db, - $@"ALTER TABLE {schemaName}.{tableName} DROP CONSTRAINT {foreignKeyName}", - transaction: tx - ) - .ConfigureAwait(false); - } - else - { - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - // get the name of the postgresql foreign key - var foreignKeyName = await ExecuteScalarAsync( - db, - $@"SELECT conname - FROM pg_constraint - WHERE conrelid = '{schemaName}.{tableName}'::regclass - AND contype = 'f' - AND conname IN ( - SELECT conname - FROM pg_constraint - WHERE conrelid = '{schemaName}.{tableName}'::regclass - AND contype = 'f' - AND conkey[1] = ( - SELECT attnum - FROM pg_attribute - WHERE attrelid = '{schemaName}.{tableName}'::regclass - AND attname = @columnName - ) - )", - new - { - schemaName, - tableName, - columnName - }, - tx - ) - .ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(foreignKeyName)) - { - await ExecuteAsync( - db, - $@"ALTER TABLE {schemaName}.{tableName} DROP CONSTRAINT {foreignKeyName}", - transaction: tx - ) - .ConfigureAwait(false); - } - } - - return true; - } -} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.IndexMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.IndexMethods.cs deleted file mode 100644 index 89e60ec..0000000 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.IndexMethods.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic.Providers.PostgreSql; - -public partial class PostgreSqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task IndexExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - return 0 - < await ExecuteScalarAsync( - db, - $@" - SELECT COUNT(*) - FROM pg_indexes - WHERE schemaname = @schemaName AND - tablename = @tableName AND - indexname = @indexName - ", - new - { - schemaName, - tableName, - indexName - }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateIndexIfNotExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string[] columnNames, - string? schemaName = null, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var uniqueString = unique ? "UNIQUE" : ""; - var columnList = string.Join(", ", columnNames); - - await ExecuteAsync( - db, - $@" - CREATE {uniqueString} INDEX {indexName} ON {schemaName}.{tableName} ({columnList}) - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetIndexesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - @$"select - s.nspname as schema_name, - t.relname as table_name, - i.relname as index_name, - a.attname as column_name, - ix.indisunique as is_unique, - idx.indexdef as index_sql - from pg_class t - join pg_index ix on t.oid = ix.indrelid - join pg_class i on i.oid = ix.indexrelid - join pg_attribute a on a.attrelid = t.oid and a.attnum = ANY(ix.indkey) - join pg_namespace s on s.oid = t.relnamespace - join pg_indexes idx on idx.schemaname = s.nspname and idx.tablename = t.relname and idx.indexname = i.relname - where - t.relkind = 'r'" - + (string.IsNullOrWhiteSpace(schemaName) ? "" : " AND s.nspname = @schemaName") - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND t.relname = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND i.relname LIKE @where") - + @" ORDER BY s.nspname, t.relname, i.relname, array_position(ix.indkey, a.attnum)"; - - var results = await QueryAsync<( - string schema_name, - string table_name, - string index_name, - string column_name, - bool is_unique, - string index_sql - )>( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - - var grouped = results.GroupBy( - r => (r.schema_name, r.table_name, r.index_name), - r => (r.is_unique, r.column_name, r.index_sql) - ); - - var indexes = new List(); - foreach (var group in grouped) - { - var (schema_name, table_name, index_name) = group.Key; - var (is_unique, column_name, index_sql) = group.First(); - var index = new TableIndex( - schema_name, - table_name, - index_name, - group - .Select(g => - { - var col = g.column_name; - var sql = g.index_sql.ToLowerInvariant().Replace("[", "").Replace("]", ""); - var direction = sql.ToLowerInvariant() - .Contains($"{col} desc", StringComparison.OrdinalIgnoreCase) - ? "DESC" - : "ASC"; - return $"{col} {direction}"; - }) - .ToArray(), - is_unique - ); - indexes.Add(index); - } - - return indexes; - } - - public async Task> GetIndexNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - @$"SELECT indexname - FROM pg_indexes - WHERE - schemaname NOT IN ('pg_catalog', 'information_schema')" - + (string.IsNullOrWhiteSpace(schemaName) ? "" : " AND schemaname = @schemaName") - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND tablename = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND indexname LIKE @where") - + @" ORDER BY indexname"; - - return await QueryAsync( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - } - - public async Task DropIndexIfExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if ( - !await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - await ExecuteAsync( - db, - $@" - DROP INDEX {schemaName}.{indexName} - ", - transaction: tx - ); - - return true; - } -} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.SchemaMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.SchemaMethods.cs deleted file mode 100644 index 3dca51c..0000000 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.SchemaMethods.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Data; -using System.Data.Common; - -namespace DapperMatic.Providers.PostgreSql; - -public partial class PostgreSqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task SchemaExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - schemaName = NormalizeSchemaName(schemaName); - - return 0 - < await ExecuteScalarAsync( - db, - "SELECT count(*) as SchemaCount FROM pg_catalog.pg_namespace WHERE nspname = @schemaName", - new { schemaName } - ) - .ConfigureAwait(false); - } - - public async Task CreateSchemaIfNotExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (await SchemaExistsAsync(db, schemaName, tx, cancellationToken).ConfigureAwait(false)) - return false; - - schemaName = NormalizeSchemaName(schemaName); - await ExecuteAsync(db, $"CREATE SCHEMA IF NOT EXISTS {schemaName}").ConfigureAwait(false); - return true; - } - - public async Task> GetSchemaNamesAsync( - IDbConnection db, - string? nameFilter = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return await QueryAsync( - db, - "SELECT DISTINCT nspname FROM pg_catalog.pg_namespace ORDER BY nspname" - ) - .ConfigureAwait(false); - ; - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - "SELECT DISTINCT nspname FROM pg_catalog.pg_namespace WHERE nspname LIKE @where ORDER BY nspname", - new { where } - ) - .ConfigureAwait(false); - } - } - - public async Task DropSchemaIfExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (!await SchemaExistsAsync(db, schemaName, tx, cancellationToken).ConfigureAwait(false)) - return false; - - schemaName = NormalizeSchemaName(schemaName); - await ExecuteAsync(db, $"DROP SCHEMA IF EXISTS {schemaName} CASCADE").ConfigureAwait(false); - return true; - } -} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.TableMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.TableMethods.cs deleted file mode 100644 index 688bf41..0000000 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.TableMethods.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Data; -using System.Text; - -namespace DapperMatic.Providers.PostgreSql; - -public partial class PostgreSqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task TableExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - return 0 - < await ExecuteScalarAsync( - db, - "SELECT COUNT(*) FROM pg_class JOIN pg_catalog.pg_namespace n ON n.oid = pg_class.relnamespace WHERE relname = @tableName AND relkind = 'r' AND nspname = @schemaName", - new { schemaName, tableName } - ) - .ConfigureAwait(false); - } - - public async Task CreateTableIfNotExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - string[]? primaryKeyColumnNames = null, - Type[]? primaryKeyDotnetTypes = null, - int?[]? primaryKeyColumnLengths = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if ( - await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - if (primaryKeyColumnNames == null || primaryKeyColumnNames.Length == 0) - { - await ExecuteAsync( - db, - @$"CREATE TABLE {schemaName}.{tableName} ( - id serial NOT NULL, - CONSTRAINT pk_{schemaName}_{tableName}_id PRIMARY KEY (id) - ) - " - ) - .ConfigureAwait(false); - return true; - } - - var sql = new StringBuilder(); - sql.AppendLine($@"CREATE TABLE {schemaName}.{tableName} ("); - var columnNamesWithOrder = new List(); - for (var i = 0; i < primaryKeyColumnNames.Length; i++) - { - var columnArr = primaryKeyColumnNames[i].Split(' '); - var (_, _, columnName) = NormalizeNames(schemaName, tableName, columnArr[0]); - if (string.IsNullOrWhiteSpace(columnName)) - continue; - - columnNamesWithOrder.Add( - '"' + columnName + '"' // + (columnArr.Length > 1 ? $" {columnArr[1]}" : " ASC") - ); - - if (primaryKeyDotnetTypes != null && primaryKeyDotnetTypes.Length > i) - { - sql.AppendLine( - $@"{columnName} {GetSqlTypeString(primaryKeyDotnetTypes[i], (primaryKeyColumnLengths != null && primaryKeyColumnLengths.Length > i) ? primaryKeyColumnLengths[i] : null)} NOT NULL," - ); - } - else - { - sql.AppendLine($@"{columnName} serial NOT NULL,"); - } - } - sql.AppendLine( - $@"CONSTRAINT pk_{schemaName}_{tableName}_id PRIMARY KEY ({string.Join(", ", columnNamesWithOrder)})" - ); - sql.AppendLine(")"); - - await ExecuteAsync(db, sql.ToString(), transaction: tx).ConfigureAwait(false); - return true; - } - - public async Task> GetTableNamesAsync( - IDbConnection db, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - schemaName = NormalizeSchemaName(schemaName); - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return await QueryAsync( - db, - "SELECT DISTINCT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_SCHEMA = @schemaName ORDER BY TABLE_NAME", - new { schemaName } - ) - .ConfigureAwait(false); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - "SELECT DISTINCT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_SCHEMA = @schemaName AND TABLE_NAME LIKE @where ORDER BY TABLE_NAME", - new { schemaName, where } - ) - .ConfigureAwait(false); - } - } - - public async Task DropTableIfExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if ( - !await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - await ExecuteAsync( - db, - $"DROP TABLE IF EXISTS \"{schemaName}\".\"{tableName}\" CASCADE", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.UniqueConstraintMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.UniqueConstraintMethods.cs deleted file mode 100644 index 7ad9ffe..0000000 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.UniqueConstraintMethods.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.PostgreSql; - -public partial class PostgreSqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task UniqueConstraintExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - return 0 - < await ExecuteScalarAsync( - db, - $@" - SELECT COUNT(*) - FROM information_schema.table_constraints - WHERE table_schema = @schemaName AND - table_name = @tableName AND - constraint_name = @uniqueConstraintName AND - constraint_type = 'UNIQUE' - ", - new { schemaName, tableName, uniqueConstraintName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateUniqueConstraintIfNotExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string[] columnNames, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - var columnList = string.Join(", ", columnNames); - - await ExecuteAsync( - db, - $@" - ALTER TABLE {schemaName}.{tableName} - ADD CONSTRAINT {uniqueConstraintName} UNIQUE ({columnList}) - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public Task> GetUniqueConstraintNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return QueryAsync( - db, - $@" - SELECT constraint_name - FROM information_schema.table_constraints - WHERE table_schema = @schemaName AND - table_name = @tableName AND - constraint_type = 'UNIQUE' - ORDER BY constraint_name - ", - new { schemaName, tableName }, - tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return QueryAsync( - db, - $@" - SELECT constraint_name - FROM information_schema.table_constraints - WHERE table_schema = @schemaName AND - table_name = @tableName AND - constraint_type = 'UNIQUE' AND - constraint_name LIKE @where - ORDER BY constraint_name - ", - new { schemaName, tableName, where }, - tx - ); - } - } - - public async Task DropUniqueConstraintIfExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - await ExecuteAsync( - db, - $@" - ALTER TABLE {schemaName}.{tableName} - DROP CONSTRAINT {uniqueConstraintName} - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.cs deleted file mode 100644 index e588359..0000000 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.PostgreSql; - -public partial class PostgreSqlExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - internal PostgreSqlExtensions() { } - - protected override string DefaultSchema => "public"; - - protected override List DataTypes => - DataTypeMapFactory.GetDefaultDatabaseTypeDataTypeMap(DatabaseTypes.PostgreSql); - - protected override string NormalizeName(string name) - { - return base.NormalizeName(name).ToLowerInvariant(); - } - - public async Task GetDatabaseVersionAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await ExecuteScalarAsync(db, $@"SELECT version()", transaction: tx) - .ConfigureAwait(false) ?? ""; - } -} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.Strings.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.Strings.cs new file mode 100644 index 0000000..b1e1628 --- /dev/null +++ b/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.Strings.cs @@ -0,0 +1,251 @@ +using DapperMatic.Models; + +namespace DapperMatic.Providers.PostgreSql; + +public partial class PostgreSqlMethods +{ + #region Schema Strings + protected override (string sql, object parameters) SqlGetSchemaNames( + string? schemaNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(schemaNameFilter) + ? "" + : ToLikeString(schemaNameFilter); + + var sql = $""" + SELECT DISTINCT nspname + FROM pg_catalog.pg_namespace + {(string.IsNullOrWhiteSpace(where) ? "" : "WHERE lower(nspname) LIKE @where")} + ORDER BY nspname + """; + + return (sql, new { where }); + } + + protected override string SqlDropSchema(string schemaName) + { + return $"DROP SCHEMA IF EXISTS {NormalizeSchemaName(schemaName)} CASCADE"; + } + #endregion // Schema Strings + + #region Table Strings + + protected override string SqlInlineColumnNullable(DxColumn column) + { + // serial columns are implicitly NOT NULL + if ( + column.IsNullable + && (column.GetProviderDataType(ProviderType) ?? "").Contains( + "serial", + StringComparison.OrdinalIgnoreCase + ) + ) + return ""; + + return column.IsNullable && !column.IsUnique && !column.IsPrimaryKey + ? " NULL" + : " NOT NULL"; + } + + protected override string SqlInlinePrimaryKeyAutoIncrementColumnConstraint(DxColumn column) + { + if ( + (column.GetProviderDataType(ProviderType) ?? "").Contains( + "serial", + StringComparison.OrdinalIgnoreCase + ) + ) + return string.Empty; + + return "GENERATED BY DEFAULT AS IDENTITY"; + } + + protected override (string sql, object parameters) SqlDoesTableExist( + string? schemaName, + string tableName + ) + { + var sql = $""" + SELECT COUNT(*) + FROM pg_class pgc + JOIN pg_catalog.pg_namespace n ON n.oid = pgc.relnamespace + WHERE + pgc.relkind = 'r' + {( + string.IsNullOrWhiteSpace(schemaName) ? "" : " AND lower(n.nspname) = @schemaName" + )} + AND lower(pgc.relname) = @tableName + """; + + return ( + sql, + new + { + schemaName = NormalizeSchemaName(schemaName), + tableName = NormalizeName(tableName) + } + ); + } + + protected override (string sql, object parameters) SqlGetTableNames( + string? schemaName, + string? tableNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(tableNameFilter) ? "" : ToLikeString(tableNameFilter); + + var sql = $""" + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE + TABLE_TYPE = 'BASE TABLE' + AND lower(TABLE_SCHEMA) = @schemaName + AND TABLE_NAME NOT IN ('spatial_ref_sys', 'geometry_columns', 'geography_columns', 'raster_columns', 'raster_overviews') + {( + string.IsNullOrWhiteSpace(where) ? null : " AND lower(TABLE_NAME) LIKE @where" + )} + ORDER BY TABLE_NAME + """; + + return ( + sql, + new + { + schemaName = NormalizeSchemaName(schemaName).ToLowerInvariant(), + where = where.ToLowerInvariant() + } + ); + } + + protected override string SqlDropTable(string? schemaName, string tableName) + { + return $"DROP TABLE IF EXISTS {GetSchemaQualifiedIdentifierName(schemaName, tableName)} CASCADE"; + } + #endregion // Table Strings + + #region Column Strings + #endregion // Column Strings + + #region Check Constraint Strings + #endregion // Check Constraint Strings + + #region Default Constraint Strings + protected override string SqlAlterTableAddDefaultConstraint( + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression + ) + { + var schemaQualifiedTableName = GetSchemaQualifiedIdentifierName(schemaName, tableName); + + return $""" + + ALTER TABLE {schemaQualifiedTableName} + ALTER COLUMN {NormalizeName(columnName)} SET DEFAULT {expression} + + """; + } + + protected override string SqlDropDefaultConstraint( + string? schemaName, + string tableName, + string columnName, + string constraintName + ) + { + return $"ALTER TABLE {GetSchemaQualifiedIdentifierName(schemaName, tableName)} ALTER COLUMN {NormalizeName(columnName)} DROP DEFAULT"; + } + #endregion // Default Constraint Strings + + #region Primary Key Strings + #endregion // Primary Key Strings + + #region Unique Constraint Strings + #endregion // Unique Constraint Strings + + #region Foreign Key Constraint Strings + #endregion // Foreign Key Constraint Strings + + #region Index Strings + protected override string SqlDropIndex(string? schemaName, string tableName, string indexName) + { + return $"DROP INDEX {GetSchemaQualifiedIdentifierName(schemaName, indexName)} CASCADE"; + } + #endregion // Index Strings + + #region View Strings + + protected override (string sql, object parameters) SqlGetViewNames( + string? schemaName, + string? viewNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + + SELECT + v.viewname as ViewName + from pg_views as v + where + v.schemaname not like 'pg_%' + and v.schemaname != 'information_schema' + and v.viewname not in ('geography_columns', 'geometry_columns', 'raster_columns', 'raster_overviews') + and lower(v.schemaname) = @schemaName + {( + string.IsNullOrWhiteSpace(where) ? "" : " AND lower(v.viewname) LIKE @where" + )} + ORDER BY + v.schemaname, v.viewname + """; + + return ( + sql, + new + { + schemaName = NormalizeSchemaName(schemaName).ToLowerInvariant(), + where = where.ToLowerInvariant() + } + ); + } + + protected override (string sql, object parameters) SqlGetViews( + string? schemaName, + string? viewNameFilter + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + + SELECT + v.schemaname as SchemaName, + v.viewname as ViewName, + v.definition as Definition + from pg_views as v + where + v.schemaname not like 'pg_%' + and v.schemaname != 'information_schema' + and v.viewname not in ('geography_columns', 'geometry_columns', 'raster_columns', 'raster_overviews') + and lower(v.schemaname) = @schemaName + {( + string.IsNullOrWhiteSpace(where) ? "" : " AND lower(v.viewname) LIKE @where" + )} + ORDER BY + v.schemaname, v.viewname + """; + + return ( + sql, + new + { + schemaName = NormalizeSchemaName(schemaName).ToLowerInvariant(), + where = where.ToLowerInvariant() + } + ); + } + #endregion // View Strings +} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.Tables.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.Tables.cs new file mode 100644 index 0000000..7781459 --- /dev/null +++ b/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.Tables.cs @@ -0,0 +1,601 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.PostgreSql; + +public partial class PostgreSqlMethods +{ + public override async Task> GetTablesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + schemaName = NormalizeSchemaName(schemaName); + + var where = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + + // columns + // we could use information_schema but it's SOOO SLOW! unbearable really, + // so we will use pg_catalog instead + var columnsSql = $""" + + SELECT + schemas.nspname as schema_name, + tables.relname as table_name, + columns.attname as column_name, + columns.attnum as column_ordinal, + pg_get_expr(column_defs.adbin, column_defs.adrelid) as column_default, + case when (coalesce(primarykeys.conname, '') = '') then 0 else 1 end AS is_primary_key, + primarykeys.conname as pk_constraint_name, + case when columns.attnotnull then 0 else 1 end AS is_nullable, + case when (columns.attidentity = '') then 0 else 1 end as is_identity, + types.typname as data_type, + format_type(columns.atttypid, columns.atttypmod) as data_type_ext + FROM pg_catalog.pg_attribute AS columns + join pg_catalog.pg_type as types on columns.atttypid = types.oid + JOIN pg_catalog.pg_class AS tables ON columns.attrelid = tables.oid and tables.relkind = 'r' and tables.relpersistence = 'p' + JOIN pg_catalog.pg_namespace AS schemas ON tables.relnamespace = schemas.oid + left outer join pg_catalog.pg_attrdef as column_defs on columns.attrelid = column_defs.adrelid and columns.attnum = column_defs.adnum + left outer join pg_catalog.pg_constraint as primarykeys on columns.attnum=ANY(primarykeys.conkey) AND primarykeys.conrelid = tables.oid and primarykeys.contype = 'p' + where + schemas.nspname not like 'pg_%' and schemas.nspname != 'information_schema' and columns.attnum > 0 and not columns.attisdropped + AND lower(schemas.nspname) = @schemaName + AND tables.relname NOT IN ('spatial_ref_sys', 'geometry_columns', 'geography_columns', 'raster_columns', 'raster_overviews') + {( + string.IsNullOrWhiteSpace(where) ? null : " AND lower(tables.relname) LIKE @where" + )} + order by schema_name, table_name, column_ordinal; + + """; + var columnResults = await QueryAsync<( + string schema_name, + string table_name, + string column_name, + int column_ordinal, + string column_default, + bool is_primary_key, + string pk_constraint_name, + bool is_nullable, + bool is_identity, + string data_type, + string data_type_ext + )>(db, columnsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + + // get indexes + var indexes = await GetIndexesInternalAsync( + db, + schemaName, + tableNameFilter, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + // get primary key, unique key, foreign key and check constraints in a single query + var constraintsSql = $""" + + select + schemas.nspname as schema_name, + tables.relname as table_name, + r.conname as constraint_name, + indexes.relname as supporting_index_name, + case + when r.contype = 'c' then 'CHECK' + when r.contype = 'f' then 'FOREIGN KEY' + when r.contype = 'p' then 'PRIMARY KEY' + when r.contype = 'u' then 'UNIQUE' + else 'OTHER' + end as constraint_type, + pg_catalog.pg_get_constraintdef(r.oid, true) as constraint_definition, + referenced_tables.relname as referenced_table_name, + array_to_string(r.conkey, ',') as column_ordinals_csv, + array_to_string(r.confkey, ',') as referenced_column_ordinals_csv, + case + when r.confdeltype = 'a' then 'NO ACTION' + when r.confdeltype = 'r' then 'RESTRICT' + when r.confdeltype = 'c' then 'CASCADE' + when r.confdeltype = 'n' then 'SET NULL' + when r.confdeltype = 'd' then 'SET DEFAULT' + else null + end as delete_rule, + case + when r.confupdtype = 'a' then 'NO ACTION' + when r.confupdtype = 'r' then 'RESTRICT' + when r.confupdtype = 'c' then 'CASCADE' + when r.confupdtype = 'n' then 'SET NULL' + when r.confupdtype = 'd' then 'SET DEFAULT' + else null + end as update_rule + from pg_catalog.pg_constraint r + join pg_catalog.pg_namespace AS schemas ON r.connamespace = schemas.oid + join pg_class as tables on r.conrelid = tables.oid + left outer join pg_class as indexes on r.conindid = indexes.oid + left outer join pg_class as referenced_tables on r.confrelid = referenced_tables.oid + where + schemas.nspname not like 'pg_%' + and schemas.nspname != 'information_schema' + and r.contype in ('c', 'f', 'p', 'u') + and lower(schemas.nspname) = @schemaName + {( + string.IsNullOrWhiteSpace(where) ? null : " AND lower(tables.relname) LIKE @where" + )} + order by schema_name, table_name, constraint_type, constraint_name + + """; + var constraintResults = await QueryAsync<( + string schema_name, + string table_name, + string constraint_name, + string supporting_index_name, + /* CHECK, UNIQUE, FOREIGN KEY, PRIMARY KEY */ + string constraint_type, + string constraint_definition, + string referenced_table_name, + string column_ordinals_csv, + string referenced_column_ordinals_csv, + string delete_rule, + string update_rule + )>(db, constraintsSql, new { schemaName, where }, tx: tx).ConfigureAwait(false); + + var referencedTableNames = constraintResults + .Where(c => c.constraint_type == "FOREIGN KEY") + .Select(c => c.referenced_table_name.ToLowerInvariant()) + .Distinct() + .ToArray(); + var referencedColumnsSql = """ + + SELECT + schemas.nspname as schema_name, + tables.relname as table_name, + columns.attname as column_name, + columns.attnum as column_ordinal + FROM pg_catalog.pg_attribute AS columns + JOIN pg_catalog.pg_class AS tables ON columns.attrelid = tables.oid and tables.relkind = 'r' and tables.relpersistence = 'p' + JOIN pg_catalog.pg_namespace AS schemas ON tables.relnamespace = schemas.oid + where + schemas.nspname not like 'pg_%' and schemas.nspname != 'information_schema' and columns.attnum > 0 and not columns.attisdropped + AND lower(schemas.nspname) = @schemaName + AND lower(tables.relname) = ANY (@referencedTableNames) + order by schema_name, table_name, column_ordinal; + + """; + var referencedColumnsResults = + referencedTableNames.Length == 0 + ? [] + : await QueryAsync<( + string schema_name, + string table_name, + string column_name, + int column_ordinal + )>(db, referencedColumnsSql, new { schemaName, referencedTableNames }, tx: tx) + .ConfigureAwait(false); + + var tables = new List(); + + foreach ( + var tableColumnResults in columnResults.GroupBy(r => new + { + r.schema_name, + r.table_name + }) + ) + { + schemaName = tableColumnResults.Key.schema_name; + var tableName = tableColumnResults.Key.table_name; + var tableConstraintResults = constraintResults + .Where(t => + t.schema_name.Equals(schemaName, StringComparison.OrdinalIgnoreCase) + && t.table_name.Equals(tableName, StringComparison.OrdinalIgnoreCase) + ) + .ToArray(); + + var tableForeignKeyConstraints = tableConstraintResults + .Where(t => + t.constraint_type.Equals("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) + ) + .Select(row => + { + var sourceColumns = row + .column_ordinals_csv.Split(',') + .Select(r => + { + return new DxOrderedColumn( + tableColumnResults + .First(c => c.column_ordinal == int.Parse(r)) + .column_name + ); + }) + .ToArray(); + var referencedColumns = row + .referenced_column_ordinals_csv.Split(',') + .Select(r => + { + return new DxOrderedColumn( + referencedColumnsResults + .First(c => + c.table_name.Equals( + row.referenced_table_name, + StringComparison.OrdinalIgnoreCase + ) + && c.column_ordinal == int.Parse(r) + ) + .column_name + ); + }) + .ToArray(); + return new DxForeignKeyConstraint( + row.schema_name, + row.table_name, + row.constraint_name, + sourceColumns, + row.referenced_table_name, + referencedColumns, + row.delete_rule.ToForeignKeyAction(), + row.update_rule.ToForeignKeyAction() + ); + }) + .ToArray(); + + var tableCheckConstraints = tableConstraintResults + .Where(t => + t.constraint_type.Equals("CHECK", StringComparison.OrdinalIgnoreCase) + && t.constraint_definition.StartsWith( + "CHECK (", + StringComparison.OrdinalIgnoreCase + ) + ) + .Select(c => + { + var columns = (c.column_ordinals_csv) + .Split(',') + .Select(r => + { + return tableColumnResults + .First(tcr => tcr.column_ordinal == int.Parse(r)) + .column_name; + }) + .ToArray(); + return new DxCheckConstraint( + c.schema_name, + c.table_name, + columns.Length == 1 ? columns[0] : null, + c.constraint_name, + c.constraint_definition.Substring(7).TrimEnd(')') + ); + }) + .ToArray(); + + var tableDefaultConstraints = tableColumnResults + // ignore default values that are sequences (from SERIAL columns) + .Where(t => + !string.IsNullOrWhiteSpace(t.column_default) + && !t.column_default.StartsWith("nextval()", StringComparison.OrdinalIgnoreCase) + ) + .Select(c => + { + return new DxDefaultConstraint( + c.schema_name, + c.table_name, + c.column_name, + $"df_{c.table_name}_{c.column_name}", + c.column_default + ); + }) + .ToArray(); + + var tablePrimaryKeyConstraint = tableConstraintResults + .Where(t => + t.constraint_type.Equals("PRIMARY KEY", StringComparison.OrdinalIgnoreCase) + ) + .Select(row => + { + var columns = row + .column_ordinals_csv.Split(',') + .Select(r => + { + return new DxOrderedColumn( + tableColumnResults + .First(c => c.column_ordinal == int.Parse(r)) + .column_name + ); + }) + .ToArray(); + return new DxPrimaryKeyConstraint( + row.schema_name, + row.table_name, + row.constraint_name, + columns + ); + }) + .FirstOrDefault(); + + var tableUniqueConstraints = tableConstraintResults + .Where(t => t.constraint_type.Equals("UNIQUE", StringComparison.OrdinalIgnoreCase)) + .Select(row => + { + var columns = row + .column_ordinals_csv.Split(',') + .Select(r => + { + return new DxOrderedColumn( + tableColumnResults + .First(c => c.column_ordinal == int.Parse(r)) + .column_name + ); + }) + .ToArray(); + return new DxUniqueConstraint( + row.schema_name, + row.table_name, + row.constraint_name, + columns + ); + }) + .ToArray(); + + var tableIndexes = indexes + .Where(i => + (i.SchemaName ?? "").Equals(schemaName, StringComparison.OrdinalIgnoreCase) + && i.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) + ) + .ToArray(); + + var columns = new List(); + foreach (var tableColumn in tableColumnResults) + { + var columnIsUniqueViaUniqueConstraintOrIndex = + tableUniqueConstraints.Any(c => + c.Columns.Length == 1 + && c.Columns.Any(col => + col.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ) + || indexes.Any(i => + i.IsUnique + && i.Columns.Length == 1 + && i.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var columnIsPartOfIndex = indexes.Any(i => + i.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var foreignKeyConstraint = tableForeignKeyConstraints.FirstOrDefault(c => + c.SourceColumns.Any(scol => + scol.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var foreignKeyColumnIndex = foreignKeyConstraint + ?.SourceColumns.Select((scol, i) => new { c = scol, i }) + .FirstOrDefault(c => + c.c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.i; + + var dotnetTypeDescriptor = GetDotnetTypeFromSqlType( + tableColumn.data_type.Length < tableColumn.data_type_ext.Length + ? tableColumn.data_type_ext + : tableColumn.data_type + ); + + var column = new DxColumn( + tableColumn.schema_name, + tableColumn.table_name, + tableColumn.column_name, + dotnetTypeDescriptor.DotnetType, + new Dictionary + { + { ProviderType, tableColumn.data_type } + }, + dotnetTypeDescriptor.Length, + dotnetTypeDescriptor.Precision, + dotnetTypeDescriptor.Scale, + tableCheckConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.Expression, + tableDefaultConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.Expression, + tableColumn.is_nullable, + tablePrimaryKeyConstraint != null + && tablePrimaryKeyConstraint.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ), + tableColumn.is_identity, + columnIsUniqueViaUniqueConstraintOrIndex, + columnIsPartOfIndex, + foreignKeyConstraint != null, + foreignKeyConstraint?.ReferencedTableName, + foreignKeyConstraint + ?.ReferencedColumns.ElementAtOrDefault(foreignKeyColumnIndex ?? 0) + ?.ColumnName, + foreignKeyConstraint?.OnDelete, + foreignKeyConstraint?.OnUpdate + ); + + columns.Add(column); + } + + var table = new DxTable( + schemaName, + tableName, + [.. columns], + tablePrimaryKeyConstraint, + tableCheckConstraints, + tableDefaultConstraints, + tableUniqueConstraints, + tableForeignKeyConstraints, + tableIndexes + ); + tables.Add(table); + } + + return tables; + } + + protected override async Task> GetIndexesInternalAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var whereSchemaLike = string.IsNullOrWhiteSpace(schemaName) + ? null + : ToLikeString(schemaName); + var whereTableLike = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + var whereIndexLike = string.IsNullOrWhiteSpace(indexNameFilter) + ? null + : ToLikeString(indexNameFilter); + + var indexesSql = $""" + + select + schemas.nspname AS schema_name, + tables.relname AS table_name, + indexes.relname AS index_name, + case when i.indisunique then 1 else 0 end as is_unique, + array_to_string(array_agg ( + a.attname + || ' ' || CASE o.option & 1 WHEN 1 THEN 'DESC' ELSE 'ASC' END + || ' ' || CASE o.option & 2 WHEN 2 THEN 'NULLS FIRST' ELSE 'NULLS LAST' END + ORDER BY c.ordinality + ),',') AS columns_csv + from + pg_index AS i + JOIN pg_class AS tables ON tables.oid = i.indrelid + JOIN pg_namespace AS schemas ON tables.relnamespace = schemas.oid + JOIN pg_class AS indexes ON indexes.oid = i.indexrelid + CROSS JOIN LATERAL unnest (i.indkey) WITH ORDINALITY AS c (colnum, ordinality) + LEFT JOIN LATERAL unnest (i.indoption) WITH ORDINALITY AS o (option, ordinality) + ON c.ordinality = o.ordinality + JOIN pg_attribute AS a ON tables.oid = a.attrelid AND a.attnum = c.colnum + where + schemas.nspname not like 'pg_%' + and schemas.nspname != 'information_schema' + and i.indislive + and not i.indisprimary + {( + string.IsNullOrWhiteSpace(whereSchemaLike) + ? "" + : " AND lower(schemas.nspname) LIKE @whereSchemaLike" + )} + {( + string.IsNullOrWhiteSpace(whereTableLike) + ? "" + : " AND lower(tables.relname) LIKE @whereTableLike" + )} + {( + string.IsNullOrWhiteSpace(whereIndexLike) + ? "" + : " AND lower(indexes.relname) LIKE @whereIndexLike" + )} + -- postgresql creates an index for primary key and unique constraints, so we don't need to include them in the results + and indexes.relname not in (select x.conname from pg_catalog.pg_constraint x + join pg_catalog.pg_namespace AS x2 ON x.connamespace = x2.oid + join pg_class as x3 on x.conrelid = x3.oid + where x2.nspname = schemas.nspname and x3.relname = tables.relname) + group by schemas.nspname, tables.relname, indexes.relname, i.indisunique + order by schema_name, table_name, index_name + + """; + + var indexResults = await QueryAsync<( + string schema_name, + string table_name, + string index_name, + bool is_unique, + string columns_csv + )>( + db, + indexesSql, + new + { + whereSchemaLike, + whereTableLike, + whereIndexLike + }, + tx + ) + .ConfigureAwait(false); + + var indexes = new List(); + foreach (var ir in indexResults) + { + var columns = ir + .columns_csv.Split(',') + .Select(c => + { + var columnName = c.Trim() + .Split( + ' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ) + .First(); + var isDescending = c.Contains("desc", StringComparison.OrdinalIgnoreCase); + return new DxOrderedColumn( + columnName, + isDescending ? DxColumnOrder.Descending : DxColumnOrder.Ascending + ); + }) + .ToArray(); + + var index = new DxIndex( + ir.schema_name, + ir.table_name, + ir.index_name, + columns, + ir.is_unique + ); + indexes.Add(index); + } + + return [.. indexes]; + } +} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.cs new file mode 100644 index 0000000..b4f6139 --- /dev/null +++ b/src/DapperMatic/Providers/PostgreSql/PostgreSqlMethods.cs @@ -0,0 +1,59 @@ +using System.Data; +using DapperMatic.Interfaces; +using DapperMatic.Providers.Base; + +namespace DapperMatic.Providers.PostgreSql; + +public partial class PostgreSqlMethods : DatabaseMethodsBase, IDatabaseMethods +{ + public override DbProviderType ProviderType => DbProviderType.PostgreSql; + + public override IDbProviderTypeMap ProviderTypeMap => PostgreSqlProviderTypeMap.Instance.Value; + + private static string _defaultSchema = "public"; + protected override string DefaultSchema => _defaultSchema; + + public static void SetDefaultSchema(string schema) + { + _defaultSchema = schema; + } + + public override Task SupportsOrderedKeysInConstraintsAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return Task.FromResult(false); + } + + internal PostgreSqlMethods() { } + + public override async Task GetDatabaseVersionAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + // sample output: PostgreSQL 15.7 (Debian 15.7-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit + const string sql = "SELECT VERSION()"; + var versionString = + await ExecuteScalarAsync(db, sql, tx: tx).ConfigureAwait(false) ?? ""; + return DbProviderUtils.ExtractVersionFromVersionString(versionString); + } + + public override char[] QuoteChars => ['"']; + + /// + /// Postgresql is case sensitive, so we need to normalize names to lowercase. + /// + public override string NormalizeName(string name) + { + return base.NormalizeName(name).ToLowerInvariant(); + } + + protected override string ToLikeString(string text, string allowedSpecialChars = "-_.*") + { + return base.ToLikeString(text, allowedSpecialChars).ToLowerInvariant(); + } +} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlProviderTypeMap.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlProviderTypeMap.cs new file mode 100644 index 0000000..02755c3 --- /dev/null +++ b/src/DapperMatic/Providers/PostgreSql/PostgreSqlProviderTypeMap.cs @@ -0,0 +1,644 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DapperMatic.Providers.PostgreSql; + +// https://www.npgsql.org/doc/types/basic.html#read-mappings +// https://www.npgsql.org/doc/types/basic.html#write-mappings +public sealed class PostgreSqlProviderTypeMap : DbProviderTypeMapBase +{ + internal static readonly Lazy Instance = + new(() => new PostgreSqlProviderTypeMap()); + + private PostgreSqlProviderTypeMap() + : base() { } + + protected override DbProviderType ProviderType => DbProviderType.PostgreSql; + + public override string SqTypeForStringLengthMax => "text"; + + public override string SqTypeForBinaryLengthMax => "bytea"; + + public override string SqlTypeForJson => "jsonb"; + + /// + /// IMPORTANT!! The order within an affinity group matters, as the first possible match will be used as the recommended sql type for a dotnet type + /// + protected override DbProviderSqlType[] ProviderSqlTypes => + [ + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_smallint, + canUseToAutoIncrement: true, + minValue: -32768, + maxValue: 32767 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_int2, + canUseToAutoIncrement: true, + minValue: -32768, + maxValue: 32767 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_integer, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_int, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_int4, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_int8, + canUseToAutoIncrement: true, + minValue: -9223372036854775808, + maxValue: 9223372036854775807 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_bigint, + canUseToAutoIncrement: true, + minValue: -9223372036854775808, + maxValue: 9223372036854775807 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_smallserial, + canUseToAutoIncrement: true, + autoIncrementsAutomatically: true, + minValue: 0, + maxValue: 32767 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_serial2, + canUseToAutoIncrement: true, + autoIncrementsAutomatically: true, + minValue: 0, + maxValue: 32767 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_serial, + canUseToAutoIncrement: true, + autoIncrementsAutomatically: true, + minValue: 0, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_serial4, + canUseToAutoIncrement: true, + autoIncrementsAutomatically: true, + minValue: 0, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_bigserial, + canUseToAutoIncrement: true, + autoIncrementsAutomatically: true, + minValue: 0, + maxValue: 9223372036854775807 + ), + new( + DbProviderSqlTypeAffinity.Integer, + PostgreSqlTypes.sql_serial8, + canUseToAutoIncrement: true, + autoIncrementsAutomatically: true, + minValue: 0, + maxValue: 9223372036854775807 + ), + new( + DbProviderSqlTypeAffinity.Real, + PostgreSqlTypes.sql_real, + minValue: float.MinValue, + maxValue: float.MaxValue + ), + new( + DbProviderSqlTypeAffinity.Real, + PostgreSqlTypes.sql_double_precision, + minValue: double.MinValue, + maxValue: double.MaxValue + ), + new( + DbProviderSqlTypeAffinity.Real, + PostgreSqlTypes.sql_float4, + minValue: float.MinValue, + maxValue: float.MaxValue + ), + new( + DbProviderSqlTypeAffinity.Real, + PostgreSqlTypes.sql_float8, + minValue: double.MinValue, + maxValue: double.MaxValue + ), + new( + DbProviderSqlTypeAffinity.Real, + PostgreSqlTypes.sql_money, + formatWithPrecision: "money({0})", + defaultPrecision: 19, + minValue: -92233720368547758.08, + maxValue: 92233720368547758.07 + ), + new( + DbProviderSqlTypeAffinity.Real, + PostgreSqlTypes.sql_numeric, + formatWithPrecision: "numeric({0})", + formatWithPrecisionAndScale: "numeric({0},{1})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + PostgreSqlTypes.sql_decimal, + formatWithPrecision: "decimal({0})", + formatWithPrecisionAndScale: "decimal({0},{1})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Boolean, + PostgreSqlTypes.sql_bool, + canUseToAutoIncrement: false + ), + new(DbProviderSqlTypeAffinity.Boolean, PostgreSqlTypes.sql_boolean), + new(DbProviderSqlTypeAffinity.DateTime, PostgreSqlTypes.sql_date, isDateOnly: true), + new(DbProviderSqlTypeAffinity.DateTime, PostgreSqlTypes.sql_interval), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_time_without_timezone, + formatWithPrecision: "time({0}) without timezone", + defaultPrecision: 6 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_time, + formatWithPrecision: "time({0})", + defaultPrecision: 6, + isTimeOnly: true + ), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_time_with_time_zone, + formatWithPrecision: "time({0}) with time zone", + defaultPrecision: 6 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_timetz, + formatWithPrecision: "timetz({0})", + defaultPrecision: 6, + isTimeOnly: true + ), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_timestamp_without_time_zone, + formatWithPrecision: "timestamp({0}) without time zone", + defaultPrecision: 6 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_timestamp, + formatWithPrecision: "timestamp({0})", + defaultPrecision: 6 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_timestamp_with_time_zone, + formatWithPrecision: "timestamp({0}) with time zone", + defaultPrecision: 6 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + PostgreSqlTypes.sql_timestamptz, + formatWithPrecision: "timestamptz({0})", + defaultPrecision: 6 + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_varchar, + formatWithLength: "varchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_character_varying, + formatWithLength: "character varying({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_character, + formatWithLength: "character({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_char, + formatWithLength: "char({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_bpchar, + formatWithLength: "bpchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_bit, + formatWithPrecision: "bit({0})", + defaultPrecision: 1, + minValue: 0, + maxValue: 1 + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_bit_varying, + formatWithPrecision: "bit varying({0})", + defaultPrecision: 63 + ), + new( + DbProviderSqlTypeAffinity.Text, + PostgreSqlTypes.sql_varbit, + formatWithPrecision: "varbit({0})", + defaultPrecision: 63 + ), + new(DbProviderSqlTypeAffinity.Text, PostgreSqlTypes.sql_text), + new(DbProviderSqlTypeAffinity.Text, PostgreSqlTypes.sql_name), + new(DbProviderSqlTypeAffinity.Text, PostgreSqlTypes.sql_uuid, isGuidOnly: true), + new(DbProviderSqlTypeAffinity.Text, PostgreSqlTypes.sql_json), + new(DbProviderSqlTypeAffinity.Text, PostgreSqlTypes.sql_jsonb), + new(DbProviderSqlTypeAffinity.Text, PostgreSqlTypes.sql_jsonpath), + new(DbProviderSqlTypeAffinity.Text, PostgreSqlTypes.sql_xml), + new(DbProviderSqlTypeAffinity.Binary, PostgreSqlTypes.sql_bytea), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_box), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_circle), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_geography), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_geometry), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_line), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_lseg), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_path), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_point), + new(DbProviderSqlTypeAffinity.Geometry, PostgreSqlTypes.sql_polygon), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_datemultirange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_daterange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_int4multirange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_int4range), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_int8multirange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_int8range), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_nummultirange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_numrange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_tsmultirange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_tsrange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_tstzmultirange), + new(DbProviderSqlTypeAffinity.RangeType, PostgreSqlTypes.sql_tstzrange), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_cidr), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_citext), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_hstore), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_inet), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_int2vector), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_lquery), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_ltree), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_ltxtquery), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_macaddr), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_macaddr8), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_oid), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_oidvector), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_pg_lsn), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_pg_snapshot), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_refcursor), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_regclass), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_regcollation), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_regconfig), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_regdictionary), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_regnamespace), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_regrole), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_regtype), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_tid), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_tsquery), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_tsvector), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_txid_snapshot), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_xid), + new(DbProviderSqlTypeAffinity.Other, PostgreSqlTypes.sql_xid8), + ]; + + protected override bool TryGetProviderSqlTypeMatchingDotnetTypeInternal( + DbProviderDotnetTypeDescriptor descriptor, + out DbProviderSqlType? providerSqlType + ) + { + providerSqlType = null; + + var dotnetType = descriptor.DotnetType; + + // handle well-known types first + providerSqlType = dotnetType.IsGenericType + ? null + : dotnetType switch + { + Type t when t == typeof(bool) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_boolean], + Type t when t == typeof(byte) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int2], + Type t when t == typeof(ReadOnlyMemory) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int2], + Type t when t == typeof(sbyte) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int2], + // it's no longer recommended to use SERIAL auto-incrementing columns + // Type t when t == typeof(short) + // => descriptor.AutoIncrement.GetValueOrDefault(false) + // ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_serial2] + // : ProviderSqlTypeLookup[PostgreSqlTypes.sql_int2], + // Type t when t == typeof(ushort) + // => descriptor.AutoIncrement.GetValueOrDefault(false) + // ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_serial2] + // : ProviderSqlTypeLookup[PostgreSqlTypes.sql_int2], + // Type t when t == typeof(int) + // => descriptor.AutoIncrement.GetValueOrDefault(false) + // ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_serial4] + // : ProviderSqlTypeLookup[PostgreSqlTypes.sql_int4], + // Type t when t == typeof(uint) + // => descriptor.AutoIncrement.GetValueOrDefault(false) + // ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_serial4] + // : ProviderSqlTypeLookup[PostgreSqlTypes.sql_int4], + // Type t when t == typeof(long) + // => descriptor.AutoIncrement.GetValueOrDefault(false) + // ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_serial8] + // : ProviderSqlTypeLookup[PostgreSqlTypes.sql_int8], + // Type t when t == typeof(ulong) + // => descriptor.AutoIncrement.GetValueOrDefault(false) + // ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_serial8] + // : ProviderSqlTypeLookup[PostgreSqlTypes.sql_int8], + Type t when t == typeof(short) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int2], + Type t when t == typeof(ushort) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int2], + Type t when t == typeof(int) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int4], + Type t when t == typeof(uint) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int4], + Type t when t == typeof(long) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int8], + Type t when t == typeof(ulong) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int8], + Type t when t == typeof(float) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_float4], + Type t when t == typeof(double) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_float8], + Type t when t == typeof(decimal) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_numeric], + Type t when t == typeof(char) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar], + Type t when t == typeof(string) + => descriptor.Length.GetValueOrDefault(255) < 8000 + ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar] + : ProviderSqlTypeLookup[PostgreSqlTypes.sql_text], + Type t when t == typeof(char[]) + => descriptor.Length.GetValueOrDefault(255) < 8000 + ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar] + : ProviderSqlTypeLookup[PostgreSqlTypes.sql_text], + Type t when t == typeof(ReadOnlyMemory[]) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar] + : ProviderSqlTypeLookup[PostgreSqlTypes.sql_text], + Type t when t == typeof(Stream) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar] + : ProviderSqlTypeLookup[PostgreSqlTypes.sql_text], + Type t when t == typeof(TextReader) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar] + : ProviderSqlTypeLookup[PostgreSqlTypes.sql_text], + Type t when t == typeof(byte[]) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_bytea], + Type t when t == typeof(object) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar] + : ProviderSqlTypeLookup[PostgreSqlTypes.sql_text], + Type t when t == typeof(object[]) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[PostgreSqlTypes.sql_varchar] + : ProviderSqlTypeLookup[PostgreSqlTypes.sql_text], + Type t when t == typeof(Guid) => ProviderSqlTypeLookup[PostgreSqlTypes.sql_uuid], + Type t when t == typeof(DateTime) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_timestamp], + Type t when t == typeof(DateTimeOffset) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_timestamptz], + Type t when t == typeof(TimeSpan) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_interval], + Type t when t == typeof(DateOnly) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_date], + Type t when t == typeof(TimeOnly) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_time], + Type t when t == typeof(BitArray) || t == typeof(BitVector32) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_varbit], + Type t + when t == typeof(ImmutableDictionary) + || t == typeof(Dictionary) + || t == typeof(IDictionary) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_hstore], + Type t + when t == typeof(JsonNode) + || t == typeof(JsonObject) + || t == typeof(JsonArray) + || t == typeof(JsonValue) + || t == typeof(JsonDocument) + || t == typeof(JsonElement) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_jsonb], + _ => null + }; + + if (providerSqlType != null) + return true; + + // handle generic types + providerSqlType = !dotnetType.IsGenericType + ? null + : dotnetType.GetGenericTypeDefinition() switch + { + Type t + when t == typeof(Dictionary<,>) + || t == typeof(IDictionary<,>) + || t == typeof(List<>) + || t == typeof(IList<>) + || t == typeof(Collection<>) + || t == typeof(ICollection<>) + || t == typeof(IEnumerable<>) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_jsonb], + Type t when t.Name.StartsWith("NpgsqlRange") + => dotnetType.GetGenericArguments().First() switch + { + Type at when at == typeof(DateOnly) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_daterange], + Type at when at == typeof(int) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int4range], + Type at when at == typeof(long) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_int8range], + Type at when at == typeof(decimal) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_numrange], + Type at when at == typeof(float) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_numrange], + Type at when at == typeof(double) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_numrange], + Type at when at == typeof(DateTime) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_tsrange], + Type at when at == typeof(DateTimeOffset) + => ProviderSqlTypeLookup[PostgreSqlTypes.sql_tstzrange], + _ => null + }, + _ => null + }; + + if (providerSqlType != null) + return true; + + // Handle Npgsql types + switch (dotnetType.FullName) + { + case "System.Net.IPAddress": + case "NpgsqlTypes.NpgsqlInet": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_inet]; + break; + case "NpgsqlTypes.NpgsqlCidr": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_cidr]; + break; + case "System.Net.NetworkInformation.PhysicalAddress": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_macaddr8]; + // providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_macaddr]; + break; + case "NpgsqlTypes.NpgsqlPoint": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_point]; + break; + case "NpgsqlTypes.NpgsqlLSeg": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_lseg]; + break; + case "NpgsqlTypes.NpgsqlPath": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_path]; + break; + case "NpgsqlTypes.NpgsqlPolygon": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_polygon]; + break; + case "NpgsqlTypes.NpgsqlLine": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_line]; + break; + case "NpgsqlTypes.NpgsqlCircle": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_circle]; + break; + case "NpgsqlTypes.NpgsqlBox": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_box]; + break; + case "NetTopologySuite.Geometries.Geometry": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_geometry]; + break; + case "NpgsqlTypes.NpgsqlInterval": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_interval]; + break; + case "NpgsqlTypes.NpgsqlTid": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_tid]; + break; + case "NpgsqlTypes.NpgsqlTsQuery": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_tsquery]; + break; + case "NpgsqlTypes.NpgsqlTsVector": + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_tsvector]; + break; + } + + if (providerSqlType != null) + return true; + + // handle array types + var elementType = dotnetType.IsArray ? dotnetType.GetElementType() : null; + if ( + elementType != null + && TryGetProviderSqlTypeMatchingDotnetTypeInternal( + new DbProviderDotnetTypeDescriptor(elementType), + out var elementProviderSqlType + ) + && elementProviderSqlType != null + ) + { + switch (elementProviderSqlType.Name) + { + case PostgreSqlTypes.sql_tsrange: + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_tsmultirange]; + break; + case PostgreSqlTypes.sql_numrange: + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_nummultirange]; + break; + case PostgreSqlTypes.sql_daterange: + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_datemultirange]; + break; + case PostgreSqlTypes.sql_int4range: + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_int4multirange]; + break; + case PostgreSqlTypes.sql_int8range: + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_int8multirange]; + break; + case PostgreSqlTypes.sql_tstzrange: + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_tstzmultirange]; + break; + default: + // in postgresql, we can have array types that end with [] or ARRA + providerSqlType = new DbProviderSqlType( + DbProviderSqlTypeAffinity.Other, + $"{elementProviderSqlType.Name}[]" + ); + break; + } + } + + if (providerSqlType != null) + return true; + + // handle POCO type + if (dotnetType.IsClass || dotnetType.IsInterface) + { + providerSqlType = ProviderSqlTypeLookup[PostgreSqlTypes.sql_jsonb]; + } + + return providerSqlType != null; + } + + protected override bool TryGetProviderSqlTypeFromFullSqlTypeName( + string fullSqlType, + out DbProviderSqlType? providerSqlType + ) + { + if (base.TryGetProviderSqlTypeFromFullSqlTypeName(fullSqlType, out providerSqlType)) + return true; + + providerSqlType = null; + + // PostgreSql (unlike other providers) supports array types for (almost) all its types + if (fullSqlType.EndsWith("[]", StringComparison.OrdinalIgnoreCase)) + { + var elementTypeName = fullSqlType.Substring(0, fullSqlType.Length - 2); + if ( + TryGetProviderSqlTypeFromFullSqlTypeName( + elementTypeName, + out var elementProviderSqlType + ) + && elementProviderSqlType != null + ) + { + providerSqlType = new DbProviderSqlType( + DbProviderSqlTypeAffinity.Other, + $"{elementProviderSqlType.Name}[]" + ); + return true; + } + } + + return false; + } +} diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlTypes.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlTypes.cs new file mode 100644 index 0000000..d710ff6 --- /dev/null +++ b/src/DapperMatic/Providers/PostgreSql/PostgreSqlTypes.cs @@ -0,0 +1,122 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Providers.PostgreSql; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static class PostgreSqlTypes +{ + // integers + public const string sql_smallint = "smallint"; + public const string sql_int2 = "int2"; + public const string sql_smallserial = "smallserial"; + public const string sql_serial2 = "serial2"; + public const string sql_integer = "integer"; + public const string sql_int = "int"; + public const string sql_int4 = "int4"; + public const string sql_serial = "serial"; + public const string sql_serial4 = "serial4"; + public const string sql_bigint = "bigint"; + public const string sql_int8 = "int8"; + public const string sql_bigserial = "bigserial"; + public const string sql_serial8 = "serial8"; + + // real + public const string sql_float4 = "float4"; + public const string sql_real = "real"; + public const string sql_double_precision = "double precision"; + public const string sql_float8 = "float8"; + public const string sql_money = "money"; + public const string sql_numeric = "numeric"; + public const string sql_decimal = "decimal"; + + // bool + public const string sql_bool = "bool"; + public const string sql_boolean = "boolean"; + + // datetime + public const string sql_date = "date"; + public const string sql_interval = "interval"; + public const string sql_time_without_timezone = "time without timezone"; + public const string sql_time = "time"; + public const string sql_time_with_time_zone = "time with time zone"; + public const string sql_timetz = "timetz"; + public const string sql_timestamp_without_time_zone = "timestamp without time zone"; + public const string sql_timestamp = "timestamp"; + public const string sql_timestamp_with_time_zone = "timestamp with time zone"; + public const string sql_timestamptz = "timestamptz"; + + // text + public const string sql_bit = "bit"; + public const string sql_bit_varying = "bit varying"; + public const string sql_varbit = "varbit"; + public const string sql_character_varying = "character varying"; + public const string sql_varchar = "varchar"; + public const string sql_character = "character"; + public const string sql_char = "char"; + public const string sql_bpchar = "bpchar"; + public const string sql_text = "text"; + public const string sql_name = "name"; + public const string sql_uuid = "uuid"; + public const string sql_json = "json"; + public const string sql_jsonb = "jsonb"; + public const string sql_jsonpath = "jsonpath"; + public const string sql_xml = "xml"; + + // binary + public const string sql_bytea = "bytea"; + + // geometry + public const string sql_box = "box"; + public const string sql_circle = "circle"; + public const string sql_geography = "geography"; + public const string sql_geometry = "geometry"; + public const string sql_line = "line"; + public const string sql_lseg = "lseg"; + public const string sql_path = "path"; + public const string sql_point = "point"; + public const string sql_polygon = "polygon"; + + // range types + public const string sql_datemultirange = "datemultirange"; + public const string sql_daterange = "daterange"; + public const string sql_int4multirange = "int4multirange"; + public const string sql_int4range = "int4range"; + public const string sql_int8multirange = "int8multirange"; + public const string sql_int8range = "int8range"; + public const string sql_nummultirange = "nummultirange"; + public const string sql_numrange = "numrange"; + public const string sql_tsmultirange = "tsmultirange"; + public const string sql_tsrange = "tsrange"; + public const string sql_tstzmultirange = "tstzmultirange"; + public const string sql_tstzrange = "tstzrange"; + + // other data types + public const string sql_cidr = "cidr"; + public const string sql_citext = "citext"; + public const string sql_hstore = "hstore"; + public const string sql_inet = "inet"; + public const string sql_int2vector = "int2vector"; + public const string sql_lquery = "lquery"; + public const string sql_ltree = "ltree"; + public const string sql_ltxtquery = "ltxtquery"; + public const string sql_macaddr = "macaddr"; + public const string sql_macaddr8 = "macaddr8"; + public const string sql_oid = "oid"; + public const string sql_oidvector = "oidvector"; + public const string sql_pg_lsn = "pg_lsn"; + public const string sql_pg_snapshot = "pg_snapshot"; + public const string sql_refcursor = "refcursor"; + public const string sql_regclass = "regclass"; + public const string sql_regcollation = "regcollation"; + public const string sql_regconfig = "regconfig"; + public const string sql_regdictionary = "regdictionary"; + public const string sql_regnamespace = "regnamespace"; + public const string sql_regrole = "regrole"; + public const string sql_regtype = "regtype"; + public const string sql_tid = "tid"; + public const string sql_tsquery = "tsquery"; + public const string sql_tsvector = "tsvector"; + public const string sql_txid_snapshot = "txid_snapshot"; + public const string sql_xid = "xid"; + public const string sql_xid8 = "xid8"; +} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ColumnMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ColumnMethods.cs deleted file mode 100644 index 8031a18..0000000 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ColumnMethods.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.SqlServer; - -public partial class SqlServerExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task ColumnExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - return 0 - < await ExecuteScalarAsync( - db, - "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName AND COLUMN_NAME = @columnName", - new { schemaName, tableName, columnName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateColumnIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - Type dotnetType, - string? type = null, - int? length = null, - int? precision = null, - int? scale = null, - string? schemaName = null, - string? defaultValue = null, - bool nullable = true, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var sqlType = type ?? GetSqlTypeString(dotnetType, length, precision, scale); - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - await ExecuteAsync( - db, - $@"ALTER TABLE {schemaName}.{tableName} - ADD {columnName} {sqlType} {(nullable ? "NULL" : "NOT NULL")} {(!string.IsNullOrWhiteSpace(defaultValue) ? $"DEFAULT {defaultValue}" : "")} {(unique ? "UNIQUE" : "")} - ", - new { schemaName, tableName, columnName }, - tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetColumnNamesAsync( - IDbConnection db, - string tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - return await QueryAsync( - db, - "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName", - new { schemaName, tableName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task DropColumnIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - // get foreign keys for the columnName - var foreignKeys = await QueryAsync( - db, - $@" - SELECT - fk.name - FROM sys.foreign_keys fk - INNER JOIN sys.foreign_key_columns fkc - ON fk.object_id = fkc.constraint_object_id - INNER JOIN sys.columns c - ON fkc.parent_object_id = c.object_id - AND fkc.parent_column_id = c.column_id - WHERE c.object_id = OBJECT_ID('[{schemaName}].[{tableName}]') - AND c.name = @columnName", - new { columnName }, - tx - ) - .ConfigureAwait(false); - - // drop foreign keys - foreach (var fk in foreignKeys) - { - await ExecuteAsync( - db, - $@" - ALTER TABLE [{schemaName}].[{tableName}] - DROP CONSTRAINT {fk}", - tx - ) - .ConfigureAwait(false); - } - - // get indexes for the columnName (indexes and unique constraints) - var indexes = await QueryAsync<(string, bool)>( - db, - $@" - SELECT - i.name, i.is_unique_constraint - FROM sys.indexes i - INNER JOIN sys.index_columns ic - ON i.object_id = ic.object_id - AND i.index_id = ic.index_id - INNER JOIN sys.columns c - ON ic.object_id = c.object_id - AND ic.column_id = c.column_id - WHERE c.object_id = OBJECT_ID('[{schemaName}].[{tableName}]') - AND is_primary_key = 0 - AND c.name = @columnName", - new { columnName }, - tx - ) - .ConfigureAwait(false); - - // drop indexes - foreach (var indexName in indexes) - { - if (indexName.Item2 == true) - { - await ExecuteAsync( - db, - $@" - ALTER TABLE [{schemaName}].[{tableName}] - DROP CONSTRAINT {indexName.Item1}", - tx - ) - .ConfigureAwait(false); - continue; - } - else - { - await ExecuteAsync( - db, - $@" - DROP INDEX [{schemaName}].[{tableName}].[{indexName}]", - tx - ) - .ConfigureAwait(false); - } - } - - // get default constraints for the columnName - var defaultConstraints = await QueryAsync( - db, - $@" - SELECT - dc.name - FROM sys.default_constraints dc - INNER JOIN sys.columns c - ON dc.parent_object_id = c.object_id - AND dc.parent_column_id = c.column_id - WHERE c.object_id = OBJECT_ID('[{schemaName}].[{tableName}]') - AND c.name = @columnName", - new { columnName }, - tx - ) - .ConfigureAwait(false); - - // drop default constraints - foreach (var dc in defaultConstraints) - { - await ExecuteAsync( - db, - $@" - ALTER TABLE [{schemaName}].[{tableName}] - DROP CONSTRAINT {dc}", - tx - ) - .ConfigureAwait(false); - } - - // drop columnName - await ExecuteAsync( - db, - $@"ALTER TABLE {schemaName}.{tableName} DROP COLUMN {columnName}", - new { schemaName, tableName, columnName }, - tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ForeignKeyMethods.cs deleted file mode 100644 index b1e0e07..0000000 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ForeignKeyMethods.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic.Providers.SqlServer; - -public partial class SqlServerExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task ForeignKeyExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - if (!string.IsNullOrWhiteSpace(foreignKey)) - { - var foreignKeyName = NormalizeName(foreignKey); - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = @schemaName AND - TABLE_NAME = @tableName AND - CONSTRAINT_NAME = @foreignKeyName AND - CONSTRAINT_TYPE = 'FOREIGN KEY'", - new - { - schemaName, - tableName, - foreignKeyName - }, - tx - ) - .ConfigureAwait(false); - } - else - { - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM sys.foreign_keys AS f - INNER JOIN sys.foreign_key_columns AS fc - ON f.object_id = fc.constraint_object_id - WHERE f.parent_object_id = OBJECT_ID('{schemaAndTableName}') AND - COL_NAME(fc.parent_object_id, fc.parent_column_id) = @columnName", - new - { - schemaName, - tableName, - columnName - }, - tx - ) - .ConfigureAwait(false); - } - } - - public async Task CreateForeignKeyIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string foreignKey, - string referenceTable, - string referenceColumn, - string? schemaName = null, - string onDelete = "NO ACTION", - string onUpdate = "NO ACTION", - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(foreignKey)) - throw new ArgumentException("Foreign key name must be specified.", nameof(foreignKey)); - if (string.IsNullOrWhiteSpace(referenceTable)) - throw new ArgumentException( - "Reference tableName name must be specified.", - nameof(referenceTable) - ); - if (string.IsNullOrWhiteSpace(referenceColumn)) - throw new ArgumentException( - "Reference columnName name must be specified.", - nameof(referenceColumn) - ); - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - - if ( - await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - var (referenceSchemaName, referenceTableName, referenceColumnName) = NormalizeNames( - schemaName, - referenceTable, - referenceColumn - ); - - var foreignKeyName = NormalizeName(foreignKey); - - await ExecuteAsync( - db, - $@" - ALTER TABLE [{schemaName}].[{tableName}] - ADD CONSTRAINT [{foreignKeyName}] - FOREIGN KEY ([{columnName}]) - REFERENCES [{referenceSchemaName}].[{referenceTableName}] ([{referenceColumnName}]) - ON DELETE {onDelete} - ON UPDATE {onUpdate}", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetForeignKeysAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"select - fk.name as constraint_name, - schema_name(fk_tab.schema_id) as schema_name, - fk_tab.name as table_name, - substring(fk_column_names, 1, len(fk_column_names)-1) as [column_name], - schema_name(pk_tab.schema_id) as referenced_schema_name, - pk_tab.name as referenced_table_name, - substring(pk_column_names, 1, len(pk_column_names)-1) as [referenced_column_name], - fk.delete_referential_action_desc as delete_rule, - fk.update_referential_action_desc as update_rule - from sys.foreign_keys fk - inner join sys.tables fk_tab on fk_tab.object_id = fk.parent_object_id - inner join sys.tables pk_tab on pk_tab.object_id = fk.referenced_object_id - cross apply (select col.[name] + ', ' - from sys.foreign_key_columns fk_c - inner join sys.columns col - on fk_c.parent_object_id = col.object_id - and fk_c.parent_column_id = col.column_id - where fk_c.parent_object_id = fk_tab.object_id - and fk_c.constraint_object_id = fk.object_id - order by col.column_id - for xml path ('') ) D (fk_column_names) - cross apply (select col.[name] + ', ' - from sys.foreign_key_columns fk_c - inner join sys.columns col - on fk_c.referenced_object_id = col.object_id - and fk_c.referenced_column_id = col.column_id - where fk_c.referenced_object_id = pk_tab.object_id - and fk_c.constraint_object_id = fk.object_id - order by col.column_id - for xml path ('') ) G (pk_column_names) - where 1 = 1"; - if (!string.IsNullOrWhiteSpace(schemaName)) - sql += $@" AND schema_name(fk_tab.schema_id) = @schemaName"; - if (!string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND fk_tab.name = @tableName"; - if (!string.IsNullOrWhiteSpace(where)) - sql += $@" AND fk.name LIKE @where"; - sql += - $@" - order by schema_name(fk_tab.schema_id), fk_tab.name, fk.name"; - - var results = await QueryAsync<( - string constraint_name, - string schema_name, - string table_name, - string column_name, - string referenced_schema_name, - string referenced_table_name, - string referenced_column_name, - string delete_rule, - string update_rule - )>( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - - return results.Select(r => - { - var deleteRule = (r.delete_rule ?? "").Replace('_', ' ') switch - { - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - "NO ACTION" => ReferentialAction.NoAction, - _ => ReferentialAction.NoAction - }; - var updateRule = (r.update_rule ?? "").Replace('_', ' ') switch - { - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - "NO ACTION" => ReferentialAction.NoAction, - _ => ReferentialAction.NoAction - }; - - return new ForeignKey( - r.schema_name, - r.constraint_name, - r.table_name, - r.column_name, - r.referenced_table_name, - r.referenced_column_name, - deleteRule, - updateRule - ); - }); - } - - public async Task> GetForeignKeyNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"SELECT CONSTRAINT_NAME - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - where - CONSTRAINT_TYPE = 'FOREIGN KEY'"; - if (!string.IsNullOrWhiteSpace(schemaName)) - sql += $@" AND TABLE_SCHEMA = @schemaName"; - if (!string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND TABLE_NAME = @tableName"; - if (!string.IsNullOrWhiteSpace(where)) - sql += $@" AND CONSTRAINT_NAME LIKE @where"; - sql += @" ORDER BY CONSTRAINT_NAME"; - - return await QueryAsync( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - } - - public async Task DropForeignKeyIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - if (!string.IsNullOrWhiteSpace(foreignKey)) - { - var foreignKeyName = NormalizeName(foreignKey); - await ExecuteAsync( - db, - $@"ALTER TABLE [{schemaName}].[{tableName}] DROP CONSTRAINT [{foreignKeyName}]", - transaction: tx - ) - .ConfigureAwait(false); - } - else - { - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - - // get the name of the foreign key - var foreignKeyName = await ExecuteScalarAsync( - db, - $@"SELECT top 1 f.name - FROM sys.foreign_keys AS f - INNER JOIN sys.foreign_key_columns AS fc - ON f.object_id = fc.constraint_object_id - WHERE f.parent_object_id = OBJECT_ID('{schemaAndTableName}') AND - COL_NAME(fc.parent_object_id, fc.parent_column_id) = @columnName", - new - { - schemaName, - tableName, - columnName - }, - tx - ) - .ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(foreignKeyName)) - { - await ExecuteAsync( - db, - $@"ALTER TABLE [{schemaName}].[{tableName}] DROP CONSTRAINT [{foreignKeyName}]", - transaction: tx - ) - .ConfigureAwait(false); - } - } - - return true; - } -} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.IndexMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.IndexMethods.cs deleted file mode 100644 index 41683c1..0000000 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.IndexMethods.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic.Providers.SqlServer; - -public partial class SqlServerExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task IndexExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) FROM sys.indexes - WHERE object_id = OBJECT_ID('{schemaAndTableName}') - AND name = @indexName and is_primary_key = 0 and is_unique_constraint = 0", - new { schemaAndTableName, indexName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateIndexIfNotExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string[] columnNames, - string? schemaName = null, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - var uniqueString = unique ? "UNIQUE" : ""; - var columnList = string.Join(", ", columnNames); - await ExecuteAsync( - db, - $@" - CREATE {uniqueString} INDEX {indexName} ON {schemaAndTableName} ({columnList}) - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetIndexesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - @$"SELECT - SCHEMA_NAME(t.schema_id) as schema_name, - t.name as table_name, - ind.name as index_name, - col.name as column_name, - ind.is_unique as is_unique, - ic.key_ordinal as key_ordinal, - ic.is_descending_key as is_descending_key - FROM sys.indexes ind - INNER JOIN sys.tables t ON ind.object_id = t.object_id - INNER JOIN sys.index_columns ic ON ind.object_id = ic.object_id and ind.index_id = ic.index_id - INNER JOIN sys.columns col ON ic.object_id = col.object_id and ic.column_id = col.column_id - WHERE ind.is_primary_key = 0 AND ind.is_unique_constraint = 0 AND t.is_ms_shipped = 0" - + ( - string.IsNullOrWhiteSpace(schemaName) - ? "" - : " AND SCHEMA_NAME(t.schema_id) = @schemaName" - ) - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND t.name = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND ind.name LIKE @where") - + @" ORDER BY schema_name, table_name, index_name, key_ordinal"; - - var results = await QueryAsync<( - string schema_name, - string table_name, - string index_name, - string column_name, - int is_unique, - string key_ordinal, - int is_descending_key - )>( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - - var grouped = results.GroupBy( - r => (r.schema_name, r.table_name, r.index_name), - r => (r.is_unique, r.column_name, r.key_ordinal, r.is_descending_key) - ); - - var indexes = new List(); - foreach (var group in grouped) - { - var (schema_name, table_name, index_name) = group.Key; - var (is_unique, column_name, key_ordinal, is_descending_key) = group.First(); - var index = new TableIndex( - schema_name, - table_name, - index_name, - group - .Select(g => - { - var col = g.column_name; - var direction = g.is_descending_key == 1 ? "DESC" : "ASC"; - return $"{col} {direction}"; - }) - .ToArray(), - is_unique == 1 - ); - indexes.Add(index); - } - - return indexes; - } - - public async Task> GetIndexNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - @$"SELECT ind.name - FROM sys.indexes ind - INNER JOIN sys.tables t ON ind.object_id = t.object_id - WHERE ind.is_primary_key = 0 and ind.is_unique_constraint = 0 AND t.is_ms_shipped = 0" - + ( - string.IsNullOrWhiteSpace(schemaName) - ? "" - : " AND SCHEMA_NAME(t.schema_id) = @schemaName" - ) - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND t.name = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND ind.name LIKE @where") - + @" ORDER BY ind.name"; - - return await QueryAsync( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - } - - public async Task DropIndexIfExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if ( - !await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - await ExecuteAsync( - db, - $@" - DROP INDEX [{schemaName}].[{tableName}].[{indexName}] - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.SchemaMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.SchemaMethods.cs deleted file mode 100644 index 9ecf735..0000000 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.SchemaMethods.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System.Data; -using System.Data.Common; - -namespace DapperMatic.Providers.SqlServer; - -public partial class SqlServerExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task SchemaExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return 0 - < await ExecuteScalarAsync( - db, - "SELECT count(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = @schemaName", - new { schemaName }, - transaction: tx - ) - .ConfigureAwait(false); - } - - public async Task CreateSchemaIfNotExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (await SchemaExistsAsync(db, schemaName, tx, cancellationToken).ConfigureAwait(false)) - return false; - - schemaName = NormalizeSchemaName(schemaName); - - await ExecuteAsync(db, $"CREATE SCHEMA {schemaName}", transaction: tx) - .ConfigureAwait(false); - return true; - } - - public async Task> GetSchemaNamesAsync( - IDbConnection db, - string? nameFilter = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(nameFilter)) - { - // get sql server schemas - return await QueryAsync( - db, - "SELECT DISTINCT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY SCHEMA_NAME", - transaction: tx - ) - .ConfigureAwait(false); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - "SELECT DISTINCT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE @where ORDER BY SCHEMA_NAME", - new { where }, - tx - ) - .ConfigureAwait(false); - } - } - - public async Task DropSchemaIfExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (!await SchemaExistsAsync(db, schemaName, tx, cancellationToken).ConfigureAwait(false)) - return false; - - schemaName = NormalizeSchemaName(schemaName); - - var innerTx = - tx - ?? await (db as DbConnection)! - .BeginTransactionAsync(cancellationToken) - .ConfigureAwait(false); - try - { - // drop all objects in the schemaName (except tables, which will be handled separately) - var dropAllRelatedTypesSqlStatement = await QueryAsync( - db, - $@" - SELECT CASE - WHEN type in ('C', 'D', 'F', 'UQ', 'PK') THEN - CONCAT('ALTER TABLE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name]), ' DROP CONSTRAINT ', QUOTENAME(o.[name])) - WHEN type in ('SN') THEN - CONCAT('DROP SYNONYM ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) - WHEN type in ('SO') THEN - CONCAT('DROP SEQUENCE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) - WHEN type in ('U') THEN - CONCAT('DROP TABLE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) - WHEN type in ('V') THEN - CONCAT('DROP VIEW ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) - WHEN type in ('TR') THEN - CONCAT('DROP TRIGGER ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) - WHEN type in ('IF', 'TF', 'FN', 'FS', 'FT') THEN - CONCAT('DROP FUNCTION ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) - WHEN type in ('P', 'PC') THEN - CONCAT('DROP PROCEDURE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) - END AS DropSqlStatement - FROM sys.objects o - WHERE o.schema_id = SCHEMA_ID('{schemaName}') - AND - type IN( - --constraints (check, default, foreign key, unique) - 'C', 'D', 'F', 'UQ', - --primary keys - 'PK', - --synonyms - 'SN', - --sequences - 'SO', - --user defined tables - 'U', - --views - 'V', - --triggers - 'TR', - --functions (inline, tableName-valued, scalar, CLR scalar, CLR tableName-valued) - 'IF', 'TF', 'FN', 'FS', 'FT', - --procedures (stored procedure, CLR stored procedure) - 'P', 'PC' - ) - ORDER BY CASE - WHEN type in ('C', 'D', 'UQ') THEN 2 - WHEN type in ('F') THEN 1 - WHEN type in ('PK') THEN 19 - WHEN type in ('SN') THEN 3 - WHEN type in ('SO') THEN 4 - WHEN type in ('U') THEN 20 - WHEN type in ('V') THEN 18 - WHEN type in ('TR') THEN 10 - WHEN type in ('IF', 'TF', 'FN', 'FS', 'FT') THEN 9 - WHEN type in ('P', 'PC') THEN 8 - END - ", - transaction: innerTx - ) - .ConfigureAwait(false); - foreach (var dropSql in dropAllRelatedTypesSqlStatement) - { - await ExecuteAsync(db, dropSql, transaction: innerTx).ConfigureAwait(false); - } - - // drop xml schemaName collection - var dropXmlSchemaCollectionSqlStatements = await QueryAsync( - db, - $@"SELECT 'DROP XML SCHEMA COLLECTION ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) - FROM sys.xml_schema_collections - WHERE schema_id = SCHEMA_ID('{schemaName}')", - transaction: innerTx - ) - .ConfigureAwait(false); - foreach (var dropSql in dropXmlSchemaCollectionSqlStatements) - { - await ExecuteAsync(db, dropSql, transaction: innerTx).ConfigureAwait(false); - } - - // drop all custom types - var dropCustomTypesSqlStatements = await QueryAsync( - db, - $@"SELECT 'DROP TYPE ' +QUOTENAME(SCHEMA_NAME(schema_id))+'.'+QUOTENAME(name) - FROM sys.types - WHERE schema_id = SCHEMA_ID('{schemaName}')", - transaction: innerTx - ) - .ConfigureAwait(false); - foreach (var dropSql in dropCustomTypesSqlStatements) - { - await ExecuteAsync(db, dropSql, transaction: innerTx).ConfigureAwait(false); - } - - // drop the schemaName itself - await ExecuteAsync(db, $"DROP SCHEMA [{schemaName}]", transaction: innerTx) - .ConfigureAwait(false); - - if (tx == null) - innerTx.Commit(); - } - catch - { - if (tx == null) - innerTx.Rollback(); - throw; - } - finally - { - if (tx == null) - innerTx.Dispose(); - } - - return true; - } -} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.TableMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.TableMethods.cs deleted file mode 100644 index ea8d658..0000000 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.TableMethods.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Data; -using System.Text; - -namespace DapperMatic.Providers.SqlServer; - -public partial class SqlServerExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task TableExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - return 0 - < await ExecuteScalarAsync( - db, - "SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @tableName AND TABLE_SCHEMA = @schemaName", - new { schemaName, tableName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateTableIfNotExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - string[]? primaryKeyColumnNames = null, - Type[]? primaryKeyDotnetTypes = null, - int?[]? primaryKeyColumnLengths = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if ( - await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - if (primaryKeyColumnNames == null || primaryKeyColumnNames.Length == 0) - { - await ExecuteAsync( - db, - @$"CREATE TABLE [{schemaName}].[{tableName}] ( - id INT NOT NULL IDENTITY(1,1), - CONSTRAINT [pk_{schemaName}_{tableName}_id] PRIMARY KEY CLUSTERED ([id] ASC) - ) - ", - transaction: tx - ) - .ConfigureAwait(false); - return true; - } - - var sql = new StringBuilder(); - sql.AppendLine($"CREATE TABLE [{schemaName}].[{tableName}] ("); - var columnNamesWithOrder = new List(); - for (var i = 0; i < primaryKeyColumnNames.Length; i++) - { - var columnArr = primaryKeyColumnNames[i].Split(' '); - var (_, _, columnName) = NormalizeNames(schemaName, tableName, columnArr[0]); - if (string.IsNullOrWhiteSpace(columnName)) - continue; - - columnNamesWithOrder.Add( - '[' + columnName + ']' + (columnArr.Length > 1 ? $" {columnArr[1]}" : " ASC") - ); - - if (primaryKeyDotnetTypes != null && primaryKeyDotnetTypes.Length > i) - { - sql.AppendLine( - $"[{columnName}] {GetSqlTypeString(primaryKeyDotnetTypes[i], (primaryKeyColumnLengths != null && primaryKeyColumnLengths.Length > i) ? primaryKeyColumnLengths[i] : null)} NOT NULL," - ); - } - else - { - sql.AppendLine($"[{columnName}] INT NOT NULL,"); - } - } - sql.AppendLine( - $"CONSTRAINT [pk_{schemaName}_{tableName}_id] PRIMARY KEY CLUSTERED ({string.Join(", ", columnNamesWithOrder)})" - ); - sql.AppendLine(")"); - - await ExecuteAsync(db, sql.ToString(), transaction: tx).ConfigureAwait(false); - return true; - } - - public async Task> GetTableNamesAsync( - IDbConnection db, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, _, _) = NormalizeNames(schemaName, null, null); - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return await QueryAsync( - db, - "SELECT DISTINCT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @schemaName ORDER BY TABLE_NAME", - new { schemaName }, - tx - ) - .ConfigureAwait(false); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - "SELECT DISTINCT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME LIKE @where ORDER BY TABLE_NAME", - new { schemaName, where }, - tx - ) - .ConfigureAwait(false); - } - } - - public async Task DropTableIfExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if ( - !await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - // drop the constraints on the tableName first - var constraints = await QueryAsync( - db, - "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_NAME = @tableName AND TABLE_SCHEMA = @schemaName", - new { tableName, schemaName }, - tx - ) - .ConfigureAwait(false); - - foreach (var constraint in constraints) - { - await ExecuteAsync( - db, - $"ALTER TABLE [{schemaName}].[{tableName}] DROP CONSTRAINT [{constraint}]", - transaction: tx - ) - .ConfigureAwait(false); - } - - await ExecuteAsync(db, $"DROP TABLE [{schemaName}].[{tableName}]", transaction: tx) - .ConfigureAwait(false); - return true; - } -} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.UniqueConstraintMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.UniqueConstraintMethods.cs deleted file mode 100644 index 3771839..0000000 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.UniqueConstraintMethods.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.SqlServer; - -public partial class SqlServerExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task UniqueConstraintExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) FROM sys.indexes - WHERE object_id = OBJECT_ID('{schemaAndTableName}') - AND name = @uniqueConstraintName and is_primary_key = 0 and is_unique_constraint = 1", - new { schemaAndTableName, uniqueConstraintName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateUniqueConstraintIfNotExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string[] columnNames, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - var columnList = string.Join(", ", columnNames); - - await ExecuteAsync( - db, - $@" - ALTER TABLE {schemaAndTableName} - ADD CONSTRAINT {uniqueConstraintName} UNIQUE ({columnList}) - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public Task> GetUniqueConstraintNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (schemaName, tableName, _) = NormalizeNames(schemaName, tableName, null); - - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return QueryAsync( - db, - $@" - SELECT name FROM sys.indexes - WHERE object_id = OBJECT_ID('{schemaAndTableName}') - and is_primary_key = 0 and is_unique_constraint = 1", - tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return QueryAsync( - db, - $@" - SELECT name FROM sys.indexes - WHERE object_id = OBJECT_ID('{schemaAndTableName}') - and name LIKE @where - and is_primary_key = 0 and is_unique_constraint = 1", - new { schemaAndTableName, where }, - tx - ); - } - } - - public async Task DropUniqueConstraintIfExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (schemaName, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - var schemaAndTableName = "[" + schemaName + "].[" + tableName + "]"; - - await ExecuteAsync( - db, - $@" - ALTER TABLE {schemaAndTableName} - DROP CONSTRAINT {uniqueConstraintName} - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.cs deleted file mode 100644 index a940b62..0000000 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.SqlServer; - -public partial class SqlServerExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - protected override string DefaultSchema => "dbo"; - - protected override List DataTypes => - DataTypeMapFactory.GetDefaultDatabaseTypeDataTypeMap(DatabaseTypes.SqlServer); - - internal SqlServerExtensions() { } - - public async Task GetDatabaseVersionAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - /* - SELECT - SERVERPROPERTY('Productversion') As [SQL Server Version], - SERVERPROPERTY('Productlevel') As [SQL Server Build Level], - SERVERPROPERTY('edition') As [SQL Server Edition] - */ - return await ExecuteScalarAsync( - db, - $@"SELECT SERVERPROPERTY('Productversion')", - transaction: tx - ) - .ConfigureAwait(false) ?? ""; - } -} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Schemas.cs b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Schemas.cs new file mode 100644 index 0000000..bc06643 --- /dev/null +++ b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Schemas.cs @@ -0,0 +1,150 @@ +using System.Data; +using System.Data.Common; + +namespace DapperMatic.Providers.SqlServer; + +public partial class SqlServerMethods +{ + public override async Task DropSchemaIfExistsAsync( + IDbConnection db, + string schemaName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await DoesSchemaExistAsync(db, schemaName, tx, cancellationToken).ConfigureAwait(false) + ) + return false; + + schemaName = NormalizeSchemaName(schemaName); + + var innerTx = + tx + ?? await (db as DbConnection)! + .BeginTransactionAsync(cancellationToken) + .ConfigureAwait(false); + try + { + // drop all objects in the schemaName (except tables, which will be handled separately) + var dropAllRelatedTypesSqlStatement = await QueryAsync( + db, + $""" + + SELECT CASE + WHEN type in ('C', 'D', 'F', 'UQ', 'PK') THEN + CONCAT('ALTER TABLE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(OBJECT_NAME(o.parent_object_id)), ' DROP CONSTRAINT ', QUOTENAME(o.[name])) + WHEN type in ('SN') THEN + CONCAT('DROP SYNONYM ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) + WHEN type in ('SO') THEN + CONCAT('DROP SEQUENCE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) + WHEN type in ('U') THEN + CONCAT('DROP TABLE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) + WHEN type in ('V') THEN + CONCAT('DROP VIEW ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) + WHEN type in ('TR') THEN + CONCAT('DROP TRIGGER ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) + WHEN type in ('IF', 'TF', 'FN', 'FS', 'FT') THEN + CONCAT('DROP FUNCTION ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) + WHEN type in ('P', 'PC') THEN + CONCAT('DROP PROCEDURE ', QUOTENAME(SCHEMA_NAME(o.schema_id))+'.'+QUOTENAME(o.[name])) + END AS DropSqlStatement + FROM sys.objects o + WHERE o.schema_id = SCHEMA_ID('{schemaName}') + AND + type IN( + --constraints (check, default, foreign key, unique) + 'C', 'D', 'F', 'UQ', + --primary keys + 'PK', + --synonyms + 'SN', + --sequences + 'SO', + --user defined tables + 'U', + --views + 'V', + --triggers + 'TR', + --functions (inline, tableName-valued, scalar, CLR scalar, CLR tableName-valued) + 'IF', 'TF', 'FN', 'FS', 'FT', + --procedures (stored procedure, CLR stored procedure) + 'P', 'PC' + ) + ORDER BY CASE + WHEN type in ('C', 'D', 'UQ') THEN 2 + WHEN type in ('F') THEN 1 + WHEN type in ('PK') THEN 19 + WHEN type in ('SN') THEN 3 + WHEN type in ('SO') THEN 4 + WHEN type in ('U') THEN 20 + WHEN type in ('V') THEN 18 + WHEN type in ('TR') THEN 10 + WHEN type in ('IF', 'TF', 'FN', 'FS', 'FT') THEN 9 + WHEN type in ('P', 'PC') THEN 8 + END + + """, + tx: innerTx + ) + .ConfigureAwait(false); + foreach (var dropSql in dropAllRelatedTypesSqlStatement) + { + await ExecuteAsync(db, dropSql, tx: innerTx).ConfigureAwait(false); + } + + // drop xml schemaName collection + var dropXmlSchemaCollectionSqlStatements = await QueryAsync( + db, + $""" + SELECT 'DROP XML SCHEMA COLLECTION ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + FROM sys.xml_schema_collections + WHERE schema_id = SCHEMA_ID('{schemaName}') + """, + tx: innerTx + ) + .ConfigureAwait(false); + foreach (var dropSql in dropXmlSchemaCollectionSqlStatements) + { + await ExecuteAsync(db, dropSql, tx: innerTx).ConfigureAwait(false); + } + + // drop all custom types + var dropCustomTypesSqlStatements = await QueryAsync( + db, + $""" + SELECT 'DROP TYPE ' +QUOTENAME(SCHEMA_NAME(schema_id))+'.'+QUOTENAME(name) + FROM sys.types + WHERE schema_id = SCHEMA_ID('{schemaName}') + """, + tx: innerTx + ) + .ConfigureAwait(false); + foreach (var dropSql in dropCustomTypesSqlStatements) + { + await ExecuteAsync(db, dropSql, tx: innerTx).ConfigureAwait(false); + } + + // drop the schemaName itself + await ExecuteAsync(db, $"DROP SCHEMA [{schemaName}]", tx: innerTx) + .ConfigureAwait(false); + + if (tx == null) + innerTx.Commit(); + } + catch + { + if (tx == null) + innerTx.Rollback(); + throw; + } + finally + { + if (tx == null) + innerTx.Dispose(); + } + + return true; + } +} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Strings.cs b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Strings.cs new file mode 100644 index 0000000..a535bb9 --- /dev/null +++ b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Strings.cs @@ -0,0 +1,130 @@ +using DapperMatic.Models; + +namespace DapperMatic.Providers.SqlServer; + +public partial class SqlServerMethods +{ + #region Schema Strings + #endregion // Schema Strings + + #region Table Strings + protected override string SqlRenameTable( + string? schemaName, + string tableName, + string newTableName + ) + { + return $"EXEC sp_rename '{GetSchemaQualifiedIdentifierName(schemaName, tableName)}', '{NormalizeName(newTableName)}'"; + } + #endregion // Table Strings + + #region Column Strings + #endregion // Column Strings + + #region Check Constraint Strings + #endregion // Check Constraint Strings + + #region Default Constraint Strings + #endregion // Default Constraint Strings + + #region Primary Key Strings + #endregion // Primary Key Strings + + #region Unique Constraint Strings + #endregion // Unique Constraint Strings + + #region Foreign Key Constraint Strings + #endregion // Foreign Key Constraint Strings + + #region Index Strings + #endregion // Index Strings + + #region View Strings + + protected override (string sql, object parameters) SqlGetViewNames( + string? schemaName, + string? viewNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + + SELECT + v.[name] AS ViewName + FROM sys.objects v + INNER JOIN sys.sql_modules m ON v.object_id = m.object_id + WHERE + v.[type] = 'V' + AND v.is_ms_shipped = 0 + AND SCHEMA_NAME(v.schema_id) = @schemaName + {(string.IsNullOrWhiteSpace(where) ? "" : " AND v.[name] LIKE @where")} + ORDER BY + SCHEMA_NAME(v.schema_id), + v.[name] + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + protected override (string sql, object parameters) SqlGetViews( + string? schemaName, + string? viewNameFilter + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + + SELECT + SCHEMA_NAME(v.schema_id) AS SchemaName, + v.[name] AS ViewName, + m.definition AS Definition + FROM sys.objects v + INNER JOIN sys.sql_modules m ON v.object_id = m.object_id + WHERE + v.[type] = 'V' + AND v.is_ms_shipped = 0 + AND SCHEMA_NAME(v.schema_id) = @schemaName + {(string.IsNullOrWhiteSpace(where) ? "" : " AND v.[name] LIKE @where")} + ORDER BY + SCHEMA_NAME(v.schema_id), + v.[name] + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + private static readonly char[] WhiteSpaceCharacters = [' ', '\t', '\n', '\r']; + + protected override string NormalizeViewDefinition(string definition) + { + definition = definition.Trim(); + + // strip off the CREATE VIEW statement ending with the AS + var indexOfAs = -1; + for (var i = 0; i < definition.Length; i++) + { + if (i == 0) + continue; + if (i == definition.Length - 2) + break; + + if ( + !WhiteSpaceCharacters.Contains(definition[i - 1]) + || char.ToUpperInvariant(definition[i]) != 'A' + || char.ToUpperInvariant(definition[i + 1]) != 'S' + || !WhiteSpaceCharacters.Contains(definition[i + 2]) + ) + continue; + + indexOfAs = i; + break; + } + if (indexOfAs == -1) + throw new Exception("Could not parse view definition: " + definition); + + return definition[(indexOfAs + 3)..].Trim(); + } + #endregion // View Strings +} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Tables.cs b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Tables.cs new file mode 100644 index 0000000..24a6e40 --- /dev/null +++ b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.Tables.cs @@ -0,0 +1,563 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.SqlServer; + +public partial class SqlServerMethods +{ + public override async Task> GetTablesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + schemaName = NormalizeSchemaName(schemaName); + + var where = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + + // columns + var columnsSql = $""" + SELECT + t.TABLE_SCHEMA AS schema_name, + t.TABLE_NAME AS table_name, + c.COLUMN_NAME AS column_name, + c.ORDINAL_POSITION AS column_ordinal, + c.COLUMN_DEFAULT AS column_default, + case when (ISNULL(pk.CONSTRAINT_NAME, '') = '') then 0 else 1 end AS is_primary_key, + pk.CONSTRAINT_NAME AS pk_constraint_name, + case when (c.IS_NULLABLE = 'YES') then 1 else 0 end AS is_nullable, + COLUMNPROPERTY(object_id(t.TABLE_SCHEMA+'.'+t.TABLE_NAME), c.COLUMN_NAME, 'IsIdentity') AS is_identity, + c.DATA_TYPE AS data_type, + c.CHARACTER_MAXIMUM_LENGTH AS max_length, + c.NUMERIC_PRECISION AS numeric_precision, + c.NUMERIC_SCALE AS numeric_scale + + FROM INFORMATION_SCHEMA.TABLES t + LEFT OUTER JOIN INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_SCHEMA = c.TABLE_SCHEMA and t.TABLE_NAME = c.TABLE_NAME + LEFT OUTER JOIN ( + SELECT tc.TABLE_SCHEMA, tc.TABLE_NAME, ccu.COLUMN_NAME, ccu.CONSTRAINT_NAME + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc + INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu + ON tc.CONSTRAINT_NAME = ccu.CONSTRAINT_NAME + WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + ) pk ON t.TABLE_SCHEMA = pk.TABLE_SCHEMA and t.TABLE_NAME = pk.TABLE_NAME and c.COLUMN_NAME = pk.COLUMN_NAME + + WHERE t.TABLE_TYPE = 'BASE TABLE' + AND t.TABLE_SCHEMA = @schemaName + {(string.IsNullOrWhiteSpace(where) ? null : " AND t.TABLE_NAME LIKE @where")} + ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME, c.ORDINAL_POSITION + """; + var columnResults = await QueryAsync<( + string schema_name, + string table_name, + string column_name, + int column_ordinal, + string column_default, + bool is_primary_key, + string pk_constraint_name, + bool is_nullable, + bool is_identity, + string data_type, + int? max_length, + int? numeric_precision, + int? numeric_scale + )>(db, columnsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + + // get primary key, unique key, and indexes in a single query + var constraintsSql = $""" + SELECT sh.name AS schema_name, + i.name AS constraint_name, + t.name AS table_name, + c.name AS column_name, + ic.key_ordinal AS column_key_ordinal, + ic.is_descending_key AS is_desc, + i.is_unique, + i.is_primary_key, + i.is_unique_constraint + FROM sys.indexes i + INNER JOIN sys.index_columns ic + ON i.index_id = ic.index_id AND i.object_id = ic.object_id + INNER JOIN sys.tables AS t + ON t.object_id = i.object_id + INNER JOIN sys.columns c + ON t.object_id = c.object_id AND ic.column_id = c.column_id + INNER JOIN sys.objects AS syso + ON syso.object_id = t.object_id AND syso.is_ms_shipped = 0 + INNER JOIN sys.schemas AS sh + ON sh.schema_id = t.schema_id + INNER JOIN information_schema.schemata sch + ON sch.schema_name = sh.name + WHERE + sh.name = @schemaName + {(string.IsNullOrWhiteSpace(where) ? null : " AND t.name LIKE @where")} + ORDER BY sh.name, i.name, ic.key_ordinal + """; + var constraintResults = await QueryAsync<( + string schema_name, + string constraint_name, + string table_name, + string column_name, + int column_key_ordinal, + bool is_desc, + bool is_unique, + bool is_primary_key, + bool is_unique_constraint + )>(db, constraintsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + + var foreignKeysSql = $""" + + SELECT + kfk.TABLE_SCHEMA schema_name, + kfk.TABLE_NAME table_name, + kfk.COLUMN_NAME AS column_name, + rc.CONSTRAINT_NAME AS constraint_name, + kpk.TABLE_SCHEMA AS referenced_schema_name, + kpk.TABLE_NAME AS referenced_table_name, + kpk.COLUMN_NAME AS referenced_column_name, + rc.UPDATE_RULE update_rule, + rc.DELETE_RULE delete_rule + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kfk ON rc.CONSTRAINT_NAME = kfk.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kpk ON rc.UNIQUE_CONSTRAINT_NAME = kpk.CONSTRAINT_NAME + WHERE + kfk.TABLE_SCHEMA = @schemaName + {( + string.IsNullOrWhiteSpace(where) ? null : " AND kfk.TABLE_NAME LIKE @where" + )} + ORDER BY kfk.TABLE_SCHEMA, kfk.TABLE_NAME, rc.CONSTRAINT_NAME + + """; + var foreignKeyResults = await QueryAsync<( + string schema_name, + string table_name, + string column_name, + string constraint_name, + string referenced_schema_name, + string referenced_table_name, + string referenced_column_name, + string update_rule, + string delete_rule + )>(db, foreignKeysSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + + var checkConstraintsSql = $""" + + select + schema_name(t.schema_id) AS schema_name, + t.[name] AS table_name, + col.[name] AS column_name, + con.[name] AS constraint_name, + con.[definition] AS check_expression + from sys.check_constraints con + left outer join sys.objects t on con.parent_object_id = t.object_id + left outer join sys.all_columns col on con.parent_column_id = col.column_id and con.parent_object_id = col.object_id + where + con.[definition] IS NOT NULL + and schema_name(t.schema_id) = @schemaName + {( + string.IsNullOrWhiteSpace(where) ? null : " AND t.[name] LIKE @where" + )} + order by schema_name, table_name, column_name, constraint_name + + """; + var checkConstraintResults = await QueryAsync<( + string schema_name, + string table_name, + string? column_name, + string constraint_name, + string check_expression + )>(db, checkConstraintsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + + var defaultConstraintsSql = $""" + + select + schema_name(t.schema_id) AS schema_name, + t.[name] AS table_name, + col.[name] AS column_name, + con.[name] AS constraint_name, + con.[definition] AS default_expression + from sys.default_constraints con + left outer join sys.objects t on con.parent_object_id = t.object_id + left outer join sys.all_columns col on con.parent_column_id = col.column_id and con.parent_object_id = col.object_id + where + schema_name(t.schema_id) = @schemaName + {( + string.IsNullOrWhiteSpace(where) ? null : " AND t.[name] LIKE @where" + )} + order by schema_name, table_name, column_name, constraint_name + + """; + var defaultConstraintResults = await QueryAsync<( + string schema_name, + string table_name, + string column_name, + string constraint_name, + string default_expression + )>(db, defaultConstraintsSql, new { schemaName, where }, tx: tx) + .ConfigureAwait(false); + + var tables = new List(); + + foreach ( + var tableColumns in columnResults.GroupBy(r => new { r.schema_name, r.table_name }) + ) + { + var tableName = tableColumns.Key.table_name; + var tableConstraints = constraintResults + .Where(t => + t.schema_name.Equals(schemaName, StringComparison.OrdinalIgnoreCase) + && t.table_name.Equals(tableName, StringComparison.OrdinalIgnoreCase) + ) + .ToArray(); + var foreignKeyConstraints = foreignKeyResults + .Where(t => + t.schema_name.Equals(schemaName, StringComparison.OrdinalIgnoreCase) + && t.table_name.Equals(tableName, StringComparison.OrdinalIgnoreCase) + ) + .GroupBy(t => new + { + t.schema_name, + t.table_name, + t.constraint_name, + t.referenced_table_name, + t.update_rule, + t.delete_rule + }) + .Select(gb => + { + return new DxForeignKeyConstraint( + gb.Key.schema_name, + gb.Key.table_name, + gb.Key.constraint_name, + gb.Select(c => new DxOrderedColumn(c.column_name)).ToArray(), + gb.Key.referenced_table_name, + gb.Select(c => new DxOrderedColumn(c.referenced_column_name)).ToArray(), + gb.Key.delete_rule.ToForeignKeyAction(), + gb.Key.update_rule.ToForeignKeyAction() + ); + }) + .ToArray(); + var checkConstraints = checkConstraintResults + .Where(t => + t.schema_name.Equals(schemaName, StringComparison.OrdinalIgnoreCase) + && t.table_name.Equals(tableName, StringComparison.OrdinalIgnoreCase) + ) + .Select(c => new DxCheckConstraint( + c.schema_name, + c.table_name, + c.column_name, + c.constraint_name, + c.check_expression + )) + .ToArray(); + var defaultConstraints = defaultConstraintResults + .Where(t => + t.schema_name.Equals(schemaName, StringComparison.OrdinalIgnoreCase) + && t.table_name.Equals(tableName, StringComparison.OrdinalIgnoreCase) + ) + .Select(c => new DxDefaultConstraint( + c.schema_name, + c.table_name, + c.column_name, + c.constraint_name, + c.default_expression + )) + .ToArray(); + + // extract primary key information from constraints query + var primaryKeyConstraintInfo = tableConstraints.Where(t => t.is_primary_key).ToArray(); + var primaryKeyConstraint = + primaryKeyConstraintInfo.Length > 0 + ? new DxPrimaryKeyConstraint( + primaryKeyConstraintInfo[0].schema_name, + primaryKeyConstraintInfo[0].table_name, + primaryKeyConstraintInfo[0].constraint_name, + primaryKeyConstraintInfo + .OrderBy(t => t.column_key_ordinal) + .Select(t => new DxOrderedColumn( + t.column_name, + t.is_desc ? DxColumnOrder.Descending : DxColumnOrder.Ascending + )) + .ToArray() + ) + : null; + + // extract unique constraint information from constraints query + var uniqueConstraintsInfo = tableConstraints + .Where(t => t is { is_unique_constraint: true, is_primary_key: false }) + .GroupBy(t => new + { + t.schema_name, + t.table_name, + t.constraint_name + }) + .ToArray(); + var uniqueConstraints = uniqueConstraintsInfo + .Select(t => new DxUniqueConstraint( + t.Key.schema_name, + t.Key.table_name, + t.Key.constraint_name, + t.OrderBy(c => c.column_key_ordinal) + .Select(c => new DxOrderedColumn( + c.column_name, + c.is_desc ? DxColumnOrder.Descending : DxColumnOrder.Ascending + )) + .ToArray() + )) + .ToArray(); + + // extract index information from constraints query + var indexesInfo = tableConstraints + .Where(t => t is { is_primary_key: false, is_unique_constraint: false }) + .GroupBy(t => new + { + t.schema_name, + t.table_name, + t.constraint_name + }) + .ToArray(); + var indexes = indexesInfo + .Select(t => new DxIndex( + t.Key.schema_name, + t.Key.table_name, + t.Key.constraint_name, + t.OrderBy(c => c.column_key_ordinal) + .Select(c => new DxOrderedColumn( + c.column_name, + c.is_desc ? DxColumnOrder.Descending : DxColumnOrder.Ascending + )) + .ToArray(), + t.First().is_unique + )) + .ToArray(); + + var columns = new List(); + foreach (var tableColumn in tableColumns) + { + var columnIsUniqueViaUniqueConstraintOrIndex = + uniqueConstraints.Any(dxuc => + dxuc.Columns.Length == 1 + && dxuc.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ) + || indexes.Any(i => + i is { IsUnique: true, Columns.Length: 1 } + && i.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var columnIsPartOfIndex = indexes.Any(i => + i.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var foreignKeyConstraint = foreignKeyConstraints.FirstOrDefault(c => + c.SourceColumns.Any(doc => + doc.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + + var foreignKeyColumnIndex = foreignKeyConstraint + ?.SourceColumns.Select((c, i) => new { c, i }) + .FirstOrDefault(c => + c.c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.i; + + var dotnetTypeDescriptor = GetDotnetTypeFromSqlType(tableColumn.data_type); + + var column = new DxColumn( + tableColumn.schema_name, + tableColumn.table_name, + tableColumn.column_name, + dotnetTypeDescriptor.DotnetType, + new Dictionary + { + { ProviderType, tableColumn.data_type } + }, + tableColumn.max_length, + tableColumn.numeric_precision, + tableColumn.numeric_scale, + checkConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.Expression, + defaultConstraints + .FirstOrDefault(c => + !string.IsNullOrWhiteSpace(c.ColumnName) + && c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ) + ?.Expression, + tableColumn.is_nullable, + primaryKeyConstraint != null + && primaryKeyConstraint.Columns.Any(c => + c.ColumnName.Equals( + tableColumn.column_name, + StringComparison.OrdinalIgnoreCase + ) + ), + tableColumn.is_identity, + columnIsUniqueViaUniqueConstraintOrIndex, + columnIsPartOfIndex, + foreignKeyConstraint != null, + foreignKeyConstraint?.ReferencedTableName, + foreignKeyConstraint + ?.ReferencedColumns.ElementAtOrDefault(foreignKeyColumnIndex ?? 0) + ?.ColumnName, + foreignKeyConstraint?.OnDelete, + foreignKeyConstraint?.OnUpdate + ); + + columns.Add(column); + } + + var table = new DxTable( + schemaName, + tableName, + [.. columns], + primaryKeyConstraint, + checkConstraints, + defaultConstraints, + uniqueConstraints, + foreignKeyConstraints, + indexes + ); + tables.Add(table); + } + + return tables; + } + + protected override async Task> GetIndexesInternalAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var whereSchemaLike = string.IsNullOrWhiteSpace(schemaName) + ? null + : ToLikeString(schemaName); + var whereTableLike = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + var whereIndexLike = string.IsNullOrWhiteSpace(indexNameFilter) + ? null + : ToLikeString(indexNameFilter); + + var sql = $""" + SELECT + SCHEMA_NAME(t.schema_id) as schema_name, + t.name as table_name, + ind.name as index_name, + col.name as column_name, + ind.is_unique as is_unique, + ic.key_ordinal as key_ordinal, + ic.is_descending_key as is_descending_key + FROM sys.indexes ind + INNER JOIN sys.tables t ON ind.object_id = t.object_id + INNER JOIN sys.index_columns ic ON ind.object_id = ic.object_id and ind.index_id = ic.index_id + INNER JOIN sys.columns col ON ic.object_id = col.object_id and ic.column_id = col.column_id + WHERE + ind.is_primary_key = 0 AND ind.is_unique_constraint = 0 AND t.is_ms_shipped = 0 + {( + string.IsNullOrWhiteSpace(whereSchemaLike) + ? "" + : " AND SCHEMA_NAME(t.schema_id) LIKE @whereSchemaLike" + )} + {( + string.IsNullOrWhiteSpace(whereTableLike) ? "" : " AND t.name LIKE @whereTableLike" + )} + {( + string.IsNullOrWhiteSpace(whereIndexLike) + ? "" + : " AND ind.name LIKE @whereIndexLike" + )} + ORDER BY schema_name, table_name, index_name, key_ordinal + """; + + var results = await QueryAsync<( + string schema_name, + string table_name, + string index_name, + string column_name, + int is_unique, + string key_ordinal, + int is_descending_key + )>( + db, + sql, + new + { + whereSchemaLike, + whereTableLike, + whereIndexLike + }, + tx + ) + .ConfigureAwait(false); + + var grouped = results.GroupBy( + r => (r.schema_name, r.table_name, r.index_name), + r => (r.is_unique, r.column_name, r.key_ordinal, r.is_descending_key) + ); + + var indexes = new List(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var group in grouped) + { + var index = new DxIndex( + group.Key.schema_name, + group.Key.table_name, + group.Key.index_name, + group + .Select(g => new DxOrderedColumn( + g.column_name, + g.is_descending_key == 1 + ? DxColumnOrder.Descending + : DxColumnOrder.Ascending + )) + .ToArray(), + group.First().is_unique == 1 + ); + indexes.Add(index); + } + + return indexes; + } +} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.cs new file mode 100644 index 0000000..e493182 --- /dev/null +++ b/src/DapperMatic/Providers/SqlServer/SqlServerMethods.cs @@ -0,0 +1,43 @@ +using System.Data; +using DapperMatic.Interfaces; +using DapperMatic.Providers.Base; + +namespace DapperMatic.Providers.SqlServer; + +public partial class SqlServerMethods : DatabaseMethodsBase, IDatabaseMethods +{ + public override DbProviderType ProviderType => DbProviderType.SqlServer; + + public override IDbProviderTypeMap ProviderTypeMap => SqlServerProviderTypeMap.Instance.Value; + + private static string _defaultSchema = "dbo"; + protected override string DefaultSchema => _defaultSchema; + + public static void SetDefaultSchema(string schema) + { + _defaultSchema = schema; + } + + internal SqlServerMethods() { } + + public override async Task GetDatabaseVersionAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + /* + SELECT + SERVERPROPERTY('Productversion') As [SQL Server Version] --> 15.0.2000.5, 15.0.4390.2 + SERVERPROPERTY('Productlevel') As [SQL Server Build Level], --> RTM + SERVERPROPERTY('edition') As [SQL Server Edition] --> Express Edition (64-bit), Developer Edition (64-bit), etc. + */ + + const string sql = "SELECT SERVERPROPERTY('Productversion')"; + var versionString = + await ExecuteScalarAsync(db, sql, tx: tx).ConfigureAwait(false) ?? ""; + return DbProviderUtils.ExtractVersionFromVersionString(versionString); + } + + public override char[] QuoteChars => ['[', ']']; +} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerProviderTypeMap.cs b/src/DapperMatic/Providers/SqlServer/SqlServerProviderTypeMap.cs new file mode 100644 index 0000000..d8de819 --- /dev/null +++ b/src/DapperMatic/Providers/SqlServer/SqlServerProviderTypeMap.cs @@ -0,0 +1,286 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DapperMatic.Providers.SqlServer; + +public sealed class SqlServerProviderTypeMap : DbProviderTypeMapBase +{ + internal static readonly Lazy Instance = + new(() => new SqlServerProviderTypeMap()); + + private SqlServerProviderTypeMap() + : base() { } + + protected override DbProviderType ProviderType => DbProviderType.SqlServer; + + public override string SqTypeForStringLengthMax => "nvarchar(max)"; + + public override string SqTypeForBinaryLengthMax => "varbinary(max)"; + + public override string SqlTypeForJson => "nvarchar(max)"; + + /// + /// IMPORTANT!! The order within an affinity group matters, as the first possible match will be used as the recommended sql type for a dotnet type + /// + protected override DbProviderSqlType[] ProviderSqlTypes => + [ + new( + DbProviderSqlTypeAffinity.Integer, + SqlServerTypes.sql_tinyint, + canUseToAutoIncrement: true, + minValue: -128, + maxValue: 128 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqlServerTypes.sql_smallint, + canUseToAutoIncrement: true, + minValue: -32768, + maxValue: 32767 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqlServerTypes.sql_int, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqlServerTypes.sql_bigint, + canUseToAutoIncrement: true, + minValue: -9223372036854775808, + maxValue: 9223372036854775807 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqlServerTypes.sql_decimal, + formatWithPrecision: "decimal({0})", + formatWithPrecisionAndScale: "decimal({0},{1})", + defaultPrecision: 18, + defaultScale: 0 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqlServerTypes.sql_numeric, + formatWithPrecision: "numeric({0})", + formatWithPrecisionAndScale: "numeric({0},{1})", + defaultPrecision: 18, + defaultScale: 0 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqlServerTypes.sql_float, + formatWithPrecision: "float({0})", + defaultPrecision: 53, + defaultScale: 0 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqlServerTypes.sql_real, + formatWithPrecision: "real({0})", + defaultPrecision: 24, + defaultScale: 0 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqlServerTypes.sql_money, + formatWithPrecision: "money({0})", + defaultPrecision: 19, + defaultScale: 4 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqlServerTypes.sql_smallmoney, + formatWithPrecision: "smallmoney({0})", + defaultPrecision: 10, + defaultScale: 4 + ), + new(DbProviderSqlTypeAffinity.Boolean, SqlServerTypes.sql_bit), + new(DbProviderSqlTypeAffinity.DateTime, SqlServerTypes.sql_date, isDateOnly: true), + new(DbProviderSqlTypeAffinity.DateTime, SqlServerTypes.sql_datetime), + new(DbProviderSqlTypeAffinity.DateTime, SqlServerTypes.sql_smalldatetime), + new(DbProviderSqlTypeAffinity.DateTime, SqlServerTypes.sql_datetime2), + new(DbProviderSqlTypeAffinity.DateTime, SqlServerTypes.sql_datetimeoffset), + new(DbProviderSqlTypeAffinity.DateTime, SqlServerTypes.sql_time, isTimeOnly: true), + new(DbProviderSqlTypeAffinity.DateTime, SqlServerTypes.sql_timestamp), + new( + DbProviderSqlTypeAffinity.Text, + SqlServerTypes.sql_nvarchar, + formatWithLength: "nvarchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqlServerTypes.sql_varchar, + formatWithLength: "varchar({0})", + defaultLength: DefaultLength + ), + new(DbProviderSqlTypeAffinity.Text, SqlServerTypes.sql_ntext), + new(DbProviderSqlTypeAffinity.Text, SqlServerTypes.sql_text), + new( + DbProviderSqlTypeAffinity.Text, + SqlServerTypes.sql_nchar, + formatWithLength: "nchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqlServerTypes.sql_char, + formatWithLength: "char({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqlServerTypes.sql_uniqueidentifier, + isGuidOnly: true + ), + new( + DbProviderSqlTypeAffinity.Binary, + SqlServerTypes.sql_varbinary, + formatWithLength: "varbinary({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Binary, + SqlServerTypes.sql_binary, + formatWithLength: "binary({0})", + defaultLength: DefaultLength + ), + new(DbProviderSqlTypeAffinity.Binary, SqlServerTypes.sql_image), + new(DbProviderSqlTypeAffinity.Geometry, SqlServerTypes.sql_geometry), + new(DbProviderSqlTypeAffinity.Geometry, SqlServerTypes.sql_geography), + new(DbProviderSqlTypeAffinity.Geometry, SqlServerTypes.sql_hierarchyid), + new(DbProviderSqlTypeAffinity.Other, SqlServerTypes.sql_variant), + new(DbProviderSqlTypeAffinity.Other, SqlServerTypes.sql_xml), + new(DbProviderSqlTypeAffinity.Other, SqlServerTypes.sql_cursor), + new(DbProviderSqlTypeAffinity.Other, SqlServerTypes.sql_table), + new(DbProviderSqlTypeAffinity.Other, SqlServerTypes.sql_json) + ]; + + protected override bool TryGetProviderSqlTypeMatchingDotnetTypeInternal( + DbProviderDotnetTypeDescriptor descriptor, + out DbProviderSqlType? providerSqlType + ) + { + providerSqlType = null; + + var dotnetType = descriptor.DotnetType; + + // handle well-known types first + providerSqlType = dotnetType.IsGenericType + ? null + : dotnetType switch + { + Type t when t == typeof(bool) => ProviderSqlTypeLookup[SqlServerTypes.sql_bit], + Type t when t == typeof(byte) => ProviderSqlTypeLookup[SqlServerTypes.sql_smallint], + Type t when t == typeof(ReadOnlyMemory) + => ProviderSqlTypeLookup[SqlServerTypes.sql_smallint], + Type t when t == typeof(sbyte) + => ProviderSqlTypeLookup[SqlServerTypes.sql_smallint], + Type t when t == typeof(short) + => ProviderSqlTypeLookup[SqlServerTypes.sql_smallint], + Type t when t == typeof(ushort) + => ProviderSqlTypeLookup[SqlServerTypes.sql_smallint], + Type t when t == typeof(int) => ProviderSqlTypeLookup[SqlServerTypes.sql_int], + Type t when t == typeof(uint) => ProviderSqlTypeLookup[SqlServerTypes.sql_int], + Type t when t == typeof(long) => ProviderSqlTypeLookup[SqlServerTypes.sql_bigint], + Type t when t == typeof(ulong) => ProviderSqlTypeLookup[SqlServerTypes.sql_bigint], + Type t when t == typeof(float) => ProviderSqlTypeLookup[SqlServerTypes.sql_float], + Type t when t == typeof(double) => ProviderSqlTypeLookup[SqlServerTypes.sql_float], + Type t when t == typeof(decimal) + => ProviderSqlTypeLookup[SqlServerTypes.sql_decimal], + Type t when t == typeof(char) + => descriptor.Unicode.GetValueOrDefault(true) + ? ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar] + : ProviderSqlTypeLookup[SqlServerTypes.sql_varchar], + Type t when t == typeof(string) + => descriptor.Unicode.GetValueOrDefault(true) + ? ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar] + : ProviderSqlTypeLookup[SqlServerTypes.sql_varchar], + Type t when t == typeof(char[]) + => descriptor.Unicode.GetValueOrDefault(true) + ? ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar] + : ProviderSqlTypeLookup[SqlServerTypes.sql_varchar], + Type t when t == typeof(ReadOnlyMemory[]) + => descriptor.Unicode.GetValueOrDefault(true) + ? ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar] + : ProviderSqlTypeLookup[SqlServerTypes.sql_varchar], + Type t when t == typeof(Stream) + => descriptor.Unicode.GetValueOrDefault(true) + ? ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar] + : ProviderSqlTypeLookup[SqlServerTypes.sql_varchar], + Type t when t == typeof(TextReader) + => descriptor.Unicode.GetValueOrDefault(true) + ? ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar] + : ProviderSqlTypeLookup[SqlServerTypes.sql_varchar], + Type t when t == typeof(byte[]) + => ProviderSqlTypeLookup[SqlServerTypes.sql_varbinary], + Type t when t == typeof(object) + => ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar], + Type t when t == typeof(object[]) + => ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar], + Type t when t == typeof(Guid) + => ProviderSqlTypeLookup[SqlServerTypes.sql_uniqueidentifier], + Type t when t == typeof(DateTime) + => ProviderSqlTypeLookup[SqlServerTypes.sql_datetime], + Type t when t == typeof(DateTimeOffset) + => ProviderSqlTypeLookup[SqlServerTypes.sql_datetimeoffset], + Type t when t == typeof(TimeSpan) + => ProviderSqlTypeLookup[SqlServerTypes.sql_bigint], + Type t when t == typeof(DateOnly) => ProviderSqlTypeLookup[SqlServerTypes.sql_date], + Type t when t == typeof(TimeOnly) => ProviderSqlTypeLookup[SqlServerTypes.sql_time], + Type t when t == typeof(BitArray) || t == typeof(BitVector32) + => ProviderSqlTypeLookup[SqlServerTypes.sql_varbinary], + Type t + when t == typeof(ImmutableDictionary) + || t == typeof(Dictionary) + || t == typeof(IDictionary) + => ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar], + Type t + when t == typeof(JsonNode) + || t == typeof(JsonObject) + || t == typeof(JsonArray) + || t == typeof(JsonValue) + || t == typeof(JsonDocument) + || t == typeof(JsonElement) + => ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar], + _ => null + }; + + if (providerSqlType != null) + return true; + + // handle generic types + providerSqlType = !dotnetType.IsGenericType + ? null + : dotnetType.GetGenericTypeDefinition() switch + { + Type t + when t == typeof(Dictionary<,>) + || t == typeof(IDictionary<,>) + || t == typeof(List<>) + || t == typeof(IList<>) + || t == typeof(Collection<>) + || t == typeof(ICollection<>) + || t == typeof(IEnumerable<>) + => ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar], + _ => null + }; + + if (providerSqlType != null) + return true; + + // handle POCO type + if (dotnetType.IsClass || dotnetType.IsInterface) + { + providerSqlType = ProviderSqlTypeLookup[SqlServerTypes.sql_nvarchar]; + } + + return providerSqlType != null; + } +} diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerTypes.cs b/src/DapperMatic/Providers/SqlServer/SqlServerTypes.cs new file mode 100644 index 0000000..cc21fa1 --- /dev/null +++ b/src/DapperMatic/Providers/SqlServer/SqlServerTypes.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Providers.SqlServer; + +/// +/// See: https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver16 +/// +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static class SqlServerTypes +{ + // integers + public const string sql_tinyint = "tinyint"; + public const string sql_smallint = "smallint"; + public const string sql_int = "int"; + public const string sql_bigint = "bigint"; + + // real + public const string sql_float = "float"; + public const string sql_real = "real"; + public const string sql_decimal = "decimal"; + public const string sql_numeric = "numeric"; + public const string sql_money = "money"; + public const string sql_smallmoney = "smallmoney"; + + // bool + public const string sql_bit = "bit"; + + // datetime + public const string sql_date = "date"; + public const string sql_datetime = "datetime"; + public const string sql_smalldatetime = "smalldatetime"; + public const string sql_datetime2 = "datetime2"; + public const string sql_datetimeoffset = "datetimeoffset"; + public const string sql_time = "time"; + public const string sql_timestamp = "timestamp"; + + // text + public const string sql_char = "char"; + public const string sql_varchar = "varchar"; + public const string sql_text = "text"; + public const string sql_nchar = "nchar"; + public const string sql_nvarchar = "nvarchar"; + public const string sql_ntext = "ntext"; + public const string sql_uniqueidentifier = "uniqueidentifier"; + + // binary + public const string sql_binary = "binary"; + public const string sql_varbinary = "varbinary"; + public const string sql_image = "image"; + + // geometry + public const string sql_geometry = "geometry"; + public const string sql_geography = "geography"; + public const string sql_hierarchyid = "hierarchyid"; + + // other data types + public const string sql_variant = "sql_variant"; + public const string sql_xml = "xml"; + public const string sql_cursor = "cursor"; + public const string sql_table = "table"; + public const string sql_json = "json"; +} \ No newline at end of file diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ColumnMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ColumnMethods.cs deleted file mode 100644 index 629abe4..0000000 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ColumnMethods.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.Sqlite; - -public partial class SqliteExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task ColumnExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - return 0 - < await ExecuteScalarAsync( - db, - @$"SELECT COUNT(*) FROM pragma_table_info('{tableName}') WHERE name = @columnName", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateColumnIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - Type dotnetType, - string? type = null, - int? length = null, - int? precision = null, - int? scale = null, - string? schemaName = null, - string? defaultValue = null, - bool nullable = true, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var sqlType = type ?? GetSqlTypeString(dotnetType, length, precision, scale); - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - await ExecuteAsync( - db, - $@"ALTER TABLE {tableName} - ADD COLUMN {columnName} {sqlType} {(nullable ? "NULL" : "NOT NULL")} {(!string.IsNullOrWhiteSpace(defaultValue) ? $"DEFAULT {defaultValue}" : "")} {(unique ? "UNIQUE" : "")}", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetColumnNamesAsync( - IDbConnection db, - string tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - // return await QueryAsync(db, $@"PRAGMA table_info({tableName})", tx) - // .ConfigureAwait(false); - return await QueryAsync( - db, - $@"select name from pragma_table_info('{tableName}')", - tx - ) - .ConfigureAwait(false); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - $@"select name from pragma_table_info('{tableName}') where name like @where", - new { where }, - tx - ) - .ConfigureAwait(false); - } - } - - public async Task DropColumnIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await ColumnExistsAsync(db, tableName, columnName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - // drop columnName - await ExecuteAsync(db, $@"ALTER TABLE {tableName} DROP COLUMN {columnName}", tx) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ForeignKeyMethods.cs deleted file mode 100644 index 398dc1c..0000000 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ForeignKeyMethods.cs +++ /dev/null @@ -1,431 +0,0 @@ -using System.Data; -using System.Data.Common; -using DapperMatic.Models; - -namespace DapperMatic.Providers.Sqlite; - -public partial class SqliteExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public override Task SupportsNamedForeignKeysAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } - - public async Task ForeignKeyExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - // foreign key names don't exist in sqlite, the columnName MUST be specified - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException( - "Column name must be specified in SQLite.", - nameof(columnName) - ); - - // this is the query to get all foreign keys for a tableName in SQLite - // for DEBUGGING purposes - // var fks = ( - // await db.QueryAsync($@"select * from pragma_foreign_key_list('{tableName}')", tx) - // .ConfigureAwait(false) - // ) - // .Cast>() - // .ToArray(); - // var fksJson = JsonConvert.SerializeObject(fks); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM pragma_foreign_key_list('{tableName}') - WHERE ""from"" = @columnName", - new { tableName, columnName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateForeignKeyIfNotExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string foreignKey, - string referenceTable, - string referenceColumn, - string? schemaName = null, - string onDelete = "NO ACTION", - string onUpdate = "NO ACTION", - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(referenceTable)) - throw new ArgumentException( - "Reference tableName name must be specified.", - nameof(referenceTable) - ); - if (string.IsNullOrWhiteSpace(referenceColumn)) - throw new ArgumentException( - "Reference columnName name must be specified.", - nameof(referenceColumn) - ); - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - - if ( - await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - var (_, referenceTableName, referenceColumnName) = NormalizeNames( - schemaName, - referenceTable, - referenceColumn - ); - - var createSqlStatement = ( - await QueryAsync( - db, - $@"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = @tableName", - new { tableName }, - tx - ) - .ConfigureAwait(false) - ).Single(); - createSqlStatement = createSqlStatement.Trim().TrimEnd(')').Trim().TrimEnd(','); - createSqlStatement += - $@", FOREIGN KEY (""{columnName}"") REFERENCES ""{referenceTableName}"" (""{referenceColumnName}"") ON DELETE {onDelete} ON UPDATE {onUpdate})"; - - var innerTx = - tx - ?? await (db as DbConnection)! - .BeginTransactionAsync(cancellationToken) - .ConfigureAwait(false); - await ExecuteAsync(db, "PRAGMA foreign_keys = 0", tx ?? innerTx).ConfigureAwait(false); - try - { - // first rename the tableName - await ExecuteAsync( - db, - $@"ALTER TABLE '{tableName}' RENAME TO '{tableName}_old'", - tx ?? innerTx - ) - .ConfigureAwait(false); - // re-create the tableName with the new constraint - await ExecuteAsync(db, createSqlStatement, tx ?? innerTx).ConfigureAwait(false); - // copy the data from the old tableName to the new tableName - await ExecuteAsync( - db, - $@"INSERT INTO '{tableName}' SELECT * FROM '{tableName}_old'", - tx ?? innerTx - ) - .ConfigureAwait(false); - // drop the old tableName - await ExecuteAsync(db, $@"DROP TABLE '{tableName}_old'", tx ?? innerTx) - .ConfigureAwait(false); - await ExecuteAsync(db, "PRAGMA foreign_keys = 1", tx ?? innerTx).ConfigureAwait(false); - if (tx == null) - innerTx.Commit(); - } - catch - { - await ExecuteAsync(db, "PRAGMA foreign_keys = 1", tx ?? innerTx).ConfigureAwait(false); - if (tx == null) - innerTx.Rollback(); - throw; - } - - return true; - } - - public async Task> GetForeignKeysAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"SELECT 'fk_'||sm.name||'_'||p.""from""||'_'||p.""table""||'_'||p.""to"" AS constraint_name, - sm.name AS table_name, - p.""from"" AS column_name, - p.""table"" AS referenced_table_name, - p.""to"" AS referenced_column_name, - p.on_delete AS delete_rule, - p.on_update AS update_rule - FROM sqlite_master sm - JOIN pragma_foreign_key_list(sm.name) p - WHERE sm.type = 'table'"; - if (!string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND sm.name = @tableName"; - if (!string.IsNullOrWhiteSpace(where)) - sql += $@" AND constraint_name LIKE @where"; - sql += $@" order by constraint_name"; - - var results = await QueryAsync<( - string constraint_name, - string table_name, - string column_name, - string referenced_table_name, - string referenced_column_name, - string delete_rule, - string update_rule - )>( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - - return results.Select(r => - { - var deleteRule = (r.delete_rule ?? "").Replace('_', ' ') switch - { - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - "NO ACTION" => ReferentialAction.NoAction, - _ => ReferentialAction.NoAction - }; - var updateRule = (r.update_rule ?? "").Replace('_', ' ') switch - { - "CASCADE" => ReferentialAction.Cascade, - "SET NULL" => ReferentialAction.SetNull, - "NO ACTION" => ReferentialAction.NoAction, - _ => ReferentialAction.NoAction - }; - - return new ForeignKey( - null, - r.constraint_name, - r.table_name, - r.column_name, - r.referenced_table_name, - r.referenced_column_name, - deleteRule, - updateRule - ); - }); - } - - public async Task> GetForeignKeyNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - $@"SELECT 'fk_'||sm.name||'_'||p.""from""||'_'||p.""table""||'_'||p.""to"" AS constraint_name - FROM sqlite_master sm - JOIN pragma_foreign_key_list(sm.name) p - WHERE sm.type = 'table'"; - if (!string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND sm.name = @tableName"; - if (!string.IsNullOrWhiteSpace(where)) - sql += $@" AND constraint_name LIKE @where"; - sql += @" ORDER BY constraint_name"; - - return await QueryAsync( - db, - sql, - new - { - schemaName, - tableName, - where - }, - tx - ) - .ConfigureAwait(false); - } - - /// - /// In SQLite, to drop a foreign key, you must re-create the tableName without the foreign key, - /// and then re-insert the data. It's a costly operation. - /// - /// - /// Example: https://www.techonthenet.com/sqlite/foreign_keys/drop.php - /// - public async Task DropForeignKeyIfExistsAsync( - IDbConnection db, - string tableName, - string columnName, - string? foreignKey = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - if (string.IsNullOrWhiteSpace(columnName)) - throw new ArgumentException("Column name must be specified.", nameof(columnName)); - - var fkExists = await ForeignKeyExistsAsync( - db, - tableName, - columnName, - foreignKey, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false); - if (!fkExists) - return false; - - (_, tableName, columnName) = NormalizeNames(schemaName, tableName, columnName); - - var originalCreateSqlStatement = ( - await QueryAsync( - db, - $@"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = @tableName", - new { tableName } - ) - .ConfigureAwait(false) - ).Single(); - - // this statement will look like this: - /* - CREATE TABLE "tableName" ( - "columnName" INTEGER, - FOREIGN KEY ("columnName") REFERENCES "referenceTable" ("referenceColumn") ON DELETE NO ACTION ON UPDATE NO ACTION - ) - */ - - // remove the foreign key constraint from the create statement - var indexOfForeignKeyClause = originalCreateSqlStatement.IndexOf( - $"FOREIGN KEY (\"{columnName}\")", - StringComparison.OrdinalIgnoreCase - ); - if (indexOfForeignKeyClause < 0) - throw new InvalidOperationException( - "Foreign key constraint not found in the tableName create statement." - ); - - var createSqlStatement = originalCreateSqlStatement; - // find the next ',' after the foreign key clause - var indexOfNextComma = createSqlStatement.IndexOf(',', indexOfForeignKeyClause); - if (indexOfNextComma > 0) - { - // replace the foreign key clause including the command with an empty string - createSqlStatement = createSqlStatement.Remove( - indexOfForeignKeyClause, - indexOfNextComma - indexOfForeignKeyClause + 1 - ); - } - else - { - // if there is no comma, assume it's the last statement, and remove the clause up until the last ')' - var indexOfLastParenthesis = createSqlStatement.LastIndexOf(')'); - if (indexOfLastParenthesis > 0) - { - createSqlStatement = - createSqlStatement - .Remove( - indexOfForeignKeyClause, - indexOfLastParenthesis - indexOfForeignKeyClause + 1 - ) - .Trim() - .TrimEnd(',') + "\n)"; - } - } - - // throw an error if the createSqlStatement is the same as the original - if (createSqlStatement == originalCreateSqlStatement) - throw new InvalidOperationException( - "Foreign key constraint not found in the tableName create statement." - ); - - var innerTx = - tx - ?? await (db as DbConnection)! - .BeginTransactionAsync(cancellationToken) - .ConfigureAwait(false); - await ExecuteAsync(db, "PRAGMA foreign_keys = 0", tx ?? innerTx).ConfigureAwait(false); - try - { - // first rename the tableName - await ExecuteAsync( - db, - $@"ALTER TABLE '{tableName}' RENAME TO '{tableName}_old'", - tx ?? innerTx - ) - .ConfigureAwait(false); - // re-create the tableName with the new constraint - await ExecuteAsync(db, createSqlStatement, tx ?? innerTx).ConfigureAwait(false); - // copy the data from the old tableName to the new tableName - await ExecuteAsync( - db, - $@"INSERT INTO '{tableName}' SELECT * FROM '{tableName}_old'", - tx ?? innerTx - ) - .ConfigureAwait(false); - // drop the old tableName - await ExecuteAsync(db, $@"DROP TABLE '{tableName}_old'", tx ?? innerTx) - .ConfigureAwait(false); - await ExecuteAsync(db, "PRAGMA foreign_keys = 1", tx ?? innerTx).ConfigureAwait(false); - if (tx == null) - innerTx.Commit(); - } - catch - { - await ExecuteAsync(db, "PRAGMA foreign_keys = 1", tx ?? innerTx).ConfigureAwait(false); - if (tx == null) - innerTx.Rollback(); - throw; - } - finally - { - if (tx == null) - innerTx.Dispose(); - } - - return true; - } -} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.IndexMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.IndexMethods.cs deleted file mode 100644 index 21e219d..0000000 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.IndexMethods.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System.Data; -using DapperMatic.Models; - -namespace DapperMatic.Providers.Sqlite; - -public partial class SqliteExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task IndexExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if (string.IsNullOrWhiteSpace(indexName)) - throw new ArgumentException( - "Index name must be specified in SQLite.", - nameof(indexName) - ); - - // this is the query to get all indexes for a tableName in SQLite - // for DEBUGGING purposes - // var fks = ( - // await db.QueryAsync($@"select * from pragma_index_list('{tableName}')", tx) - // .ConfigureAwait(false) - // ) - // .Cast>() - // .ToArray(); - // var fksJson = JsonConvert.SerializeObject(fks); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM pragma_index_list('{tableName}') - WHERE ""origin"" = 'c' and ""name"" = @indexName", - new { tableName, indexName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateIndexIfNotExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string[] columnNames, - string? schemaName = null, - bool unique = false, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, indexName) = NormalizeNames(schemaName, tableName, indexName); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - var uniqueString = unique ? "UNIQUE" : ""; - var columnList = string.Join(", ", columnNames); - await ExecuteAsync( - db, - $@" - CREATE {uniqueString} INDEX {indexName} ON {tableName} ({columnList}) - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - public async Task> GetIndexesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - @$"SELECT - tbl.name AS table_name, - idx.name AS index_name, - idc.name AS column_name, - idx.[unique] AS is_unique, - idx_master.sql AS index_sql - FROM sqlite_master AS tbl - LEFT JOIN pragma_index_list(tbl.name) AS idx - LEFT JOIN pragma_index_info(idx.name) AS idc - JOIN sqlite_master AS idx_master ON tbl.name = idx_master.tbl_name AND idx.name = idx_master.name - WHERE - tbl.type = 'table' - AND idx.origin = 'c' - AND idx_master.type = 'index' - AND idx_master.sql IS NOT NULL" - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND tbl.name = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND idx.name LIKE @where") - + @" ORDER BY tbl.name, idx.name"; - - var results = await QueryAsync<( - string table_name, - string index_name, - string column_name, - int is_unique, - string index_sql - )>(db, sql, new { tableName, where }, tx) - .ConfigureAwait(false); - - var grouped = results.GroupBy( - r => (r.table_name, r.index_name), - r => (r.is_unique, r.column_name, r.index_sql) - ); - - var indexes = new List(); - foreach (var group in grouped) - { - var (table_name, index_name) = group.Key; - var (is_unique, column_name, index_sql) = group.First(); - var index = new TableIndex( - null, - table_name, - index_name, - group - .Select(g => - { - var col = g.column_name; - var sql = g.index_sql.ToLowerInvariant().Replace("[", "").Replace("]", ""); - var direction = sql.ToLowerInvariant() - .Contains($"{col} desc", StringComparison.OrdinalIgnoreCase) - ? "DESC" - : "ASC"; - return $"{col} {direction}"; - }) - .ToArray(), - is_unique == 1 - ); - indexes.Add(index); - } - - return indexes; - } - - public async Task> GetIndexNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - - var sql = - @$"SELECT name FROM sqlite_master WHERE type = 'index'" - + (string.IsNullOrWhiteSpace(tableName) ? "" : " AND tbl_name = @tableName") - + (string.IsNullOrWhiteSpace(where) ? "" : " AND name LIKE @where") - + @" ORDER BY name"; - - return await QueryAsync(db, sql, new { tableName, where }, tx).ConfigureAwait(false); - } - - public async Task DropIndexIfExistsAsync( - IDbConnection db, - string tableName, - string indexName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await IndexExistsAsync(db, tableName, indexName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - indexName = NormalizeName(indexName); - - await ExecuteAsync( - db, - $@" - DROP INDEX {indexName} - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.SchemaMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.SchemaMethods.cs deleted file mode 100644 index 78ae7a6..0000000 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.SchemaMethods.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.Sqlite; - -public partial class SqliteExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public override Task SupportsSchemasAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } - - public Task SchemaExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } - - public Task CreateSchemaIfNotExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } - - public Task> GetSchemaNamesAsync( - IDbConnection db, - string? nameFilter = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - // does not support schemas, so we return an empty list - return Task.FromResult(Enumerable.Empty()); - } - - public Task DropSchemaIfExistsAsync( - IDbConnection db, - string schemaName, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(false); - } -} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.TableMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.TableMethods.cs deleted file mode 100644 index 4049e69..0000000 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.TableMethods.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Data; -using System.Text; - -namespace DapperMatic.Providers.Sqlite; - -public partial class SqliteExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task TableExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - return 0 - < await ExecuteScalarAsync( - db, - "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = @tableName", - new { tableName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateTableIfNotExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - string[]? primaryKeyColumnNames = null, - Type[]? primaryKeyDotnetTypes = null, - int?[]? primaryKeyColumnLengths = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - if (primaryKeyColumnNames == null || primaryKeyColumnNames.Length == 0) - { - // sqlite automatically sets the rowid as the primary key and autoincrements it - await ExecuteAsync( - db, - @$"CREATE TABLE ""{tableName}"" ( - id INTEGER NOT NULL, - CONSTRAINT ""pk_{tableName}_id"" PRIMARY KEY (id) - ) - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - var sql = new StringBuilder(); - sql.AppendLine(@$"CREATE TABLE ""{tableName}"" ("); - var columnNamesWithOrder = new List(); - for (var i = 0; i < primaryKeyColumnNames.Length; i++) - { - var columnArr = primaryKeyColumnNames[i].Split(' '); - var (_, _, columnName) = NormalizeNames(schemaName, tableName, columnArr[0]); - if (string.IsNullOrWhiteSpace(columnName)) - continue; - - columnNamesWithOrder.Add( - columnName + (columnArr.Length > 1 ? $" {columnArr[1]}" : " ASC") - ); - - if (primaryKeyDotnetTypes != null && primaryKeyDotnetTypes.Length > i) - { - sql.AppendLine( - $"{columnName} {GetSqlTypeString(primaryKeyDotnetTypes[i], (primaryKeyColumnLengths != null && primaryKeyColumnLengths.Length > i) ? primaryKeyColumnLengths[i] : null)} NOT NULL," - ); - } - else - { - sql.AppendLine($"[{columnName}] INTEGER NOT NULL,"); - } - } - sql.AppendLine( - $"CONSTRAINT [pk_{tableName}_id] PRIMARY KEY ({string.Join(", ", columnNamesWithOrder)})" - ); - sql.AppendLine(")"); - - await ExecuteAsync(db, sql.ToString(), transaction: tx).ConfigureAwait(false); - return true; - } - - public async Task> GetTableNamesAsync( - IDbConnection db, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return await QueryAsync( - db, - "SELECT distinct name FROM sqlite_master WHERE type ='table' AND name NOT LIKE 'sqlite_%' ORDER BY name", - transaction: tx - ) - .ConfigureAwait(false); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return await QueryAsync( - db, - "SELECT distinct name FROM sqlite_master WHERE type ='table' AND name NOT LIKE 'sqlite_%' AND name LIKE @where ORDER BY name", - new { where }, - transaction: tx - ) - .ConfigureAwait(false); - } - } - - public async Task DropTableIfExistsAsync( - IDbConnection db, - string tableName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await TableExistsAsync(db, tableName, schemaName, tx, cancellationToken) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, _) = NormalizeNames(schemaName, tableName, null); - - await ExecuteAsync(db, @$"DROP TABLE ""{tableName}""", transaction: tx) - .ConfigureAwait(false); - - return true; - } -} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.UniqueConstraintMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.UniqueConstraintMethods.cs deleted file mode 100644 index f27575a..0000000 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.UniqueConstraintMethods.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.Sqlite; - -public partial class SqliteExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - public async Task UniqueConstraintExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - if (string.IsNullOrWhiteSpace(uniqueConstraintName)) - throw new ArgumentException( - "Unique constraint name must be specified in SQLite.", - nameof(uniqueConstraintName) - ); - - // this is the query to get all indexes for a tableName in SQLite - // for DEBUGGING purposes - // var fks = ( - // await db.QueryAsync($@"select * from pragma_index_list('{tableName}')", tx) - // .ConfigureAwait(false) - // ) - // .Cast>() - // .ToArray(); - // var fksJson = JsonConvert.SerializeObject(fks); - - return 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM pragma_index_list('{tableName}') - WHERE (""origin"" = 'u' or ""unique"" = 1) and ""name"" = @uniqueConstraintName", - new { tableName, uniqueConstraintName }, - tx - ) - .ConfigureAwait(false); - } - - public async Task CreateUniqueConstraintIfNotExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string[] columnNames, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - if (columnNames == null || columnNames.Length == 0) - throw new ArgumentException( - "At least one columnName must be specified.", - nameof(columnNames) - ); - - if ( - await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - // to create a unique index, you have to re-create the tableName in sqlite - // so we will just create a regular index - var columnList = string.Join(", ", columnNames); - await ExecuteAsync( - db, - $@" - CREATE UNIQUE INDEX {uniqueConstraintName} ON {tableName} ({columnList}) - ", - transaction: tx - ) - .ConfigureAwait(false); - return true; - } - - public Task> GetUniqueConstraintNamesAsync( - IDbConnection db, - string? tableName, - string? nameFilter = null, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - (_, tableName, _) = NormalizeNames(schemaName, tableName); - - // can also query using - // SELECT type, name, tbl_name, sql FROM sqlite_master WHERE type= 'index'; - - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return QueryAsync( - db, - $@"SELECT ""name"" INDEX_NAME - FROM pragma_index_list('{tableName}') - WHERE (""origin"" = 'u' or ""unique"" = 1) - ORDER BY INDEX_NAME", - new { tableName }, - tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return QueryAsync( - db, - $@"SELECT ""name"" INDEX_NAME - FROM pragma_index_list('{tableName}') - WHERE (""origin"" = 'u' or ""unique"" = 1) and INDEX_NAME LIKE @where - ORDER BY INDEX_NAME", - new { tableName, where }, - tx - ); - } - } - - public async Task DropUniqueConstraintIfExistsAsync( - IDbConnection db, - string tableName, - string uniqueConstraintName, - string? schemaName = null, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - if ( - !await UniqueConstraintExistsAsync( - db, - tableName, - uniqueConstraintName, - schemaName, - tx, - cancellationToken - ) - .ConfigureAwait(false) - ) - return false; - - (_, tableName, uniqueConstraintName) = NormalizeNames( - schemaName, - tableName, - uniqueConstraintName - ); - - // if it's an index, we can delete it (ASSUME THIS FOR NOW) - if ( - 0 - < await ExecuteScalarAsync( - db, - $@"SELECT COUNT(*) - FROM pragma_index_list('{tableName}') - WHERE (""origin"" = 'c' and ""unique"" = 1) and ""name"" = @uniqueConstraintName", - new { tableName, uniqueConstraintName }, - tx - ) - .ConfigureAwait(false) - ) - { - await ExecuteAsync( - db, - $@" - DROP INDEX {uniqueConstraintName} - ", - transaction: tx - ) - .ConfigureAwait(false); - - return true; - } - - // if it's a true unique constraint, we have to drop the tableName and re-create it - return false; - } -} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.cs deleted file mode 100644 index e32a45e..0000000 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Data; - -namespace DapperMatic.Providers.Sqlite; - -public partial class SqliteExtensions : DatabaseExtensionsBase, IDatabaseExtensions -{ - protected override string DefaultSchema => ""; - - protected override List DataTypes => - DataTypeMapFactory.GetDefaultDatabaseTypeDataTypeMap(DatabaseTypes.Sqlite); - - internal SqliteExtensions() { } - - public async Task GetDatabaseVersionAsync( - IDbConnection db, - IDbTransaction? tx = null, - CancellationToken cancellationToken = default - ) - { - return await ExecuteScalarAsync(db, $@"select sqlite_version()", transaction: tx) - .ConfigureAwait(false) ?? ""; - } -} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.CheckConstraints.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.CheckConstraints.cs new file mode 100644 index 0000000..fd64896 --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.CheckConstraints.cs @@ -0,0 +1,125 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + public override async Task CreateCheckConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string? columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required.", nameof(expression)); + + (_, tableName, constraintName) = NormalizeNames(schemaName, tableName, constraintName); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + if (!string.IsNullOrWhiteSpace(columnName)) + { + return table.CheckConstraints.All(x => + !x.ConstraintName.Equals(constraintName) + ) + && table.CheckConstraints.All(x => + string.IsNullOrWhiteSpace(x.ColumnName) + || !x.ColumnName.Equals( + columnName, + StringComparison.OrdinalIgnoreCase + ) + ); + } + return table.CheckConstraints.All(x => + !x.ConstraintName.Equals(constraintName) + ); + }, + table => + { + table.CheckConstraints.Add( + new DxCheckConstraint( + schemaName, + tableName, + columnName, + constraintName, + expression + ) + ); + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public override async Task DropCheckConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + (schemaName, tableName, constraintName) = NormalizeNames( + schemaName, + tableName, + constraintName + ); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.CheckConstraints.Any(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + var checkConstraint = table.CheckConstraints.SingleOrDefault(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + if (!string.IsNullOrWhiteSpace(checkConstraint?.ColumnName)) + { + var column = table.Columns.SingleOrDefault(x => + x.ColumnName.Equals( + checkConstraint.ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ); + if (column != null) + { + column.CheckExpression = null; + } + } + table.CheckConstraints.RemoveAll(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.Columns.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.Columns.cs new file mode 100644 index 0000000..4b81220 --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.Columns.cs @@ -0,0 +1,84 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + /// + /// The restrictions on creating a column in a SQLite database are too many. + /// Unfortunately, we have to re-create the table in SQLite to avoid these limitations. + /// See: https://www.sqlite.org/lang_altertable.html + /// + public override async Task CreateColumnIfNotExistsAsync( + IDbConnection db, + DxColumn column, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(column.TableName)) + throw new ArgumentException("Table name is required", nameof(column.TableName)); + + if (string.IsNullOrWhiteSpace(column.ColumnName)) + throw new ArgumentException("Column name is required", nameof(column.ColumnName)); + + var (_, tableName, columnName) = NormalizeNames( + column.SchemaName, + column.TableName, + column.ColumnName + ); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + DefaultSchema, + tableName, + table => + { + return table.Columns.All(x => + !x.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + table.Columns.Add(column); + return table; + }, + tx: tx, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + } + + public override async Task DropColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.Columns.Any(x => + x.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + table.Columns.RemoveAll(c => + c.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ); + return table; + }, + tx: tx, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.DefaultConstraints.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.DefaultConstraints.cs new file mode 100644 index 0000000..1fccd7b --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.DefaultConstraints.cs @@ -0,0 +1,149 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + public override async Task CreateDefaultConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + string constraintName, + string expression, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(columnName)) + throw new ArgumentException("Column name is required.", nameof(columnName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression is required.", nameof(expression)); + + (_, tableName, constraintName) = NormalizeNames(schemaName, tableName, constraintName); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.DefaultConstraints.All(x => + !x.ConstraintName.Equals(constraintName) + && !x.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + table.DefaultConstraints.Add( + new DxDefaultConstraint( + schemaName, + tableName, + columnName, + constraintName, + expression + ) + ); + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public override async Task DropDefaultConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + (schemaName, tableName, constraintName) = NormalizeNames( + schemaName, + tableName, + constraintName + ); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.DefaultConstraints.Any(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + var defaultConstraint = table.DefaultConstraints.SingleOrDefault(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + if (!string.IsNullOrWhiteSpace(defaultConstraint?.ColumnName)) + { + var column = table.Columns.SingleOrDefault(x => + x.ColumnName.Equals( + defaultConstraint.ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ); + if (column != null) + { + column.DefaultExpression = null; + } + } + table.DefaultConstraints.RemoveAll(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public override async Task DropDefaultConstraintOnColumnIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string columnName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var constraintName = await GetDefaultConstraintNameOnColumnAsync( + db, + schemaName, + tableName, + columnName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(constraintName)) + return false; + + return await DropDefaultConstraintIfExistsAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.ForeignKeyConstraints.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.ForeignKeyConstraints.cs new file mode 100644 index 0000000..21bdaaa --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.ForeignKeyConstraints.cs @@ -0,0 +1,154 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + public override async Task CreateForeignKeyConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] sourceColumns, + string referencedTableName, + DxOrderedColumn[] referencedColumns, + DxForeignKeyAction onDelete = DxForeignKeyAction.NoAction, + DxForeignKeyAction onUpdate = DxForeignKeyAction.NoAction, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (sourceColumns.Length == 0) + throw new ArgumentException( + "At least one column must be specified.", + nameof(sourceColumns) + ); + + if (string.IsNullOrWhiteSpace(referencedTableName)) + throw new ArgumentException( + "Referenced table name is required.", + nameof(referencedTableName) + ); + + if (referencedColumns.Length == 0) + throw new ArgumentException( + "At least one column must be specified.", + nameof(referencedColumns) + ); + + if (sourceColumns.Length != referencedColumns.Length) + throw new ArgumentException( + "The number of source columns must match the number of referenced columns.", + nameof(referencedColumns) + ); + + (schemaName, tableName, constraintName) = NormalizeNames( + schemaName, + tableName, + constraintName + ); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.ForeignKeyConstraints.All(x => + !x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + table.ForeignKeyConstraints.Add( + new DxForeignKeyConstraint( + schemaName, + tableName, + constraintName, + sourceColumns, + referencedTableName, + referencedColumns, + onDelete, + onUpdate + ) + ); + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public override async Task DropForeignKeyConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + (schemaName, tableName, constraintName) = NormalizeNames( + schemaName, + tableName, + constraintName + ); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.ForeignKeyConstraints.Any(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + var foreignKey = table.ForeignKeyConstraints.FirstOrDefault(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + if (foreignKey is not null) + { + // remove the foreign key from the related column + foreach (var column in foreignKey.SourceColumns) + { + var sc = table.Columns.FirstOrDefault(x => + x.ColumnName.Equals( + column.ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ); + // ReSharper disable once InvertIf + if (sc is not null) + { + sc.IsForeignKey = false; + sc.ReferencedTableName = null; + sc.ReferencedColumnName = null; + sc.OnDelete = null; + sc.OnUpdate = null; + } + } + + table.ForeignKeyConstraints.Remove(foreignKey); + } + table.ForeignKeyConstraints.RemoveAll(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.PrimaryKeyConstraints.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.PrimaryKeyConstraints.cs new file mode 100644 index 0000000..1365af6 --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.PrimaryKeyConstraints.cs @@ -0,0 +1,105 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + public override async Task CreatePrimaryKeyConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (columns.Length == 0) + throw new ArgumentException("At least one column must be specified.", nameof(columns)); + + if ( + await DoesPrimaryKeyConstraintExistAsync( + db, + schemaName, + tableName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + (_, tableName, constraintName) = NormalizeNames(schemaName, tableName, constraintName); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => table.PrimaryKeyConstraint is null, + table => + { + table.PrimaryKeyConstraint = new DxPrimaryKeyConstraint( + schemaName, + tableName, + constraintName, + columns + ); + + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public override async Task DropPrimaryKeyConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if ( + !await DoesPrimaryKeyConstraintExistAsync( + db, + schemaName, + tableName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => table.PrimaryKeyConstraint is not null, + table => + { + table.PrimaryKeyConstraint = null; + foreach (var column in table.Columns) + { + column.IsPrimaryKey = false; + } + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.Strings.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.Strings.cs new file mode 100644 index 0000000..44ded9c --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.Strings.cs @@ -0,0 +1,184 @@ +using DapperMatic.Models; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + #region Schema Strings + #endregion // Schema Strings + + #region Table Strings + + protected override string SqlInlineColumnNameAndType(DxColumn column, Version dbVersion) + { + // IF the column is an autoincrement column, the type MUST be INTEGER + // https://www.sqlite.org/autoinc.html + if (column.IsAutoIncrement) + { + column.SetProviderDataType(ProviderType, SqliteTypes.sql_integer); + } + + return base.SqlInlineColumnNameAndType(column, dbVersion); + } + + protected override string SqlInlinePrimaryKeyAutoIncrementColumnConstraint(DxColumn column) + { + return "AUTOINCREMENT"; + } + + protected override (string sql, object parameters) SqlDoesTableExist( + string? schemaName, + string tableName + ) + { + const string sql = """ + SELECT COUNT(*) + FROM sqlite_master + WHERE + type = 'table' + AND name = @tableName + """; + + return ( + sql, + new + { + schemaName = NormalizeSchemaName(schemaName), + tableName = NormalizeName(tableName) + } + ); + } + + protected override (string sql, object parameters) SqlGetTableNames( + string? schemaName, + string? tableNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(tableNameFilter) ? "" : ToLikeString(tableNameFilter); + + var sql = $""" + + SELECT name + FROM sqlite_master + WHERE + type = 'table' + AND name NOT LIKE 'sqlite_%' + {( + string.IsNullOrWhiteSpace(where) ? null : " AND name LIKE @where" + )} + ORDER BY name + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + #endregion // Table Strings + + #region Column Strings + #endregion // Column Strings + + #region Check Constraint Strings + #endregion // Check Constraint Strings + + #region Default Constraint Strings + #endregion // Default Constraint Strings + + #region Primary Key Strings + #endregion // Primary Key Strings + + #region Unique Constraint Strings + #endregion // Unique Constraint Strings + + #region Foreign Key Constraint Strings + #endregion // Foreign Key Constraint Strings + + #region Index Strings + protected override string SqlDropIndex(string? schemaName, string tableName, string indexName) + { + return $"DROP INDEX {GetSchemaQualifiedIdentifierName(schemaName, indexName)}"; + } + #endregion // Index Strings + + #region View Strings + + protected override (string sql, object parameters) SqlGetViewNames( + string? schemaName, + string? viewNameFilter = null + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + + SELECT + m.name AS ViewName + FROM sqlite_master AS m + WHERE + m.TYPE = 'view' + AND m.name NOT LIKE 'sqlite_%' + {( + string.IsNullOrWhiteSpace(where) ? "" : " AND m.name LIKE @where" + )} + ORDER BY + m.name + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + protected override (string sql, object parameters) SqlGetViews( + string? schemaName, + string? viewNameFilter + ) + { + var where = string.IsNullOrWhiteSpace(viewNameFilter) ? "" : ToLikeString(viewNameFilter); + + var sql = $""" + + SELECT + NULL as SchemaName, + m.name AS ViewName, + m.SQL AS Definition + FROM sqlite_master AS m + WHERE + m.TYPE = 'view' + AND m.name NOT LIKE 'sqlite_%' + {( + string.IsNullOrWhiteSpace(where) ? "" : " AND m.name LIKE @where" + )} + ORDER BY + m.name + """; + + return (sql, new { schemaName = NormalizeSchemaName(schemaName), where }); + } + + private static readonly char[] WhiteSpaceCharacters = [' ', '\t', '\n', '\r']; + + protected override string NormalizeViewDefinition(string definition) + { + definition = definition.Trim(); + + // split the view by the first AS keyword surrounded by whitespace + string? viewDefinition = null; + for (var i = 0; i < definition.Length; i++) + { + if ( + i <= 0 + || definition[i] != 'A' + || definition[i + 1] != 'S' + || !WhiteSpaceCharacters.Contains(definition[i - 1]) + || !WhiteSpaceCharacters.Contains(definition[i + 2]) + ) + continue; + + viewDefinition = definition[(i + 3)..].Trim(); + break; + } + + if (string.IsNullOrWhiteSpace(viewDefinition)) + throw new Exception("Could not parse view definition: " + definition); + + return viewDefinition; + } + #endregion // View Strings +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.Tables.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.Tables.cs new file mode 100644 index 0000000..4d72448 --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.Tables.cs @@ -0,0 +1,413 @@ +using System.Data; +using System.Data.Common; +using System.Text; +using DapperMatic.Models; +// ReSharper disable LoopCanBeConvertedToQuery +// ReSharper disable ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + public override async Task> GetTablesAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var where = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + + var sql = new StringBuilder(); + sql.AppendLine( + """ + SELECT name as table_name, sql as table_sql + FROM sqlite_master + WHERE type = 'table' AND name NOT LIKE 'sqlite_%' + """ + ); + if (!string.IsNullOrWhiteSpace(where)) + sql.AppendLine(" AND name LIKE @where"); + sql.AppendLine("ORDER BY name"); + + var results = await QueryAsync<(string table_name, string table_sql)>( + db, + sql.ToString(), + new { where }, + tx: tx + ) + .ConfigureAwait(false); + + var tables = new List(); + foreach (var result in results) + { + var table = SqliteSqlParser.ParseCreateTableStatement(result.table_sql); + if (table == null) + continue; + tables.Add(table); + } + + // attach indexes to tables + var indexes = await GetIndexesInternalAsync( + db, + schemaName, + tableNameFilter, + null, + tx, + cancellationToken + ) + .ConfigureAwait(false); + + if (indexes.Count <= 0) return tables; + + foreach (var table in tables) + { + table.Indexes = indexes + .Where(i => + i.TableName.Equals(table.TableName, StringComparison.OrdinalIgnoreCase) + ) + .ToList(); + + if (table.Indexes.Count <= 0) continue; + + foreach (var column in table.Columns) + { + column.IsIndexed = table.Indexes.Any(i => + i.Columns.Any(c => + c.ColumnName.Equals( + column.ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + if (column is { IsIndexed: true, IsUnique: false }) + { + column.IsUnique = table + .Indexes.Where(i => i.IsUnique) + .Any(i => + i.Columns.Any(c => + c.ColumnName.Equals( + column.ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ) + ); + } + } + } + + return tables; + } + + public override async Task TruncateTableIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if ( + !await DoesTableExistAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false) + ) + return false; + + (_, tableName, _) = NormalizeNames(schemaName, tableName); + + // in SQLite, you could either delete all the records and reset the index (this could take a while if it's a big table) + // - DELETE FROM table_name; + // - DELETE FROM sqlite_sequence WHERE name = 'table_name'; + + // or just drop the table (this is faster) and recreate it + var createTableSql = await ExecuteScalarAsync( + db, + "select sql FROM sqlite_master WHERE type = 'table' AND name = @tableName", + new { tableName }, + tx: tx + ) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(createTableSql)) + return false; + + await DropTableIfExistsAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + await ExecuteAsync(db, createTableSql, tx: tx).ConfigureAwait(false); + + return true; + } + + protected override async Task> GetIndexesInternalAsync( + IDbConnection db, + string? schemaName, + string? tableNameFilter = null, + string? indexNameFilter = null, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + var whereTableLike = string.IsNullOrWhiteSpace(tableNameFilter) + ? null + : ToLikeString(tableNameFilter); + var whereIndexLike = string.IsNullOrWhiteSpace(indexNameFilter) + ? null + : ToLikeString(indexNameFilter); + + var sql = + $""" + + SELECT DISTINCT + m.name AS table_name, + il.name AS index_name, + il."unique" AS is_unique, + ii.name AS column_name, + ii.DESC AS is_descending + FROM sqlite_schema AS m, + pragma_index_list(m.name) AS il, + pragma_index_xinfo(il.name) AS ii + WHERE m.type='table' + and ii.name IS NOT NULL + AND il.origin = 'c' + {(string.IsNullOrWhiteSpace(whereTableLike) ? "" : " AND m.name LIKE @whereTableLike")} + {(string.IsNullOrWhiteSpace(whereIndexLike) ? "" : " AND il.name LIKE @whereIndexLike")} + ORDER BY m.name, il.name, ii.seqno + """; + + var results = await QueryAsync<( + string table_name, + string index_name, + bool is_unique, + string column_name, + bool is_descending + )>(db, sql, new { whereTableLike, whereIndexLike }, tx: tx) + .ConfigureAwait(false); + + var indexes = new List(); + + foreach ( + var group in results.GroupBy(r => new + { + r.table_name, + r.index_name, + r.is_unique + }) + ) + { + var index = new DxIndex + { + SchemaName = null, + TableName = group.Key.table_name, + IndexName = group.Key.index_name, + IsUnique = group.Key.is_unique, + Columns = group + .Select(r => new DxOrderedColumn( + r.column_name, + r.is_descending ? DxColumnOrder.Descending : DxColumnOrder.Ascending + )) + .ToArray() + }; + indexes.Add(index); + } + + return indexes; + } + + // private async Task> GetCreateIndexSqlStatementsForTable( + // IDbConnection db, + // string? schemaName, + // string tableName, + // IDbTransaction? tx = null, + // CancellationToken cancellationToken = default + // ) + // { + // var getSqlCreateIndexStatements = + // @" + // SELECT DISTINCT + // m.sql + // FROM sqlite_schema AS m, + // pragma_index_list(m.name) AS il, + // pragma_index_xinfo(il.name) AS ii + // WHERE m.type='table' + // AND ii.name IS NOT NULL + // AND il.origin = 'c' + // AND m.name = @tableName + // AND m.sql IS NOT NULL + // ORDER BY m.name, il.name, ii.seqno + // "; + // return ( + // await QueryAsync(db, getSqlCreateIndexStatements, new { tableName }, tx: tx) + // .ConfigureAwait(false) + // ) + // .Select(sql => + // { + // return sql.Contains("IF NOT EXISTS", StringComparison.OrdinalIgnoreCase) + // ? sql + // : sql.Replace( + // "CREATE INDEX", + // "CREATE INDEX IF NOT EXISTS", + // StringComparison.OrdinalIgnoreCase + // ) + // .Replace( + // "CREATE UNIQUE INDEX", + // "CREATE UNIQUE INDEX IF NOT EXISTS", + // StringComparison.OrdinalIgnoreCase + // ) + // .Trim(); + // }) + // .ToList(); + // } + + private async Task AlterTableUsingRecreateTableStrategyAsync( + IDbConnection db, + string? schemaName, + string tableName, + Func? validateTable, + Func updateTable, + IDbTransaction? tx, + CancellationToken cancellationToken + ) + { + var table = await GetTableAsync(db, schemaName, tableName, tx, cancellationToken) + .ConfigureAwait(false); + + if (table == null) + return false; + + if (validateTable != null && !validateTable(table)) + return false; + + // create a temporary table with the updated schema + var tmpTable = new DxTable( + table.SchemaName, + table.TableName, + [.. table.Columns], + table.PrimaryKeyConstraint, + [.. table.CheckConstraints], + [.. table.DefaultConstraints], + [.. table.UniqueConstraints], + [.. table.ForeignKeyConstraints], + [.. table.Indexes] + ); + var newTable = updateTable(tmpTable); + + await AlterTableUsingRecreateTableStrategyAsync( + db, + table, + newTable, + tx, + cancellationToken + ); + + return true; + } + + private async Task AlterTableUsingRecreateTableStrategyAsync( + IDbConnection db, + DxTable existingTable, + DxTable updatedTable, + IDbTransaction? tx, + CancellationToken cancellationToken + ) + { + var tableName = existingTable.TableName; + var tempTableName = $"{tableName}_tmp_{Guid.NewGuid():N}"; + // updatedTable.TableName = newTableName; + + // get the create index sql statements for the existing table + // var createIndexStatements = await GetCreateIndexSqlStatementsForTable( + // db, + // schemaName, + // tableName, + // tx, + // cancellationToken + // ) + // .ConfigureAwait(false); + + // disable foreign key constraints temporarily + await ExecuteAsync(db, "PRAGMA foreign_keys = 0", tx).ConfigureAwait(false); + + var innerTx = (DbTransaction)( + tx + ?? await (db as DbConnection)! + .BeginTransactionAsync(cancellationToken) + .ConfigureAwait(false) + ); + try + { + // create a temporary table from the existing table's data + await ExecuteAsync( + db, + $"CREATE TEMP TABLE {tempTableName} AS SELECT * FROM {tableName}", + tx: innerTx + ) + .ConfigureAwait(false); + + // drop the old table + await ExecuteAsync(db, $"DROP TABLE {tableName}", tx: innerTx).ConfigureAwait(false); + + var created = await CreateTableIfNotExistsAsync( + db, + updatedTable, + innerTx, + cancellationToken + ) + .ConfigureAwait(false); + + if (created) + { + // populate the new table with the data from the old table + var previousColumnNames = existingTable.Columns.Select(c => c.ColumnName); + + // make sure to only copy columns that exist in both tables + var columnNamesInBothTables = previousColumnNames.Where(c => + updatedTable.Columns.Any(x => + x.ColumnName.Equals(c, StringComparison.OrdinalIgnoreCase) + ) + ).ToArray(); + + if (columnNamesInBothTables.Length > 0) + { + var columnsToCopyString = string.Join(", ", columnNamesInBothTables); + await ExecuteAsync( + db, + $"INSERT INTO {updatedTable.TableName} ({columnsToCopyString}) SELECT {columnsToCopyString} FROM {tempTableName}", + tx: innerTx + ) + .ConfigureAwait(false); + } + + // drop the temp table + await ExecuteAsync(db, $"DROP TABLE {tempTableName}", tx: innerTx) + .ConfigureAwait(false); + + // commit the transaction + if (tx == null) + { + await innerTx.CommitAsync(cancellationToken).ConfigureAwait(false); + } + } + } + catch + { + if (tx == null) + { + await innerTx.RollbackAsync(cancellationToken).ConfigureAwait(false); + } + throw; + } + finally + { + if (tx == null) + { + await innerTx.DisposeAsync(); + } + // re-enable foreign key constraints + await ExecuteAsync(db, "PRAGMA foreign_keys = 1", tx).ConfigureAwait(false); + } + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.UniqueConstraints.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.UniqueConstraints.cs new file mode 100644 index 0000000..00d25f1 --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.UniqueConstraints.cs @@ -0,0 +1,139 @@ +using System.Data; +using DapperMatic.Models; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods +{ + public override async Task CreateUniqueConstraintIfNotExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + DxOrderedColumn[] columns, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if (columns.Length == 0) + throw new ArgumentException("At least one column must be specified.", nameof(columns)); + + if ( + await DoesUniqueConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + (_, tableName, constraintName) = NormalizeNames(schemaName, tableName, constraintName); + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.UniqueConstraints.All(uc => + !uc.ConstraintName.Equals( + constraintName, + StringComparison.OrdinalIgnoreCase + ) + ); + }, + table => + { + table.UniqueConstraints.Add( + new DxUniqueConstraint(schemaName, tableName, constraintName, columns) + ); + + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } + + public override async Task DropUniqueConstraintIfExistsAsync( + IDbConnection db, + string? schemaName, + string tableName, + string constraintName, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentException("Table name is required.", nameof(tableName)); + + if (string.IsNullOrWhiteSpace(constraintName)) + throw new ArgumentException("Constraint name is required.", nameof(constraintName)); + + if ( + !await DoesUniqueConstraintExistAsync( + db, + schemaName, + tableName, + constraintName, + tx, + cancellationToken + ) + .ConfigureAwait(false) + ) + return false; + + return await AlterTableUsingRecreateTableStrategyAsync( + db, + schemaName, + tableName, + table => + { + return table.UniqueConstraints.Any(uc => + uc.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + }, + table => + { + var uc = table.UniqueConstraints.FirstOrDefault(uc => + uc.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + if (uc is not null) + { + if (uc.Columns.Length == 1) + { + var tableColumn = table.Columns.First(x => + x.ColumnName.Equals( + uc.Columns[0].ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ); + if (!tableColumn.IsIndexed) + { + tableColumn.IsUnique = false; + } + } + table.UniqueConstraints.Remove(uc); + } + table.UniqueConstraints.RemoveAll(x => + x.ConstraintName.Equals(constraintName, StringComparison.OrdinalIgnoreCase) + ); + return table; + }, + tx, + cancellationToken + ) + .ConfigureAwait(false); + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteMethods.cs new file mode 100644 index 0000000..357cf35 --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteMethods.cs @@ -0,0 +1,31 @@ +using System.Data; +using DapperMatic.Interfaces; +using DapperMatic.Providers.Base; + +namespace DapperMatic.Providers.Sqlite; + +public partial class SqliteMethods : DatabaseMethodsBase, IDatabaseMethods +{ + public override DbProviderType ProviderType => DbProviderType.Sqlite; + + public override IDbProviderTypeMap ProviderTypeMap => SqliteProviderTypeMap.Instance.Value; + + protected override string DefaultSchema => ""; + + internal SqliteMethods() { } + + public override async Task GetDatabaseVersionAsync( + IDbConnection db, + IDbTransaction? tx = null, + CancellationToken cancellationToken = default + ) + { + // sample output: 3.44.1 + const string sql = "SELECT sqlite_version()"; + var versionString = + await ExecuteScalarAsync(db, sql, tx: tx).ConfigureAwait(false) ?? ""; + return DbProviderUtils.ExtractVersionFromVersionString(versionString); + } + + public override char[] QuoteChars => ['"']; +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteProviderTypeMap.cs b/src/DapperMatic/Providers/Sqlite/SqliteProviderTypeMap.cs new file mode 100644 index 0000000..d19d9b7 --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteProviderTypeMap.cs @@ -0,0 +1,330 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DapperMatic.Providers.Sqlite; + +public sealed class SqliteProviderTypeMap : DbProviderTypeMapBase +{ + internal static readonly Lazy Instance = + new(() => new SqliteProviderTypeMap()); + + private SqliteProviderTypeMap() + : base() { } + + protected override DbProviderType ProviderType => DbProviderType.Sqlite; + + public override string SqTypeForStringLengthMax => "text"; + + public override string SqTypeForBinaryLengthMax => "blob"; + + public override string SqlTypeForJson => "text"; + + /// + /// IMPORTANT!! The order within an affinity group matters, as the first possible match will be used as the recommended sql type for a dotnet type + /// + protected override DbProviderSqlType[] ProviderSqlTypes => + [ + new( + DbProviderSqlTypeAffinity.Integer, + SqliteTypes.sql_integer, + formatWithPrecision: "integer({0})", + defaultPrecision: 11, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqliteTypes.sql_int, + aliasOf: "integer", + formatWithPrecision: "int({0})", + defaultPrecision: 11, + canUseToAutoIncrement: true, + minValue: -2147483648, + maxValue: 2147483647 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqliteTypes.sql_tinyint, + formatWithPrecision: "tinyint({0})", + defaultPrecision: 4, + canUseToAutoIncrement: true, + minValue: -128, + maxValue: 128 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqliteTypes.sql_smallint, + formatWithPrecision: "smallint({0})", + defaultPrecision: 5, + canUseToAutoIncrement: true, + minValue: -32768, + maxValue: 32767 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqliteTypes.sql_mediumint, + formatWithPrecision: "mediumint({0})", + defaultPrecision: 7, + canUseToAutoIncrement: true, + minValue: -8388608, + maxValue: 8388607 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqliteTypes.sql_bigint, + formatWithPrecision: "bigint({0})", + defaultPrecision: 19, + canUseToAutoIncrement: true, + minValue: -9223372036854775808, + maxValue: 9223372036854775807 + ), + new( + DbProviderSqlTypeAffinity.Integer, + SqliteTypes.sql_unsigned_big_int, + formatWithPrecision: "unsigned big int({0})", + defaultPrecision: 20, + canUseToAutoIncrement: true, + minValue: 0, + maxValue: 18446744073709551615 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqliteTypes.sql_real, + formatWithPrecision: "real({0})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqliteTypes.sql_double, + formatWithPrecision: "double({0})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqliteTypes.sql_float, + formatWithPrecision: "float({0})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqliteTypes.sql_numeric, + formatWithPrecision: "numeric({0})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Real, + SqliteTypes.sql_decimal, + formatWithPrecision: "decimal({0})", + defaultPrecision: 12, + defaultScale: 2 + ), + new( + DbProviderSqlTypeAffinity.Boolean, + SqliteTypes.sql_bool, + formatWithPrecision: "bool({0})", + defaultPrecision: 1 + ), + new( + DbProviderSqlTypeAffinity.Boolean, + SqliteTypes.sql_boolean, + formatWithPrecision: "boolean({0})", + defaultPrecision: 1 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + SqliteTypes.sql_date, + formatWithPrecision: "date({0})", + defaultPrecision: 10, + isDateOnly: true + ), + new( + DbProviderSqlTypeAffinity.DateTime, + SqliteTypes.sql_datetime, + formatWithPrecision: "datetime({0})", + defaultPrecision: 19 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + SqliteTypes.sql_timestamp, + formatWithPrecision: "timestamp({0})", + defaultPrecision: 19 + ), + new( + DbProviderSqlTypeAffinity.DateTime, + SqliteTypes.sql_time, + formatWithPrecision: "time({0})", + defaultPrecision: 8, + isTimeOnly: true + ), + new( + DbProviderSqlTypeAffinity.DateTime, + SqliteTypes.sql_year, + formatWithPrecision: "year({0})", + defaultPrecision: 4 + ), + new( + DbProviderSqlTypeAffinity.Text, + SqliteTypes.sql_nvarchar, + formatWithLength: "nvarchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqliteTypes.sql_varchar, + formatWithLength: "varchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqliteTypes.sql_nchar, + formatWithLength: "nchar({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqliteTypes.sql_char, + formatWithLength: "char({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqliteTypes.sql_varying_character, + formatWithLength: "varying character({0})", + defaultLength: DefaultLength + ), + new( + DbProviderSqlTypeAffinity.Text, + SqliteTypes.sql_native_character, + formatWithLength: "native character({0})", + defaultLength: DefaultLength + ), + new(DbProviderSqlTypeAffinity.Text, SqliteTypes.sql_text), + new(DbProviderSqlTypeAffinity.Text, SqliteTypes.sql_clob), + new(DbProviderSqlTypeAffinity.Binary, SqliteTypes.sql_blob), + ]; + + protected override bool TryGetProviderSqlTypeMatchingDotnetTypeInternal( + DbProviderDotnetTypeDescriptor descriptor, + out DbProviderSqlType? providerSqlType + ) + { + providerSqlType = null; + + var dotnetType = descriptor.DotnetType; + + // handle well-known types first + providerSqlType = dotnetType.IsGenericType + ? null + : dotnetType switch + { + Type t when t == typeof(bool) => ProviderSqlTypeLookup[SqliteTypes.sql_boolean], + Type t when t == typeof(byte) => ProviderSqlTypeLookup[SqliteTypes.sql_smallint], + Type t when t == typeof(ReadOnlyMemory) + => ProviderSqlTypeLookup[SqliteTypes.sql_smallint], + Type t when t == typeof(sbyte) => ProviderSqlTypeLookup[SqliteTypes.sql_smallint], + Type t when t == typeof(short) => ProviderSqlTypeLookup[SqliteTypes.sql_smallint], + Type t when t == typeof(ushort) => ProviderSqlTypeLookup[SqliteTypes.sql_smallint], + Type t when t == typeof(int) => ProviderSqlTypeLookup[SqliteTypes.sql_int], + Type t when t == typeof(uint) => ProviderSqlTypeLookup[SqliteTypes.sql_int], + Type t when t == typeof(long) => ProviderSqlTypeLookup[SqliteTypes.sql_bigint], + Type t when t == typeof(ulong) => ProviderSqlTypeLookup[SqliteTypes.sql_bigint], + Type t when t == typeof(float) => ProviderSqlTypeLookup[SqliteTypes.sql_float], + Type t when t == typeof(double) => ProviderSqlTypeLookup[SqliteTypes.sql_double], + Type t when t == typeof(decimal) => ProviderSqlTypeLookup[SqliteTypes.sql_decimal], + Type t when t == typeof(char) => ProviderSqlTypeLookup[SqliteTypes.sql_varchar], + Type t when t == typeof(string) + => descriptor.Length.GetValueOrDefault(255) < 8000 + ? ProviderSqlTypeLookup[SqliteTypes.sql_varchar] + : ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t when t == typeof(char[]) + => descriptor.Length.GetValueOrDefault(255) < 8000 + ? ProviderSqlTypeLookup[SqliteTypes.sql_varchar] + : ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t when t == typeof(ReadOnlyMemory[]) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[SqliteTypes.sql_varchar] + : ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t when t == typeof(Stream) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[SqliteTypes.sql_varchar] + : ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t when t == typeof(TextReader) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[SqliteTypes.sql_varchar] + : ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t when t == typeof(byte[]) => ProviderSqlTypeLookup[SqliteTypes.sql_blob], + Type t when t == typeof(object) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[SqliteTypes.sql_varchar] + : ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t when t == typeof(object[]) + => descriptor.Length.GetValueOrDefault(int.MaxValue) < 8000 + ? ProviderSqlTypeLookup[SqliteTypes.sql_varchar] + : ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t when t == typeof(Guid) => ProviderSqlTypeLookup[SqliteTypes.sql_varchar], + Type t when t == typeof(DateTime) + => ProviderSqlTypeLookup[SqliteTypes.sql_datetime], + Type t when t == typeof(DateTimeOffset) + => ProviderSqlTypeLookup[SqliteTypes.sql_timestamp], + Type t when t == typeof(TimeSpan) => ProviderSqlTypeLookup[SqliteTypes.sql_bigint], + Type t when t == typeof(DateOnly) => ProviderSqlTypeLookup[SqliteTypes.sql_date], + Type t when t == typeof(TimeOnly) => ProviderSqlTypeLookup[SqliteTypes.sql_time], + Type t when t == typeof(BitArray) || t == typeof(BitVector32) + => ProviderSqlTypeLookup[SqliteTypes.sql_blob], + Type t + when t == typeof(ImmutableDictionary) + || t == typeof(Dictionary) + || t == typeof(IDictionary) + => ProviderSqlTypeLookup[SqliteTypes.sql_text], + Type t + when t == typeof(JsonNode) + || t == typeof(JsonObject) + || t == typeof(JsonArray) + || t == typeof(JsonValue) + || t == typeof(JsonDocument) + || t == typeof(JsonElement) + => ProviderSqlTypeLookup[SqliteTypes.sql_text], + _ => null + }; + + if (providerSqlType != null) + return true; + + // handle generic types + providerSqlType = !dotnetType.IsGenericType + ? null + : dotnetType.GetGenericTypeDefinition() switch + { + Type t + when t == typeof(Dictionary<,>) + || t == typeof(IDictionary<,>) + || t == typeof(List<>) + || t == typeof(IList<>) + || t == typeof(Collection<>) + || t == typeof(ICollection<>) + || t == typeof(IEnumerable<>) + => ProviderSqlTypeLookup[SqliteTypes.sql_text], + _ => null + }; + + if (providerSqlType != null) + return true; + + // handle POCO type + if (dotnetType.IsClass || dotnetType.IsInterface) + { + providerSqlType = ProviderSqlTypeLookup[SqliteTypes.sql_text]; + } + + return providerSqlType != null; + } +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteSqlParser.cs b/src/DapperMatic/Providers/Sqlite/SqliteSqlParser.cs new file mode 100644 index 0000000..febb8ab --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteSqlParser.cs @@ -0,0 +1,1286 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.RegularExpressions; +using DapperMatic.Models; + +// ReSharper disable ForCanBeConvertedToForeach + +namespace DapperMatic.Providers.Sqlite; + +[SuppressMessage("ReSharper", "ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator")] +public static partial class SqliteSqlParser +{ + public static DxTable? ParseCreateTableStatement(string createTableSql) + { + var statements = ParseDdlSql(createTableSql); + if ( + statements.SingleOrDefault() is not SqlCompoundClause createTableStatement + || createTableStatement.FindTokenIndex("CREATE") != 0 + && createTableStatement.FindTokenIndex("TABLE") != 1 + ) + return null; + + var tableName = createTableStatement.GetChild(2)?.Text; + if (string.IsNullOrWhiteSpace(tableName)) + return null; + + var table = new DxTable(null, tableName); + + // there are lots of variation of CREATE TABLE statements in SQLite, so we need to handle each variation + // we can iterate this process to parse different variations and improve this over time, for now, we will + // brute-force this to get it to work + + // statements we are interested in will look like this, where everything inside the first ( ... ) represent the guts of a table, + // so we are looking for the first compount clause that has children and is wrapped in parentheses + // CREATE TABLE table_name ( ... ) + + // see: https://www.sqlite.org/lang_createtable.html + + var tableGuts = createTableStatement.GetChild(x => + x.Children.Count > 0 && x.Parenthesis + ); + if (tableGuts == null || tableGuts.Children.Count == 0) + return table; + + // we now iterate over these guts to parse out columns, primary keys, unique constraints, check constraints, default constraints, and foreign key constraints + // constraint clauses can appear as part of the column definition, or as separate clauses: + // - if as part of column definition, they appear inline + // - if separate as table constraint definitions, they always start with either the word "CONSTRAINT" or the constraint type identifier "PRIMARY KEY", "FOREIGN KEY", "UNIQUE", "CHECK", "DEFAULT" + + bool IsColumnDefinitionClause(SqlClause clause) + { + return !( + clause.FindTokenIndex("CONSTRAINT") == 0 + || clause.FindTokenIndex("PRIMARY KEY") == 0 + || clause.FindTokenIndex("FOREIGN KEY") == 0 + || clause.FindTokenIndex("UNIQUE") == 0 + || clause.FindTokenIndex("CHECK") == 0 + || clause.FindTokenIndex("DEFAULT") == 0 + ); + } + + // based on the documentation of the CREATE TABLE statement, we know that column definitions appear before table constraint clauses, + // so we can safely assume that by the time we start parsing constraints, all the column definitions will have been added to the table.columns list + for (var clauseIndex = 0; clauseIndex < tableGuts.Children.Count; clauseIndex++) + { + var clause = tableGuts.Children[clauseIndex]; + // see if it's a column definition or a table constraint + if (IsColumnDefinitionClause(clause)) + { + // it's a column definition, parse it + // see:https://www.sqlite.org/syntax/column-def.html + if (clause is not SqlCompoundClause columnDefinition) + continue; + + // first word in the column name + var columnName = columnDefinition.GetChild(0)?.Text; + if (string.IsNullOrWhiteSpace(columnName)) + continue; + + // second word is the column type + var columnDataType = columnDefinition.GetChild(1)?.Text; + if (string.IsNullOrWhiteSpace(columnDataType)) + continue; + + int? length = null; + int? precision = null; + int? scale = null; + + var remainingWordsIndex = 2; + if (columnDefinition.Children.Count > 2) + { + var thirdChild = columnDefinition.GetChild(2); + if (thirdChild is { Children.Count: > 0 and <= 2 }) + { + switch (thirdChild.Children.Count) + { + case 1: + { + if ( + thirdChild.Children[0] is SqlWordClause sw1 + && int.TryParse(sw1.Text, out var intValue) + ) + { + length = intValue; + } + + break; + } + case 2: + { + if ( + thirdChild.Children[0] is SqlWordClause sw1 + && int.TryParse(sw1.Text, out var intValue) + ) + { + precision = intValue; + } + if ( + thirdChild.Children[1] is SqlWordClause sw2 + && int.TryParse(sw2.Text, out var intValue2) + ) + { + scale = intValue2; + } + + break; + } + } + + remainingWordsIndex = 3; + } + } + + var providerTypeMap = SqliteProviderTypeMap.Instance.Value; + + // if we don't recognize the column data type, we skip it + if ( + !providerTypeMap.TryGetDotnetTypeDescriptorMatchingFullSqlTypeName( + columnDataType, + out var dotnetTypeDescriptor + ) + || dotnetTypeDescriptor == null + ) + continue; + + var column = new DxColumn( + null, + tableName, + columnName, + dotnetTypeDescriptor.DotnetType, + new Dictionary + { + { DbProviderType.Sqlite, columnDataType } + }, + length, + precision, + scale + ); + table.Columns.Add(column); + + // remaining words are optional in the column definition + if (columnDefinition.Children.Count <= remainingWordsIndex) + continue; + + string? inlineConstraintName = null; + for (var i = remainingWordsIndex; i < columnDefinition.Children.Count; i++) + { + var opt = columnDefinition.Children[i]; + if (opt is SqlWordClause swc) + { + switch (swc.Text.ToUpper()) + { + case "NOT NULL": + column.IsNullable = false; + break; + + case "AUTOINCREMENT": + column.IsAutoIncrement = true; + break; + + case "CONSTRAINT": + inlineConstraintName = columnDefinition + .GetChild(i + 1) + ?.Text; + // skip the next opt + i++; + break; + + case "DEFAULT": + // the clause can be a compound clause, or literal-value (quoted), or a number (integer, float, etc.) + // if the clause is a compound parenthesized clause, we will remove the parentheses and trim the text + column.DefaultExpression = columnDefinition + .GetChild(i + 1) + ?.ToString() + ?.Trim('(', ')', ' '); + // skip the next opt + i++; + if (!string.IsNullOrWhiteSpace(column.DefaultExpression)) + { + // add the default constraint to the table + var defaultConstraintName = + inlineConstraintName + ?? DbProviderUtils.GenerateDefaultConstraintName( + tableName, + columnName + ); + table.DefaultConstraints.Add( + new DxDefaultConstraint( + null, + tableName, + column.ColumnName, + defaultConstraintName, + column.DefaultExpression + ) + ); + } + inlineConstraintName = null; + break; + + case "UNIQUE": + column.IsUnique = true; + // add the default constraint to the table + var uniqueConstraintName = + inlineConstraintName + ?? DbProviderUtils.GenerateUniqueConstraintName( + tableName, + columnName + ); + table.UniqueConstraints.Add( + new DxUniqueConstraint( + null, + tableName, + uniqueConstraintName, + [new DxOrderedColumn(column.ColumnName)] + ) + ); + inlineConstraintName = null; + break; + + case "CHECK": + // the check expression is typically a compound clause based on the SQLite documentation + // if the check expression is a compound parenthesized clause, we will remove the parentheses and trim the text + column.CheckExpression = columnDefinition + .GetChild(i + 1) + ?.ToString() + ?.Trim('(', ')', ' '); + // skip the next opt + i++; + if (!string.IsNullOrWhiteSpace(column.CheckExpression)) + { + // add the default constraint to the table + var checkConstraintName = + inlineConstraintName + ?? DbProviderUtils.GenerateCheckConstraintName( + tableName, + columnName + ); + table.CheckConstraints.Add( + new DxCheckConstraint( + null, + tableName, + column.ColumnName, + checkConstraintName, + column.CheckExpression + ) + ); + } + inlineConstraintName = null; + break; + + case "PRIMARY KEY": + column.IsPrimaryKey = true; + // add the default constraint to the table + var pkConstraintName = + inlineConstraintName + ?? DbProviderUtils.GeneratePrimaryKeyConstraintName( + tableName, + columnName + ); + var columnOrder = DxColumnOrder.Ascending; + if ( + columnDefinition + .GetChild(i + 1) + ?.ToString() + ?.Equals("DESC", StringComparison.OrdinalIgnoreCase) == true + ) + { + columnOrder = DxColumnOrder.Descending; + // skip the next opt + i++; + } + table.PrimaryKeyConstraint = new DxPrimaryKeyConstraint( + null, + tableName, + pkConstraintName, + [new DxOrderedColumn(column.ColumnName, columnOrder)] + ); + inlineConstraintName = null; + break; + + case "REFERENCES": + // see: https://www.sqlite.org/syntax/foreign-key-clause.html + column.IsForeignKey = true; + + var referenceTableNameIndex = i + 1; + var referenceColumnNamesIndex = i + 2; + + var referencedTableName = columnDefinition + .GetChild(referenceTableNameIndex) + ?.Text; + if (string.IsNullOrWhiteSpace(referencedTableName)) + break; + + // skip next opt + i++; + + // TODO: sqlite doesn't require the referenced column name, but we will for now in our library + var referenceColumnName = columnDefinition + .GetChild(referenceColumnNamesIndex) + ?.GetChild(0) + ?.Text; + if (string.IsNullOrWhiteSpace(referenceColumnName)) + break; + + // skip next opt + i++; + + var constraintName = + inlineConstraintName + ?? DbProviderUtils.GenerateForeignKeyConstraintName( + tableName, + columnName, + referencedTableName, + referenceColumnName + ); + + var foreignKey = new DxForeignKeyConstraint( + null, + tableName, + constraintName, + [new DxOrderedColumn(column.ColumnName)], + referencedTableName, + [new DxOrderedColumn(referenceColumnName)] + ); + + var onDeleteTokenIndex = columnDefinition.FindTokenIndex( + "ON DELETE" + ); + if (onDeleteTokenIndex >= i) + { + var onDelete = columnDefinition + .GetChild(onDeleteTokenIndex + 1) + ?.Text; + if (!string.IsNullOrWhiteSpace(onDelete)) + foreignKey.OnDelete = onDelete.ToForeignKeyAction(); + } + + var onUpdateTokenIndex = columnDefinition.FindTokenIndex( + "ON UPDATE" + ); + if (onUpdateTokenIndex >= i) + { + var onUpdate = columnDefinition + .GetChild(onUpdateTokenIndex + 1) + ?.Text; + if (!string.IsNullOrWhiteSpace(onUpdate)) + foreignKey.OnUpdate = onUpdate.ToForeignKeyAction(); + } + + column.ReferencedTableName = foreignKey.ReferencedTableName; + column.ReferencedColumnName = foreignKey + .ReferencedColumns[0] + .ColumnName; + column.OnDelete = foreignKey.OnDelete; + column.OnUpdate = foreignKey.OnUpdate; + + table.ForeignKeyConstraints.Add(foreignKey); + + inlineConstraintName = null; + break; + + case "COLLATE": + var collation = columnDefinition + .GetChild(i + 1) + ?.ToString(); + if (!string.IsNullOrWhiteSpace(collation)) + { + // TODO: not supported at this time + // column.Collation = collation; + // skip the next opt + i++; + } + break; + } + } + } + } + else + { + // it's a table constraint clause, parse it + // see: https://www.sqlite.org/syntax/table-constraint.html + if (clause is not SqlCompoundClause tableConstraint) + continue; + + string? inlineConstraintName = null; + for (var i = 0; i < tableConstraint.Children.Count; i++) + { + var opt = tableConstraint.Children[i]; + if (opt is SqlWordClause swc) + { + switch (swc.Text.ToUpper()) + { + case "CONSTRAINT": + inlineConstraintName = tableConstraint + .GetChild(i + 1) + ?.Text; + // skip the next opt + i++; + break; + case "PRIMARY KEY": + var pkColumnsClause = tableConstraint.GetChild( + i + 1 + ); + + var pkOrderedColumns = ExtractOrderedColumnsFromClause( + pkColumnsClause + ); + + var pkColumnNames = pkOrderedColumns + .Select(oc => oc.ColumnName) + .ToArray(); + + if (pkColumnNames.Length == 0) + continue; // skip this clause as it's invalid + + table.PrimaryKeyConstraint = new DxPrimaryKeyConstraint( + null, + tableName, + inlineConstraintName + ?? DbProviderUtils.GeneratePrimaryKeyConstraintName( + tableName, + pkColumnNames + ), + pkOrderedColumns + ); + foreach (var column in table.Columns) + { + if ( + pkColumnNames.Contains( + column.ColumnName, + StringComparer.OrdinalIgnoreCase + ) + ) + { + column.IsPrimaryKey = true; + } + } + continue; // we're done with this clause, so we can move on to the next constraint + case "UNIQUE": + var ucColumnsClause = tableConstraint.GetChild( + i + 1 + ); + + var ucOrderedColumns = ExtractOrderedColumnsFromClause( + ucColumnsClause + ); + + var ucColumnNames = ucOrderedColumns + .Select(oc => oc.ColumnName) + .ToArray(); + + if (ucColumnNames.Length == 0) + continue; // skip this clause as it's invalid + + var ucConstraint = new DxUniqueConstraint( + null, + tableName, + inlineConstraintName + ?? DbProviderUtils.GenerateUniqueConstraintName( + tableName, + ucColumnNames + ), + ucOrderedColumns + ); + table.UniqueConstraints.Add(ucConstraint); + if (ucConstraint.Columns.Length == 1) + { + var column = table.Columns.FirstOrDefault(c => + c.ColumnName.Equals( + ucConstraint.Columns[0].ColumnName, + StringComparison.OrdinalIgnoreCase + ) + ); + if (column != null) + column.IsUnique = true; + } + continue; // we're done with this clause, so we can move on to the next constraint + case "CHECK": + var checkConstraintExpression = tableConstraint + .GetChild(i + 1) + ?.ToString() + .Trim('(', ')', ' '); + + if (!string.IsNullOrWhiteSpace(checkConstraintExpression)) + { + // add the default constraint to the table + var checkConstraintName = + inlineConstraintName + ?? DbProviderUtils.GenerateCheckConstraintName( + tableName, + table.CheckConstraints.Count > 0 + ? $"{table.CheckConstraints.Count}" + : "" + ); + table.CheckConstraints.Add( + new DxCheckConstraint( + null, + tableName, + null, + checkConstraintName, + checkConstraintExpression + ) + ); + } + continue; // we're done with this clause, so we can move on to the next constraint + case "FOREIGN KEY": + var fkSourceColumnsClause = + tableConstraint.GetChild(i + 1); + if (fkSourceColumnsClause == null) + continue; // skip this clause as it's invalid + + var fkOrderedSourceColumns = ExtractOrderedColumnsFromClause( + fkSourceColumnsClause + ); + var fkSourceColumnNames = fkOrderedSourceColumns + .Select(oc => oc.ColumnName) + .ToArray(); + if (fkSourceColumnNames.Length == 0) + continue; // skip this clause as it's invalid + + var referencesClauseIndex = tableConstraint.FindTokenIndex( + "REFERENCES" + ); + if (referencesClauseIndex == -1) + continue; // skip this clause as it's invalid + + var referencedTableName = tableConstraint + .GetChild(referencesClauseIndex + 1) + ?.Text; + var fkReferencedColumnsClause = + tableConstraint.GetChild( + referencesClauseIndex + 2 + ); + if ( + string.IsNullOrWhiteSpace(referencedTableName) + || fkReferencedColumnsClause == null + ) + continue; // skip this clause as it's invalid + + var fkOrderedReferencedColumns = ExtractOrderedColumnsFromClause( + fkReferencedColumnsClause + ); + var fkReferencedColumnNames = fkOrderedReferencedColumns + .Select(oc => oc.ColumnName) + .ToArray(); + if (fkReferencedColumnNames.Length == 0) + continue; // skip this clause as it's invalid + + var constraintName = + inlineConstraintName + ?? DbProviderUtils.GenerateForeignKeyConstraintName( + tableName, + fkSourceColumnNames, + referencedTableName, + fkReferencedColumnNames + ); + + var foreignKey = new DxForeignKeyConstraint( + null, + tableName, + constraintName, + fkOrderedSourceColumns, + referencedTableName, + fkOrderedReferencedColumns + ); + + var onDeleteTokenIndex = tableConstraint.FindTokenIndex( + "ON DELETE" + ); + if (onDeleteTokenIndex >= i) + { + var onDelete = tableConstraint + .GetChild(onDeleteTokenIndex + 1) + ?.Text; + if (!string.IsNullOrWhiteSpace(onDelete)) + foreignKey.OnDelete = onDelete.ToForeignKeyAction(); + } + + var onUpdateTokenIndex = tableConstraint.FindTokenIndex( + "ON UPDATE" + ); + if (onUpdateTokenIndex >= i) + { + var onUpdate = tableConstraint + .GetChild(onUpdateTokenIndex + 1) + ?.Text; + if (!string.IsNullOrWhiteSpace(onUpdate)) + foreignKey.OnUpdate = onUpdate.ToForeignKeyAction(); + } + + if ( + fkSourceColumnNames.Length == 1 + && fkReferencedColumnNames.Length == 1 + ) + { + var column = table.Columns.FirstOrDefault(c => + c.ColumnName.Equals( + fkSourceColumnNames[0], + StringComparison.OrdinalIgnoreCase + ) + ); + if (column != null) + { + column.IsForeignKey = true; + column.ReferencedTableName = foreignKey.ReferencedTableName; + column.ReferencedColumnName = foreignKey + .ReferencedColumns[0] + .ColumnName; + column.OnDelete = foreignKey.OnDelete; + column.OnUpdate = foreignKey.OnUpdate; + } + } + + table.ForeignKeyConstraints.Add(foreignKey); + continue; // we're done processing the FOREIGN KEY clause, so we can move on to the next constraint + } + } + } + } + } + + return table; + } + + private static DxOrderedColumn[] ExtractOrderedColumnsFromClause( + SqlCompoundClause? pkColumnsClause + ) + { + if ( + pkColumnsClause == null + || pkColumnsClause.Children.Count == 0 + || pkColumnsClause.Parenthesis == false + ) + return []; + + var pkOrderedColumns = pkColumnsClause + .Children.Select(child => + { + switch (child) + { + case SqlWordClause wc: + return new DxOrderedColumn(wc.Text); + case SqlCompoundClause cc: + { + var ccName = cc.GetChild(0)?.Text; + if (string.IsNullOrWhiteSpace(ccName)) + return null; + var ccOrder = DxColumnOrder.Ascending; + if ( + cc.GetChild(1) + ?.Text.Equals("DESC", StringComparison.OrdinalIgnoreCase) == true + ) + { + ccOrder = DxColumnOrder.Descending; + } + return new DxOrderedColumn(ccName, ccOrder); + } + default: + return null; + } + }) + .Where(oc => oc != null) + .Cast() + .ToArray(); + return pkOrderedColumns; + } +} + +[SuppressMessage("ReSharper", "ForCanBeConvertedToForeach")] +[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement")] +[SuppressMessage("ReSharper", "InvertIf")] +[SuppressMessage("ReSharper", "ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator")] +public static partial class SqliteSqlParser +{ + private static List ParseDdlSql(string sql) + { + var statementParts = ParseSqlIntoStatementParts(sql); + + var statements = new List(); + foreach (var parts in statementParts) + { + var clauseBuilder = new ClauseBuilder(); + foreach (var part in parts) + { + clauseBuilder.AddPart(part); + } + // clauseBuilder.Complete(); + + var rootClause = clauseBuilder.GetRootClause(); + rootClause = ClauseBuilder.ReduceNesting(rootClause); + statements.Add(rootClause); + } + + return statements; + } + + private static string StripCommentsFromSql(string sqlQuery) + { + // Remove multi-line comments (non-greedy) + sqlQuery = MultiLineCommentRegex().Replace(sqlQuery, ""); + + // Remove single-line comments + sqlQuery = SingleLineCommentRegex().Replace(sqlQuery, ""); + + return sqlQuery; + } + + [SuppressMessage("ReSharper", "RedundantAssignment")] + private static List ParseSqlIntoStatementParts(string sql) + { + sql = StripCommentsFromSql(sql); + + sql = substitute_encode(sql); + + var statements = new List(); + + var parts = new List(); + + // split the SQL into parts + sql = string.Join( + ' ', + sql.Split( + [' ', '\r', '\n'], + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries + ) + ); + var cpart = string.Empty; + var inQuotes = false; + for (var ci = 0; ci < sql.Length; ci++) + { + var c = sql[ci]; + if (inQuotes && c != '\"') + { + cpart += c; + continue; + } + if (inQuotes && c == '\"') + { + cpart += c; + parts.Add(cpart); + cpart = string.Empty; + inQuotes = false; + continue; + } + if (!inQuotes && c == '\"') + { + if (!string.IsNullOrWhiteSpace(cpart)) + { + parts.Add(cpart); + cpart = string.Empty; + } + inQuotes = true; + cpart += c; + continue; + } + // detect end of statement + if (!inQuotes && c == ';') + { + if (parts.Count != 0) + { + statements.Add(substitute_decode(parts).ToArray()); + parts = []; + } + continue; + } + if (c.Equals(' ')) + { + if (!string.IsNullOrWhiteSpace(cpart)) + { + parts.Add(cpart); + cpart = string.Empty; + } + continue; + } + if (c.Equals('(') || c.Equals(')') || c.Equals(',')) + { + if (!string.IsNullOrWhiteSpace(cpart)) + { + parts.Add(cpart); + cpart = string.Empty; + } + parts.Add(c.ToString()); + continue; + } + cpart += c; + } + if (!string.IsNullOrWhiteSpace(cpart)) + { + parts.Add(cpart); + cpart = string.Empty; + } + + if (parts.Count != 0) + { + statements.Add(substitute_decode(parts).ToArray()); + parts = []; + } + + return statements; + } + + #region Static Variables + + private static string substitute_encode(string text) + { + foreach (var s in Substitutions) + { + text = text.Replace(s.Key, s.Value, StringComparison.OrdinalIgnoreCase); + } + return text; + } + + private static List substitute_decode(List strings) + { + var parts = new List(); + foreach (var t in strings) + { + parts.Add(substitute_decode(t)); + } + return parts; + } + + private static string substitute_decode(string text) + { + foreach (var s in Substitutions) + { + text = text.Replace(s.Value, s.Key, StringComparison.OrdinalIgnoreCase); + } + return text; + } + + /// + /// Keep certain words together that belong together while parsing a CREATE TABLE statement + /// + private static readonly Dictionary Substitutions = new List + { + "FOREIGN KEY", + "PRIMARY KEY", + "ON DELETE", + "ON UPDATE", + "SET NULL", + "SET DEFAULT", + "NO ACTION", + "NOT NULL", + "UNSIGNED BIG INT", + "VARYING CHARACTER", + "NATIVE CHARACTER", + "DOUBLE PRECISION" + }.ToDictionary(x => x, v => v.Replace(' ', '_')); + + /// + /// Don't mistake words as identifiers with keywords + /// + public static readonly List Keyword = + [ + "ABORT", + "ACTION", + "ADD", + "AFTER", + "ALL", + "ALTER", + "ALWAYS", + "ANALYZE", + "AND", + "AS", + "ASC", + "ATTACH", + "AUTOINCREMENT", + "BEFORE", + "BEGIN", + "BETWEEN", + "BY", + "CASCADE", + "CASE", + "CAST", + "CHECK", + "COLLATE", + "COLUMN", + "COMMIT", + "CONFLICT", + "CONSTRAINT", + "CREATE", + "CROSS", + "CURRENT", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "DATABASE", + "DEFAULT", + "DEFERRABLE", + "DEFERRED", + "DELETE", + "DESC", + "DETACH", + "DISTINCT", + "DO", + "DROP", + "EACH", + "ELSE", + "END", + "ESCAPE", + "EXCEPT", + "EXCLUDE", + "EXCLUSIVE", + "EXISTS", + "EXPLAIN", + "FAIL", + "FILTER", + "FIRST", + "FOLLOWING", + "FOR", + "FOREIGN", + "FROM", + "FULL", + "GENERATED", + "GLOB", + "GROUP", + "GROUPS", + "HAVING", + "IF", + "IGNORE", + "IMMEDIATE", + "IN", + "INDEX", + "INDEXED", + "INITIALLY", + "INNER", + "INSERT", + "INSTEAD", + "INTERSECT", + "INTO", + "IS", + "ISNULL", + "JOIN", + "KEY", + "LAST", + "LEFT", + "LIKE", + "LIMIT", + "MATCH", + "MATERIALIZED", + "NATURAL", + "NO", + "NOT", + "NOTHING", + "NOTNULL", + "NULL", + "NULLS", + "OF", + "OFFSET", + "ON", + "OR", + "ORDER", + "OTHERS", + "OUTER", + "OVER", + "PARTITION", + "PLAN", + "PRAGMA", + "PRECEDING", + "PRIMARY", + "QUERY", + "RAISE", + "RANGE", + "RECURSIVE", + "REFERENCES", + "REGEXP", + "REINDEX", + "RELEASE", + "RENAME", + "REPLACE", + "RESTRICT", + "RETURNING", + "RIGHT", + "ROLLBACK", + "ROW", + "ROWS", + "SAVEPOINT", + "SELECT", + "SET", + "TABLE", + "TEMP", + "TEMPORARY", + "THEN", + "TIES", + "TO", + "TRANSACTION", + "TRIGGER", + "UNBOUNDED", + "UNION", + "UNIQUE", + "UPDATE", + "USING", + "VACUUM", + "VALUES", + "VIEW", + "VIRTUAL", + "WHEN", + "WHERE", + "WINDOW", + "WITH", + "WITHOUT" + ]; + #endregion // Static Variables + + #region ClauseBuilder Classes + + public abstract class SqlClause(SqlCompoundClause? parent) + { + private SqlCompoundClause? _parent = parent; + + public bool HasParent() + { + return _parent != null; + } + + public SqlCompoundClause? GetParent() + { + return _parent; + } + + public void SetParent(SqlCompoundClause clause) + { + _parent = clause; + } + + public int FindTokenIndex(string token) + { + if (this is SqlCompoundClause scc) + { + return scc.Children.FindIndex(c => + c is SqlWordClause swc + && swc.Text.Equals(token, StringComparison.OrdinalIgnoreCase) + ); + } + return -1; + } + + public TClause? GetChild(int index) + where TClause : SqlClause + { + if (this is not SqlCompoundClause scc) + return null; + + if (index >= 0 && index < scc.Children.Count) + return scc.Children[index] as TClause; + + return null; + } + + public TClause? GetChild(Func predicate) + where TClause : SqlClause + { + if (this is not SqlCompoundClause scc) + return null; + + foreach (var child in scc.Children) + { + if (child is TClause tc && predicate(tc)) + return tc; + } + + return null; + } + } + + public class SqlWordClause : SqlClause + { + public SqlWordClause(SqlCompoundClause? parent, string text) + : base(parent) + { + if (text.StartsWith('[') && text.EndsWith(']')) + { + Quotes = ['[', ']']; + Text = text.Trim('[', ']'); + } + else if (text.StartsWith('\'') && text.EndsWith('\'')) + { + Quotes = ['\'', '\'']; + Text = text.Trim('\''); + } + else if (text.StartsWith('"') && text.EndsWith('"')) + { + Quotes = ['"', '"']; + Text = text.Trim('"'); + } + else if (text.StartsWith('`') && text.EndsWith('`')) + { + Quotes = ['`', '`']; + Text = text.Trim('`'); + } + else + { + Quotes = null; + Text = text; + } + } + + public string Text { get; set; } + + // ReSharper disable once MemberCanBePrivate.Global + public char[]? Quotes { get; set; } + + public override string ToString() + { + return Quotes is not { Length: 2 } ? Text : $"{Quotes[0]}{Text}{Quotes[1]}"; + } + } + + public class SqlStatementClause(SqlCompoundClause? parent) : SqlCompoundClause(parent) + { + public override string ToString() + { + return $"{base.ToString()};"; + } + } + + public class SqlCompoundClause(SqlCompoundClause? parent) : SqlClause(parent) + { + public List Children { get; set; } = []; + public bool Parenthesis { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + if (Parenthesis) + { + sb.Append('('); + } + var first = true; + foreach (var child in Children) + { + if (!first) + sb.Append(Parenthesis ? ", " : " "); + else + first = false; + + sb.Append(child); + } + if (Parenthesis) + { + sb.Append(')'); + } + return sb.ToString(); + } + } + + public class ClauseBuilder + { + private readonly SqlCompoundClause _rootClause; + private SqlCompoundClause _activeClause; + + private readonly List _allCompoundClauses = []; + + public ClauseBuilder() + { + _rootClause = new SqlStatementClause(null); + _activeClause = _rootClause; + } + + public SqlClause GetRootClause() + { + return _rootClause; + } + + public void AddPart(string part) + { + if (part == "(") + { + // start a new compound clause and add it to the current active clause + var newClause = new SqlCompoundClause(_activeClause) { Parenthesis = true }; + _allCompoundClauses.Add(newClause); + _activeClause.Children.Add(newClause); + // add a compound clause to this clause, and make that the active clause + var firstChildClause = new SqlCompoundClause(newClause); + _allCompoundClauses.Add(firstChildClause); + newClause.Children.Add(firstChildClause); + // switch the active clause to the new clause + _activeClause = firstChildClause; + return; + } + if (part == ")") + { + // end the existing clause by making the active clause the parent (up 2 levels) + if (_activeClause.HasParent()) + { + _activeClause = _activeClause.GetParent()!; + if (_activeClause.HasParent()) + { + _activeClause = _activeClause.GetParent()!; + } + } + return; + } + if (part == ",") + { + // start a new clause and add it to the current active clause + var newClause = new SqlCompoundClause(_activeClause.GetParent()); + _allCompoundClauses.Add(newClause); + _activeClause.GetParent()!.Children.Add(newClause); + _activeClause = newClause; + return; + } + + _activeClause.Children.Add(new SqlWordClause(_activeClause, part)); + } + + public void Complete() + { + foreach ( + var c in _allCompoundClauses /*.Where(x => x.parenthesis)*/ + ) + { + if (c.Children.Count != 1) + continue; + + var child = c.Children[0]; + if (child is not SqlCompoundClause { Parenthesis: false } scc) + continue; + + if (scc.Children.Count != 1) + continue; + + // reduce indentation, reduce nesting + var gscc = scc.Children[0]; + gscc.SetParent(c); + c.Children = [gscc]; + } + } + + public static SqlClause ReduceNesting(SqlClause clause) + { + if (clause is not SqlCompoundClause scc) + return clause; + + var children = new List(); + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (var child in scc.Children) + { + var reducedChild = ReduceNesting(child); + children.Add(reducedChild); + } + scc.Children = children; + + // reduce nesting + if (!scc.Parenthesis && children is [SqlWordClause cswc]) + { + return cswc; + } + + return scc; + } + } + + // Regular expression patterns to match single-line and multi-line comments + // const string singleLineCommentPattern = @"--.*?$"; + // const string multiLineCommentPattern = @"/\*.*?\*/"; + + [GeneratedRegex(@"/\*.*?\*/", RegexOptions.Singleline)] + private static partial Regex MultiLineCommentRegex(); + + [GeneratedRegex("--.*?$", RegexOptions.Multiline)] + private static partial Regex SingleLineCommentRegex(); + + #endregion // ClauseBuilder Classes +} diff --git a/src/DapperMatic/Providers/Sqlite/SqliteTypes.cs b/src/DapperMatic/Providers/Sqlite/SqliteTypes.cs new file mode 100644 index 0000000..e717d1c --- /dev/null +++ b/src/DapperMatic/Providers/Sqlite/SqliteTypes.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DapperMatic.Providers.Sqlite; + +/// +/// See: https://www.sqlite.org/datatype3.html +/// +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static class SqliteTypes +{ + // integers + // If the declared type contains the string "INT" then it is assigned INTEGER affinity + // Deviation from int or integer is only useful to allow backward determination of intended min/max values. + public const string sql_integer = "integer"; + public const string sql_int = "int"; + public const string sql_tinyint = "tinyint"; + public const string sql_smallint = "smallint"; + public const string sql_mediumint = "mediumint"; + public const string sql_bigint = "bigint"; + public const string sql_unsigned_big_int = "unsigned big int"; + + // real + // If the declared type for a column contains any of the strings "REAL", + // "FLOA", or "DOUB" then the column has REAL affinity. + // If no rule applies, the affinity is NUMERIC. + // Using a `precision/scale` is only useful to allow backward determination of intended precision/scale. + public const string sql_real = "real"; + public const string sql_double = "double"; + public const string sql_float = "float"; + public const string sql_numeric = "numeric"; + public const string sql_decimal = "decimal"; + + // bool + // bool is not a valid type in sqlite, and therefore is stored as numeric + public const string sql_bool = "bool"; + public const string sql_boolean = "boolean"; + + // datetime + // datetime is not a valid type in sqlite, and therefore is stored as numeric + public const string sql_date = "date"; + public const string sql_datetime = "datetime"; + public const string sql_timestamp = "timestamp"; + public const string sql_time = "time"; + public const string sql_year = "year"; + + // text + // If the declared type of the column contains any of the strings "CHAR", "CLOB", or "TEXT" then that column has TEXT affinity. + // Notice that the type VARCHAR contains the string "CHAR" and is thus assigned TEXT affinity. + // Using a `length` is only useful to allow backward determination of intended length. + public const string sql_char = "char"; + public const string sql_nchar = "nchar"; + public const string sql_varchar = "varchar"; + public const string sql_nvarchar = "nvarchar"; + public const string sql_varying_character = "varying character"; + public const string sql_native_character = "native character"; + public const string sql_text = "text"; + public const string sql_clob = "clob"; + + // binary + // If the declared type for a column contains the string "BLOB" or if no type is specified then the column has affinity BLOB. + public const string sql_blob = "blob"; +} \ No newline at end of file diff --git a/tests/DapperMatic.Tests/DapperMatic.Tests.csproj b/tests/DapperMatic.Tests/DapperMatic.Tests.csproj index d3ff70f..4e482c0 100644 --- a/tests/DapperMatic.Tests/DapperMatic.Tests.csproj +++ b/tests/DapperMatic.Tests/DapperMatic.Tests.csproj @@ -10,13 +10,15 @@ + + - - - - + + + + diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.CheckConstraints.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.CheckConstraints.cs new file mode 100644 index 0000000..6072e4a --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.CheckConstraints.cs @@ -0,0 +1,116 @@ +using DapperMatic.Models; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_CheckConstraints_Async( + string? schemaName + ) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var supportsCheckConstraints = await db.SupportsCheckConstraintsAsync(); + + var testTableName = "testTableCheckConstraints"; + await db.CreateTableIfNotExistsAsync( + schemaName, + testTableName, + [new DxColumn(schemaName, testTableName, "testColumn", typeof(int))] + ); + + var constraintName = "ck_testTable"; + var exists = await db.DoesCheckConstraintExistAsync( + schemaName, + testTableName, + constraintName + ); + + if (exists) + await db.DropCheckConstraintIfExistsAsync(schemaName, testTableName, constraintName); + + await db.CreateCheckConstraintIfNotExistsAsync( + schemaName, + testTableName, + null, + constraintName, + "testColumn > 0" + ); + + exists = await db.DoesCheckConstraintExistAsync(schemaName, testTableName, constraintName); + Assert.True(supportsCheckConstraints ? exists : !exists); + + var existingConstraint = await db.GetCheckConstraintAsync( + schemaName, + testTableName, + constraintName + ); + if (!supportsCheckConstraints) + Assert.Null(existingConstraint); + else + Assert.Equal( + constraintName, + existingConstraint?.ConstraintName, + StringComparer.OrdinalIgnoreCase + ); + + var checkConstraintNames = await db.GetCheckConstraintNamesAsync(schemaName, testTableName); + if (!supportsCheckConstraints) + Assert.Empty(checkConstraintNames); + else + Assert.Contains(constraintName, checkConstraintNames, StringComparer.OrdinalIgnoreCase); + + var dropped = await db.DropCheckConstraintIfExistsAsync( + schemaName, + testTableName, + constraintName + ); + if (!supportsCheckConstraints) + Assert.False(dropped); + else + { + Assert.True(dropped); + exists = await db.DoesCheckConstraintExistAsync( + schemaName, + testTableName, + constraintName + ); + } + + exists = await db.DoesCheckConstraintExistAsync(schemaName, testTableName, constraintName); + Assert.False(exists); + + await db.DropTableIfExistsAsync(schemaName, testTableName); + + await db.CreateTableIfNotExistsAsync( + schemaName, + testTableName, + [ + new DxColumn(schemaName, testTableName, "testColumn", typeof(int)), + new DxColumn( + schemaName, + testTableName, + "testColumn2", + typeof(int), + checkExpression: "testColumn2 > 0" + ) + ] + ); + + var checkConstraint = await db.GetCheckConstraintOnColumnAsync( + schemaName, + testTableName, + "testColumn2" + ); + if (!supportsCheckConstraints) + Assert.Null(checkConstraint); + else + Assert.NotNull(checkConstraint); + + await db.DropTableIfExistsAsync(schemaName, testTableName); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.Columns.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.Columns.cs new file mode 100644 index 0000000..dffd37d --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.Columns.cs @@ -0,0 +1,284 @@ +using DapperMatic.Models; +using DapperMatic.Providers; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_set_common_default_expressions_on_Columns_Async( + string? schemaName + ) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + const string tableName = "testTableWithExpressions"; + const string columnName1 = "testColumnWithDefaultDate"; + const string columnName2 = "testColumnWithDefaultDateSetAfterCreate"; + const string columnName3 = "testColumnWithDefaultUuid"; + const string columnName4 = "testColumnWithDefaultShort"; + const string columnName5 = "testColumnWithDefaultBool"; + + string? defaultDateTimeSql = null; + string? defaultGuidSql = null; + var dbType = db.GetDbProviderType(); + + var version = await db.GetDatabaseVersionAsync(); + + switch (dbType) + { + case DbProviderType.SqlServer: + defaultDateTimeSql = "GETUTCDATE()"; + defaultGuidSql = "NEWID()"; + break; + case DbProviderType.Sqlite: + defaultDateTimeSql = "CURRENT_TIMESTAMP"; + //this could be supported IF the sqlite UUID extension was loaded and enabled + //defaultGuidSql = "uuid_blob(uuid())"; + defaultGuidSql = null; + break; + case DbProviderType.PostgreSql: + defaultDateTimeSql = "CURRENT_TIMESTAMP"; + defaultGuidSql = "uuid_generate_v4()"; + break; + case DbProviderType.MySql: + defaultDateTimeSql = version > new Version(5, 6, 5) ? "CURRENT_TIMESTAMP" : null; + // only supported after 8.0.13 + // LEADS TO THIS ERROR: + // Statement is unsafe because it uses a system function that may return a different value on the replication slave. + // MySQL isn't a good database for auto-generating GUIDs. Don't do it! + // defaultGuidSql = + // version > new Version(8, 0, 13) && version < new Version(10, 0, 0) + // ? "(UUID())" + // : null; + defaultGuidSql = null; + break; + } + + // Create table with a column with an expression + var tableCreated = await db.CreateTableIfNotExistsAsync( + schemaName, + tableName, + [ + new DxColumn( + schemaName, + tableName, + "id", + typeof(int), + isPrimaryKey: true, + isAutoIncrement: true + ), + new DxColumn( + schemaName, + tableName, + columnName1, + typeof(DateTime), + defaultExpression: defaultDateTimeSql + ) + ] + ); + Assert.True(tableCreated); + + // Add a column with a default expression after the table is created + var columnCreated = await db.CreateColumnIfNotExistsAsync( + new DxColumn( + schemaName, + tableName, + columnName2, + typeof(DateTime), + defaultExpression: defaultDateTimeSql + ) + ); + Assert.True(columnCreated); + + if (defaultGuidSql != null) + { + // Add a column with a default expression after the table is created + columnCreated = await db.CreateColumnIfNotExistsAsync( + new DxColumn( + schemaName, + tableName, + columnName3, + typeof(Guid), + defaultExpression: defaultGuidSql + ) + ); + Assert.True(columnCreated); + } + + // Add a column with a default expression after the table is created + columnCreated = await db.CreateColumnIfNotExistsAsync( + new DxColumn(schemaName, tableName, columnName4, typeof(short), defaultExpression: "4") + ); + Assert.True(columnCreated); + + if (db.GetDbProviderType() == DbProviderType.PostgreSql) + { + columnCreated = await db.CreateColumnIfNotExistsAsync( + new DxColumn( + schemaName, + tableName, + columnName5, + typeof(bool), + defaultExpression: "true" + ) + ); + Assert.True(columnCreated); + } + else + { + // other databases take an integer + columnCreated = await db.CreateColumnIfNotExistsAsync( + new DxColumn( + schemaName, + tableName, + columnName5, + typeof(bool), + defaultExpression: "1" + ) + ); + Assert.True(columnCreated); + } + + // Now check to make sure the default expressions are set + var table = await db.GetTableAsync(schemaName, tableName); + var columns = await db.GetColumnsAsync(schemaName, tableName); + var column1 = columns.SingleOrDefault(c => + c.ColumnName.Equals(columnName1, StringComparison.OrdinalIgnoreCase) + ); + var column2 = columns.SingleOrDefault(c => + c.ColumnName.Equals(columnName2, StringComparison.OrdinalIgnoreCase) + ); + var column4 = columns.SingleOrDefault(c => + c.ColumnName.Equals(columnName4, StringComparison.OrdinalIgnoreCase) + ); + var column5 = columns.SingleOrDefault(c => + c.ColumnName.Equals(columnName5, StringComparison.OrdinalIgnoreCase) + ); + + Assert.NotNull(column1); + Assert.NotNull(column1.DefaultExpression); + Assert.NotEmpty(column1.DefaultExpression); + + Assert.NotNull(column2); + Assert.NotNull(column2.DefaultExpression); + Assert.NotEmpty(column2.DefaultExpression); + + Assert.NotNull(column4); + Assert.NotNull(column4.DefaultExpression); + Assert.NotEmpty(column4.DefaultExpression); + + Assert.NotNull(column5); + Assert.NotNull(column5.DefaultExpression); + Assert.NotEmpty(column5.DefaultExpression); + + if (defaultGuidSql != null) + { + var column3 = columns.SingleOrDefault(c => + c.ColumnName.Equals(columnName3, StringComparison.OrdinalIgnoreCase) + ); + Assert.NotNull(column3); + Assert.NotNull(column3.DefaultExpression); + Assert.NotEmpty(column3.DefaultExpression); + } + + // Now try to remove the default expressions (first using the column name, then using the constraint name) + Assert.True( + await db.DropDefaultConstraintOnColumnIfExistsAsync(schemaName, tableName, columnName1) + ); + var constraintName = table + ?.DefaultConstraints.First(dc => + dc.ColumnName.Equals(column2.ColumnName, StringComparison.OrdinalIgnoreCase) + ) + .ConstraintName; + Assert.NotNull(constraintName); + Assert.NotEmpty(constraintName); + Assert.True( + await db.DropDefaultConstraintIfExistsAsync(schemaName, tableName, constraintName) + ); + + // TODO: timestamp columns can't have default values dropped in MariaDB, WEIRD! + // might have to change syntax to use ALTER TABLE table_name MODIFY COLUMN column_name TIMESTAMP NULL; + if ( + db.GetDbProviderType() != DbProviderType.MySql + || (version.Major != 10 && version.Major != 11) + ) + { + var table2 = await db.GetTableAsync(schemaName, tableName); + columns = await db.GetColumnsAsync(schemaName, tableName); + column1 = columns.SingleOrDefault(c => + c.ColumnName.Equals(columnName1, StringComparison.OrdinalIgnoreCase) + ); + column2 = columns.SingleOrDefault(c => + c.ColumnName.Equals(columnName2, StringComparison.OrdinalIgnoreCase) + ); + + Assert.Equal(table!.DefaultConstraints.Count - 2, table2!.DefaultConstraints.Count); + + Assert.NotNull(column1); + Assert.Null(column1.DefaultExpression); + + Assert.NotNull(column2); + Assert.Null(column2.DefaultExpression); + } + + await db.DropTableIfExistsAsync(schemaName, tableName); + } + + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_Columns_Async(string? schemaName) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var dbType = db.GetDbProviderType(); + var dbTypeMap = db.GetProviderTypeMap(); + + const string tableName = "testWithTypedColumns"; + const string columnName = "testColumn"; + + await db.CreateTableIfNotExistsAsync( + schemaName, + tableName, + [ + new DxColumn( + schemaName, + tableName, + "id", + typeof(int), + isPrimaryKey: true, + isAutoIncrement: true + ) + ] + ); + + // try adding a columnName of all the supported types + var i = 0; + foreach (var type in GetSupportedTypes(dbTypeMap)) + { + // create a column with the supported type + var uniqueColumnName = $"{columnName}_{type.Name.ToAlpha()}_{i++}"; + var column = new DxColumn( + schemaName, + tableName, + uniqueColumnName, + type, + isNullable: true + ); + var columnCreated = await db.CreateColumnIfNotExistsAsync(column); + + if (!columnCreated) + { + columnCreated = await db.CreateColumnIfNotExistsAsync(column); + Assert.True(columnCreated); + } + } + + await db.DropTableIfExistsAsync(schemaName, tableName); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.DefaultConstraints.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.DefaultConstraints.cs new file mode 100644 index 0000000..1ebb890 --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.DefaultConstraints.cs @@ -0,0 +1,95 @@ +using DapperMatic.Models; +using DapperMatic.Providers; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_DefaultConstraints_Async( + string? schemaName + ) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var testTableName = "testTableDefaultConstraints"; + var testColumnName = "testColumn"; + await db.CreateTableIfNotExistsAsync( + schemaName, + testTableName, + [new DxColumn(schemaName, testTableName, testColumnName, typeof(int))] + ); + + // in MySQL, default constraints are not named, so this MUST use the ProviderUtils method which is what DapperMatic uses internally + var constraintName = DbProviderUtils.GenerateDefaultConstraintName( + testTableName, + testColumnName + ); + var exists = await db.DoesDefaultConstraintExistAsync( + schemaName, + testTableName, + constraintName + ); + if (exists) + await db.DropDefaultConstraintIfExistsAsync(schemaName, testTableName, constraintName); + + await db.CreateDefaultConstraintIfNotExistsAsync( + schemaName, + testTableName, + testColumnName, + constraintName, + "0" + ); + var existingConstraint = await db.GetDefaultConstraintAsync( + schemaName, + testTableName, + constraintName + ); + Assert.Equal(constraintName, existingConstraint?.ConstraintName, true); + + var defaultConstraintNames = await db.GetDefaultConstraintNamesAsync( + schemaName, + testTableName + ); + Assert.Contains(constraintName, defaultConstraintNames, StringComparer.OrdinalIgnoreCase); + + await db.DropDefaultConstraintIfExistsAsync(schemaName, testTableName, constraintName); + exists = await db.DoesDefaultConstraintExistAsync( + schemaName, + testTableName, + constraintName + ); + Assert.False(exists); + + await db.DropTableIfExistsAsync(schemaName, testTableName); + + await db.CreateTableIfNotExistsAsync( + schemaName, + testTableName, + [ + new DxColumn(schemaName, testTableName, testColumnName, typeof(int)), + new DxColumn( + schemaName, + testTableName, + "testColumn2", + typeof(int), + defaultExpression: "0" + ) + ] + ); + var defaultConstraint = await db.GetDefaultConstraintOnColumnAsync( + schemaName, + testTableName, + "testColumn2" + ); + Assert.NotNull(defaultConstraint); + + var tableDeleted = await db.DropTableIfExistsAsync(schemaName, testTableName); + Assert.True(tableDeleted); + + await InitFreshSchemaAsync(db, schemaName); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.ForeignKeyConstraints.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.ForeignKeyConstraints.cs new file mode 100644 index 0000000..6e18917 --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.ForeignKeyConstraints.cs @@ -0,0 +1,123 @@ +using DapperMatic.Models; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_ForeignKeyConstraints_Async( + string? schemaName + ) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + const string tableName = "testWithFk"; + const string columnName = "testFkColumn"; + const string foreignKeyName = "testFk"; + const string refTableName = "testRefPk"; + const string refTableColumn = "id"; + + await db.CreateTableIfNotExistsAsync( + schemaName, + tableName, + [ + new DxColumn( + schemaName, + tableName, + columnName, + typeof(int), + defaultExpression: "1", + isNullable: false + ) + ] + ); + await db.CreateTableIfNotExistsAsync( + schemaName, + refTableName, + [ + new DxColumn( + schemaName, + refTableName, + refTableColumn, + typeof(int), + defaultExpression: "1", + isPrimaryKey: true, + isNullable: false + ) + ] + ); + + Output.WriteLine("Foreign Key Exists: {0}.{1}", tableName, foreignKeyName); + var exists = await db.DoesForeignKeyConstraintExistAsync( + schemaName, + tableName, + foreignKeyName + ); + Assert.False(exists); + + Output.WriteLine("Creating foreign key: {0}.{1}", tableName, foreignKeyName); + var created = await db.CreateForeignKeyConstraintIfNotExistsAsync( + schemaName, + tableName, + foreignKeyName, + [new DxOrderedColumn(columnName)], + refTableName, + [new DxOrderedColumn("id")], + onDelete: DxForeignKeyAction.Cascade + ); + Assert.True(created); + + Output.WriteLine("Foreign Key Exists: {0}.{1}", tableName, foreignKeyName); + exists = await db.DoesForeignKeyConstraintExistAsync(schemaName, tableName, foreignKeyName); + Assert.True(exists); + exists = await db.DoesForeignKeyConstraintExistOnColumnAsync( + schemaName, + tableName, + columnName + ); + Assert.True(exists); + + Output.WriteLine("Get Foreign Key Names: {0}", tableName); + var fkNames = await db.GetForeignKeyConstraintNamesAsync(schemaName, tableName); + Assert.Contains( + fkNames, + fk => fk.Equals(foreignKeyName, StringComparison.OrdinalIgnoreCase) + ); + + Output.WriteLine("Get Foreign Keys: {0}", tableName); + var fks = await db.GetForeignKeyConstraintsAsync(schemaName, tableName); + Assert.Contains( + fks, + fk => + fk.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) + && fk.SourceColumns.Any(sc => + sc.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) + ) + && fk.ConstraintName.Equals(foreignKeyName, StringComparison.OrdinalIgnoreCase) + && fk.ReferencedTableName.Equals(refTableName, StringComparison.OrdinalIgnoreCase) + && fk.ReferencedColumns.Any(sc => + sc.ColumnName.Equals("id", StringComparison.OrdinalIgnoreCase) + ) + && fk.OnDelete.Equals(DxForeignKeyAction.Cascade) + ); + + Output.WriteLine("Dropping foreign key: {0}", foreignKeyName); + await db.DropForeignKeyConstraintIfExistsAsync(schemaName, tableName, foreignKeyName); + + Output.WriteLine("Foreign Key Exists: {0}", foreignKeyName); + exists = await db.DoesForeignKeyConstraintExistAsync(schemaName, tableName, foreignKeyName); + Assert.False(exists); + exists = await db.DoesForeignKeyConstraintExistOnColumnAsync( + schemaName, + tableName, + columnName + ); + Assert.False(exists); + + await db.DropTableIfExistsAsync(schemaName, tableName); + await db.DropTableIfExistsAsync(schemaName, refTableName); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.Indexes.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.Indexes.cs new file mode 100644 index 0000000..2ebf896 --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.Indexes.cs @@ -0,0 +1,178 @@ +using DapperMatic.Models; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_Indexes_Async(string? schemaName) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var version = await db.GetDatabaseVersionAsync(); + Assert.True(version.Major > 0); + + var supportsDescendingColumnSorts = true; + var dbType = db.GetDbProviderType(); + if (dbType.HasFlag(DbProviderType.MySql)) + { + if (version.Major == 5) + { + supportsDescendingColumnSorts = false; + } + } + try + { + const string tableName = "testWithIndex"; + const string columnName = "testColumn"; + const string indexName = "testIndex"; + + var columns = new List + { + new( + schemaName, + tableName, + columnName, + typeof(int), + defaultExpression: "1", + isNullable: false + ) + }; + for (var i = 0; i < 10; i++) + { + columns.Add( + new DxColumn( + schemaName, + tableName, + columnName + "_" + i, + typeof(int), + defaultExpression: i.ToString(), + isNullable: false + ) + ); + } + + await db.DropTableIfExistsAsync(schemaName, tableName); + await db.CreateTableIfNotExistsAsync(schemaName, tableName, columns: [.. columns]); + + Output.WriteLine("Index Exists: {0}.{1}", tableName, indexName); + var exists = await db.DoesIndexExistAsync(schemaName, tableName, indexName); + Assert.False(exists); + + Output.WriteLine("Creating unique index: {0}.{1}", tableName, indexName); + await db.CreateIndexIfNotExistsAsync( + schemaName, + tableName, + indexName, + [new DxOrderedColumn(columnName)], + isUnique: true + ); + + Output.WriteLine( + "Creating multiple column unique index: {0}.{1}_multi", + tableName, + indexName + "_multi" + ); + await db.CreateIndexIfNotExistsAsync( + schemaName, + tableName, + indexName + "_multi", + [ + new DxOrderedColumn(columnName + "_1", DxColumnOrder.Descending), + new DxOrderedColumn(columnName + "_2") + ], + isUnique: true + ); + + Output.WriteLine( + "Creating multiple column non unique index: {0}.{1}_multi2", + tableName, + indexName + ); + await db.CreateIndexIfNotExistsAsync( + schemaName, + tableName, + indexName + "_multi2", + [ + new DxOrderedColumn(columnName + "_3"), + new DxOrderedColumn(columnName + "_4", DxColumnOrder.Descending) + ] + ); + + Output.WriteLine("Index Exists: {0}.{1}", tableName, indexName); + exists = await db.DoesIndexExistAsync(schemaName, tableName, indexName); + Assert.True(exists); + exists = await db.DoesIndexExistAsync(schemaName, tableName, indexName + "_multi"); + Assert.True(exists); + exists = await db.DoesIndexExistAsync(schemaName, tableName, indexName + "_multi2"); + Assert.True(exists); + + var indexNames = await db.GetIndexNamesAsync(schemaName, tableName); + Assert.Contains( + indexNames, + i => i.Equals(indexName, StringComparison.OrdinalIgnoreCase) + ); + Assert.Contains( + indexNames, + i => i.Equals(indexName + "_multi", StringComparison.OrdinalIgnoreCase) + ); + Assert.Contains( + indexNames, + i => i.Equals(indexName + "_multi2", StringComparison.OrdinalIgnoreCase) + ); + + var indexes = await db.GetIndexesAsync(schemaName, tableName); + Assert.True(indexes.Count() >= 3); + var idxMulti1 = indexes.SingleOrDefault(i => + i.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) + && i.IndexName.Equals(indexName + "_multi", StringComparison.OrdinalIgnoreCase) + ); + var idxMulti2 = indexes.SingleOrDefault(i => + i.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) + && i.IndexName.Equals(indexName + "_multi2", StringComparison.OrdinalIgnoreCase) + ); + Assert.NotNull(idxMulti1); + Assert.NotNull(idxMulti2); + Assert.NotNull(idxMulti1); + Assert.NotNull(idxMulti2); + Assert.True(idxMulti1.IsUnique); + Assert.Equal(2, idxMulti1.Columns.Length); + if (supportsDescendingColumnSorts) + { + Assert.Equal(DxColumnOrder.Descending, idxMulti1.Columns[0].Order); + Assert.Equal(DxColumnOrder.Ascending, idxMulti1.Columns[1].Order); + } + Assert.False(idxMulti2.IsUnique); + Assert.True(idxMulti2.Columns.Length == 2); + Assert.Equal(DxColumnOrder.Ascending, idxMulti2.Columns[0].Order); + if (supportsDescendingColumnSorts) + { + Assert.Equal(DxColumnOrder.Descending, idxMulti2.Columns[1].Order); + } + + var indexesOnColumn = await db.GetIndexesOnColumnAsync( + schemaName, + tableName, + columnName + ); + Assert.NotEmpty(indexesOnColumn); + + Output.WriteLine("Dropping indexName: {0}.{1}", tableName, indexName); + await db.DropIndexIfExistsAsync(schemaName, tableName, indexName); + + Output.WriteLine("Index Exists: {0}.{1}", tableName, indexName); + exists = await db.DoesIndexExistAsync(schemaName, tableName, indexName); + Assert.False(exists); + + await db.DropTableIfExistsAsync(schemaName, tableName); + } + finally + { + var sql = db.GetLastSql(); + Output.WriteLine("Last sql: {0}", sql); + } + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.PrimaryKeyConstraints.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.PrimaryKeyConstraints.cs new file mode 100644 index 0000000..307023b --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.PrimaryKeyConstraints.cs @@ -0,0 +1,55 @@ +using DapperMatic.Models; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_PrimaryKeyConstraints_Async( + string? schemaName + ) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + const string tableName = "testWithPk"; + const string columnName = "testColumn"; + const string primaryKeyName = "testPk"; + + await db.CreateTableIfNotExistsAsync( + schemaName, + tableName, + [ + new DxColumn( + schemaName, + tableName, + columnName, + typeof(int), + defaultExpression: "1", + isNullable: false + ) + ] + ); + Output.WriteLine("Primary Key Exists: {0}.{1}", tableName, primaryKeyName); + var exists = await db.DoesPrimaryKeyConstraintExistAsync(schemaName, tableName); + Assert.False(exists); + Output.WriteLine("Creating primary key: {0}.{1}", tableName, primaryKeyName); + await db.CreatePrimaryKeyConstraintIfNotExistsAsync( + schemaName, + tableName, + primaryKeyName, + [new DxOrderedColumn(columnName)] + ); + Output.WriteLine("Primary Key Exists: {0}.{1}", tableName, primaryKeyName); + exists = await db.DoesPrimaryKeyConstraintExistAsync(schemaName, tableName); + Assert.True(exists); + Output.WriteLine("Dropping primary key: {0}.{1}", tableName, primaryKeyName); + await db.DropPrimaryKeyConstraintIfExistsAsync(schemaName, tableName); + Output.WriteLine("Primary Key Exists: {0}.{1}", tableName, primaryKeyName); + exists = await db.DoesPrimaryKeyConstraintExistAsync(schemaName, tableName); + Assert.False(exists); + await db.DropTableIfExistsAsync(schemaName, tableName); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.Schemas.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.Schemas.cs new file mode 100644 index 0000000..cf66d4c --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.Schemas.cs @@ -0,0 +1,41 @@ +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_Schemas_Async(string schemaName) + { + using var db = await OpenConnectionAsync(); + + var supportsSchemas = db.SupportsSchemas(); + if (!supportsSchemas) + { + Output.WriteLine("This test requires a database that supports schemas."); + return; + } + + var exists = await db.DoesSchemaExistAsync(schemaName); + if (exists) + await db.DropSchemaIfExistsAsync(schemaName); + + exists = await db.DoesSchemaExistAsync(schemaName); + Assert.False(exists); + + Output.WriteLine("Creating schemaName: {0}", schemaName); + var created = await db.CreateSchemaIfNotExistsAsync(schemaName); + Assert.True(created); + exists = await db.DoesSchemaExistAsync(schemaName); + Assert.True(exists); + + var schemas = await db.GetSchemaNamesAsync(); + Assert.Contains(schemaName, schemas, StringComparer.OrdinalIgnoreCase); + + Output.WriteLine("Dropping schemaName: {0}", schemaName); + var dropped = await db.DropSchemaIfExistsAsync(schemaName); + Assert.True(dropped); + + exists = await db.DoesSchemaExistAsync(schemaName); + Assert.False(exists); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.TableFactory.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.TableFactory.cs new file mode 100644 index 0000000..97afec5 --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.TableFactory.cs @@ -0,0 +1,179 @@ +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Dapper; +using DapperMatic.DataAnnotations; +using DapperMatic.Models; +using DapperMatic.Providers; +using Microsoft.Data.SqlClient.DataClassification; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(typeof(TestDao1))] + [InlineData(typeof(TestDao2))] + [InlineData(typeof(TestDao3))] + [InlineData(typeof(TestTable4))] + protected virtual async Task Can_create_tables_from_model_classes_async(Type type) + { + var tableDef = DxTableFactory.GetTable(type); + + using var db = await OpenConnectionAsync(); + + if (!string.IsNullOrWhiteSpace(tableDef.SchemaName)) + { + await db.CreateSchemaIfNotExistsAsync(tableDef.SchemaName); + } + + await db.CreateTableIfNotExistsAsync(tableDef); + + var tableExists = await db.DoesTableExistAsync(tableDef.SchemaName, tableDef.TableName); + Assert.True(tableExists); + + var dropped = await db.DropTableIfExistsAsync(tableDef.SchemaName, tableDef.TableName); + Assert.True(dropped); + } +} + +[Table("TestTable1")] +public class TestDao1 +{ + [Key] + public Guid Id { get; set; } +} + +[Table("TestTable2", Schema = "my_app")] +public class TestDao2 +{ + [Key] + public Guid Id { get; set; } +} + +[DxTable("TestTable3")] +public class TestDao3 +{ + [DxPrimaryKeyConstraint] + public Guid Id { get; set; } +} + +[DxPrimaryKeyConstraint([nameof(TestTable4.Id)])] +public class TestTable4 +{ + public Guid Id { get; set; } + + // create column of all supported types + public string StringColumn { get; set; } = null!; + public int IntColumn { get; set; } + public long LongColumn { get; set; } + public short ShortColumn { get; set; } + public byte ByteColumn { get; set; } + public decimal DecimalColumn { get; set; } + public double DoubleColumn { get; set; } + public float FloatColumn { get; set; } + public bool BoolColumn { get; set; } + public DateTime DateTimeColumn { get; set; } + public DateTimeOffset DateTimeOffsetColumn { get; set; } + public TimeSpan TimeSpanColumn { get; set; } + public byte[] ByteArrayColumn { get; set; } = null!; + public Guid GuidColumn { get; set; } + public char CharColumn { get; set; } + public char[] CharArrayColumn { get; set; } = null!; + public object ObjectColumn { get; set; } = null!; + + // create column of all supported nullable types + public string? NullableStringColumn { get; set; } + public int? NullableIntColumn { get; set; } + public long? NullableLongColumn { get; set; } + public short? NullableShortColumn { get; set; } + public byte? NullableByteColumn { get; set; } + public decimal? NullableDecimalColumn { get; set; } + public double? NullableDoubleColumn { get; set; } + public float? NullableFloatColumn { get; set; } + public bool? NullableBoolColumn { get; set; } + public DateTime? NullableDateTimeColumn { get; set; } + public DateTimeOffset? NullableDateTimeOffsetColumn { get; set; } + public TimeSpan? NullableTimeSpanColumn { get; set; } + public byte[]? NullableByteArrayColumn { get; set; } + public Guid? NullableGuidColumn { get; set; } + public char? NullableCharColumn { get; set; } + public char[]? NullableCharArrayColumn { get; set; } + public object? NullableObjectColumn { get; set; } + + // create columns of all enumerable types + public IDictionary IDictionaryColumn { get; set; } = null!; + public IDictionary? NullableIDictionaryColumn { get; set; } + public Dictionary DictionaryColumn { get; set; } = null!; + public Dictionary? NullableDictionaryColumn { get; set; } + public IDictionary IObjectDictionaryColumn { get; set; } = null!; + public IDictionary? NullableIObjectDictionaryColumn { get; set; } + public Dictionary ObjectDictionaryColumn { get; set; } = null!; + public Dictionary? NullableObjectDictionaryColumn { get; set; } + public IList IListColumn { get; set; } = null!; + public IList? NullableIListColumn { get; set; } + public List ListColumn { get; set; } = null!; + public List? NullableListColumn { get; set; } + public ICollection ICollectionColumn { get; set; } = null!; + public ICollection? NullableICollectionColumn { get; set; } + public Collection CollectionColumn { get; set; } = null!; + public Collection? NullableCollectionColumn { get; set; } + public IEnumerable IEnumerableColumn { get; set; } = null!; + public IEnumerable? NullableIEnumerableColumn { get; set; } + + // create columns of arrays + public string[] StringArrayColumn { get; set; } = null!; + public string[]? NullableStringArrayColumn { get; set; } + public int[] IntArrayColumn { get; set; } = null!; + public int[]? NullableIntArrayColumn { get; set; } + public long[] LongArrayColumn { get; set; } = null!; + public long[]? NullableLongArrayColumn { get; set; } + public Guid[] GuidArrayColumn { get; set; } = null!; + public Guid[]? NullableGuidArrayColumn { get; set; } + + // create columns of enums, structs and classes + public TestEnum EnumColumn { get; set; } + public TestEnum? NullableEnumColumn { get; set; } + public TestStruct StructColumn { get; set; } + public TestStruct? NullableStructColumn { get; set; } + public TestClass ClassColumn { get; set; } = null!; + public TestClass? NullableClassColumn { get; set; } + public TestInterface InterfaceColumn { get; set; } = null!; + public TestInterface? NullableInterfaceColumn { get; set; } + public TestAbstractClass AbstractClassColumn { get; set; } = null!; + public TestAbstractClass? NullableAbstractClassColumn { get; set; } + public TestConcreteClass ConcreteClass { get; set; } = null!; + public TestConcreteClass? NullableConcreteClass { get; set; } +} + +public enum TestEnum +{ + Value1, + Value2, + Value3 +} + +public struct TestStruct +{ + public int Value { get; set; } +} + +public class TestClass +{ + public int Value { get; set; } +} + +public interface TestInterface +{ + int Value { get; set; } +} + +public abstract class TestAbstractClass +{ + public int Value { get; set; } +} + +public class TestConcreteClass : TestAbstractClass +{ + public int Value2 { get; set; } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.Tables.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.Tables.cs new file mode 100644 index 0000000..bfd784c --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.Tables.cs @@ -0,0 +1,116 @@ +using Dapper; +using DapperMatic.Models; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_Tables_Async(string? schemaName) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var supportsSchemas = db.SupportsSchemas(); + + var tableName = "testTable"; + + var exists = await db.DoesTableExistAsync(schemaName, tableName); + if (exists) + await db.DropTableIfExistsAsync(schemaName, tableName); + + exists = await db.DoesTableExistAsync(schemaName, tableName); + Assert.False(exists); + + var nonExistentTable = await db.GetTableAsync(schemaName, tableName); + Assert.Null(nonExistentTable); + + var table = new DxTable( + schemaName, + tableName, + [ + new DxColumn( + schemaName, + tableName, + "id", + typeof(int), + isPrimaryKey: true, + isAutoIncrement: true + ), + new DxColumn(schemaName, tableName, "name", typeof(string), isUnique: true) + ] + ); + var created = await db.CreateTableIfNotExistsAsync(table); + Assert.True(created); + + var createdAgain = await db.CreateTableIfNotExistsAsync(table); + Assert.False(createdAgain); + + exists = await db.DoesTableExistAsync(schemaName, tableName); + Assert.True(exists); + + var tableNames = await db.GetTableNamesAsync(schemaName); + Assert.NotEmpty(tableNames); + Assert.Contains(tableName, tableNames, StringComparer.OrdinalIgnoreCase); + + var existingTable = await db.GetTableAsync(schemaName, tableName); + Assert.NotNull(existingTable); + + if (supportsSchemas) + { + Assert.NotNull(existingTable.SchemaName); + Assert.NotEmpty(existingTable.SchemaName); + } + Assert.Equal(tableName, existingTable.TableName, true); + Assert.Equal(2, existingTable.Columns.Count); + + // rename the table + var newName = "newTestTable"; + var renamed = await db.RenameTableIfExistsAsync(schemaName, tableName, newName); + Assert.True(renamed); + + exists = await db.DoesTableExistAsync(schemaName, tableName); + Assert.False(exists); + + exists = await db.DoesTableExistAsync(schemaName, newName); + Assert.True(exists); + + existingTable = await db.GetTableAsync(schemaName, newName); + Assert.NotNull(existingTable); + Assert.Equal(newName, existingTable.TableName, true); + + tableNames = await db.GetTableNamesAsync(schemaName); + Assert.Contains(newName, tableNames, StringComparer.OrdinalIgnoreCase); + + var schemaQualifiedTableName = db.GetSchemaQualifiedTableName(schemaName, newName); + + // add a new row + var newRow = new { id = 0, name = "Test" }; + await db.ExecuteAsync( + @$"INSERT INTO {schemaQualifiedTableName} (name) VALUES (@name)", + newRow + ); + + // get all rows + var rows = await db.QueryAsync( + @$"SELECT * FROM {schemaQualifiedTableName}", + new { } + ); + Assert.Single(rows); + + // truncate the table + await db.TruncateTableIfExistsAsync(schemaName, newName); + rows = await db.QueryAsync(@$"SELECT * FROM {schemaQualifiedTableName}", new { }); + Assert.Empty(rows); + + // drop the table + await db.DropTableIfExistsAsync(schemaName, newName); + + exists = await db.DoesTableExistAsync(schemaName, newName); + Assert.False(exists); + + Output.WriteLine($"Table names: {0}", string.Join(", ", tableNames)); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.Types.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.Types.cs new file mode 100644 index 0000000..5735d1f --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.Types.cs @@ -0,0 +1,110 @@ +using DapperMatic.Providers; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + private static Type[] GetSupportedTypes(IDbProviderTypeMap dbTypeMap) + { + // Type[] supportedTypes = dbTypeMap + // .GetProviderSqlTypes() + // .SelectMany(t => + // { + // var dotnetTypes = new List(); + // if ( + // dbTypeMap.TryGetRecommendedDotnetTypeMatchingSqlType( + // t.SqlType, + // out var dotnetTypeInfo + // ) + // && dotnetTypeInfo != null + // ) + // { + // dotnetTypes.AddRange(dotnetTypeInfo.Value.allSupportedTypes); + // } + // return dotnetTypes; + // }) + // .Distinct() + // .ToArray(); + + Type[] typesToSupport = + [ + typeof(bool), + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(int), + typeof(long), + typeof(float), + typeof(double), + typeof(decimal), + typeof(char), + typeof(string), + typeof(char[]), + typeof(ReadOnlyMemory[]), + typeof(Stream), + typeof(Guid), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(DateOnly), + typeof(TimeOnly), + typeof(byte[]), + typeof(object), + // generic definitions + typeof(IDictionary<,>), + typeof(Dictionary<,>), + typeof(IEnumerable<>), + typeof(ICollection<>), + typeof(List<>), + typeof(object[]), + // generics + typeof(IDictionary), + typeof(Dictionary), + typeof(Dictionary), + typeof(Dictionary), + typeof(Dictionary), + typeof(Dictionary), + typeof(Dictionary), + typeof(IEnumerable), + typeof(IEnumerable), + typeof(ICollection), + typeof(ICollection), + typeof(List), + typeof(List), + typeof(List), + typeof(string[]), + typeof(Guid[]), + typeof(int[]), + typeof(long[]), + typeof(double[]), + typeof(decimal[]), + typeof(TimeSpan[]), + // custom classes + typeof(TestClassDao) + ]; + + return typesToSupport; + } + + public class TestClassDao + { + public Guid Id { get; set; } + } + + [Fact] + protected virtual async Task Provider_type_map_supports_all_desired_dotnet_types() + { + using var db = await OpenConnectionAsync(); + var dbTypeMap = db.GetProviderTypeMap(); + foreach (var desiredType in GetSupportedTypes(dbTypeMap)) + { + var exists = dbTypeMap.TryGetProviderSqlTypeMatchingDotnetType( + new DbProviderDotnetTypeDescriptor(desiredType), + out var sqlType + ); + + Assert.True(exists, "Could not find a SQL type for " + desiredType.FullName); + Assert.NotNull(sqlType); + } + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.UniqueConstraints.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.UniqueConstraints.cs new file mode 100644 index 0000000..dd7fa71 --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.UniqueConstraints.cs @@ -0,0 +1,209 @@ +using DapperMatic.Models; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_UniqueConstraints_Async( + string? schemaName + ) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var tableName = "testWithUc" + DateTime.Now.Ticks; + var columnName = "testColumn"; + var columnName2 = "testColumn2"; + var uniqueConstraintName = "testUc"; + var uniqueConstraintName2 = "testUc2"; + + await db.CreateTableIfNotExistsAsync( + schemaName, + tableName, + [ + new DxColumn( + schemaName, + tableName, + columnName, + typeof(int), + defaultExpression: "1", + isNullable: false + ), + new DxColumn( + schemaName, + tableName, + columnName2, + typeof(int), + defaultExpression: "1", + isNullable: false + ) + ], + uniqueConstraints: + [ + new DxUniqueConstraint( + schemaName, + tableName, + uniqueConstraintName2, + [new DxOrderedColumn(columnName2)] + ) + ] + ); + + Output.WriteLine("Unique Constraint Exists: {0}.{1}", tableName, uniqueConstraintName); + var exists = await db.DoesUniqueConstraintExistAsync( + schemaName, + tableName, + uniqueConstraintName + ); + Assert.False(exists); + + Output.WriteLine("Unique Constraint2 Exists: {0}.{1}", tableName, uniqueConstraintName2); + exists = await db.DoesUniqueConstraintExistAsync( + schemaName, + tableName, + uniqueConstraintName2 + ); + Assert.True(exists); + exists = await db.DoesUniqueConstraintExistOnColumnAsync( + schemaName, + tableName, + columnName2 + ); + Assert.True(exists); + + Output.WriteLine("Creating unique constraint: {0}.{1}", tableName, uniqueConstraintName); + await db.CreateUniqueConstraintIfNotExistsAsync( + schemaName, + tableName, + uniqueConstraintName, + [new DxOrderedColumn(columnName)] + ); + + // make sure the new constraint is there + Output.WriteLine("Unique Constraint Exists: {0}.{1}", tableName, uniqueConstraintName); + exists = await db.DoesUniqueConstraintExistAsync( + schemaName, + tableName, + uniqueConstraintName + ); + Assert.True(exists); + exists = await db.DoesUniqueConstraintExistOnColumnAsync(schemaName, tableName, columnName); + Assert.True(exists); + + // make sure the original constraint is still there + Output.WriteLine("Unique Constraint Exists: {0}.{1}", tableName, uniqueConstraintName2); + exists = await db.DoesUniqueConstraintExistAsync( + schemaName, + tableName, + uniqueConstraintName2 + ); + Assert.True(exists); + exists = await db.DoesUniqueConstraintExistOnColumnAsync( + schemaName, + tableName, + columnName2 + ); + Assert.True(exists); + + Output.WriteLine("Get Unique Constraint Names: {0}", tableName); + var uniqueConstraintNames = await db.GetUniqueConstraintNamesAsync(schemaName, tableName); + Assert.Contains( + uniqueConstraintName2, + uniqueConstraintNames, + StringComparer.OrdinalIgnoreCase + ); + Assert.Contains( + uniqueConstraintName, + uniqueConstraintNames, + StringComparer.OrdinalIgnoreCase + ); + + var uniqueConstraints = await db.GetUniqueConstraintsAsync(schemaName, tableName); + Assert.Contains( + uniqueConstraints, + uc => + uc.ConstraintName.Equals(uniqueConstraintName2, StringComparison.OrdinalIgnoreCase) + ); + Assert.Contains( + uniqueConstraints, + uc => uc.ConstraintName.Equals(uniqueConstraintName, StringComparison.OrdinalIgnoreCase) + ); + + Output.WriteLine("Dropping unique constraint: {0}.{1}", tableName, uniqueConstraintName); + await db.DropUniqueConstraintIfExistsAsync(schemaName, tableName, uniqueConstraintName); + + Output.WriteLine("Unique Constraint Exists: {0}.{1}", tableName, uniqueConstraintName); + exists = await db.DoesUniqueConstraintExistAsync( + schemaName, + tableName, + uniqueConstraintName + ); + Assert.False(exists); + + // test key ordering + tableName = "testWithUc2"; + uniqueConstraintName = "uc_testWithUc2"; + await db.CreateTableIfNotExistsAsync( + schemaName, + tableName, + [ + new DxColumn( + schemaName, + tableName, + columnName, + typeof(int), + defaultExpression: "1", + isNullable: false + ), + new DxColumn( + schemaName, + tableName, + columnName2, + typeof(int), + defaultExpression: "1", + isNullable: false + ) + ], + uniqueConstraints: + [ + new DxUniqueConstraint( + schemaName, + tableName, + uniqueConstraintName, + [ + new DxOrderedColumn(columnName2), + new DxOrderedColumn(columnName, DxColumnOrder.Descending) + ] + ) + ] + ); + + var uniqueConstraint = await db.GetUniqueConstraintAsync( + schemaName, + tableName, + uniqueConstraintName + ); + Assert.NotNull(uniqueConstraint); + Assert.NotNull(uniqueConstraint.Columns); + Assert.Equal(2, uniqueConstraint.Columns.Length); + Assert.Equal( + columnName2, + uniqueConstraint.Columns[0].ColumnName, + StringComparer.OrdinalIgnoreCase + ); + Assert.Equal(DxColumnOrder.Ascending, uniqueConstraint.Columns[0].Order); + Assert.Equal( + columnName, + uniqueConstraint.Columns[1].ColumnName, + StringComparer.OrdinalIgnoreCase + ); + if (await db.SupportsOrderedKeysInConstraintsAsync()) + { + Assert.Equal(DxColumnOrder.Descending, uniqueConstraint.Columns[1].Order); + } + await db.DropTableIfExistsAsync(schemaName, tableName); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.Views.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.Views.cs new file mode 100644 index 0000000..6bba80c --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.Views.cs @@ -0,0 +1,105 @@ +using Dapper; +using DapperMatic.Models; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests +{ + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task Can_perform_simple_CRUD_on_Views_Async(string? schemaName) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var supportsSchemas = db.SupportsSchemas(); + + var tableForView = "testTableForView"; + + var schemaQualifiedTableName = db.GetSchemaQualifiedTableName(schemaName, tableForView); + + await db.CreateTableIfNotExistsAsync( + schemaName, + tableForView, + [ + new DxColumn( + schemaName, + tableForView, + "id", + typeof(int), + isPrimaryKey: true, + isAutoIncrement: true + ), + new DxColumn(schemaName, tableForView, "name", typeof(string)) + ] + ); + + var viewName = "testView"; + var definition = $"SELECT * FROM {schemaQualifiedTableName}"; + var created = await db.CreateViewIfNotExistsAsync(schemaName, viewName, definition); + Assert.True(created); + + var createdAgain = await db.CreateViewIfNotExistsAsync(schemaName, viewName, definition); + Assert.False(createdAgain); + + var exists = await db.DoesViewExistAsync(schemaName, viewName); + Assert.True(exists); + + var view = await db.GetViewAsync(schemaName, viewName); + Assert.NotNull(view); + + var viewNames = await db.GetViewNamesAsync(schemaName); + Assert.Contains(viewName, viewNames, StringComparer.OrdinalIgnoreCase); + + await db.ExecuteAsync($"INSERT INTO {schemaQualifiedTableName} (name) VALUES ('test123')"); + await db.ExecuteAsync($"INSERT INTO {schemaQualifiedTableName} (name) VALUES ('test456')"); + var tableRowCount = await db.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM {schemaQualifiedTableName}" + ); + var viewRowCount = await db.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM {schemaQualifiedTableName}" + ); + + Assert.Equal(2, tableRowCount); + Assert.Equal(2, viewRowCount); + + var updatedName = viewName + "blahblahblah"; + var updatedDefinition = $"SELECT * FROM {schemaQualifiedTableName} WHERE id = 1"; + var updated = await db.UpdateViewIfExistsAsync(schemaName, updatedName, updatedDefinition); + Assert.False(updated); // view doesn't exist + + var renamed = await db.RenameViewIfExistsAsync(schemaName, viewName, updatedName); + Assert.True(renamed); + + var renamedView = await db.GetViewAsync(schemaName, updatedName); + Assert.NotNull(renamedView); + Assert.Equal(view.Definition, renamedView.Definition); + + updated = await db.UpdateViewIfExistsAsync(schemaName, updatedName, updatedDefinition); + Assert.True(updated); + + var updatedView = await db.GetViewAsync(schemaName, updatedName); + Assert.NotNull(updatedView); + Assert.Contains("= 1", updatedView.Definition, StringComparison.OrdinalIgnoreCase); + + // databases often rewrite the definition, so we just check that it contains the updated definition + Assert.StartsWith( + "select ", + updatedView.Definition.Trim(), + StringComparison.OrdinalIgnoreCase + ); + + var dropped = await db.DropViewIfExistsAsync(schemaName, viewName); + Assert.False(dropped); + dropped = await db.DropViewIfExistsAsync(schemaName, updatedName); + Assert.True(dropped); + + exists = await db.DoesViewExistAsync(schemaName, viewName); + Assert.False(exists); + exists = await db.DoesViewExistAsync(schemaName, updatedName); + Assert.False(exists); + + await db.DropTableIfExistsAsync(schemaName, tableForView); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseMethodsTests.cs b/tests/DapperMatic.Tests/DatabaseMethodsTests.cs new file mode 100644 index 0000000..ea04c9a --- /dev/null +++ b/tests/DapperMatic.Tests/DatabaseMethodsTests.cs @@ -0,0 +1,107 @@ +using System.Data; +using Dapper; +using Newtonsoft.Json; +using Xunit.Abstractions; + +namespace DapperMatic.Tests; + +public abstract partial class DatabaseMethodsTests : TestBase +{ + protected DatabaseMethodsTests(ITestOutputHelper output) + : base(output) { } + + public abstract Task OpenConnectionAsync(); + + [Fact] + protected virtual async Task Database_Can_RunArbitraryQueriesAsync() + { + using var db = await OpenConnectionAsync(); + const int expected = 1; + var actual = await db.QueryFirstAsync("SELECT 1"); + Assert.Equal(expected, actual); + + // run a statement with many sql statements at the same time + await db.ExecuteAsync( + """ + + CREATE TABLE test (id INT PRIMARY KEY); + INSERT INTO test VALUES (1); + INSERT INTO test VALUES (2); + INSERT INTO test VALUES (3); + + """ + ); + var values = await db.QueryAsync("SELECT id FROM test"); + Assert.Equal(3, values.Count()); + + // run multiple select statements and read multiple result sets + var result = await db.QueryMultipleAsync( + """ + + SELECT id FROM test WHERE id = 1; + SELECT id FROM test WHERE id = 2; + SELECT id FROM test; + -- this statement is ignored by the grid reader + -- because it doesn't return any results + INSERT INTO test VALUES (4); + SELECT id FROM test WHERE id = 4; + + """ + ); + var id1 = result.Read().Single(); + var id2 = result.Read().Single(); + var allIds = result.Read().ToArray(); + var id4 = result.Read().Single(); + Assert.Equal(1, id1); + Assert.Equal(2, id2); + Assert.Equal(3, allIds.Length); + Assert.Equal(4, id4); + } + + [Fact] + protected virtual async Task GetDatabaseVersionAsync_ReturnsVersion() + { + using var db = await OpenConnectionAsync(); + + var version = await db.GetDatabaseVersionAsync(); + Assert.True(version.Major > 0); + + Output.WriteLine("Database version: {0}", version); + } + + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task GetLastSqlWithParamsAsync_ReturnsLastSqlWithParams( + string? schemaName + ) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var tableNames = await db.GetTableNamesAsync(schemaName, "testing*"); + + var (lastSql, lastParams) = db.GetLastSqlWithParams(); + Assert.NotEmpty(lastSql); + Assert.NotNull(lastParams); + + Output.WriteLine("Last SQL: {0}", lastSql); + Output.WriteLine("Last Parameters: {0}", JsonConvert.SerializeObject(lastParams)); + } + + [Theory] + [InlineData(null)] + [InlineData("my_app")] + protected virtual async Task GetLastSqlAsync_ReturnsLastSql(string? schemaName) + { + using var db = await OpenConnectionAsync(); + await InitFreshSchemaAsync(db, schemaName); + + var tableNames = await db.GetTableNamesAsync(schemaName, "testing*"); + + var lastSql = db.GetLastSql(); + Assert.NotEmpty(lastSql); + + Output.WriteLine("Last SQL: {0}", lastSql); + } +} diff --git a/tests/DapperMatic.Tests/DatabaseTests.cs b/tests/DapperMatic.Tests/DatabaseTests.cs deleted file mode 100644 index e34bef8..0000000 --- a/tests/DapperMatic.Tests/DatabaseTests.cs +++ /dev/null @@ -1,724 +0,0 @@ -using System.Data; -using Dapper; -using DapperMatic.Models; -using Microsoft.VisualBasic; -using Xunit.Abstractions; - -namespace DapperMatic.Tests; - -public abstract class DatabaseTests -{ - private readonly ITestOutputHelper output; - - protected DatabaseTests(ITestOutputHelper output) - { - Console.WriteLine($"Initializing tests for {GetType().Name}"); - output.WriteLine($"Initializing tests for {GetType().Name}"); - this.output = output; - } - - public abstract Task OpenConnectionAsync(); - - [Fact] - protected virtual async Task Database_Can_RunArbitraryQueriesAsync() - { - using var connection = await OpenConnectionAsync(); - const int expected = 1; - var actual = await connection.QueryFirstAsync("SELECT 1"); - Assert.Equal(expected, actual); - - // run a statement with many sql statements at the same time - await connection.ExecuteAsync( - @" - CREATE TABLE test (id INT PRIMARY KEY); - INSERT INTO test VALUES (1); - INSERT INTO test VALUES (2); - INSERT INTO test VALUES (3); - " - ); - var values = await connection.QueryAsync("SELECT id FROM test"); - Assert.Equal(3, values.Count()); - - // run multiple select statements and read multiple result sets - var result = await connection.QueryMultipleAsync( - @" - SELECT id FROM test WHERE id = 1; - SELECT id FROM test WHERE id = 2; - SELECT id FROM test; - -- this statement is ignored by the grid reader - -- because it doesn't return any results - INSERT INTO test VALUES (4); - SELECT id FROM test WHERE id = 4; - " - ); - var id1 = result.Read().Single(); - var id2 = result.Read().Single(); - var allIds = result.Read().ToArray(); - var id4 = result.Read().Single(); - Assert.Equal(1, id1); - Assert.Equal(2, id2); - Assert.Equal(3, allIds.Length); - Assert.Equal(4, id4); - } - - [Fact] - protected virtual async Task Database_Can_CrudSchemasAsync() - { - using var connection = await OpenConnectionAsync(); - - var supportsSchemas = await connection.SupportsSchemasAsync(); - - const string schemaName = "test"; - - // providers should just ignore this if the database doesn't support schemas - await connection.DropSchemaIfExistsAsync(schemaName); - - output.WriteLine($"Schema Exists: {schemaName}"); - var exists = await connection.SchemaExistsAsync(schemaName); - Assert.False(exists); - - output.WriteLine($"Creating schemaName: {schemaName}"); - var created = await connection.CreateSchemaIfNotExistsAsync(schemaName); - if (supportsSchemas) - { - Assert.True(created); - } - else - { - Assert.False(created); - } - - output.WriteLine($"Retrieving schemas"); - var schemas = (await connection.GetSchemaNamesAsync()).ToArray(); - if (supportsSchemas) - { - Assert.True(schemas.Length > 0 && schemas.Contains(schemaName)); - } - else - { - Assert.Empty(schemas); - } - - schemas = (await connection.GetSchemaNamesAsync(schemaName)).ToArray(); - if (supportsSchemas) - { - Assert.Single(schemas); - Assert.Equal(schemaName, schemas.Single()); - } - else - { - Assert.Empty(schemas); - } - - output.WriteLine($"Dropping schemaName: {schemaName}"); - var dropped = await connection.DropSchemaIfExistsAsync(schemaName); - if (supportsSchemas) - { - Assert.True(dropped); - } - else - { - Assert.False(dropped); - } - - schemas = (await connection.GetSchemaNamesAsync(schemaName)).ToArray(); - Assert.Empty(schemas); - } - - [Fact] - protected virtual async Task Database_Can_CrudTablesWithoutSchemasAsync() - { - using IDbConnection connection = await OpenConnectionAsync(); - const string tableName = "test"; - - await connection.DropTableIfExistsAsync(tableName); - - output.WriteLine($"Table Exists: {tableName}"); - var exists = await connection.TableExistsAsync(tableName); - Assert.False(exists); - - output.WriteLine($"Creating table: {tableName}"); - await connection.CreateTableIfNotExistsAsync(tableName); - - output.WriteLine($"Retrieving tables"); - var tables = (await connection.GetTableNamesAsync()).ToArray(); - Assert.True(tables.Length > 0 && tables.Contains(tableName)); - - tables = (await connection.GetTableNamesAsync(tableName)).ToArray(); - Assert.Single(tables); - Assert.Equal(tableName, tables.Single()); - - output.WriteLine("Testing auto increment"); - for (var i = 0; i < 10; i++) - { - await connection.ExecuteAsync($"INSERT INTO {tableName} DEFAULT VALUES"); - } - var count = await connection.ExecuteScalarAsync($"SELECT COUNT(*) FROM {tableName}"); - Assert.Equal(10, count); - - output.WriteLine($"Dropping table: {tableName}"); - await connection.DropTableIfExistsAsync(tableName); - - const string columnIdName = "id"; - - output.WriteLine($"Column Exists: {tableName}.{columnIdName}"); - await connection.ColumnExistsAsync(tableName, columnIdName); - - output.WriteLine($"Creating table with Guid PK: tableWithGuidPk"); - await connection.CreateTableIfNotExistsAsync( - "tableWithGuidPk", - primaryKeyColumnNames: new[] { "guidId" }, - primaryKeyDotnetTypes: new[] { typeof(Guid) } - ); - exists = await connection.TableExistsAsync("tableWithGuidPk"); - Assert.True(exists); - - output.WriteLine($"Creating table with string PK: tableWithStringPk"); - await connection.CreateTableIfNotExistsAsync( - "tableWithStringPk", - primaryKeyColumnNames: new[] { "strId" }, - primaryKeyDotnetTypes: new[] { typeof(string) } - ); - exists = await connection.TableExistsAsync("tableWithStringPk"); - Assert.True(exists); - - output.WriteLine($"Creating table with string PK 64 length: tableWithStringPk64"); - await connection.CreateTableIfNotExistsAsync( - "tableWithStringPk64", - primaryKeyColumnNames: new[] { "strId64" }, - primaryKeyDotnetTypes: new[] { typeof(string) }, - primaryKeyColumnLengths: new[] { (int?)64 } - ); - exists = await connection.TableExistsAsync("tableWithStringPk64"); - Assert.True(exists); - - output.WriteLine($"Creating table with compound PK: tableWithCompoundPk"); - await connection.CreateTableIfNotExistsAsync( - "tableWithCompoundPk", - primaryKeyColumnNames: new[] { "longId", "guidId", "strId" }, - primaryKeyDotnetTypes: new[] { typeof(long), typeof(Guid), typeof(string) }, - primaryKeyColumnLengths: new int?[] { null, null, 128 } - ); - exists = await connection.TableExistsAsync("tableWithCompoundPk"); - Assert.True(exists); - } - - [Fact] - protected virtual async Task Database_Can_CrudTableColumnsAsync() - { - using IDbConnection connection = await OpenConnectionAsync(); - const string tableName = "testWithColumn"; - const string columnName = "testColumn"; - - string? defaultDateTimeSql = null; - string? defaultGuidSql = null; - var dbType = connection.GetDatabaseType(); - switch (dbType) - { - case DatabaseTypes.SqlServer: - defaultDateTimeSql = "GETUTCDATE()"; - defaultGuidSql = "NEWID()"; - break; - case DatabaseTypes.Sqlite: - defaultDateTimeSql = "CURRENT_TIMESTAMP"; - //this could be supported IF the sqlite UUID extension was loaded and enabled - //defaultGuidSql = "uuid_blob(uuid())"; - defaultGuidSql = null; - break; - case DatabaseTypes.PostgreSql: - defaultDateTimeSql = "CURRENT_TIMESTAMP"; - defaultGuidSql = "uuid_generate_v4()"; - break; - case DatabaseTypes.MySql: - defaultDateTimeSql = "CURRENT_TIMESTAMP"; - // only supported after 8.0.13 - // defaultGuidSql = "UUID()"; - break; - } - - await connection.CreateTableIfNotExistsAsync(tableName); - - output.WriteLine($"Column Exists: {tableName}.{columnName}"); - var exists = await connection.ColumnExistsAsync(tableName, columnName); - Assert.False(exists); - - output.WriteLine($"Creating columnName: {tableName}.{columnName}"); - await connection.CreateColumnIfNotExistsAsync( - tableName, - columnName, - typeof(int), - defaultValue: "1", - nullable: false - ); - - output.WriteLine($"Column Exists: {tableName}.{columnName}"); - exists = await connection.ColumnExistsAsync(tableName, columnName); - Assert.True(exists); - - output.WriteLine($"Dropping columnName: {tableName}.{columnName}"); - await connection.DropColumnIfExistsAsync(tableName, columnName); - - output.WriteLine($"Column Exists: {tableName}.{columnName}"); - exists = await connection.ColumnExistsAsync(tableName, columnName); - Assert.False(exists); - - // try adding a columnName of all the supported types - await connection.CreateTableIfNotExistsAsync("testWithAllColumns"); - var columnCount = 1; - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "createdDateColumn" + columnCount++, - typeof(DateTime), - defaultValue: defaultDateTimeSql - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "newidColumn" + columnCount++, - typeof(Guid), - defaultValue: defaultGuidSql - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "bigintColumn" + columnCount++, - typeof(long) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "binaryColumn" + columnCount++, - typeof(byte[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "bitColumn" + columnCount++, - typeof(bool) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "charColumn" + columnCount++, - typeof(string), - length: 10 - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "dateColumn" + columnCount++, - typeof(DateTime) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "datetimeColumn" + columnCount++, - typeof(DateTime) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "datetime2Column" + columnCount++, - typeof(DateTime) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "datetimeoffsetColumn" + columnCount++, - typeof(DateTimeOffset) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "decimalColumn" + columnCount++, - typeof(decimal) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "decimalColumnWithPrecision" + columnCount++, - typeof(decimal), - precision: 10 - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "decimalColumnWithPrecisionAndScale" + columnCount++, - typeof(decimal), - precision: 10, - scale: 5 - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "floatColumn" + columnCount++, - typeof(double) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "imageColumn" + columnCount++, - typeof(byte[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "intColumn" + columnCount++, - typeof(int) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "moneyColumn" + columnCount++, - typeof(decimal) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "ncharColumn" + columnCount++, - typeof(string), - length: 10 - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "ntextColumn" + columnCount++, - typeof(string), - length: int.MaxValue - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "floatColumn2" + columnCount++, - typeof(float) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "doubleColumn2" + columnCount++, - typeof(double) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "guidArrayColumn" + columnCount++, - typeof(Guid[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "intArrayColumn" + columnCount++, - typeof(int[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "longArrayColumn" + columnCount++, - typeof(long[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "doubleArrayColumn" + columnCount++, - typeof(double[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "decimalArrayColumn" + columnCount++, - typeof(decimal[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "stringArrayColumn" + columnCount++, - typeof(string[]) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "stringDectionaryArrayColumn" + columnCount++, - typeof(Dictionary) - ); - await connection.CreateColumnIfNotExistsAsync( - "testWithAllColumns", - "objectDectionaryArrayColumn" + columnCount++, - typeof(Dictionary) - ); - - var columnNames = await connection.GetColumnNamesAsync("testWithAllColumns"); - Assert.Equal(columnCount, columnNames.Count()); - } - - [Fact] - protected virtual async Task Database_Can_CrudTableIndexesAsync() - { - using IDbConnection connection = await OpenConnectionAsync(); - - var version = await connection.GetDatabaseVersionAsync(); - Assert.NotEmpty(version); - - var supportsDescendingColumnSorts = true; - var dbType = connection.GetDatabaseType(); - if (dbType.HasFlag(DatabaseTypes.MySql)) - { - if (version.StartsWith("5.")) - { - supportsDescendingColumnSorts = false; - } - } - try - { - // await connection.ExecuteAsync("DROP TABLE testWithIndex"); - const string tableName = "testWithIndex"; - const string columnName = "testColumn"; - const string indexName = "testIndex"; - - await connection.DropTableIfExistsAsync(tableName); - await connection.CreateTableIfNotExistsAsync(tableName); - await connection.CreateColumnIfNotExistsAsync( - tableName, - columnName, - typeof(int), - defaultValue: "1", - nullable: false - ); - for (var i = 0; i < 10; i++) - { - await connection.CreateColumnIfNotExistsAsync( - tableName, - columnName + "_" + i, - typeof(int), - defaultValue: i.ToString(), - nullable: false - ); - } - - output.WriteLine($"Index Exists: {tableName}.{indexName}"); - var exists = await connection.IndexExistsAsync(tableName, columnName, indexName); - Assert.False(exists); - - output.WriteLine($"Creating unique index: {tableName}.{indexName}"); - await connection.CreateIndexIfNotExistsAsync( - tableName, - indexName, - [columnName], - unique: true - ); - - output.WriteLine( - $"Creating multiple column unique index: {tableName}.{indexName}_multi" - ); - await connection.CreateIndexIfNotExistsAsync( - tableName, - indexName + "_multi", - [columnName + "_1 DESC", columnName + "_2"], - unique: true - ); - - output.WriteLine( - $"Creating multiple column non unique index: {tableName}.{indexName}_multi2" - ); - await connection.CreateIndexIfNotExistsAsync( - tableName, - indexName + "_multi2", - [columnName + "_3 ASC", columnName + "_4 DESC"] - ); - - output.WriteLine($"Index Exists: {tableName}.{indexName}"); - exists = await connection.IndexExistsAsync(tableName, indexName); - Assert.True(exists); - exists = await connection.IndexExistsAsync(tableName, indexName + "_multi"); - Assert.True(exists); - exists = await connection.IndexExistsAsync(tableName, indexName + "_multi2"); - Assert.True(exists); - - var indexNames = await connection.GetIndexNamesAsync(tableName); - // get all indexes in the database - var indexNames2 = await connection.GetIndexNamesAsync(null); - Assert.Contains( - indexNames, - i => i.Equals(indexName, StringComparison.OrdinalIgnoreCase) - ); - Assert.Contains( - indexNames2, - i => i.Equals(indexName, StringComparison.OrdinalIgnoreCase) - ); - Assert.Contains( - indexNames, - i => i.Equals(indexName + "_multi", StringComparison.OrdinalIgnoreCase) - ); - Assert.Contains( - indexNames2, - i => i.Equals(indexName + "_multi", StringComparison.OrdinalIgnoreCase) - ); - Assert.Contains( - indexNames, - i => i.Equals(indexName + "_multi2", StringComparison.OrdinalIgnoreCase) - ); - Assert.Contains( - indexNames2, - i => i.Equals(indexName + "_multi2", StringComparison.OrdinalIgnoreCase) - ); - - var indexes = await connection.GetIndexesAsync(tableName); - // get all indexes in the database - var indexes2 = await connection.GetIndexesAsync(null); - Assert.True(indexes.Count() >= 3); - Assert.True(indexes2.Count() >= 3); - var idxMulti1 = indexes.SingleOrDefault(i => - i.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) - && i.IndexName.Equals(indexName + "_multi", StringComparison.OrdinalIgnoreCase) - ); - var idxMulti2 = indexes.SingleOrDefault(i => - i.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) - && i.IndexName.Equals(indexName + "_multi2", StringComparison.OrdinalIgnoreCase) - ); - Assert.NotNull(idxMulti1); - Assert.NotNull(idxMulti2); - idxMulti1 = indexes2.SingleOrDefault(i => - i.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) - && i.IndexName.Equals(indexName + "_multi", StringComparison.OrdinalIgnoreCase) - ); - idxMulti2 = indexes2.SingleOrDefault(i => - i.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) - && i.IndexName.Equals(indexName + "_multi2", StringComparison.OrdinalIgnoreCase) - ); - Assert.NotNull(idxMulti1); - Assert.NotNull(idxMulti2); - Assert.True(idxMulti1.Unique); - Assert.True(idxMulti1.ColumnNames.Length == 2); - if (supportsDescendingColumnSorts) - Assert.EndsWith( - "desc", - idxMulti1.ColumnNames[0], - StringComparison.OrdinalIgnoreCase - ); - Assert.EndsWith("asc", idxMulti1.ColumnNames[1], StringComparison.OrdinalIgnoreCase); - Assert.False(idxMulti2.Unique); - Assert.True(idxMulti2.ColumnNames.Length == 2); - Assert.EndsWith("asc", idxMulti2.ColumnNames[0], StringComparison.OrdinalIgnoreCase); - if (supportsDescendingColumnSorts) - Assert.EndsWith( - "desc", - idxMulti2.ColumnNames[1], - StringComparison.OrdinalIgnoreCase - ); - - output.WriteLine($"Dropping indexName: {tableName}.{indexName}"); - await connection.DropIndexIfExistsAsync(tableName, indexName); - - output.WriteLine($"Index Exists: {tableName}.{indexName}"); - exists = await connection.IndexExistsAsync(tableName, indexName); - Assert.False(exists); - - await connection.DropTableIfExistsAsync(tableName); - } - finally - { - var sql = connection.GetLastSql(); - output.WriteLine("Last sql: " + sql); - } - } - - [Fact] - protected virtual async Task Database_Can_CrudTableForeignKeysAsync() - { - using IDbConnection connection = await OpenConnectionAsync(); - const string tableName = "testWithFk"; - const string refTableName = "testPk"; - const string columnName = "testFkColumn"; - const string foreignKeyName = "testFk"; - - var supportsForeignKeyNaming = await connection.SupportsNamedForeignKeysAsync(); - - await connection.CreateTableIfNotExistsAsync(tableName); - await connection.CreateTableIfNotExistsAsync(refTableName); - await connection.CreateColumnIfNotExistsAsync( - tableName, - columnName, - typeof(int), - defaultValue: "1", - nullable: false - ); - - output.WriteLine($"Foreign Key Exists: {tableName}.{foreignKeyName}"); - var exists = await connection.ForeignKeyExistsAsync(tableName, columnName, foreignKeyName); - Assert.False(exists); - - output.WriteLine($"Creating foreign key: {tableName}.{foreignKeyName}"); - await connection.CreateForeignKeyIfNotExistsAsync( - tableName, - columnName, - foreignKeyName, - refTableName, - "id", - onDelete: ReferentialAction.Cascade.ToSql() - ); - - output.WriteLine($"Foreign Key Exists: {tableName}.{foreignKeyName}"); - exists = await connection.ForeignKeyExistsAsync(tableName, columnName, foreignKeyName); - Assert.True(exists); - - output.WriteLine($"Get Foreign Key Names: {tableName}"); - var fkNames = await connection.GetForeignKeyNamesAsync(tableName); - if (supportsForeignKeyNaming) - { - Assert.Contains( - fkNames, - fk => fk.Equals(foreignKeyName, StringComparison.OrdinalIgnoreCase) - ); - } - - output.WriteLine($"Get Foreign Keys: {tableName}"); - var fks = await connection.GetForeignKeysAsync(tableName); - Assert.Contains( - fks, - fk => - fk.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) - && fk.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) - && ( - !supportsForeignKeyNaming - || fk.ForeignKeyName.Equals(foreignKeyName, StringComparison.OrdinalIgnoreCase) - ) - && fk.ReferenceTableName.Equals(refTableName, StringComparison.OrdinalIgnoreCase) - && fk.ReferenceColumnName.Equals("id", StringComparison.OrdinalIgnoreCase) - && fk.OnDelete.Equals(ReferentialAction.Cascade) - ); - - output.WriteLine($"Dropping foreign key: {foreignKeyName}"); - if (supportsForeignKeyNaming) - { - await connection.DropForeignKeyIfExistsAsync(tableName, columnName, foreignKeyName); - } - else - { - await connection.DropForeignKeyIfExistsAsync(tableName, columnName); - } - - output.WriteLine($"Foreign Key Exists: {foreignKeyName}"); - exists = supportsForeignKeyNaming - ? await connection.ForeignKeyExistsAsync(tableName, columnName, foreignKeyName) - : await connection.ForeignKeyExistsAsync(tableName, columnName); - Assert.False(exists); - } - - [Fact] - protected virtual async Task Database_Can_CrudTableUniqueConstraintsAsync() - { - using IDbConnection connection = await OpenConnectionAsync(); - const string tableName = "testWithUc"; - const string columnName = "testColumn"; - const string uniqueConstraintName = "testUc"; - - await connection.CreateTableIfNotExistsAsync(tableName); - await connection.CreateColumnIfNotExistsAsync( - tableName, - columnName, - typeof(int), - defaultValue: "1", - nullable: false - ); - - output.WriteLine($"Unique Constraint Exists: {tableName}.{uniqueConstraintName}"); - var exists = await connection.UniqueConstraintExistsAsync( - tableName, - columnName, - uniqueConstraintName - ); - Assert.False(exists); - - output.WriteLine($"Creating unique constraint: {tableName}.{uniqueConstraintName}"); - await connection.CreateUniqueConstraintIfNotExistsAsync( - tableName, - uniqueConstraintName, - [columnName] - ); - - output.WriteLine($"Unique Constraint Exists: {tableName}.{uniqueConstraintName}"); - exists = await connection.UniqueConstraintExistsAsync(tableName, uniqueConstraintName); - Assert.True(exists); - - output.WriteLine($"Dropping unique constraint: {tableName}.{uniqueConstraintName}"); - await connection.DropUniqueConstraintIfExistsAsync(tableName, uniqueConstraintName); - - output.WriteLine($"Unique Constraint Exists: {tableName}.{uniqueConstraintName}"); - exists = await connection.UniqueConstraintExistsAsync(tableName, uniqueConstraintName); - Assert.False(exists); - } - - public virtual void Dispose() => output.WriteLine(GetType().Name); -} diff --git a/tests/DapperMatic.Tests/ExtensionMethodTests.cs b/tests/DapperMatic.Tests/ExtensionMethodTests.cs new file mode 100644 index 0000000..d5eed47 --- /dev/null +++ b/tests/DapperMatic.Tests/ExtensionMethodTests.cs @@ -0,0 +1,44 @@ +namespace DapperMatic.Tests; + +public class ExtensionMethodTests +{ + // tes the GetFriendlyName method + [Theory] + [InlineData(typeof(bool), "Boolean")] + [InlineData(typeof(List), "List")] + [InlineData( + typeof(IEnumerable>>), + "IEnumerable>>" + )] + public void TestGetFriendlyName(Type input, string expected) + { + var actual = input.GetFriendlyName(); + Assert.Equal(expected, actual); + } + + // test the ToAlpha method + [Theory] + [InlineData("abc", "abc")] + [InlineData("abc123", "abc")] + [InlineData("abc123def", "abcdef")] + [InlineData("abc123def456", "abcdef")] + [InlineData("abc (&__-1234)123def456ghi", "abcdefghi")] + [InlineData("abc (&__-1234)123def456ghi", "abc(&__)defghi", "_&()")] + public void TestToAlpha(string input, string expected, string additionalAllowedCharacters = "") + { + var actual = input.ToAlpha(additionalAllowedCharacters); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("char", "char")] + [InlineData("decimal(10,2)", "decimal")] + [InlineData("abc(12,2) dd aa", "abc dd aa")] + [InlineData("abc ( 12 ,2 ) dd aa", "abc dd aa")] + [InlineData(" nvarchar ( 255 ) ", "nvarchar")] + public void TestDiscardLengthPrecisionAndScaleFromSqlTypeName(string input, string expected) + { + var actual = input.DiscardLengthPrecisionAndScaleFromSqlTypeName(); + Assert.Equal(expected, actual); + } +} diff --git a/tests/DapperMatic.Tests/Logging/TestLogger.cs b/tests/DapperMatic.Tests/Logging/TestLogger.cs new file mode 100644 index 0000000..e5bc66e --- /dev/null +++ b/tests/DapperMatic.Tests/Logging/TestLogger.cs @@ -0,0 +1,55 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace DapperMatic.Tests.Logging; + +public class TestLogger : ILogger, IDisposable +{ + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + + private LogLevel _minLogLevel = LogLevel.Debug; + private ITestOutputHelper _output; + private string _categoryName; + + public TestLogger(ITestOutputHelper output, string categoryName) + { + _output = output; + _categoryName = categoryName; + } + + public IDisposable? BeginScope(TState state) + where TState : notnull + { + return this; + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= _minLogLevel; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter + ) + { + if (IsEnabled(logLevel)) + { + _output.WriteLine( + "[DapperMatic {0:hh\\:mm\\:ss\\.ff}] {1}", + _stopwatch.Elapsed, + formatter.Invoke(state, exception) + ); + } + } + + /// + public void Dispose() + { + // The default console logger does not support scopes. We return itself as IDisposable implementation. + } +} diff --git a/tests/DapperMatic.Tests/Logging/TestLoggerFactory.cs b/tests/DapperMatic.Tests/Logging/TestLoggerFactory.cs new file mode 100644 index 0000000..2203941 --- /dev/null +++ b/tests/DapperMatic.Tests/Logging/TestLoggerFactory.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace DapperMatic.Tests.Logging; + +public class TestLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _output; + + public TestLoggerProvider(ITestOutputHelper output) + { + _output = output; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public ILogger CreateLogger(string categoryName) + { + return new TestLogger(_output, categoryName); + } + + public void Dispose() { } +} diff --git a/tests/DapperMatic.Tests/ProviderFixtures/DatabaseFixtureBase.cs b/tests/DapperMatic.Tests/ProviderFixtures/DatabaseFixtureBase.cs index b92bf15..895fdd1 100644 --- a/tests/DapperMatic.Tests/ProviderFixtures/DatabaseFixtureBase.cs +++ b/tests/DapperMatic.Tests/ProviderFixtures/DatabaseFixtureBase.cs @@ -13,4 +13,6 @@ public abstract class DatabaseFixtureBase : IDatabaseFixture, IAsync public virtual Task InitializeAsync() => Container.StartAsync(); public virtual Task DisposeAsync() => Container.DisposeAsync().AsTask(); + + public virtual bool IgnoreSqlType(string sqlType) => false; } diff --git a/tests/DapperMatic.Tests/ProviderFixtures/MariaDbDatabaseFixture.cs b/tests/DapperMatic.Tests/ProviderFixtures/MariaDbDatabaseFixture.cs new file mode 100644 index 0000000..c4524f4 --- /dev/null +++ b/tests/DapperMatic.Tests/ProviderFixtures/MariaDbDatabaseFixture.cs @@ -0,0 +1,44 @@ +using Testcontainers.MariaDb; +using Testcontainers.MySql; + +namespace DapperMatic.Tests.ProviderFixtures; + +public class MariaDb_11_1_DatabaseFixture : MariaDbDatabaseFixture +{ + public MariaDb_11_1_DatabaseFixture() + : base("mariadb:11.1") { } + + public override bool IgnoreSqlType(string sqlType) + { + return sqlType.Equals("geomcollection", StringComparison.OrdinalIgnoreCase) + || base.IgnoreSqlType(sqlType); + } +} + +public class MariaDb_10_11_DatabaseFixture : MariaDbDatabaseFixture +{ + public MariaDb_10_11_DatabaseFixture() + : base("mariadb:10.11") { } + + public override bool IgnoreSqlType(string sqlType) + { + return sqlType.Equals("geomcollection", StringComparison.OrdinalIgnoreCase) + || base.IgnoreSqlType(sqlType); + } +} + +public abstract class MariaDbDatabaseFixture(string imageName) + : DatabaseFixtureBase +{ + private readonly MariaDbContainer _container = new MariaDbBuilder() + .WithImage(imageName) + .WithPassword("Strong_password_123!") + .WithAutoRemove(true) + .WithCleanUp(true) + .Build(); + + public override MariaDbContainer Container + { + get { return _container; } + } +} diff --git a/tests/DapperMatic.Tests/ProviderFixtures/MySqlDatabaseFixture.cs b/tests/DapperMatic.Tests/ProviderFixtures/MySqlDatabaseFixture.cs index 35dfbbb..c13cd90 100644 --- a/tests/DapperMatic.Tests/ProviderFixtures/MySqlDatabaseFixture.cs +++ b/tests/DapperMatic.Tests/ProviderFixtures/MySqlDatabaseFixture.cs @@ -18,23 +18,17 @@ public class MySql_57_DatabaseFixture : MySqlDatabaseFixture { public MySql_57_DatabaseFixture() : base("mysql:5.7") { } -} - -public class MariaDb_11_2_DatabaseFixture : MySqlDatabaseFixture -{ - public MariaDb_11_2_DatabaseFixture() - : base("mariadb:11.2") { } -} -public class MariaDb_10_11_DatabaseFixture : MySqlDatabaseFixture -{ - public MariaDb_10_11_DatabaseFixture() - : base("mariadb:10.11") { } + public override bool IgnoreSqlType(string sqlType) + { + return sqlType.Equals("geomcollection", StringComparison.OrdinalIgnoreCase) + || base.IgnoreSqlType(sqlType); + } } public abstract class MySqlDatabaseFixture(string imageName) : DatabaseFixtureBase { - private readonly MySqlContainer container = new MySqlBuilder() + private readonly MySqlContainer _container = new MySqlBuilder() .WithImage(imageName) .WithPassword("Strong_password_123!") .WithAutoRemove(true) @@ -43,6 +37,6 @@ public abstract class MySqlDatabaseFixture(string imageName) : DatabaseFixtureBa public override MySqlContainer Container { - get { return container; } + get { return _container; } } } diff --git a/tests/DapperMatic.Tests/ProviderFixtures/PostgreSqlDatabaseFixtures.cs b/tests/DapperMatic.Tests/ProviderFixtures/PostgreSqlDatabaseFixtures.cs index 7b07410..cd94df8 100644 --- a/tests/DapperMatic.Tests/ProviderFixtures/PostgreSqlDatabaseFixtures.cs +++ b/tests/DapperMatic.Tests/ProviderFixtures/PostgreSqlDatabaseFixtures.cs @@ -29,7 +29,7 @@ public PostgreSql_Postgis16_DatabaseFixture() public abstract class PostgreSqlDatabaseFixture(string imageName) : DatabaseFixtureBase { - private readonly PostgreSqlContainer container = new PostgreSqlBuilder() + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder() .WithImage(imageName) .WithPassword("Strong_password_123!") .WithAutoRemove(true) @@ -38,6 +38,6 @@ public abstract class PostgreSqlDatabaseFixture(string imageName) public override PostgreSqlContainer Container { - get { return container; } + get { return _container; } } } diff --git a/tests/DapperMatic.Tests/ProviderFixtures/SqlServerDatabaseFixtures.cs b/tests/DapperMatic.Tests/ProviderFixtures/SqlServerDatabaseFixtures.cs index 20ce8d1..c1e4afb 100644 --- a/tests/DapperMatic.Tests/ProviderFixtures/SqlServerDatabaseFixtures.cs +++ b/tests/DapperMatic.Tests/ProviderFixtures/SqlServerDatabaseFixtures.cs @@ -26,7 +26,7 @@ public SqlServer_2017_CU29_DatabaseFixture() public abstract class SqlServerDatabaseFixture(string imageName) : DatabaseFixtureBase { - private readonly MsSqlContainer container = new MsSqlBuilder() + private readonly MsSqlContainer _container = new MsSqlBuilder() .WithImage(imageName) .WithPassword("Strong_password_123!") .WithAutoRemove(true) @@ -35,6 +35,6 @@ public abstract class SqlServerDatabaseFixture(string imageName) public override MsSqlContainer Container { - get { return container; } + get { return _container; } } } diff --git a/tests/DapperMatic.Tests/ProviderTests/MariaDbDatabaseMethodsTests.cs b/tests/DapperMatic.Tests/ProviderTests/MariaDbDatabaseMethodsTests.cs new file mode 100644 index 0000000..22ae0d8 --- /dev/null +++ b/tests/DapperMatic.Tests/ProviderTests/MariaDbDatabaseMethodsTests.cs @@ -0,0 +1,51 @@ +using System.Data; +using DapperMatic.Tests.ProviderFixtures; +using MySql.Data.MySqlClient; +using Xunit.Abstractions; + +namespace DapperMatic.Tests.ProviderTests; + +/// +/// Testing MariaDb 11.2 +/// +public class MariaDb_11_1_DatabaseMethodsTests( + MariaDb_11_1_DatabaseFixture fixture, + ITestOutputHelper output +) : MariaDbDatabaseMethodsTests(fixture, output) { } + +/// +/// Testing MariaDb 10.11 +/// +public class MariaDb_10_11_DatabaseMethodsTests( + MariaDb_10_11_DatabaseFixture fixture, + ITestOutputHelper output +) : MariaDbDatabaseMethodsTests(fixture, output) { } + +/// +/// Abstract class for MySql database tests +/// +/// +public abstract class MariaDbDatabaseMethodsTests( + TDatabaseFixture fixture, + ITestOutputHelper output +) : DatabaseMethodsTests(output), IClassFixture, IDisposable + where TDatabaseFixture : MariaDbDatabaseFixture +{ + public override async Task OpenConnectionAsync() + { + var connectionString = fixture.ConnectionString; + // Disable SSL for local testing and CI environments + if (!connectionString.Contains("SSL Mode", StringComparison.OrdinalIgnoreCase)) + { + connectionString += ";SSL Mode=None"; + } + var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + return db; + } + + public override bool IgnoreSqlType(string sqlType) + { + return fixture.IgnoreSqlType(sqlType); + } +} diff --git a/tests/DapperMatic.Tests/ProviderTests/MySqlDatabaseMethodsTests.cs b/tests/DapperMatic.Tests/ProviderTests/MySqlDatabaseMethodsTests.cs new file mode 100644 index 0000000..f54976a --- /dev/null +++ b/tests/DapperMatic.Tests/ProviderTests/MySqlDatabaseMethodsTests.cs @@ -0,0 +1,59 @@ +using System.Data; +using DapperMatic.Tests.ProviderFixtures; +using MySql.Data.MySqlClient; +using Xunit.Abstractions; + +namespace DapperMatic.Tests.ProviderTests; + +/// +/// Testing MySql 90 +/// +public class MySql_90_DatabaseMethodsTests( + MySql_90_DatabaseFixture fixture, + ITestOutputHelper output +) : MySqlDatabaseMethodsTests(fixture, output) { } + +/// +/// Testing MySql 84 +/// +public class MySql_84_DatabaseMethodsTests( + MySql_84_DatabaseFixture fixture, + ITestOutputHelper output +) : MySqlDatabaseMethodsTests(fixture, output) { } + +/// +/// Testing MySql 57 +/// +public class MySql_57_DatabaseMethodsTests( + MySql_57_DatabaseFixture fixture, + ITestOutputHelper output +) : MySqlDatabaseMethodsTests(fixture, output) { } + +/// +/// Abstract class for MySql database tests +/// +/// +public abstract class MySqlDatabaseMethodsTests( + TDatabaseFixture fixture, + ITestOutputHelper output +) : DatabaseMethodsTests(output), IClassFixture, IDisposable + where TDatabaseFixture : MySqlDatabaseFixture +{ + public override async Task OpenConnectionAsync() + { + var connectionString = fixture.ConnectionString; + // Disable SSL for local testing and CI environments + if (!connectionString.Contains("SSL Mode", StringComparison.OrdinalIgnoreCase)) + { + connectionString += ";SSL Mode=None"; + } + var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + return db; + } + + public override bool IgnoreSqlType(string sqlType) + { + return fixture.IgnoreSqlType(sqlType); + } +} diff --git a/tests/DapperMatic.Tests/ProviderTests/MySqlDatabaseTests.cs b/tests/DapperMatic.Tests/ProviderTests/MySqlDatabaseTests.cs deleted file mode 100644 index 430229c..0000000 --- a/tests/DapperMatic.Tests/ProviderTests/MySqlDatabaseTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Data; -using DapperMatic.Tests.ProviderFixtures; -using MySql.Data.MySqlClient; -using Xunit.Abstractions; - -namespace DapperMatic.Tests.ProviderTests; - -/// -/// Testing MySql 90 -/// -public class MySql_90_DatabaseTests(MySql_90_DatabaseFixture fixture, ITestOutputHelper output) - : MySqlDatabaseTests(fixture, output) { } - -/// -/// Testing MySql 84 -/// -public class MySql_84_DatabaseTests(MySql_84_DatabaseFixture fixture, ITestOutputHelper output) - : MySqlDatabaseTests(fixture, output) { } - -/// -/// Testing MySql 57 -/// -public class MySql_57_DatabaseTests(MySql_57_DatabaseFixture fixture, ITestOutputHelper output) - : MySqlDatabaseTests(fixture, output) { } - -/// -/// Testing MariaDb 11.2 (short-term release, not LTS) -/// -// public class MariaDb_11_2_DatabaseTests( -// MariaDb_11_2_DatabaseFixture fixture, -// ITestOutputHelper output -// ) : MySqlDatabaseTests(fixture, output) { } - -/// -/// Testing MariaDb 10.11 -/// -public class MariaDb_10_11_DatabaseTests( - MariaDb_10_11_DatabaseFixture fixture, - ITestOutputHelper output -) : MySqlDatabaseTests(fixture, output) { } - -/// -/// Abstract class for Postgres database tests -/// -/// -public abstract class MySqlDatabaseTests( - TDatabaseFixture fixture, - ITestOutputHelper output -) : DatabaseTests(output), IClassFixture, IDisposable - where TDatabaseFixture : MySqlDatabaseFixture -{ - public override async Task OpenConnectionAsync() - { - var connectionString = fixture.ConnectionString; - // Disable SSL for local testing and CI environments - if (!connectionString.Contains("SSL Mode", StringComparison.OrdinalIgnoreCase)) - { - connectionString += ";SSL Mode=None"; - } - var connection = new MySqlConnection(connectionString); - await connection.OpenAsync(); - return connection; - } -} diff --git a/tests/DapperMatic.Tests/ProviderTests/PostgreSqlDatabaseMethodsTests.cs b/tests/DapperMatic.Tests/ProviderTests/PostgreSqlDatabaseMethodsTests.cs new file mode 100644 index 0000000..7f3d143 --- /dev/null +++ b/tests/DapperMatic.Tests/ProviderTests/PostgreSqlDatabaseMethodsTests.cs @@ -0,0 +1,68 @@ +using System.Data; +using Dapper; +using DapperMatic.Tests.ProviderFixtures; +using Npgsql; +using Xunit.Abstractions; + +namespace DapperMatic.Tests.ProviderTests; + +/// +/// Testing Postgres 15 +/// +public class PostgreSql_Postgres15_DatabaseMethodsTests( + PostgreSql_Postgres15_DatabaseFixture fixture, + ITestOutputHelper output +) : PostgreSqlDatabaseMethodsTests(fixture, output) { } + +/// +/// Testing Postgres 16 +/// +public class PostgreSql_Postgres16_DatabaseMethodsTests( + PostgreSql_Postgres16_DatabaseFixture fixture, + ITestOutputHelper output +) : PostgreSqlDatabaseMethodsTests(fixture, output) { } + +/// +/// Testing Postgres 156 with Postgis +/// +public class PostgreSql_Postgis15_DatabaseMethodsTests( + PostgreSql_Postgis15_DatabaseFixture fixture, + ITestOutputHelper output +) : PostgreSqlDatabaseMethodsTests(fixture, output) { } + +/// +/// Testing Postgres 16 with Postgis +/// +public class PostgreSql_Postgis16_DatabaseMethodsTests( + PostgreSql_Postgis16_DatabaseFixture fixture, + ITestOutputHelper output +) : PostgreSqlDatabaseMethodsTests(fixture, output) { } + +/// +/// Abstract class for Postgres database tests +/// +/// +public abstract class PostgreSqlDatabaseMethodsTests( + TDatabaseFixture fixture, + ITestOutputHelper output +) : DatabaseMethodsTests(output), IClassFixture, IDisposable + where TDatabaseFixture : PostgreSqlDatabaseFixture +{ + public override async Task OpenConnectionAsync() + { + var db = new NpgsqlConnection(fixture.ConnectionString); + await db.OpenAsync(); + await db.ExecuteAsync("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";"); + await db.ExecuteAsync("CREATE EXTENSION IF NOT EXISTS \"hstore\";"); + if ( + await db.ExecuteScalarAsync( + @"select count(*) from pg_extension where extname = 'postgis'" + ) > 0 + ) + { + await db.ExecuteAsync("CREATE EXTENSION IF NOT EXISTS \"postgis\";"); + await db.ExecuteAsync("CREATE EXTENSION IF NOT EXISTS \"postgis_topology\";"); + } + return db; + } +} diff --git a/tests/DapperMatic.Tests/ProviderTests/PostgreSqlDatabaseTests.cs b/tests/DapperMatic.Tests/ProviderTests/PostgreSqlDatabaseTests.cs deleted file mode 100644 index 2fe2cbf..0000000 --- a/tests/DapperMatic.Tests/ProviderTests/PostgreSqlDatabaseTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Data; -using Dapper; -using DapperMatic.Tests.ProviderFixtures; -using Npgsql; -using Xunit.Abstractions; - -namespace DapperMatic.Tests.ProviderTests; - -/// -/// Testing Postgres 15 -/// -public class PostgreSql_Postgres15_DatabaseTests( - PostgreSql_Postgres15_DatabaseFixture fixture, - ITestOutputHelper output -) : PostgreSqlDatabaseTests(fixture, output) { } - -/// -/// Testing Postgres 16 -/// -public class PostgreSql_Postgres16_DatabaseTests( - PostgreSql_Postgres16_DatabaseFixture fixture, - ITestOutputHelper output -) : PostgreSqlDatabaseTests(fixture, output) { } - -/// -/// Testing Postgres 156 with Postgis -/// -public class PostgreSql_Postgis15_DatabaseTests( - PostgreSql_Postgis15_DatabaseFixture fixture, - ITestOutputHelper output -) : PostgreSqlDatabaseTests(fixture, output) { } - -/// -/// Testing Postgres 16 with Postgis -/// -public class PostgreSql_Postgis16_DatabaseTests( - PostgreSql_Postgis16_DatabaseFixture fixture, - ITestOutputHelper output -) : PostgreSqlDatabaseTests(fixture, output) { } - -/// -/// Abstract class for Postgres database tests -/// -/// -public abstract class PostgreSqlDatabaseTests( - TDatabaseFixture fixture, - ITestOutputHelper output -) : DatabaseTests(output), IClassFixture, IDisposable - where TDatabaseFixture : PostgreSqlDatabaseFixture -{ - public override async Task OpenConnectionAsync() - { - var connection = new NpgsqlConnection(fixture.ConnectionString); - await connection.OpenAsync(); - await connection.ExecuteAsync("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";"); - return connection; - } -} diff --git a/tests/DapperMatic.Tests/ProviderTests/SQLiteDatabaseTests.cs b/tests/DapperMatic.Tests/ProviderTests/SQLiteDatabaseMethodsTests.cs similarity index 72% rename from tests/DapperMatic.Tests/ProviderTests/SQLiteDatabaseTests.cs rename to tests/DapperMatic.Tests/ProviderTests/SQLiteDatabaseMethodsTests.cs index affa9fe..f76c48a 100644 --- a/tests/DapperMatic.Tests/ProviderTests/SQLiteDatabaseTests.cs +++ b/tests/DapperMatic.Tests/ProviderTests/SQLiteDatabaseMethodsTests.cs @@ -4,18 +4,20 @@ namespace DapperMatic.Tests.ProviderTests; -public class SQLiteDatabaseTests(ITestOutputHelper output) : DatabaseTests(output), IDisposable +public class SQLiteDatabaseMethodsTests(ITestOutputHelper output) + : DatabaseMethodsTests(output), + IDisposable { public override async Task OpenConnectionAsync() { if (File.Exists("sqlite_tests.sqlite")) File.Delete("sqlite_tests.sqlite"); - var connection = new SQLiteConnection( + var db = new SQLiteConnection( "Data Source=sqlite_tests.sqlite;Version=3;BinaryGuid=False;" ); - await connection.OpenAsync(); - return connection; + await db.OpenAsync(); + return db; } public override void Dispose() diff --git a/tests/DapperMatic.Tests/ProviderTests/SqlServerDatabaseTests.cs b/tests/DapperMatic.Tests/ProviderTests/SqlServerDatabaseMethodsTests.cs similarity index 56% rename from tests/DapperMatic.Tests/ProviderTests/SqlServerDatabaseTests.cs rename to tests/DapperMatic.Tests/ProviderTests/SqlServerDatabaseMethodsTests.cs index 31c8326..1e6bccb 100644 --- a/tests/DapperMatic.Tests/ProviderTests/SqlServerDatabaseTests.cs +++ b/tests/DapperMatic.Tests/ProviderTests/SqlServerDatabaseMethodsTests.cs @@ -8,41 +8,41 @@ namespace DapperMatic.Tests.ProviderTests; /// /// Testing SqlServer 2022 Linux (CU image) /// -public class SqlServer_2022_CU13_Ubuntu_DatabaseTests( +public class SqlServer_2022_CU13_Ubuntu_DatabaseMethodsTests( SqlServer_2022_CU13_Ubuntu_DatabaseFixture fixture, ITestOutputHelper output -) : SqlServerDatabaseTests(fixture, output) { } +) : SqlServerDatabaseMethodsests(fixture, output) { } /// /// Testing SqlServer 2019 /// -public class SqlServer_2019_CU27_DatabaseTests( +public class SqlServer_2019_CU27_DatabaseMethodsTests( SqlServer_2019_CU27_DatabaseFixture fixture, ITestOutputHelper output -) : SqlServerDatabaseTests(fixture, output) { } +) : SqlServerDatabaseMethodsests(fixture, output) { } /// /// Testing SqlServer 2017 /// -public class SqlServer_2017_CU29_DatabaseTests( +public class SqlServer_2017_CU29_DatabaseMethodsTests( SqlServer_2017_CU29_DatabaseFixture fixture, ITestOutputHelper output -) : SqlServerDatabaseTests(fixture, output) { } +) : SqlServerDatabaseMethodsests(fixture, output) { } /// /// Abstract class for Postgres database tests /// /// -public abstract class SqlServerDatabaseTests( +public abstract class SqlServerDatabaseMethodsests( TDatabaseFixture fixture, ITestOutputHelper output -) : DatabaseTests(output), IClassFixture, IDisposable +) : DatabaseMethodsTests(output), IClassFixture, IDisposable where TDatabaseFixture : SqlServerDatabaseFixture { public override async Task OpenConnectionAsync() { - var connection = new SqlConnection(fixture.ConnectionString); - await connection.OpenAsync(); - return connection; + var db = new SqlConnection(fixture.ConnectionString); + await db.OpenAsync(); + return db; } } diff --git a/tests/DapperMatic.Tests/TestBase.cs b/tests/DapperMatic.Tests/TestBase.cs new file mode 100644 index 0000000..85b2be4 --- /dev/null +++ b/tests/DapperMatic.Tests/TestBase.cs @@ -0,0 +1,59 @@ +using System.Data; +using DapperMatic.Logging; +using DapperMatic.Tests.Logging; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace DapperMatic.Tests; + +public abstract class TestBase : IDisposable +{ + protected readonly ITestOutputHelper Output; + + protected TestBase(ITestOutputHelper output) + { + Output = output; + + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(output)); + }); + DxLogger.SetLoggerFactory(loggerFactory); + } + + protected async Task InitFreshSchemaAsync(IDbConnection db, string? schemaName) + { + if (db.SupportsSchemas()) + { + foreach (var view in await db.GetViewsAsync(schemaName)) + { + try + { + await db.DropViewIfExistsAsync(schemaName, view.ViewName); + } + catch { } + } + foreach (var table in await db.GetTablesAsync(schemaName)) + { + await db.DropTableIfExistsAsync(schemaName, table.TableName); + } + // await db.DropSchemaIfExistsAsync(schemaName); + } + if (!string.IsNullOrEmpty(schemaName)) + { + await db.CreateSchemaIfNotExistsAsync(schemaName); + } + } + + public virtual void Dispose() + { + DxLogger.SetLoggerFactory(LoggerFactory.Create(builder => builder.ClearProviders())); + } + + protected void Log(string message) + { + Output.WriteLine(message); + } + + public virtual bool IgnoreSqlType(string sqlType) => false; +}