|
| 1 | +// Copyright (c) .NET Foundation. All rights reserved. |
| 2 | +// Licensed under the Apache License, Version 2.0. |
| 3 | +// Available at https://github.com/dotnet/efcore/blob/main/LICENSE.txt |
| 4 | +// Altered by Jon P Smith, GitHub @JonPSmith, https://www.thereformedprogrammer.net/ |
| 5 | + |
| 6 | +using System.Collections.Generic; |
| 7 | +using System.Data.Common; |
| 8 | +using System.Diagnostics; |
| 9 | +using System.Linq; |
| 10 | +using System.Text; |
| 11 | +using Microsoft.EntityFrameworkCore; |
| 12 | +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; |
| 13 | +using Microsoft.EntityFrameworkCore.Infrastructure; |
| 14 | +using Microsoft.EntityFrameworkCore.Internal; |
| 15 | +using Microsoft.EntityFrameworkCore.Scaffolding; |
| 16 | +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; |
| 17 | +using Microsoft.EntityFrameworkCore.Storage; |
| 18 | +using Microsoft.Extensions.DependencyInjection; |
| 19 | +using Microsoft.Extensions.Logging; |
| 20 | +using Npgsql; |
| 21 | +using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; |
| 22 | +using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; |
| 23 | +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; |
| 24 | + |
| 25 | +namespace TestSupport.EfHelpers.Internal |
| 26 | +{ |
| 27 | + internal class NpgsqlDatabaseCleaner : RelationalDatabaseCleaner |
| 28 | + { |
| 29 | +#pragma warning disable EF1001 // Internal EF Core API usage. |
| 30 | + private readonly NpgsqlSqlGenerationHelper _sqlGenerationHelper; |
| 31 | + |
| 32 | + public NpgsqlDatabaseCleaner() |
| 33 | + => _sqlGenerationHelper = new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()); |
| 34 | +#pragma warning restore EF1001 // Internal EF Core API usage. |
| 35 | + |
| 36 | + private readonly DatabaseFacade _databaseFacade; |
| 37 | + public NpgsqlDatabaseCleaner(DatabaseFacade databaseFacade) |
| 38 | + { |
| 39 | + _databaseFacade = databaseFacade; |
| 40 | + } |
| 41 | + |
| 42 | + //protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) |
| 43 | + // => new NpgsqlDatabaseModelFactory( |
| 44 | + // new DiagnosticsLogger<DbLoggerCategory.Scaffolding>( |
| 45 | + // loggerFactory, |
| 46 | + // new LoggingOptions(), |
| 47 | + // new DiagnosticListener("Fake"), |
| 48 | + // new NpgsqlLoggingDefinitions(), |
| 49 | + // new NullDbContextLogger())); |
| 50 | + |
| 51 | + protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) |
| 52 | + { |
| 53 | + var designTimeService = _databaseFacade.GetDesignTimeService(); |
| 54 | + var serviceProvider = designTimeService.GetDesignTimeProvider(); |
| 55 | + return serviceProvider.GetService<IDatabaseModelFactory>(); |
| 56 | + } |
| 57 | + |
| 58 | + protected override bool AcceptIndex(DatabaseIndex index) |
| 59 | + => false; |
| 60 | + |
| 61 | + public override void Clean(DatabaseFacade facade, bool setUpSchema) |
| 62 | + { |
| 63 | + // The following is somewhat hacky |
| 64 | + // PostGIS creates some system tables (e.g. spatial_ref_sys) which can't be dropped until the extension |
| 65 | + // is dropped. But our tests create some user tables which depend on PostGIS. So we clean out PostGIS |
| 66 | + // and all tables that depend on it (CASCADE) before the database model is built. |
| 67 | + var creator = facade.GetService<IRelationalDatabaseCreator>(); |
| 68 | + var connection = facade.GetService<IRelationalConnection>(); |
| 69 | + if (creator.Exists()) |
| 70 | + { |
| 71 | + connection.Open(); |
| 72 | + try |
| 73 | + { |
| 74 | + var conn = (NpgsqlConnection)connection.DbConnection; |
| 75 | + DropExtensions(conn); |
| 76 | + DropTypes(conn); |
| 77 | + DropFunctions(conn); |
| 78 | + DropCollations(conn); |
| 79 | + } |
| 80 | + finally |
| 81 | + { |
| 82 | + connection.Close(); |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + base.Clean(facade, setUpSchema); |
| 87 | + } |
| 88 | + |
| 89 | + private void DropExtensions(NpgsqlConnection conn) |
| 90 | + { |
| 91 | + const string getExtensions = @" |
| 92 | +SELECT name FROM pg_available_extensions WHERE installed_version IS NOT NULL AND name <> 'plpgsql'"; |
| 93 | + |
| 94 | + List<string> extensions; |
| 95 | + using (var cmd = new NpgsqlCommand(getExtensions, conn)) |
| 96 | + { |
| 97 | + using var reader = cmd.ExecuteReader(); |
| 98 | + extensions = reader.Cast<DbDataRecord>().Select(r => r.GetString(0)).ToList(); |
| 99 | + } |
| 100 | + |
| 101 | + if (extensions.Any()) |
| 102 | + { |
| 103 | + var dropExtensionsSql = string.Join("", extensions.Select(e => $"DROP EXTENSION \"{e}\" CASCADE;")); |
| 104 | + using var cmd = new NpgsqlCommand(dropExtensionsSql, conn); |
| 105 | + cmd.ExecuteNonQuery(); |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + /// <summary> |
| 110 | + /// Drop user-defined ranges and enums, cascading to all tables which depend on them |
| 111 | + /// </summary> |
| 112 | + private void DropTypes(NpgsqlConnection conn) |
| 113 | + { |
| 114 | + const string getUserDefinedRangesEnums = @" |
| 115 | +SELECT ns.nspname, typname |
| 116 | +FROM pg_type |
| 117 | +JOIN pg_namespace AS ns ON ns.oid = pg_type.typnamespace |
| 118 | +WHERE typtype IN ('r', 'e') AND nspname <> 'pg_catalog'"; |
| 119 | + |
| 120 | + (string Schema, string Name)[] userDefinedTypes; |
| 121 | + using (var cmd = new NpgsqlCommand(getUserDefinedRangesEnums, conn)) |
| 122 | + { |
| 123 | + using var reader = cmd.ExecuteReader(); |
| 124 | + userDefinedTypes = reader.Cast<DbDataRecord>().Select(r => (r.GetString(0), r.GetString(1))).ToArray(); |
| 125 | + } |
| 126 | + |
| 127 | + if (userDefinedTypes.Any()) |
| 128 | + { |
| 129 | + var dropTypes = string.Concat(userDefinedTypes.Select(t => $@"DROP TYPE ""{t.Schema}"".""{t.Name}"" CASCADE;")); |
| 130 | + using var cmd = new NpgsqlCommand(dropTypes, conn); |
| 131 | + cmd.ExecuteNonQuery(); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + /// <summary> |
| 136 | + /// Drop all user-defined functions and procedures |
| 137 | + /// </summary> |
| 138 | + private void DropFunctions(NpgsqlConnection conn) |
| 139 | + { |
| 140 | + const string getUserDefinedFunctions = @" |
| 141 | +SELECT 'DROP ROUTINE ""' || nspname || '"".""' || proname || '""(' || oidvectortypes(proargtypes) || ');' FROM pg_proc |
| 142 | +JOIN pg_namespace AS ns ON ns.oid = pg_proc.pronamespace |
| 143 | +WHERE |
| 144 | + nspname NOT IN ('pg_catalog', 'information_schema') AND |
| 145 | + NOT EXISTS ( |
| 146 | + SELECT * FROM pg_depend AS dep |
| 147 | + WHERE dep.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_proc') AND |
| 148 | + dep.objid = pg_proc.oid AND |
| 149 | + deptype = 'e');"; |
| 150 | + |
| 151 | + string dropSql; |
| 152 | + using (var cmd = new NpgsqlCommand(getUserDefinedFunctions, conn)) |
| 153 | + { |
| 154 | + using var reader = cmd.ExecuteReader(); |
| 155 | + dropSql = string.Join("", reader.Cast<DbDataRecord>().Select(r => r.GetString(0))); |
| 156 | + } |
| 157 | + |
| 158 | + if (dropSql != "") |
| 159 | + { |
| 160 | + using var cmd = new NpgsqlCommand(dropSql, conn); |
| 161 | + cmd.ExecuteNonQuery(); |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + private void DropCollations(NpgsqlConnection conn) |
| 166 | + { |
| 167 | +#if NET6_0_OR_GREATER |
| 168 | + if (conn.Settings.ServerCompatibilityMode == ServerCompatibilityMode.Redshift) |
| 169 | + { |
| 170 | + return; |
| 171 | + } |
| 172 | +#endif |
| 173 | + |
| 174 | + const string getUserCollations = @"SELECT nspname, collname |
| 175 | +FROM pg_collation coll |
| 176 | + JOIN pg_namespace ns ON ns.oid=coll.collnamespace |
| 177 | + JOIN pg_authid auth ON auth.oid = coll.collowner WHERE rolname <> 'postgres'; |
| 178 | +"; |
| 179 | + |
| 180 | + (string Schema, string Name)[] userDefinedTypes; |
| 181 | + using (var cmd = new NpgsqlCommand(getUserCollations, conn)) |
| 182 | + { |
| 183 | + using var reader = cmd.ExecuteReader(); |
| 184 | + userDefinedTypes = reader.Cast<DbDataRecord>().Select(r => (r.GetString(0), r.GetString(1))).ToArray(); |
| 185 | + } |
| 186 | + |
| 187 | + if (userDefinedTypes.Any()) |
| 188 | + { |
| 189 | + var dropTypes = string.Concat(userDefinedTypes.Select(t => $@"DROP COLLATION ""{t.Schema}"".""{t.Name}"" CASCADE;")); |
| 190 | + using var cmd = new NpgsqlCommand(dropTypes, conn); |
| 191 | + cmd.ExecuteNonQuery(); |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + protected override string BuildCustomSql(DatabaseModel databaseModel) |
| 196 | + // Some extensions create tables (e.g. PostGIS), so we must drop them first. |
| 197 | + => databaseModel.GetPostgresExtensions() |
| 198 | + .Select(e => _sqlGenerationHelper.DelimitIdentifier(e.Name, e.Schema)) |
| 199 | + .Aggregate(new StringBuilder(), |
| 200 | + (builder, s) => builder.Append("DROP EXTENSION ").Append(s).Append(";"), |
| 201 | + builder => builder.ToString()); |
| 202 | + |
| 203 | + protected override string BuildCustomEndingSql(DatabaseModel databaseModel) |
| 204 | + => databaseModel.GetPostgresEnums() |
| 205 | + .Select(e => _sqlGenerationHelper.DelimitIdentifier(e.Name, e.Schema)) |
| 206 | + .Aggregate(new StringBuilder(), |
| 207 | + (builder, s) => builder.Append("DROP TYPE ").Append(s).Append(" CASCADE;"), |
| 208 | + builder => builder.ToString()); |
| 209 | + } |
| 210 | +} |
0 commit comments