From 7aa8be01820f9a5cfb918ce19bbdded8babb4e76 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 18 Nov 2024 16:30:17 +0100 Subject: [PATCH 01/24] Bump version to 9.0.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 809a7eca0..ffeac1f3b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 9.0.0 + 9.0.1 latest true latest From 2e9a27fa9e7c2988a39d98c9b89cafdcf5ccf42b Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 19 Nov 2024 13:28:14 +0200 Subject: [PATCH 02/24] Depend on Npgsql 9.0.1 (#3376) (cherry picked from commit 773a330bf9ea59207b7a45b1496f77734847e859) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8ebfed249..59515d0f6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ [9.0.0,10.0.0) 9.0.0 - 9.0.0 + 9.0.1 From 051d00cbbc51384c8c9a359881e569a1153356fa Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 19 Nov 2024 13:05:56 +0100 Subject: [PATCH 03/24] Bump version to 9.0.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index ffeac1f3b..712d9ae07 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 9.0.1 + 9.0.2 latest true latest From 17e2fc83c823823260e1e92a27ca53f5227ad02f Mon Sep 17 00:00:00 2001 From: Victor Irzak <6209775+virzak@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:15:59 -0500 Subject: [PATCH 04/24] Restore virtual modifier (#3382) (cherry picked from commit c6fb5e21e11466c7a0bdbbecdf09693966701fbd) --- .../Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs index fa467d127..f4cf99980 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs @@ -27,6 +27,6 @@ public NpgsqlParameterBasedSqlProcessorFactory( /// /// Parameters for . /// A relational parameter based sql processor. - public RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) + public virtual RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) => new NpgsqlParameterBasedSqlProcessor(_dependencies, parameters); } From 697dd7182173ff3450eddc63b44837fba76331df Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 7 Dec 2024 11:04:54 +0100 Subject: [PATCH 05/24] Bump Npgsql dependency to 9.0.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 59515d0f6..362c7099e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ [9.0.0,10.0.0) 9.0.0 - 9.0.1 + 9.0.2 From e5e901e32c5515834f362d309cabda2bb692e1c2 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 7 Dec 2024 18:58:14 +0100 Subject: [PATCH 06/24] Bump version to 9.0.3 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 712d9ae07..6c16b9a8b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 9.0.2 + 9.0.3 latest true latest From c240ce58862c4aa4708026b4f5ceab4c1aa1000b Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 28 Dec 2024 17:02:47 +0100 Subject: [PATCH 07/24] Redo enum label addition (#3425) To better handling reordering scenarios. Fixes #3424 (cherry picked from commit c2b3de42d62ff96423d5e7dcba54f3994d7b8477) --- .../NpgsqlMigrationsSqlGenerator.cs | 40 ++++++++++++++++--- .../Migrations/MigrationsNpgsqlTest.cs | 20 +++++++++- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index 800941791..f93fd5c59 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -1266,18 +1266,34 @@ protected virtual void GenerateEnumStatements(AlterDatabaseOperation operation, + "https://www.postgresql.org/docs/current/sql-altertype.html)"); } - for (var (newPos, oldPos) = (0, 0); newPos < newLabels.Count; newPos++) + for (var newPos = 0; newPos < newLabels.Count; newPos++) { var newLabel = newLabels[newPos]; - var oldLabel = oldPos < oldLabels.Count ? oldLabels[oldPos] : null; - - if (newLabel == oldLabel) + if (oldLabels.Contains(newLabel)) { - oldPos++; continue; } - GenerateAddEnumLabel(newEnum, newLabel, oldLabel, model, builder); + // We add the new label just after the last one we have in the new labels definition (when the last one is new, it will have + // just been added). + // If the new label happens to be the first one, add it before the first old label. Otherwise, if there are no old labels, + // just append the label (no before/after). + if (newPos == newLabels.Count - 1) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: null, model, builder); + } + else if (newPos > 0) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: newLabels[newPos - 1], model, builder); + } + else if (oldLabels.Count > 0) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: oldLabels[0], afterLabel: null, model, builder); + } + else + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: null, model, builder); + } } } } @@ -1328,9 +1344,15 @@ protected virtual void GenerateAddEnumLabel( PostgresEnum enumType, string addedLabel, string? beforeLabel, + string? afterLabel, IModel? model, MigrationCommandListBuilder builder) { + if (beforeLabel is not null && afterLabel is not null) + { + throw new UnreachableException("Both beforeLabel and afterLabel can't be specified"); + } + var schema = enumType.Schema ?? model?.GetDefaultSchema(); builder @@ -1345,6 +1367,12 @@ protected virtual void GenerateAddEnumLabel( .Append(" BEFORE ") .Append(_stringTypeMapping.GenerateSqlLiteral(beforeLabel)); } + else if (afterLabel is not null) + { + builder + .Append(" AFTER ") + .Append(_stringTypeMapping.GenerateSqlLiteral(afterLabel)); + } builder.AppendLine(";"); diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index db6cfe3fd..a641e9f0c 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -2895,7 +2895,25 @@ await Test( l => Assert.Equal("Sad", l)); }); - AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry' BEFORE 'Sad';"""); + AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry' AFTER 'Happy';"""); + } + + [Fact] + public virtual async Task Alter_enum_change_label_ordering_does_nothing() + { + await Test( + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), + builder => builder.HasPostgresEnum("Mood", ["Sad", "Happy"]), + model => + { + var moodEnum = Assert.Single(model.GetPostgresEnums()); + Assert.Collection( + moodEnum.Labels, + l => Assert.Equal("Happy", l), + l => Assert.Equal("Sad", l)); + }); + + AssertSql(); } [Fact] From 2e59bf59d63383ba90be7ffe2f10c506a0264540 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 17 Jan 2025 17:25:50 +0100 Subject: [PATCH 08/24] Fix quoted enum handling (#3434) Fixes #3433 (cherry picked from commit 068a7c6b4ec50ad4f6a62a260ddc523b9a8ac0c5) --- EFCore.PG.sln.DotSettings | 1 + .../Internal/NpgsqlTypeMappingSource.cs | 190 ++++++++++++++++-- .../Query/EnumQueryTest.cs | 52 ++++- .../Storage/NpgsqlTypeMappingSourceTest.cs | 29 +++ 4 files changed, 250 insertions(+), 22 deletions(-) diff --git a/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings index 9055198e9..ff5652c30 100644 --- a/EFCore.PG.sln.DotSettings +++ b/EFCore.PG.sln.DotSettings @@ -189,4 +189,5 @@ True True True + True \ No newline at end of file diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 6dbbb7e40..e5719e76e 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Data; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.NetworkInformation; @@ -771,6 +772,8 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) { var storeType = mappingInfo.StoreTypeName; var clrType = mappingInfo.ClrType; + string? schema; + string name; if (clrType is not null and not { IsEnum: true, IsClass: false }) { @@ -783,20 +786,31 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) if (storeType is null) { enumDefinition = _enumDefinitions.SingleOrDefault(m => m.ClrType == clrType); + + if (enumDefinition is null) + { + return null; + } + + (name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema); } else { - // TODO: Not sure what to do about quoting. Is the user expected to configure properties - // TODO: with a quoted (schema-qualified) store type or not? - var dot = storeType.IndexOf('.'); - enumDefinition = dot is -1 - ? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType) - : _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType[(dot + 1)..] && m.StoreTypeSchema == storeType[..dot]); - } - - if (enumDefinition is null) - { - return null; + // If the user is specifying the store type manually, they are not expected to have quotes in the name (e.g. because of upper- + // case characters). + // However, if we infer an enum array type mapping from an element (e.g. someEnums.Contains(b.SomeEnumColumn)), we get the + // element's store type - which for enums is quoted - and add []; so we get e.g. "MyEnum"[]. So we need to support quoted + // names here, by parsing the name and stripping the quotes. + ParseStoreTypeName(storeType, out name, out schema, out var size, out var precision, out var scale); + + enumDefinition = schema is null + ? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name) + : _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name && m.StoreTypeSchema == schema); + + if (enumDefinition is null) + { + return null; + } } // We now have an enum definition from the context options. @@ -805,7 +819,6 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) // 1. The quoted type name is used in migrations, where quoting is needed // 2. The unquoted type name is set on NpgsqlParameter.DataTypeName // (though see https://github.com/npgsql/npgsql/issues/5710). - var (name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema); return new NpgsqlEnumTypeMapping( _sqlGenerationHelper.DelimitIdentifier(name, schema), schema is null ? name : schema + "." + name, @@ -972,6 +985,8 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan span) ref int? precision, ref int? scale) { + // TODO: Reimplement over ParseStoreTypeName below + if (storeTypeName is null) { return null; @@ -1056,4 +1071,155 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan span) return new StringBuilder(preParens.Length).Append(preParens).Append(postParens).ToString(); } + + internal static void ParseStoreTypeName( + string storeTypeName, + out string name, + out string? schema, + out int? size, + out int? precision, + out int? scale) + { + var s = storeTypeName.AsSpan().Trim(); + var i = 0; + size = precision = scale = null; + + if (s.EndsWith("[]", StringComparison.Ordinal)) + { + // If this is an array store type, any facets (size, precision...) apply to the element and not to the array (e.g. varchar(32)[] + // is an array mapping with Size=null over an element mapping of varchar with Size=32). So just add everything up to the end. + // Note that if there's a schema (e.g. foo.varchar(32)[]), we return name=varchar(32), schema=foo. + name = s.ToString(); + schema = null; + return; + } + + name = ParseNameComponent(s); + + if (i < s.Length && s[i] == '.') + { + i++; + schema = name; + name = ParseNameComponent(s); + } + else + { + schema = null; + } + + s = s[i..]; + + if (s.Length == 0 || s[0] != '(') + { + // No facets + return; + } + + s = s[1..]; + + var closeParen = s.IndexOf(")", StringComparison.Ordinal); + if (closeParen == -1) + { + return; + } + + var inParens = s[..closeParen].Trim(); + // There may be stuff after the closing parentheses (e.g. timestamp(3) with time zone) + var postParens = s.Slice(closeParen + 1); + + switch (s.IndexOf(",", StringComparison.Ordinal)) + { + // No comma inside the parentheses, parse the value either as size or precision + case -1: + if (!int.TryParse(inParens, out var p)) + { + return; + } + + if (NameBasesUsesPrecision(name)) + { + precision = p; + // scale = 0; + } + else + { + size = p; + } + + break; + + case var comma: + if (int.TryParse(s[..comma].Trim(), out var parsedPrecision)) + { + precision = parsedPrecision; + } + else + { + return; + } + + if (int.TryParse(s[(comma + 1)..closeParen].Trim(), out var parsedScale)) + { + scale = parsedScale; + } + else + { + return; + } + + break; + } + + if (postParens.Length > 0) + { + // There's stuff after the parentheses (e.g. time(3) with time zone), append to the name + name += postParens.ToString(); + } + + string ParseNameComponent(ReadOnlySpan s) + { + var inQuotes = false; + StringBuilder builder = new(); + + if (s[i] == '"') + { + inQuotes = true; + i++; + } + + var start = i; + + for (; i < s.Length; i++) + { + var c = s[i]; + + if (inQuotes) + { + if (c == '"') + { + if (i + 1 < s.Length && s[i + 1] == '"') + { + builder.Append('"'); + i++; + continue; + } + + i++; + break; + } + } + else if (!char.IsWhiteSpace(c) && !char.IsAsciiLetterOrDigit(c) && c != '_') + { + break; + } + + builder.Append(c); + } + + var length = i - start; + return length == storeTypeName.Length + ? storeTypeName + : builder.ToString(); + } + } } diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index a3b899507..ce3826ba8 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs @@ -39,7 +39,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = 'sad'::test.mapped_enum """); @@ -57,7 +57,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."SchemaQualifiedEnum" = 'Happy (PgName)'::test.schema_qualified_enum """); @@ -78,7 +78,7 @@ await AssertQuery( """ @__sad_0='Sad' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = @__sad_0 """); @@ -99,7 +99,7 @@ await AssertQuery( """ @__sad_0='1' -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedEnum" = @__sad_0 """); @@ -120,7 +120,7 @@ await AssertQuery( """ @__sad_0='1' -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedEnum" = @__sad_0 """); @@ -141,7 +141,7 @@ await AssertQuery( """ @__sad_0='Sad' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = @__sad_0 """); @@ -160,7 +160,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum"::text LIKE '%sa%' """); @@ -181,7 +181,7 @@ await AssertQuery( """ @__values_0='0x01' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."ByteEnum" = ANY (@__values_0) """); @@ -202,12 +202,33 @@ await AssertQuery( """ @__values_0='0x01' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedByteEnum" = ANY (@__values_0) """); } + [ConditionalTheory] // #3433 + [MemberData(nameof(IsAsyncData))] + public async Task Where_uppercase_enum_array_contains_enum(bool async) + { + await using var ctx = CreateContext(); + + List values = [UppercaseNamedEnum.Sad]; + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.UppercaseNamedEnum))); + + AssertSql( + """ +@values={ 'Sad' } (DbType = Object) + +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" +FROM test."SomeEntities" AS s +WHERE s."UppercaseNamedEnum" = ANY (@values) +"""); + } + #endregion #region Support @@ -240,6 +261,7 @@ public class SomeEnumEntity public UnmappedEnum UnmappedEnum { get; set; } public InferredEnum InferredEnum { get; set; } public SchemaQualifiedEnum SchemaQualifiedEnum { get; set; } + public UppercaseNamedEnum UppercaseNamedEnum { get; set; } public ByteEnum ByteEnum { get; set; } public UnmappedByteEnum UnmappedByteEnum { get; set; } public int EnumValue { get; set; } @@ -270,6 +292,12 @@ public enum SchemaQualifiedEnum Sad } + public enum UppercaseNamedEnum + { + Happy, + Sad + } + public enum ByteEnum : byte { Happy, @@ -304,7 +332,8 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build .MapEnum("mapped_enum", "test") .MapEnum("inferred_enum", "test") .MapEnum("byte_enum", "test") - .MapEnum("schema_qualified_enum", "test"); + .MapEnum("schema_qualified_enum", "test") + .MapEnum("UpperCaseEnum", "test"); return optionsBuilder; } @@ -341,6 +370,7 @@ public IReadOnlyDictionary EntityAsserters Assert.Equal(ee.UnmappedEnum, aa.UnmappedEnum); Assert.Equal(ee.InferredEnum, aa.InferredEnum); Assert.Equal(ee.SchemaQualifiedEnum, aa.SchemaQualifiedEnum); + Assert.Equal(ee.UppercaseNamedEnum, aa.UppercaseNamedEnum); Assert.Equal(ee.ByteEnum, aa.ByteEnum); Assert.Equal(ee.UnmappedByteEnum, aa.UnmappedByteEnum); Assert.Equal(ee.EnumValue, aa.EnumValue); @@ -370,6 +400,7 @@ public static IReadOnlyList CreateSomeEnumEntities() UnmappedEnum = UnmappedEnum.Happy, InferredEnum = InferredEnum.Happy, SchemaQualifiedEnum = SchemaQualifiedEnum.Happy, + UppercaseNamedEnum = UppercaseNamedEnum.Happy, ByteEnum = ByteEnum.Happy, UnmappedByteEnum = UnmappedByteEnum.Happy, EnumValue = (int)MappedEnum.Happy @@ -381,6 +412,7 @@ public static IReadOnlyList CreateSomeEnumEntities() UnmappedEnum = UnmappedEnum.Sad, InferredEnum = InferredEnum.Sad, SchemaQualifiedEnum = SchemaQualifiedEnum.Sad, + UppercaseNamedEnum = UppercaseNamedEnum.Sad, ByteEnum = ByteEnum.Sad, UnmappedByteEnum = UnmappedByteEnum.Sad, EnumValue = (int)MappedEnum.Sad diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs index 3809227de..be5062dda 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs @@ -291,6 +291,35 @@ public void Multirange_by_store_type_across_pg_versions() Assert.Same(typeof(List>), mappingDefault.ClrType); } +#nullable enable + [Theory] + [InlineData("integer", "integer", null, null, null, null)] + [InlineData("integer[]", "integer[]", null, null, null, null)] + [InlineData("foo.bar", "bar", "foo", null, null, null)] + [InlineData("foo.bar[]", "foo.bar[]", null, null, null, null)] + [InlineData("\"foo\"", "foo", null, null, null, null)] + [InlineData("\"fo.o\"", "fo.o", null, null, null, null)] + [InlineData("\"foo\".\"bar\"", "bar", "foo", null, null, null)] + [InlineData("\"f\"\"oo\"", "f\"oo", null, null, null, null)] + [InlineData("character varying", "character varying", null, null, null, null)] + [InlineData("with_underscore", "with_underscore", null, null, null, null)] + [InlineData("varchar(30)", "varchar", null, 30, null, null)] + [InlineData("varchar(30)[]", "varchar(30)[]", null, null, null, null)] + [InlineData("numeric(30)", "numeric", null, null, 30, null)] + [InlineData("numeric(30,3)", "numeric", null, null, 30, 3)] + public void ParseStoreType(string storeTypeName, string expectedName, string? expectedSchema, int? expectedSize, int? expectedPrecision, int? expectedScale) + { + NpgsqlTypeMappingSource.ParseStoreTypeName( + storeTypeName, out var name, out var schema, out var size, out var precision, out var scale); + + Assert.Equal(expectedName, name); + Assert.Equal(expectedSchema, schema); + Assert.Equal(expectedSize, size); + Assert.Equal(expectedPrecision, precision); + Assert.Equal(expectedScale, scale); + } +#nullable restore + #region Support private NpgsqlTypeMappingSource CreateTypeMappingSource(Version postgresVersion = null) From 74b858ae63efc603ffa74874e49c82eb58d43b09 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 17 Jan 2025 17:30:21 +0100 Subject: [PATCH 09/24] Depend on EF 9.0.1 --- Directory.Packages.props | 4 +-- .../MigrationsInfrastructureNpgsqlTest.cs | 25 +++++++++++-------- .../Query/EnumQueryTest.cs | 4 +-- .../PrimitiveCollectionsQueryNpgsqlTest.cs | 22 ++++++++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 362c7099e..1b0afc7ba 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - [9.0.0,10.0.0) - 9.0.0 + [9.0.1,10.0.0) + 9.0.1 9.0.2 diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs index a67c241fa..542d3ddda 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs @@ -7,16 +7,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations public class MigrationsInfrastructureNpgsqlTest(MigrationsInfrastructureNpgsqlTest.MigrationsInfrastructureNpgsqlFixture fixture) : MigrationsInfrastructureTestBase(fixture) { - // TODO: Remove once we sync to https://github.com/dotnet/efcore/pull/35106 - public override void Can_generate_no_migration_script() - { - } - - // TODO: Remove once we sync to https://github.com/dotnet/efcore/pull/35106 - public override void Can_generate_migration_from_initial_database_to_initial() - { - } - public override void Can_get_active_provider() { base.Can_get_active_provider(); @@ -24,6 +14,21 @@ public override void Can_get_active_provider() Assert.Equal("Npgsql.EntityFrameworkCore.PostgreSQL", ActiveProvider); } + // See #3407 + public override void Can_apply_two_migrations_in_transaction() + => Assert.ThrowsAny(() => base.Can_apply_two_migrations_in_transaction()); + + // See #3407 + public override Task Can_apply_two_migrations_in_transaction_async() + => Assert.ThrowsAnyAsync(() => base.Can_apply_two_migrations_in_transaction_async()); + + // This tests uses Fixture.CreateEmptyContext(), which does not go through MigrationsInfrastructureNpgsqlFixture.CreateContext() + // and therefore does not set the PostgresVersion in the context options. As a result, we try to drop the database with + // WITH (FORCE), which is only supported starting with PG 13. + [MinimumPostgresVersion(13, 0)] + public override Task Can_generate_no_migration_script() + => base.Can_generate_no_migration_script(); + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/33056")] public override void Can_apply_all_migrations() => base.Can_apply_all_migrations(); diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index ce3826ba8..7467b28f6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs @@ -221,11 +221,11 @@ await AssertQuery( AssertSql( """ -@values={ 'Sad' } (DbType = Object) +@__values_0={ 'Sad' } (DbType = Object) SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s -WHERE s."UppercaseNamedEnum" = ANY (@values) +WHERE s."UppercaseNamedEnum" = ANY (@__values_0) """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs index 1fd148a09..91f9d4e29 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs @@ -528,6 +528,28 @@ await AssertQuery( """); } + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0={ '10', '999' } (Nullable = false) (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (@__ints_0) +""", + // + """ +@__ints_0={ '10', '999' } (Nullable = false) (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@__ints_0) AND p."Int" = ANY (@__ints_0) IS NOT NULL) +"""); + } + public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async) { await base.Parameter_collection_of_ints_Contains_nullable_int(async); From e4a74e7968e2d453e4b4f5caca765b69a031f158 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 17 Jan 2025 19:01:49 +0100 Subject: [PATCH 10/24] Bump version to 9.0.4 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6c16b9a8b..9d7160b71 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 9.0.3 + 9.0.4 latest true latest From badb51eb49088fdf3f5a2b8c5887e4a0f73a1ea4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 27 Jan 2025 17:48:38 +0100 Subject: [PATCH 11/24] Fix calling base method in NpgsqlMigrationsSqlGenerator (#3443) Fixes #3440 (cherry picked from commit 83f2cc37d97d0e3074ddcc628e8366c5825be4cd) --- src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index f93fd5c59..6e09ed6be 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -184,8 +184,8 @@ protected override void Generate( using (builder.Indent()) { - base.CreateTableColumns(operation, model, builder); - base.CreateTableConstraints(operation, model, builder); + CreateTableColumns(operation, model, builder); + CreateTableConstraints(operation, model, builder); builder.AppendLine(); } From 953ad1c83b22f44602eb799875ed2d4e564686df Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 15 Feb 2025 00:24:01 +0100 Subject: [PATCH 12/24] Generate UUIDv7 values for value-generated strings (#3462) Fixes #3460 (cherry picked from commit 16841f93e715456dfdffed3983498e3639d096f8) --- .../Internal/NpgsqlValueGeneratorSelector.cs | 15 ++++++++----- .../NpgsqlSequentialStringValueGenerator.cs | 22 +++++++++++++++++++ .../NpgsqlValueGeneratorSelectorTest.cs | 4 ++-- 3 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs index a9274f617..808532b42 100644 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs +++ b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs @@ -101,9 +101,14 @@ public override bool TrySelect(IProperty property, ITypeBase typeBase, out Value /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override ValueGenerator? FindForType(IProperty property, ITypeBase typeBase, Type clrType) - => property.ClrType.UnwrapNullableType() == typeof(Guid) - ? property.ValueGenerated == ValueGenerated.Never || property.GetDefaultValueSql() is not null - ? new TemporaryGuidValueGenerator() - : new NpgsqlSequentialGuidValueGenerator() - : base.FindForType(property, typeBase, clrType); + => property.ClrType.UnwrapNullableType() switch + { + var t when t == typeof(Guid) && property.ValueGenerated is not ValueGenerated.Never && property.GetDefaultValueSql() is null + => new NpgsqlSequentialGuidValueGenerator(), + + var t when t == typeof(string) && property.ValueGenerated is not ValueGenerated.Never && property.GetDefaultValueSql() is null + => new NpgsqlSequentialStringValueGenerator(), + + _ => base.FindForType(property, typeBase, clrType) + }; } diff --git a/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs b/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs new file mode 100644 index 000000000..8c4f595dc --- /dev/null +++ b/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs @@ -0,0 +1,22 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration; + +/// +/// This API supports the Entity Framework Core infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public class NpgsqlSequentialStringValueGenerator : ValueGenerator +{ + private readonly NpgsqlSequentialGuidValueGenerator _guidGenerator = new(); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool GeneratesTemporaryValues => false; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override string Next(EntityEntry entry) => _guidGenerator.Next(entry).ToString(); +} diff --git a/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs b/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs index 57b0d1b9a..9b6b3ae42 100644 --- a/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs @@ -21,7 +21,7 @@ public void Returns_built_in_generators_for_types_setup_for_value_generation() AssertGenerator("NullableShort"); AssertGenerator("NullableByte"); AssertGenerator("Decimal"); - AssertGenerator("String"); + AssertGenerator("String"); AssertGenerator("Guid"); AssertGenerator("Binary"); } @@ -128,7 +128,7 @@ public void Returns_sequence_value_generators_when_configured_for_model() AssertGenerator>("NullableInt", setSequences: true); AssertGenerator>("NullableLong", setSequences: true); AssertGenerator>("NullableShort", setSequences: true); - AssertGenerator("String", setSequences: true); + AssertGenerator("String", setSequences: true); AssertGenerator("Guid", setSequences: true); AssertGenerator("Binary", setSequences: true); } From e952c84779500c65fd99393b887e341e523f6c33 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 15 Feb 2025 01:26:25 +0200 Subject: [PATCH 13/24] Stop testing on PG12 (out of support) (cherry picked from commit 3cbdba452533fa223acf0e39cf0d57c4a4c1e74a) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fca39f9ce..d6a5ceaa7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, windows-2022] - pg_major: [17, 16, 15, 14, 13, 12] + pg_major: [17, 16, 15, 14, 13] config: [Release] include: - os: ubuntu-22.04 From 6a3445557d14cb2eb6a766df02f89c06ff5cec00 Mon Sep 17 00:00:00 2001 From: Marcus Bernander Date: Sat, 15 Feb 2025 18:39:14 +0100 Subject: [PATCH 14/24] Always close connection after reloading types in migration. (#3465) Closes #3464 (cherry picked from commit 786b635d93d12917d20b8a791e547d7d67f8899c) --- src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs index e8a0bea28..601af5604 100644 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs @@ -85,7 +85,7 @@ public override void Migrate(string? targetMigration) { npgsqlConnection.ReloadTypes(); } - catch + finally { _connection.Close(); } @@ -128,7 +128,7 @@ public override async Task MigrateAsync(string? targetMigration, CancellationTok { await npgsqlConnection.ReloadTypesAsync(cancellationToken).ConfigureAwait(false); } - catch + finally { await _connection.CloseAsync().ConfigureAwait(false); } From 0fdf784b1f695a7e77f487f4d7b812d95744a093 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 28 Feb 2025 15:45:03 +0100 Subject: [PATCH 15/24] Fix nullability for IndexOf (array_position) (#3477) Fixes #3474 (cherry picked from commit 0212cae5c5b693c0a83a8c93bd1e6de79c2f219a) --- .../Internal/NpgsqlArrayMethodTranslator.cs | 10 ++++++++-- src/EFCore.PG/Utilities/Util.cs | 7 ++++++- .../Query/ArrayArrayQueryTest.cs | 4 ++-- .../Query/ArrayListQueryTest.cs | 4 ++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs index e128dacea..847da4c17 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs @@ -141,7 +141,10 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList) "array_position", [array, item], nullable: true, - TrueArrays[2], + // array_position can return NULL even if both its arguments are non-nullable; + // this is currently the way to express that (see + // https://github.com/dotnet/efcore/pull/33814#issuecomment-2687857927). + FalseArrays[2], arrayOrList.Type), _sqlExpressionFactory.Constant(1)), _sqlExpressionFactory.Constant(-1)); @@ -161,7 +164,10 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList) "array_position", [array, item, startIndex], nullable: true, - TrueArrays[3], + // array_position can return NULL even if both its arguments are non-nullable; + // this is currently the way to express that (see + // https://github.com/dotnet/efcore/pull/33814#issuecomment-2687857927). + FalseArrays[3], arrayOrList.Type), _sqlExpressionFactory.Constant(1)), _sqlExpressionFactory.Constant(-1)); diff --git a/src/EFCore.PG/Utilities/Util.cs b/src/EFCore.PG/Utilities/Util.cs index bdbceb282..97bca187c 100644 --- a/src/EFCore.PG/Utilities/Util.cs +++ b/src/EFCore.PG/Utilities/Util.cs @@ -15,5 +15,10 @@ internal static class Statics [true, true, true, true, true, true, true, true] ]; - internal static readonly bool[][] FalseArrays = [[], [false], [false, false]]; + internal static readonly bool[][] FalseArrays = [ + [], + [false], + [false, false], + [false, false, false] + ]; } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs index e00543860..b89306d03 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs @@ -835,7 +835,7 @@ await AssertQuery( """ SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE array_position(s."IntArray", 6) - 1 = 1 +WHERE COALESCE(array_position(s."IntArray", 6) - 1, -1) = 1 """); } @@ -849,7 +849,7 @@ await AssertQuery( """ SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE array_position(s."IntArray", 6, 2) - 1 = 1 +WHERE COALESCE(array_position(s."IntArray", 6, 2) - 1, -1) = 1 """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs index 361fb653b..99e88ded6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs @@ -855,7 +855,7 @@ await AssertQuery( """ SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE array_position(s."IntList", 6) - 1 = 1 +WHERE COALESCE(array_position(s."IntList", 6) - 1, -1) = 1 """); } @@ -871,7 +871,7 @@ await AssertQuery( """ SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE array_position(s."IntList", 6, 2) - 1 = 1 +WHERE COALESCE(array_position(s."IntList", 6, 2) - 1, -1) = 1 """); } From 4bf32483bccf20335ea16848bc8e6fb68eb68e7a Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 28 Feb 2025 16:35:22 +0100 Subject: [PATCH 16/24] Preserve collation when changing column type (#3479) Fixes #3476 (cherry picked from commit e327d6bccfe56748ae04bf276ec7455ff1dcf48e) --- .../NpgsqlMigrationsSqlGenerator.cs | 10 ++++++-- .../Migrations/MigrationsNpgsqlTest.cs | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index 6e09ed6be..fccab7d18 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -456,9 +456,15 @@ protected override void Generate(AlterColumnOperation operation, IModel? model, .Append("TYPE ") .Append(type); - if (newCollation != oldCollation) + if (newCollation is not null) { - builder.Append(" COLLATE ").Append(DelimitIdentifier(newCollation ?? "default")); + builder.Append(" COLLATE ").Append(DelimitIdentifier(newCollation)); + } + else if (type == oldType) + { + // If the type is the same, make it more explicit that we're just resetting the collation to the default + // (this isn't really required) + builder.Append(" COLLATE ").Append(DelimitIdentifier("default")); } builder.AppendLine(";"); diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index a641e9f0c..aff93c789 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -972,6 +972,30 @@ public override async Task Alter_column_change_type() AssertSql("""ALTER TABLE "People" ALTER COLUMN "SomeColumn" TYPE bigint;"""); } + [Fact] + public virtual async Task Alter_column_change_type_preserves_collation() + { + await Test( + builder => builder.Entity("People").Property("Id"), + builder => builder.Entity("People").Property("SomeColumn") + .HasColumnType("varchar") + .UseCollation(NonDefaultCollation), + builder => builder.Entity("People").Property("SomeColumn") + .HasColumnType("text") + .UseCollation(NonDefaultCollation), + model => + { + var table = Assert.Single(model.Tables); + var column = Assert.Single(table.Columns, c => c.Name == "SomeColumn"); + Assert.Equal(NonDefaultCollation, column.Collation); + }); + + AssertSql( + """ +ALTER TABLE "People" ALTER COLUMN "SomeColumn" TYPE text COLLATE "POSIX"; +"""); + } + public override async Task Alter_column_make_required() { await base.Alter_column_make_required(); From 80ec4ae3cec6ded1a9a660e67899b6f03c430ca6 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 1 Mar 2025 08:17:30 +0100 Subject: [PATCH 17/24] Preserve ConfigureDataSource() callback when applying other context options (#3482) Fixes #3478 (cherry picked from commit 0db010c8dc454aacee3eee61c669c6eeca2f5d80) --- src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs index 7dba76190..269a66fea 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs @@ -109,6 +109,7 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom) : base(copyFrom) { DataSource = copyFrom.DataSource; + DataSourceBuilderAction = copyFrom.DataSourceBuilderAction; AdminDatabase = copyFrom.AdminDatabase; _postgresVersion = copyFrom._postgresVersion; UseRedshift = copyFrom.UseRedshift; From fd2380957bee5cd86f336466af36b08c0163f1a5 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 1 Mar 2025 09:24:20 +0200 Subject: [PATCH 18/24] Bump Npgsql to 9.0.3 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1b0afc7ba..5a263de4f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ [9.0.1,10.0.0) 9.0.1 - 9.0.2 + 9.0.3 From 5da4a58fd551e74eceec662343631174d3e8c818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Harrtell?= Date: Sat, 1 Mar 2025 14:47:48 +0100 Subject: [PATCH 19/24] Attempt to implement IntersectsBbox --- ...qlNetTopologySuiteDbFunctionsExtensions.cs | 6 ++++++ ...TopologySuiteMethodCallTranslatorPlugin.cs | 5 +++++ .../Query/SpatialQueryNpgsqlGeometryTest.cs | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs index 4dabe27a1..c462aedd8 100644 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs @@ -7,6 +7,12 @@ namespace Microsoft.EntityFrameworkCore; /// public static class NpgsqlNetTopologySuiteDbFunctionsExtensions { + /// + /// + /// + public static bool IntersectsBbox(this DbFunctions _, Geometry geometry, Geometry anotherGeometry) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IntersectsBbox))); + /// /// Returns a new geometry with its coordinates transformed to a different spatial reference system. /// Translates to ST_Transform(geometry, srid). diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs index 896589719..05624ac73 100644 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs @@ -104,6 +104,11 @@ public NpgsqlGeometryMethodTranslator( method.ReturnType, arguments[1].TypeMapping), + nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.IntersectsBbox) => _sqlExpressionFactory.MakePostgresBinary( + PgExpressionType.Overlaps, + arguments[1], + arguments[2]), + nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.DistanceKnn) => _sqlExpressionFactory.MakePostgresBinary( PgExpressionType.Distance, arguments[1], diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs index 6bf97811b..ba8c6ee7b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs @@ -362,6 +362,27 @@ public override async Task Intersection(bool async) """); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task IntersectsBbox(bool async) + { + var polygon = Fixture.GeometryFactory.CreatePolygon([new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0)]); + + await AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Id, IntersectsBbox = (bool?)EF.Functions.IntersectsBbox(e.Polygon, polygon) }), + ss => ss.Set().Select(e => new { e.Id, IntersectsBbox = (e.Polygon == null ? (bool?)false : EF.Functions.IntersectsBbox(e.Polygon, polygon)) }), + x => x.Id); + + AssertSql( + """ +@__Polygon_0='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT l."Id", l."Polygon" && @__Polygon_0 AS "IntersectsBbox" +FROM "PolygonEntity" AS l +"""); + } + public override async Task Intersects(bool async) { await base.Intersects(async); From c4c729c2ee8815f63cbe7309a79e98abc2132cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Harrtell?= Date: Sat, 1 Mar 2025 16:27:02 +0100 Subject: [PATCH 20/24] Fix test --- .../Query/SpatialQueryNpgsqlGeometryTest.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs index ba8c6ee7b..e0dcd8c57 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs @@ -371,15 +371,20 @@ public async Task IntersectsBbox(bool async) await AssertQuery( async, ss => ss.Set().Select(e => new { e.Id, IntersectsBbox = (bool?)EF.Functions.IntersectsBbox(e.Polygon, polygon) }), - ss => ss.Set().Select(e => new { e.Id, IntersectsBbox = (e.Polygon == null ? (bool?)false : EF.Functions.IntersectsBbox(e.Polygon, polygon)) }), - x => x.Id); + ss => ss.Set().Select(e => new { e.Id, IntersectsBbox = (e.Polygon == null ? (bool?)null : e.Polygon.EnvelopeInternal.Intersects(polygon.EnvelopeInternal)) }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.IntersectsBbox, a.IntersectsBbox); + }); AssertSql( """ -@__Polygon_0='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) +@__polygon_1='POLYGON ((0 0, 1 0, 0 1, 0 0))' (DbType = Object) -SELECT l."Id", l."Polygon" && @__Polygon_0 AS "IntersectsBbox" -FROM "PolygonEntity" AS l +SELECT p."Id", p."Polygon" && @__polygon_1 AS "IntersectsBbox" +FROM "PolygonEntity" AS p """); } From 94f23a3e70f38cd0553a2813d093cb2900317cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Harrtell?= Date: Sun, 1 Feb 2026 17:38:06 +0100 Subject: [PATCH 21/24] Fix merge --- .../Translations/EnumTranslationsTest.cs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs index bacea23f8..dc1432593 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs @@ -228,27 +228,6 @@ FROM test."SomeEntities" AS s """); } - [ConditionalTheory] // #3433 - [MemberData(nameof(IsAsyncData))] - public async Task Where_uppercase_enum_array_contains_enum(bool async) - { - await using var ctx = CreateContext(); - - List values = [UppercaseNamedEnum.Sad]; - await AssertQuery( - async, - ss => ss.Set().Where(e => values.Contains(e.UppercaseNamedEnum))); - - AssertSql( - """ -@__values_0={ 'Sad' } (DbType = Object) - -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" -FROM test."SomeEntities" AS s -WHERE s."UppercaseNamedEnum" = ANY (@__values_0) -"""); - } - #endregion #region Support From 27db760115fcfa47fa3d28aa86a85713a6c5e4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Harrtell?= Date: Sun, 1 Feb 2026 17:41:23 +0100 Subject: [PATCH 22/24] Remove not used using --- src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 4ee1479e6..4908b1ee8 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Data; -using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.NetworkInformation; From 58807f2b8a7c927013571939b0c9175879799c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Harrtell?= Date: Sun, 1 Feb 2026 17:45:02 +0100 Subject: [PATCH 23/24] Add summary --- .../Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs index c462aedd8..4eb33058e 100644 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs @@ -8,7 +8,8 @@ namespace Microsoft.EntityFrameworkCore; public static class NpgsqlNetTopologySuiteDbFunctionsExtensions { /// - /// + /// Checks whether the 2D bounding boxes of two geometries intersect. + /// Translates to the PostGIS && operator. /// public static bool IntersectsBbox(this DbFunctions _, Geometry geometry, Geometry anotherGeometry) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IntersectsBbox))); From 72ff57c0bde31ecdfaeec7c2f04be42afea4f6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Harrtell?= Date: Sun, 1 Feb 2026 23:03:29 +0100 Subject: [PATCH 24/24] Fix test case expect syntax --- .../Query/SpatialQueryNpgsqlGeometryTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs index 764a385b8..4af518795 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs @@ -382,9 +382,9 @@ await AssertQuery( AssertSql( """ -@__polygon_1='POLYGON ((0 0, 1 0, 0 1, 0 0))' (DbType = Object) +@polygon='POLYGON ((0 0, 1 0, 0 1, 0 0))' (DbType = Object) -SELECT p."Id", p."Polygon" && @__polygon_1 AS "IntersectsBbox" +SELECT p."Id", p."Polygon" && @polygon AS "IntersectsBbox" FROM "PolygonEntity" AS p """); }