From bf4b329a96f675ca2a93d22487abd8dd14578d2f Mon Sep 17 00:00:00 2001 From: mjc Date: Wed, 11 Sep 2024 23:31:06 -0500 Subject: [PATCH] Updates to foreignkey retrieval --- .../MySqlExtensions.ForeignKeyMethods.cs | 130 ++++++------ .../PostgreSqlExtensions.ForeignKeyMethods.cs | 186 +++++++++++++++--- .../SqlServerExtensions.ForeignKeyMethods.cs | 178 +++++++++++++---- .../SqlServerExtensions.IndexMethods.cs | 70 ++++--- .../SqliteExtensions.ForeignKeyMethods.cs | 126 +++++++++--- tests/DapperMatic.Tests/DatabaseTests.cs | 47 +++-- 6 files changed, 536 insertions(+), 201 deletions(-) diff --git a/src/DapperMatic/Providers/MySql/MySqlExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/MySql/MySqlExtensions.ForeignKeyMethods.cs index e9bea2c..8701fc0 100644 --- a/src/DapperMatic/Providers/MySql/MySqlExtensions.ForeignKeyMethods.cs +++ b/src/DapperMatic/Providers/MySql/MySqlExtensions.ForeignKeyMethods.cs @@ -141,12 +141,12 @@ public async Task> GetForeignKeysAsync( (_, tableName, _) = NormalizeNames(schemaName, tableName); var where = string.IsNullOrWhiteSpace(nameFilter) - ? "" - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); + ? "" + : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); var sql = $@"SELECT - kcu.CONSTRAINT_NAME as constraing_name, + 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, @@ -155,54 +155,54 @@ public async Task> GetForeignKeysAsync( 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()"; - if (string.IsNullOrWhiteSpace(tableName)) - sql += $@" AND kcu.TABLE_NAME = @tableName AND REFERENCED_TABLE_NAME IS NOT NULL"; + 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); + 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 => + return results.Select(r => + { + var deleteRule = r.delete_rule switch { - 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 - ); - } - ); + "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 Task> GetForeignKeyNamesAsync( + public async Task> GetForeignKeyNamesAsync( IDbConnection db, string? tableName, string? nameFilter = null, @@ -211,41 +211,23 @@ public Task> GetForeignKeyNamesAsync( CancellationToken cancellationToken = default ) { - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - (_, tableName, _) = NormalizeNames(schemaName, tableName); - 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 = 'FOREIGN KEY' - ORDER BY CONSTRAINT_NAME", - new { tableName }, - tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return QueryAsync( - db, - $@"SELECT CONSTRAINT_NAME + var where = string.IsNullOrWhiteSpace(nameFilter) + ? null + : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); + + var sql = + $@"SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND - TABLE_NAME = @tableName AND - CONSTRAINT_TYPE = 'FOREIGN KEY' AND - CONSTRAINT_NAME LIKE @where - ORDER BY CONSTRAINT_NAME", - new { tableName, where }, - tx - ); - } + 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); } /// diff --git a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ForeignKeyMethods.cs index dddd33e..43ae676 100644 --- a/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ForeignKeyMethods.cs +++ b/src/DapperMatic/Providers/PostgreSql/PostgreSqlExtensions.ForeignKeyMethods.cs @@ -29,7 +29,12 @@ FROM information_schema.table_constraints table_name = @tableName AND constraint_name = @foreignKeyName AND constraint_type = 'FOREIGN KEY'", - new { schemaName, tableName, foreignKeyName }, + new + { + schemaName, + tableName, + foreignKeyName + }, tx ) .ConfigureAwait(false); @@ -54,7 +59,12 @@ FROM information_schema.key_column_usage table_name = @tableName AND column_name = @columnName )", - new { schemaName, tableName, columnName }, + new + { + schemaName, + tableName, + columnName + }, tx ) .ConfigureAwait(false); @@ -130,7 +140,7 @@ ON DELETE {onDelete} return true; } - public Task> GetForeignKeysAsync( + public async Task> GetForeignKeysAsync( IDbConnection db, string? tableName, string? nameFilter = null, @@ -139,10 +149,116 @@ public Task> GetForeignKeysAsync( CancellationToken cancellationToken = default ) { - throw new NotImplementedException(); + (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 Task> GetForeignKeyNamesAsync( + public async Task> GetForeignKeyNamesAsync( IDbConnection db, string? tableName, string? nameFilter = null, @@ -153,31 +269,36 @@ public Task> GetForeignKeyNamesAsync( { (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return QueryAsync( - db, - $@"SELECT conname - FROM pg_constraint - WHERE conrelid = '{schemaName}.{tableName}'::regclass - AND contype = 'f'", - transaction: tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); - return QueryAsync( + 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, - $@"SELECT conname - FROM pg_constraint - WHERE conrelid = '{schemaName}.{tableName}'::regclass - AND contype = 'f' - AND conname LIKE @where", - new { schemaName, tableName, where }, - transaction: tx - ); - } + sql, + new + { + schemaName, + tableName, + where + }, + tx + ) + .ConfigureAwait(false); } public async Task DropForeignKeyIfExistsAsync( @@ -241,7 +362,12 @@ FROM pg_attribute AND attname = @columnName ) )", - new { schemaName, tableName, columnName }, + new + { + schemaName, + tableName, + columnName + }, tx ) .ConfigureAwait(false); diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ForeignKeyMethods.cs index 43b6774..b1e0e07 100644 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ForeignKeyMethods.cs +++ b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.ForeignKeyMethods.cs @@ -28,7 +28,12 @@ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TABLE_NAME = @tableName AND CONSTRAINT_NAME = @foreignKeyName AND CONSTRAINT_TYPE = 'FOREIGN KEY'", - new { schemaName, tableName, foreignKeyName }, + new + { + schemaName, + tableName, + foreignKeyName + }, tx ) .ConfigureAwait(false); @@ -49,7 +54,12 @@ 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 }, + new + { + schemaName, + tableName, + columnName + }, tx ) .ConfigureAwait(false); @@ -126,7 +136,7 @@ ON DELETE {onDelete} return true; } - public Task> GetForeignKeysAsync( + public async Task> GetForeignKeysAsync( IDbConnection db, string? tableName, string? nameFilter = null, @@ -135,10 +145,109 @@ public Task> GetForeignKeysAsync( CancellationToken cancellationToken = default ) { - throw new NotImplementedException(); + (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 Task> GetForeignKeyNamesAsync( + public async Task> GetForeignKeyNamesAsync( IDbConnection db, string? tableName, string? nameFilter = null, @@ -149,37 +258,35 @@ public Task> GetForeignKeyNamesAsync( { (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return QueryAsync( - db, - $@"SELECT CONSTRAINT_NAME + var where = string.IsNullOrWhiteSpace(nameFilter) + ? null + : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); + + var sql = + $@"SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = @schemaName AND - TABLE_NAME = @tableName AND - CONSTRAINT_TYPE = 'FOREIGN KEY' - ORDER BY CONSTRAINT_NAME", - new { schemaName, tableName }, - tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); + 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 QueryAsync( + return await QueryAsync( db, - $@"SELECT CONSTRAINT_NAME - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_SCHEMA = @schemaName AND - TABLE_NAME = @tableName AND - CONSTRAINT_TYPE = 'FOREIGN KEY' AND - CONSTRAINT_NAME LIKE @where - ORDER BY CONSTRAINT_NAME", - new { schemaName, tableName, where }, + sql, + new + { + schemaName, + tableName, + where + }, tx - ); - } + ) + .ConfigureAwait(false); } public async Task DropForeignKeyIfExistsAsync( @@ -234,7 +341,12 @@ 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 }, + new + { + schemaName, + tableName, + columnName + }, tx ) .ConfigureAwait(false); diff --git a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.IndexMethods.cs b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.IndexMethods.cs index df76bd2..41683c1 100644 --- a/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.IndexMethods.cs +++ b/src/DapperMatic/Providers/SqlServer/SqlServerExtensions.IndexMethods.cs @@ -81,8 +81,8 @@ public async Task> GetIndexesAsync( (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); + ? null + : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); var sql = @$"SELECT @@ -100,21 +100,33 @@ FROM sys.indexes ind 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" + ? "" + : " 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 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), @@ -131,14 +143,12 @@ FROM sys.indexes ind table_name, index_name, group - .Select( - g => - { - var col = g.column_name; - var direction = g.is_descending_key == 1 ? "DESC" : "ASC"; - return $"{col} {direction}"; - } - ) + .Select(g => + { + var col = g.column_name; + var direction = g.is_descending_key == 1 ? "DESC" : "ASC"; + return $"{col} {direction}"; + }) .ToArray(), is_unique == 1 ); @@ -160,8 +170,8 @@ public async Task> GetIndexNamesAsync( (schemaName, tableName, _) = NormalizeNames(schemaName, tableName); var where = string.IsNullOrWhiteSpace(nameFilter) - ? null - : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); + ? null + : $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); var sql = @$"SELECT ind.name @@ -170,14 +180,24 @@ FROM sys.indexes ind 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" + ? "" + : " 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) + return await QueryAsync( + db, + sql, + new + { + schemaName, + tableName, + where + }, + tx + ) .ConfigureAwait(false); } diff --git a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ForeignKeyMethods.cs b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ForeignKeyMethods.cs index fc7b4bd..398dc1c 100644 --- a/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ForeignKeyMethods.cs +++ b/src/DapperMatic/Providers/Sqlite/SqliteExtensions.ForeignKeyMethods.cs @@ -161,7 +161,7 @@ await ExecuteAsync(db, $@"DROP TABLE '{tableName}_old'", tx ?? innerTx) return true; } - public Task> GetForeignKeysAsync( + public async Task> GetForeignKeysAsync( IDbConnection db, string? tableName, string? nameFilter = null, @@ -170,10 +170,81 @@ public Task> GetForeignKeysAsync( CancellationToken cancellationToken = default ) { - throw new NotImplementedException(); + (_, 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 Task> GetForeignKeyNamesAsync( + public async Task> GetForeignKeyNamesAsync( IDbConnection db, string? tableName, string? nameFilter = null, @@ -182,36 +253,35 @@ public Task> GetForeignKeyNamesAsync( CancellationToken cancellationToken = default ) { - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentException("Table name must be specified.", nameof(tableName)); - (_, tableName, _) = NormalizeNames(schemaName, tableName); - if (string.IsNullOrWhiteSpace(nameFilter)) - { - return QueryAsync( - db, - $@"SELECT 'fk_{tableName}'||'_'||""from""||'_'||""tableName""||'_'||""to"" CONSTRAINT_NAME, * - FROM pragma_foreign_key_list('{tableName}') - ORDER BY CONSTRAINT_NAME", - new { tableName }, - tx - ); - } - else - { - var where = $"{ToAlphaNumericString(nameFilter)}".Replace("*", "%"); + 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 QueryAsync( + return await QueryAsync( db, - $@"SELECT 'fk_{tableName}'||'_'||""from""||'_'||""tableName""||'_'||""to"" CONSTRAINT_NAME, * - FROM pragma_foreign_key_list('{tableName}') - WHERE CONSTRAINT_NAME LIKE @where - ORDER BY CONSTRAINT_NAME", - new { tableName, where }, + sql, + new + { + schemaName, + tableName, + where + }, tx - ); - } + ) + .ConfigureAwait(false); } /// diff --git a/tests/DapperMatic.Tests/DatabaseTests.cs b/tests/DapperMatic.Tests/DatabaseTests.cs index c45aeef..e34bef8 100644 --- a/tests/DapperMatic.Tests/DatabaseTests.cs +++ b/tests/DapperMatic.Tests/DatabaseTests.cs @@ -430,10 +430,10 @@ protected virtual async Task Database_Can_CrudTableIndexesAsync() var version = await connection.GetDatabaseVersionAsync(); Assert.NotEmpty(version); - + var supportsDescendingColumnSorts = true; var dbType = connection.GetDatabaseType(); - if (dbType.HasFlag(DatabaseTypes.MySql)) + if (dbType.HasFlag(DatabaseTypes.MySql)) { if (version.StartsWith("5.")) { @@ -561,12 +561,22 @@ await connection.CreateIndexIfNotExistsAsync( Assert.NotNull(idxMulti2); Assert.True(idxMulti1.Unique); Assert.True(idxMulti1.ColumnNames.Length == 2); - if (supportsDescendingColumnSorts) Assert.EndsWith("desc", idxMulti1.ColumnNames[0], StringComparison.OrdinalIgnoreCase); + 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); + if (supportsDescendingColumnSorts) + Assert.EndsWith( + "desc", + idxMulti2.ColumnNames[1], + StringComparison.OrdinalIgnoreCase + ); output.WriteLine($"Dropping indexName: {tableName}.{indexName}"); await connection.DropIndexIfExistsAsync(tableName, indexName); @@ -593,6 +603,8 @@ protected virtual async Task Database_Can_CrudTableForeignKeysAsync() const string columnName = "testFkColumn"; const string foreignKeyName = "testFk"; + var supportsForeignKeyNaming = await connection.SupportsNamedForeignKeysAsync(); + await connection.CreateTableIfNotExistsAsync(tableName); await connection.CreateTableIfNotExistsAsync(refTableName); await connection.CreateColumnIfNotExistsAsync( @@ -623,7 +635,7 @@ await connection.CreateForeignKeyIfNotExistsAsync( output.WriteLine($"Get Foreign Key Names: {tableName}"); var fkNames = await connection.GetForeignKeyNamesAsync(tableName); - if (await connection.SupportsNamedForeignKeysAsync()) + if (supportsForeignKeyNaming) { Assert.Contains( fkNames, @@ -635,19 +647,32 @@ await connection.CreateForeignKeyIfNotExistsAsync( var fks = await connection.GetForeignKeysAsync(tableName); Assert.Contains( fks, - fk => fk.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) + fk => + fk.TableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) && fk.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase) - && fk.ForeignKeyName.Equals(foreignKeyName, 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: {tableName}.{foreignKeyName}"); - await connection.DropForeignKeyIfExistsAsync(tableName, columnName, foreignKeyName); + 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: {tableName}.{foreignKeyName}"); - exists = await connection.ForeignKeyExistsAsync(tableName, columnName, foreignKeyName); + output.WriteLine($"Foreign Key Exists: {foreignKeyName}"); + exists = supportsForeignKeyNaming + ? await connection.ForeignKeyExistsAsync(tableName, columnName, foreignKeyName) + : await connection.ForeignKeyExistsAsync(tableName, columnName); Assert.False(exists); }