Skip to content

Commit a360244

Browse files
dkottisroji
andcommitted
Filter out extension-related operations in creation scripts of history table (#3713)
Fixes #3496 Co-authored-by: Shay Rojansky <roji@roji.org> (cherry picked from commit 8237cba)
1 parent 632d7fb commit a360244

File tree

2 files changed

+102
-11
lines changed

2 files changed

+102
-11
lines changed

src/EFCore.PG/Migrations/Internal/NpgsqlHistoryRepository.cs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
2+
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
23

34
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal;
45

@@ -101,14 +102,40 @@ protected override IReadOnlyList<MigrationCommand> GetCreateCommands()
101102
{
102103
// TODO: This is all a hack around https://github.com/dotnet/efcore/issues/34991: we have provider-specific conventions which add
103104
// enums and extensions to the model, and the default EF logic causes them to be created at this point, when the history table is
104-
// being created.
105+
// being created. This causes problems when:
106+
// (a) we create an enum here when creating the history table, and then try to create it again when the actual migration
107+
// runs (#3324), and
108+
// (b) we shouldn't be creating extensions at this early point either, and doing so can cause issues (e.g. #3496).
109+
//
110+
// So we filter out any extension/enum migration operations.
111+
// Note that the approach in EF is to remove specific conventions (e.g. DbSetFindingConvention), but we don't want to hardcode
112+
// specific conventions here; for example, the NetTopologySuite plugin has its NpgsqlNetTopologySuiteExtensionAddingConvention
113+
// which adds PostGIS. So we just filter out the annotations on the operations themselves.
105114
var model = EnsureModel();
106115

107116
var operations = Dependencies.ModelDiffer.GetDifferences(null, model.GetRelationalModel());
108-
var commandList = Dependencies.MigrationsSqlGenerator.Generate(operations, model);
109-
return commandList;
117+
118+
foreach (var operation in operations)
119+
{
120+
if (operation is not AlterDatabaseOperation alterDatabaseOperation)
121+
{
122+
continue;
123+
}
124+
125+
foreach (var annotation in alterDatabaseOperation.GetAnnotations())
126+
{
127+
if (annotation.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal)
128+
|| annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal))
129+
{
130+
alterDatabaseOperation.RemoveAnnotation(annotation.Name);
131+
}
132+
}
133+
}
134+
135+
return Dependencies.MigrationsSqlGenerator.Generate(operations, model);
110136
}
111137

138+
// Copied as-is from EF's HistoryRepository, since it's private (see https://github.com/dotnet/efcore/issues/34991)
112139
private IModel EnsureModel()
113140
{
114141
if (_model == null)
@@ -117,16 +144,13 @@ private IModel EnsureModel()
117144

118145
conventionSet.Remove(typeof(DbSetFindingConvention));
119146
conventionSet.Remove(typeof(RelationalDbFunctionAttributeConvention));
120-
// TODO: this whole method exists only so we can remove this convention (https://github.com/dotnet/efcore/issues/34991)
121-
conventionSet.Remove(typeof(NpgsqlPostgresModelFinalizingConvention));
122147

123148
var modelBuilder = new ModelBuilder(conventionSet);
124-
modelBuilder.Entity<HistoryRow>(
125-
x =>
126-
{
127-
ConfigureTable(x);
128-
x.ToTable(TableName, TableSchema);
129-
});
149+
modelBuilder.Entity<HistoryRow>(x =>
150+
{
151+
ConfigureTable(x);
152+
x.ToTable(TableName, TableSchema);
153+
});
130154

131155
_model = Dependencies.ModelRuntimeInitializer.Initialize(
132156
(IModel)modelBuilder.Model, designTime: true, validationLogger: null);

test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,73 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'my') THEN
7777
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
7878
);
7979
80+
""", sql, ignoreLineEndingDifferences: true);
81+
}
82+
83+
[ConditionalFact]
84+
public void GetCreateIfNotExistsScript_does_not_include_extensions()
85+
{
86+
var historyRepository = new TestDbContext(
87+
new DbContextOptionsBuilder()
88+
.UseInternalServiceProvider(
89+
NpgsqlTestHelpers.Instance.CreateServiceProvider(
90+
new ServiceCollection().AddEntityFrameworkNpgsqlNetTopologySuite()))
91+
.UseNpgsql(
92+
new NpgsqlConnection("Host=localhost;Database=DummyDatabase"),
93+
b => b.MigrationsHistoryTable(HistoryRepository.DefaultTableName, "some_schema")
94+
.UseNetTopologySuite())
95+
.Options)
96+
.GetService<IHistoryRepository>();;
97+
98+
var sql = historyRepository.GetCreateIfNotExistsScript();
99+
100+
Assert.Equal(
101+
"""
102+
DO $EF$
103+
BEGIN
104+
IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN
105+
CREATE SCHEMA some_schema;
106+
END IF;
107+
END $EF$;
108+
CREATE TABLE IF NOT EXISTS some_schema."__EFMigrationsHistory" (
109+
"MigrationId" character varying(150) NOT NULL,
110+
"ProductVersion" character varying(32) NOT NULL,
111+
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
112+
);
113+
114+
""", sql, ignoreLineEndingDifferences: true);
115+
}
116+
117+
enum TestEnum { Value1, Value2 }
118+
119+
[ConditionalFact]
120+
public void GetCreateIfNotExistsScript_does_not_include_enums()
121+
{
122+
var historyRepository = new TestDbContext(
123+
new DbContextOptionsBuilder()
124+
.UseNpgsql(
125+
new NpgsqlConnection("Host=localhost;Database=DummyDatabase"),
126+
b => b.MigrationsHistoryTable(HistoryRepository.DefaultTableName, "some_schema")
127+
.MapEnum<TestEnum>("test_enum"))
128+
.Options)
129+
.GetService<IHistoryRepository>();;
130+
131+
var sql = historyRepository.GetCreateIfNotExistsScript();
132+
133+
Assert.Equal(
134+
"""
135+
DO $EF$
136+
BEGIN
137+
IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN
138+
CREATE SCHEMA some_schema;
139+
END IF;
140+
END $EF$;
141+
CREATE TABLE IF NOT EXISTS some_schema."__EFMigrationsHistory" (
142+
"MigrationId" character varying(150) NOT NULL,
143+
"ProductVersion" character varying(32) NOT NULL,
144+
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
145+
);
146+
80147
""", sql, ignoreLineEndingDifferences: true);
81148
}
82149

0 commit comments

Comments
 (0)