Skip to content

Commit 222bffb

Browse files
committed
New PostgreSQL database clean
1 parent 2de096b commit 222bffb

File tree

4 files changed

+271
-5
lines changed

4 files changed

+271
-5
lines changed

Test/UnitTests/TestDataLayer/TestPostgreSqlHelpers.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,49 @@ public void TestEnsureDeletedEnsureCreatedOk()
7777
context.Books.Count().ShouldEqual(4);
7878
}
7979

80+
[Fact]
81+
public void TestEnsureCleanExistingDatabaseOk()
82+
{
83+
//SETUP
84+
var options = this.CreatePostgreSqlUniqueDatabaseOptions<BookContext>();
85+
using var context = new BookContext(options);
86+
87+
context.Database.EnsureCreated();
88+
89+
using (new TimeThings(_output, "Time to EnsureClean"))
90+
{
91+
context.Database.EnsureClean();
92+
}
93+
94+
//ATTEMPT
95+
context.SeedDatabaseFourBooks();
96+
97+
//VERIFY
98+
context.Books.Count().ShouldEqual(4);
99+
}
100+
101+
102+
[Fact]
103+
public void TestEnsureCleanNoExistingDatabaseOk()
104+
{
105+
//SETUP
106+
var options = this.CreatePostgreSqlUniqueDatabaseOptions<BookContext>();
107+
using var context = new BookContext(options);
108+
109+
context.Database.EnsureDeleted();
110+
111+
using (new TimeThings(_output, "Time to EnsureClean"))
112+
{
113+
context.Database.EnsureClean();
114+
}
115+
116+
//ATTEMPT
117+
context.SeedDatabaseFourBooks();
118+
119+
//VERIFY
120+
context.Books.Count().ShouldEqual(4);
121+
}
122+
80123
[Fact]
81124
public async Task TestEnsureCreatedAndEmptyPostgreSqlOk()
82125
{
@@ -90,6 +133,10 @@ public async Task TestEnsureCreatedAndEmptyPostgreSqlOk()
90133
using (var context = new BookContext(options))
91134
{
92135
//ATTEMPT
136+
<<<<<<< Updated upstream
137+
=======
138+
//logOptions.ShowLog = true;
139+
>>>>>>> Stashed changes
93140
using (new TimeThings(_output, "Time to empty database"))
94141
{
95142
await context.EnsureCreatedAndEmptyPostgreSqlAsync();

TestSupport/EfHelpers/CleanDatabaseExtensions.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ public static class CleanDatabaseExtensions
2323
/// <param name="setUpSchema">Optional: by default it will set the schema to match the current DbContext configuration. If false leaves the database empty</param>
2424
public static void EnsureClean(this DatabaseFacade databaseFacade, bool setUpSchema = true)
2525
{
26-
if (!databaseFacade.IsSqlServer())
27-
throw new InvalidOperationException("The EnsureClean method only works with ");
28-
29-
databaseFacade.CreateExecutionStrategy()
30-
.Execute(databaseFacade, database => new SqlServerDatabaseCleaner(databaseFacade).Clean(database, setUpSchema));
26+
if (databaseFacade.IsSqlServer())
27+
//SQL Server
28+
databaseFacade.CreateExecutionStrategy()
29+
.Execute(databaseFacade, database => new SqlServerDatabaseCleaner(databaseFacade).Clean(database, setUpSchema));
30+
else if (databaseFacade.IsNpgsql())
31+
//PostgreSQL
32+
databaseFacade.CreateExecutionStrategy()
33+
.Execute(databaseFacade, database => new NpgsqlDatabaseCleaner(databaseFacade).Clean(database, setUpSchema));
34+
else
35+
throw new InvalidOperationException("The EnsureClean method only works with SQL Server or PostgreSQL databases.");
3136
}
3237
}
3338
}

TestSupport/EfHelpers/Internal/DesignProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.EntityFrameworkCore.Infrastructure;
99
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
1010
using Microsoft.Extensions.DependencyInjection;
11+
using Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal;
1112

1213
#pragma warning disable EF1001 // Internal EF Core API usage.
1314
namespace TestSupport.EfHelpers.Internal
@@ -31,6 +32,9 @@ public static IDesignTimeServices GetDesignTimeService(this DatabaseFacade datab
3132
if (databaseFacade.IsSqlServer())
3233
//Only handles SQL Server
3334
return new SqlServerDesignTimeServices();
35+
else if (databaseFacade.IsNpgsql())
36+
//Only handles SQL Server
37+
return new NpgsqlDesignTimeServices();
3438

3539
throw new InvalidOperationException("This is not a database provider that we currently support.");
3640
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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

Comments
 (0)