diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index e0e93d676..a2fc624ea 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -14,110 +14,108 @@ public class NpgsqlAnnotationCodeGenerator : AnnotationCodeGenerator { #region MethodInfos - private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo1 - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string)); - - private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo2 + // ReSharper disable InconsistentNaming + private static readonly MethodInfo Model_HasPostgresExtension = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string)); - private static readonly MethodInfo ModelHasPostgresEnumMethodInfo1 + private static readonly MethodInfo Model_HasPostgresEnum1 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string[])); - private static readonly MethodInfo ModelHasPostgresEnumMethodInfo2 + private static readonly MethodInfo Model_HasPostgresEnum2 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string[])); - private static readonly MethodInfo ModelHasPostgresRangeMethodInfo1 + private static readonly MethodInfo Model_HasPostgresRange1 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string)); - private static readonly MethodInfo ModelHasPostgresRangeMethodInfo2 + private static readonly MethodInfo Model_HasPostgresRange2 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string),typeof(string), typeof(string),typeof(string), typeof(string),typeof(string)); - private static readonly MethodInfo ModelUseSerialColumnsMethodInfo + private static readonly MethodInfo Model_UseSerialColumns = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseSerialColumns), typeof(ModelBuilder)); - private static readonly MethodInfo ModelUseIdentityAlwaysColumnsMethodInfo + private static readonly MethodInfo Model_UseIdentityAlwaysColumns = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseIdentityAlwaysColumns), typeof(ModelBuilder)); - private static readonly MethodInfo ModelUseIdentityByDefaultColumnsMethodInfo + private static readonly MethodInfo Model_UseIdentityByDefaultColumns = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns), typeof(ModelBuilder)); - private static readonly MethodInfo ModelUseHiLoMethodInfo + private static readonly MethodInfo Model_UseHiLo = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseHiLo), typeof(ModelBuilder), typeof(string), typeof(string)); - private static readonly MethodInfo ModelHasAnnotationMethodInfo + private static readonly MethodInfo Model_HasAnnotation = typeof(ModelBuilder).GetRequiredRuntimeMethod( nameof(ModelBuilder.HasAnnotation), typeof(string), typeof(object)); - private static readonly MethodInfo ModelUseKeySequencesMethodInfo + private static readonly MethodInfo Model_UseKeySequences = typeof(NpgsqlModelBuilderExtensions).GetRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseKeySequences), new[] { typeof(ModelBuilder), typeof(string), typeof(string) })!; - private static readonly MethodInfo EntityTypeIsUnloggedMethodInfo + private static readonly MethodInfo EntityType_IsUnlogged = typeof(NpgsqlEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlEntityTypeBuilderExtensions.IsUnlogged), typeof(EntityTypeBuilder), typeof(bool)); - private static readonly MethodInfo PropertyUseSerialColumnMethodInfo + private static readonly MethodInfo Property_UseSerialColumn = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseSerialColumn), typeof(PropertyBuilder)); - private static readonly MethodInfo PropertyUseIdentityAlwaysColumnMethodInfo + private static readonly MethodInfo Property_UseIdentityAlwaysColumn = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn), typeof(PropertyBuilder)); - private static readonly MethodInfo PropertyUseIdentityByDefaultColumnMethodInfo + private static readonly MethodInfo Property_UseIdentityByDefaultColumn = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn), typeof(PropertyBuilder)); - private static readonly MethodInfo PropertyUseHiLoMethodInfo + private static readonly MethodInfo Property_UseHiLo = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseHiLo), typeof(PropertyBuilder), typeof(string), typeof(string)); - private static readonly MethodInfo PropertyHasIdentityOptionsMethodInfo + private static readonly MethodInfo Property_HasIdentityOptions = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.HasIdentityOptions), typeof(PropertyBuilder), typeof(long?), typeof(long?), typeof(long?), typeof(long?), typeof(bool?), typeof(long?)); - private static readonly MethodInfo PropertyUseSequenceMethodInfo + private static readonly MethodInfo Property_UseSequence = typeof(NpgsqlPropertyBuilderExtensions).GetRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseSequence), new[] { typeof(PropertyBuilder), typeof(string), typeof(string) })!; - private static readonly MethodInfo IndexUseCollationMethodInfo + private static readonly MethodInfo Index_UseCollation = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.UseCollation), typeof(IndexBuilder), typeof(string[])); - private static readonly MethodInfo IndexHasMethodMethodInfo + private static readonly MethodInfo Index_HasMethod = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasMethod), typeof(IndexBuilder), typeof(string)); - private static readonly MethodInfo IndexHasOperatorsMethodInfo + private static readonly MethodInfo Index_HasOperators = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasOperators), typeof(IndexBuilder), typeof(string[])); - private static readonly MethodInfo IndexHasSortOrderMethodInfo + private static readonly MethodInfo Index_HasSortOrder = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasSortOrder), typeof(IndexBuilder), typeof(SortOrder[])); - private static readonly MethodInfo IndexHasNullSortOrderMethodInfo + private static readonly MethodInfo Index_HasNullSortOrder = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasNullSortOrder), typeof(IndexBuilder), typeof(NullSortOrder[])); - private static readonly MethodInfo IndexIncludePropertiesMethodInfo + private static readonly MethodInfo Index_IncludeProperties = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.IncludeProperties), typeof(IndexBuilder), typeof(string[])); - private static readonly MethodInfo IndexAreNullsDistinctMethodInfo + private static readonly MethodInfo Index_AreNullsDistinct = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.AreNullsDistinct), typeof(IndexBuilder), typeof(bool)); + // ReSharper restore InconsistentNaming #endregion MethodInfos @@ -217,6 +215,20 @@ public override IReadOnlyList GenerateFluentApiCalls( fragments.Add(valueGenerationStrategy); } + if (TryGetAndRemove(annotations, NpgsqlAnnotationNames.PostgresExtensions, out SortedDictionary<(string, string?), IPostgresExtension>? postgresExtensions)) + { + foreach (var postgresExtension in postgresExtensions.Values) + { + var schema = postgresExtension.Schema is "public" ? null : postgresExtension.Schema; + + fragments.Add(postgresExtension.Version is not null + ? new MethodCallCodeFragment(Model_HasPostgresExtension, schema, postgresExtension.Name, postgresExtension.Version) + : schema is not null + ? new MethodCallCodeFragment(Model_HasPostgresExtension, schema, postgresExtension.Name) + : new MethodCallCodeFragment(Model_HasPostgresExtension, postgresExtension.Name)); + } + } + return fragments; } @@ -231,22 +243,13 @@ public override IReadOnlyList GenerateFluentApiCalls( Check.NotNull(model, nameof(model)); Check.NotNull(annotation, nameof(annotation)); - if (annotation.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal)) - { - var extension = new PostgresExtension(model, annotation.Name); - - return extension.Schema is "public" or null - ? new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo1, extension.Name) - : new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo2, extension.Schema, extension.Name); - } - if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal)) { var enumTypeDef = new PostgresEnum(model, annotation.Name); return enumTypeDef.Schema is null - ? new MethodCallCodeFragment(ModelHasPostgresEnumMethodInfo1, enumTypeDef.Name, enumTypeDef.Labels) - : new MethodCallCodeFragment(ModelHasPostgresEnumMethodInfo2, enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels); + ? new MethodCallCodeFragment(Model_HasPostgresEnum1, enumTypeDef.Name, enumTypeDef.Labels) + : new MethodCallCodeFragment(Model_HasPostgresEnum2, enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels); } if (annotation.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal)) @@ -259,10 +262,10 @@ rangeTypeDef.SubtypeOpClass is null && rangeTypeDef.Collation is null && rangeTypeDef.SubtypeDiff is null) { - return new MethodCallCodeFragment(ModelHasPostgresRangeMethodInfo1, rangeTypeDef.Name, rangeTypeDef.Subtype); + return new MethodCallCodeFragment(Model_HasPostgresRange1, rangeTypeDef.Name, rangeTypeDef.Subtype); } - return new MethodCallCodeFragment(ModelHasPostgresRangeMethodInfo2, + return new MethodCallCodeFragment(Model_HasPostgresRange2, rangeTypeDef.Schema, rangeTypeDef.Name, rangeTypeDef.Subtype, @@ -288,7 +291,7 @@ rangeTypeDef.Collation is null && if (annotation.Name == NpgsqlAnnotationNames.UnloggedTable) { - return new MethodCallCodeFragment(EntityTypeIsUnloggedMethodInfo, annotation.Value); + return new MethodCallCodeFragment(EntityType_IsUnlogged, annotation.Value); } return null; @@ -329,20 +332,20 @@ public override IReadOnlyList GenerateFluentApiCalls( switch (strategy) { case NpgsqlValueGenerationStrategy.SerialColumn: - return new(onModel ? ModelUseSerialColumnsMethodInfo : PropertyUseSerialColumnMethodInfo); + return new(onModel ? Model_UseSerialColumns : Property_UseSerialColumn); case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - return new(onModel ? ModelUseIdentityAlwaysColumnsMethodInfo : PropertyUseIdentityAlwaysColumnMethodInfo); + return new(onModel ? Model_UseIdentityAlwaysColumns : Property_UseIdentityAlwaysColumn); case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - return new(onModel ? ModelUseIdentityByDefaultColumnsMethodInfo : PropertyUseIdentityByDefaultColumnMethodInfo); + return new(onModel ? Model_UseIdentityByDefaultColumns : Property_UseIdentityByDefaultColumn); case NpgsqlValueGenerationStrategy.SequenceHiLo: { var name = GetAndRemove(NpgsqlAnnotationNames.HiLoSequenceName)!; var schema = GetAndRemove(NpgsqlAnnotationNames.HiLoSequenceSchema); return new( - onModel ? ModelUseHiLoMethodInfo : PropertyUseHiLoMethodInfo, + onModel ? Model_UseHiLo : Property_UseHiLo, (name, schema) switch { (null, null) => Array.Empty(), @@ -358,7 +361,7 @@ public override IReadOnlyList GenerateFluentApiCalls( var schema = GetAndRemove(NpgsqlAnnotationNames.SequenceSchema); return new MethodCallCodeFragment( - onModel ? ModelUseKeySequencesMethodInfo : PropertyUseSequenceMethodInfo, + onModel ? Model_UseKeySequences : Property_UseSequence, (name: nameOrSuffix, schema) switch { (null, null) => Array.Empty(), @@ -367,7 +370,7 @@ public override IReadOnlyList GenerateFluentApiCalls( }); } case NpgsqlValueGenerationStrategy.None: - return new(ModelHasAnnotationMethodInfo, NpgsqlAnnotationNames.ValueGenerationStrategy, NpgsqlValueGenerationStrategy.None); + return new(Model_HasAnnotation, NpgsqlAnnotationNames.ValueGenerationStrategy, NpgsqlValueGenerationStrategy.None); default: throw new ArgumentOutOfRangeException(strategy.ToString()); @@ -389,7 +392,7 @@ public override IReadOnlyList GenerateFluentApiCalls( var identityOptions = IdentitySequenceOptionsData.Deserialize(annotationValue); return new( - PropertyHasIdentityOptionsMethodInfo, + Property_HasIdentityOptions, identityOptions.StartValue, identityOptions.IncrementBy == 1 ? null : (long?)identityOptions.IncrementBy, identityOptions.MinValue, @@ -408,20 +411,20 @@ public override IReadOnlyList GenerateFluentApiCalls( => annotation.Name switch { RelationalAnnotationNames.Collation - => new MethodCallCodeFragment(IndexUseCollationMethodInfo, annotation.Value), + => new MethodCallCodeFragment(Index_UseCollation, annotation.Value), NpgsqlAnnotationNames.IndexMethod - => new MethodCallCodeFragment(IndexHasMethodMethodInfo, annotation.Value), + => new MethodCallCodeFragment(Index_HasMethod, annotation.Value), NpgsqlAnnotationNames.IndexOperators - => new MethodCallCodeFragment(IndexHasOperatorsMethodInfo, annotation.Value), + => new MethodCallCodeFragment(Index_HasOperators, annotation.Value), NpgsqlAnnotationNames.IndexSortOrder - => new MethodCallCodeFragment(IndexHasSortOrderMethodInfo, annotation.Value), + => new MethodCallCodeFragment(Index_HasSortOrder, annotation.Value), NpgsqlAnnotationNames.IndexNullSortOrder - => new MethodCallCodeFragment(IndexHasNullSortOrderMethodInfo, annotation.Value), + => new MethodCallCodeFragment(Index_HasNullSortOrder, annotation.Value), NpgsqlAnnotationNames.IndexInclude - => new MethodCallCodeFragment(IndexIncludePropertiesMethodInfo, annotation.Value), + => new MethodCallCodeFragment(Index_IncludeProperties, annotation.Value), NpgsqlAnnotationNames.NullsDistinct - => new MethodCallCodeFragment(IndexAreNullsDistinctMethodInfo, annotation.Value), + => new MethodCallCodeFragment(Index_AreNullsDistinct, annotation.Value), _ => null }; diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs index 8fcea2dc5..e6fba4b68 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs @@ -295,23 +295,18 @@ public static ModelBuilder UseKeySequences( /// The model builder in which to define the extension. /// The schema in which to create the extension. /// The name of the extension to create. - /// The version of the extension. + /// The extension version. Defaults to the latest installed, and is very rarely needed. /// The same builder instance so that multiple calls can be chained. /// /// See: https://www.postgresql.org/docs/current/external-extensions.html /// - /// public static ModelBuilder HasPostgresExtension( this ModelBuilder modelBuilder, string? schema, string name, string? version = null) { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - modelBuilder.Model.GetOrAddPostgresExtension(schema, name, version); + HasPostgresExtension(modelBuilder.Model, name, schema, version, ConfigurationSource.Explicit); return modelBuilder; } @@ -325,11 +320,12 @@ public static ModelBuilder HasPostgresExtension( /// /// See: https://www.postgresql.org/docs/current/external-extensions.html /// - /// - public static ModelBuilder HasPostgresExtension( - this ModelBuilder modelBuilder, - string name) - => modelBuilder.HasPostgresExtension(null, name); + public static ModelBuilder HasPostgresExtension(this ModelBuilder modelBuilder, string name) + { + HasPostgresExtension(modelBuilder.Model, name, schema: null, version: null, ConfigurationSource.Explicit); + + return modelBuilder; + } /// /// Registers a PostgreSQL extension in the model. @@ -344,63 +340,41 @@ public static ModelBuilder HasPostgresExtension( /// See: https://www.postgresql.org/docs/current/external-extensions.html /// /// - public static IConventionModelBuilder? HasPostgresExtension( + public static IConventionModelBuilder HasPostgresExtension( this IConventionModelBuilder modelBuilder, - string? schema, string name, + string? schema = null, string? version = null, bool fromDataAnnotation = false) { - if (modelBuilder.CanSetPostgresExtension(schema, name, version, fromDataAnnotation)) - { - modelBuilder.Metadata.GetOrAddPostgresExtension(schema, name, version); - return modelBuilder; - } + HasPostgresExtension( + (IMutableModel)modelBuilder.Metadata, + name, + schema, + version, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - return null; + return modelBuilder; } - /// - /// Registers a PostgreSQL extension in the model. - /// - /// The model builder in which to define the extension. - /// The name of the extension to create. - /// Indicates whether the configuration was specified using a data annotation. - /// The same builder instance so that multiple calls can be chained. - /// - /// See: https://www.postgresql.org/docs/current/external-extensions.html - /// - /// - public static IConventionModelBuilder? HasPostgresExtension( - this IConventionModelBuilder modelBuilder, + private static void HasPostgresExtension( + IMutableModel model, string name, - bool fromDataAnnotation = false) - => modelBuilder.HasPostgresExtension(schema: null, name, version: null, fromDataAnnotation); - - /// - /// Returns a value indicating whether the given PostgreSQL extension can be registered in the model. - /// - /// - /// See Modeling entity types and relationships, and - /// Accessing SQL Server and SQL Azure databases with EF Core - /// for more information and examples. - /// - /// The model builder. - /// The schema in which to create the extension. - /// The name of the extension to create. - /// The version of the extension. - /// Indicates whether the configuration was specified using a data annotation. - /// if the given value can be set as the default increment for SQL Server IDENTITY. - public static bool CanSetPostgresExtension( - this IConventionModelBuilder modelBuilder, string? schema, - string name, - string? version = null, - bool fromDataAnnotation = false) + string? version, + ConfigurationSource configurationSource) { - var annotationName = PostgresExtension.BuildAnnotationName(schema, name); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + + var postgresExtension = (PostgresExtension?)PostgresExtension.FindPostgresExtension(model, name, schema); + if (postgresExtension is not null) + { + postgresExtension.UpdateConfigurationSource(configurationSource); + return; + } - return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name},{version}", fromDataAnnotation); + PostgresExtension.AddPostgresExtension(model, name, schema, version, configurationSource); } #endregion diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs index 3bc52e04f..8b36d4219 100644 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs +++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs @@ -271,35 +271,9 @@ public static void SetValueGenerationStrategy(this IMutableModel model, NpgsqlVa /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static PostgresExtension GetOrAddPostgresExtension( - this IMutableModel model, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(model, schema, name, version); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static IReadOnlyList GetPostgresExtensions(this IReadOnlyModel model) + public static IReadOnlyList GetPostgresExtensions(this IReadOnlyModel model) => PostgresExtension.GetPostgresExtensions(model).ToArray(); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static PostgresExtension GetOrAddPostgresExtension( - this IConventionModel model, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(model, schema, name, version); - #endregion #region Enum types diff --git a/src/EFCore.PG/Extensions/NpgsqlAlterDatabaseOperationAnnotations.cs b/src/EFCore.PG/Extensions/NpgsqlAlterDatabaseOperationAnnotations.cs index 88db45a06..35e6b3b07 100644 --- a/src/EFCore.PG/Extensions/NpgsqlAlterDatabaseOperationAnnotations.cs +++ b/src/EFCore.PG/Extensions/NpgsqlAlterDatabaseOperationAnnotations.cs @@ -26,24 +26,6 @@ public static IReadOnlyList GetPostgresCollations(this AlterD public static IReadOnlyList GetOldPostgresCollations(this AlterDatabaseOperation operation) => PostgresCollation.GetCollations(operation.OldDatabase).ToArray(); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static IReadOnlyList GetPostgresExtensions(this AlterDatabaseOperation operation) - => PostgresExtension.GetPostgresExtensions(operation).ToArray(); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static IReadOnlyList GetOldPostgresExtensions(this AlterDatabaseOperation operation) - => PostgresExtension.GetPostgresExtensions(operation.OldDatabase).ToArray(); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -79,17 +61,4 @@ public static IReadOnlyList GetPostgresRanges(this AlterDatabaseO /// public static IReadOnlyList GetOldPostgresRanges(this AlterDatabaseOperation operation) => PostgresRange.GetPostgresRanges(operation.OldDatabase).ToArray(); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static PostgresExtension GetOrAddPostgresExtension( - this AlterDatabaseOperation operation, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(operation, schema, name, version); } diff --git a/src/EFCore.PG/Extensions/NpgsqlDatabaseModelExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlDatabaseModelExtensions.cs index 3c82a0626..661221d0c 100644 --- a/src/EFCore.PG/Extensions/NpgsqlDatabaseModelExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlDatabaseModelExtensions.cs @@ -11,28 +11,6 @@ namespace Microsoft.EntityFrameworkCore; /// public static class NpgsqlDatabaseModelExtensions { - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static PostgresExtension GetOrAddPostgresExtension( - this DatabaseModel model, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(model, schema, name, version); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static IReadOnlyList GetPostgresExtensions(this DatabaseModel model) - => PostgresExtension.GetPostgresExtensions(model).ToArray(); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs index eafc7fb5c..ef88d4e16 100644 --- a/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs @@ -17,53 +17,4 @@ public static class NpgsqlMigrationBuilderExtensions /// True if Npgsql is being used; false otherwise. public static bool IsNpgsql(this MigrationBuilder builder) => builder.ActiveProvider == typeof(NpgsqlMigrationBuilderExtensions).GetTypeInfo().Assembly.GetName().Name; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static MigrationBuilder EnsurePostgresExtension( - this MigrationBuilder builder, - string name, - string? schema = null, - string? version = null) - { - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NullButNotEmpty(version, nameof(schema)); - - var op = new AlterDatabaseOperation(); - op.GetOrAddPostgresExtension(schema, name, version); - builder.Operations.Add(op); - - return builder; - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - [Obsolete("Use EnsurePostgresExtension instead")] - public static MigrationBuilder CreatePostgresExtension( - this MigrationBuilder builder, - string name, - string? schema = null, - string? version = null) - => EnsurePostgresExtension(builder, name, schema, version); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - [Obsolete("This no longer does anything and should be removed.")] - public static MigrationBuilder DropPostgresExtension( - this MigrationBuilder builder, - string name) - => builder; } diff --git a/src/EFCore.PG/Metadata/IConventionPostgresExtension.cs b/src/EFCore.PG/Metadata/IConventionPostgresExtension.cs new file mode 100644 index 000000000..bd6588ce3 --- /dev/null +++ b/src/EFCore.PG/Metadata/IConventionPostgresExtension.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +/// +public interface IConventionPostgresExtension : IReadOnlyPostgresExtension +{ + /// + /// Gets the in which this PostgreSQL extension is defined. + /// + new IConventionModel? Model { get; } + + /// + /// Gets the configuration source for this . + /// + /// The configuration source for . + ConfigurationSource GetConfigurationSource(); + + // TODO: Schema, version? +} diff --git a/src/EFCore.PG/Metadata/IMutablePostgresExtension.cs b/src/EFCore.PG/Metadata/IMutablePostgresExtension.cs new file mode 100644 index 000000000..36b1197fd --- /dev/null +++ b/src/EFCore.PG/Metadata/IMutablePostgresExtension.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +/// +public interface IMutablePostgresExtension : IReadOnlyPostgresExtension +{ + /// + /// Gets the in which this PostgreSQL extension is defined. + /// + new IMutableModel? Model { get; } + + // TODO: Schema? +} diff --git a/src/EFCore.PG/Metadata/IPostgresExtension.cs b/src/EFCore.PG/Metadata/IPostgresExtension.cs new file mode 100644 index 000000000..5da2246a4 --- /dev/null +++ b/src/EFCore.PG/Metadata/IPostgresExtension.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +/// +public interface IPostgresExtension : IReadOnlyPostgresExtension +{ + /// + /// Gets the database schema that contains the extension. + /// + new IModel? Model { get; } +} diff --git a/src/EFCore.PG/Metadata/IReadOnlyPostgresExtension.cs b/src/EFCore.PG/Metadata/IReadOnlyPostgresExtension.cs new file mode 100644 index 000000000..51ce83802 --- /dev/null +++ b/src/EFCore.PG/Metadata/IReadOnlyPostgresExtension.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +/// +/// Represents a PostgreSQL extension in the model. +/// +public interface IReadOnlyPostgresExtension +{ + /// + /// Gets the name of the extension in the database. + /// + string Name { get; } + + /// + /// Gets the database schema that contains the extension. + /// + string? Schema { get; } + + /// + /// Gets the extension version. + /// + string? Version { get; } + + /// + /// Gets the model in which this extension is defined. + /// + IReadOnlyModel? Model { get; } + + /// + /// + /// Creates a human-readable representation of the given metadata. + /// + /// + /// Warning: Do not rely on the format of the returned string. + /// It is designed for debugging only and may change arbitrarily between releases. + /// + /// + /// Options for generating the string. + /// The number of indent spaces to use before each new line. + /// A human-readable representation. + string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) + { + var builder = new StringBuilder(); + var indentString = new string(' ', indent); + + builder + .Append(indentString) + .Append("PG Extension: "); + + if (Schema is not null) + { + builder.Append(Schema).Append('.'); + } + + builder.Append(Name); + + if (Version is not null) + { + builder.Append(" (Version=").Append(Version).Append(')'); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs index e063bdf6e..72713e2bb 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs @@ -183,15 +183,15 @@ public static class NpgsqlAnnotationNames /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public const string EnumPrefix = Prefix + "Enum:"; - + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public const string PostgresExtensionPrefix = Prefix + "PostgresExtension:"; - + public const string PostgresExtensions = Prefix + "PostgresExtensions"; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -271,4 +271,13 @@ public static class NpgsqlAnnotationNames /// // Replaced by IsDescending in EF Core 7.0 public const string IndexSortOrder = Prefix + "IndexSortOrder"; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + // Replaced by PostgresExtensions in EF Core 7.0 + public const string PostgresExtensionPrefix = Prefix + "PostgresExtension:"; } diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs index 72789148c..cbed305f0 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs @@ -1,3 +1,4 @@ +using System.Text; using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; @@ -217,14 +218,30 @@ public override IEnumerable For(IRelationalModel model, bool design { if (!designTime) { - return Array.Empty(); + yield break; + } + + // If only the extension name is given (the 99% case), just integrate the name directly. Otherwise integrate an array with + // the name, schema and version. + var postgresExtensions = PostgresExtension.GetPostgresExtensions(model.Model).ToArray(); + if (postgresExtensions.Length > 0) + { + yield return new Annotation( + NpgsqlAnnotationNames.PostgresExtensions, + postgresExtensions.All(e => e.Schema is null && e.Version is null) + ? postgresExtensions.Select(e => e.Name).ToArray() + : postgresExtensions.Select( + e => e.Schema is null && e.Version is null ? (object)e.Name : new[] { e.Name, e.Schema, e.Version }).ToArray()); } - return model.Model.GetAnnotations().Where( - a => - a.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal)); + foreach (var annotation in model.Model.GetAnnotations()) + { + if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) + || annotation.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal) + || annotation.Name.StartsWith(NpgsqlAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal)) + { + yield return annotation; + } + } } } diff --git a/src/EFCore.PG/Metadata/Internal/PostgresExtension.cs b/src/EFCore.PG/Metadata/Internal/PostgresExtension.cs new file mode 100644 index 000000000..3d3d48001 --- /dev/null +++ b/src/EFCore.PG/Metadata/Internal/PostgresExtension.cs @@ -0,0 +1,196 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; + +/// +/// Represents the metadata for a PostgreSQL extension. +/// +public class PostgresExtension : IPostgresExtension, IMutablePostgresExtension, IConventionPostgresExtension +{ + private readonly string? _schema; + + private ConfigurationSource _configurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + internal PostgresExtension( + string name, + string? schema, + string? version, + IReadOnlyModel? model, + ConfigurationSource configurationSource) + { + Model = model; + Name = name; + Version = version; + _schema = schema; + _configurationSource = configurationSource; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyModel? Model { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string Name { get; set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? Schema + => _schema ?? Model?.GetDefaultSchema(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? Version { get; set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource GetConfigurationSource() + => _configurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) + => _configurationSource = _configurationSource.Max(configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IPostgresExtension? FindPostgresExtension(IReadOnlyModel model, string name, string? schema) + => model[NpgsqlAnnotationNames.PostgresExtensions] is SortedDictionary<(string, string?), IPostgresExtension> postgresExtensions + && postgresExtensions.TryGetValue((name, schema), out var postgresExtension) + ? postgresExtension + : null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static PostgresExtension AddPostgresExtension( + IMutableAnnotatable annotatable, + string name, + string? schema, + string? version, + ConfigurationSource configurationSource) + { + var postgresExtension = new PostgresExtension(name, schema, version, annotatable as IReadOnlyModel, configurationSource); + var postgresExtensions = (SortedDictionary<(string, string?), IPostgresExtension>?)annotatable[NpgsqlAnnotationNames.PostgresExtensions]; + if (postgresExtensions == null) + { + postgresExtensions = new SortedDictionary<(string, string?), IPostgresExtension>(); + annotatable[NpgsqlAnnotationNames.PostgresExtensions] = postgresExtensions; + } + + postgresExtensions.Add((name, schema), postgresExtension); + + return postgresExtension; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IEnumerable GetPostgresExtensions(IReadOnlyAnnotatable annotatable) + { + switch (annotatable[NpgsqlAnnotationNames.PostgresExtensions]) + { + // This is the standard representation of the PostgresExtensions annotation in the model + case SortedDictionary<(string, string?), IPostgresExtension> postgresExtensions: + foreach (var postgresExtension in postgresExtensions.Values) + { + yield return postgresExtension; + } + break; + + // In migration Up/Down code, we can't have complex types, so we have a simplified serialized format - a simple array, + // whose elements are either a string (for a name-only extension) or a name-schema-version array. + case object[] extensionsArray: + foreach (var extensionData in extensionsArray) + { + yield return extensionData switch + { + string name => new PostgresExtension(name, schema: null, version: null, model: null, ConfigurationSource.Explicit), + string[] array => new PostgresExtension(name: array[0], schema: array[1], version: array[2], model: null, ConfigurationSource.Explicit), + _ => throw new InvalidOperationException("Invalid PostgreSQL extensions annotation value") + }; + } + break; + + case null: + yield break; + + default: + throw new InvalidOperationException("Invalid PostgreSQL extensions annotation value"); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IModel? IPostgresExtension.Model + { + [DebuggerStepThrough] + get => (IModel?)Model; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IMutableModel? IMutablePostgresExtension.Model + { + [DebuggerStepThrough] + get => (IMutableModel?)Model; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IConventionModel? IConventionPostgresExtension.Model + { + [DebuggerStepThrough] + get => (IConventionModel?)Model; + } +} \ No newline at end of file diff --git a/src/EFCore.PG/Metadata/PostgresExtension.cs b/src/EFCore.PG/Metadata/PostgresExtension.cs deleted file mode 100644 index 7b0899054..000000000 --- a/src/EFCore.PG/Metadata/PostgresExtension.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -/// -/// Represents the metadata for a PostgreSQL extension. -/// -public class PostgresExtension -{ - private readonly IReadOnlyAnnotatable _annotatable; - private readonly string _annotationName; - - /// - /// Creates a . - /// - /// The annotatable to search for the annotation. - /// The annotation name to search for in the annotatable. - /// - /// - internal PostgresExtension(IReadOnlyAnnotatable annotatable, string annotationName) - { - _annotatable = Check.NotNull(annotatable, nameof(annotatable)); - _annotationName = Check.NotNull(annotationName, nameof(annotationName)); - } - - /// - /// Gets or adds a from or to the . - /// - /// The annotatable from which to get or add the extension. - /// The extension schema or null to use the model's default schema. - /// The extension name. - /// The extension version. - /// - /// The from the . - /// - /// - /// - /// - public static PostgresExtension GetOrAddPostgresExtension( - IMutableAnnotatable annotatable, - string? schema, - string name, - string? version) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotNull(name, nameof(name)); - - if (FindPostgresExtension(annotatable, schema, name) is { } postgresExtension) - { - return postgresExtension; - } - - var annotationName = BuildAnnotationName(schema, name); - - return new PostgresExtension(annotatable, annotationName) { Version = version }; - } - - /// - /// Gets or adds a from or to the . - /// - /// The annotatable from which to get or add the extension. - /// The extension schema or null to use the model's default schema. - /// The extension name. - /// The extension version. - /// - /// The from the . - /// - /// - /// - /// - public static PostgresExtension GetOrAddPostgresExtension( - IConventionAnnotatable annotatable, - string? schema, - string name, - string? version) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotNull(name, nameof(name)); - - if (FindPostgresExtension(annotatable, schema, name) is { } postgresExtension) - { - return postgresExtension; - } - - var annotationName = BuildAnnotationName(schema, name); - - return new PostgresExtension(annotatable, annotationName) { Version = version }; - } - - /// - /// Gets or adds a from or to the . - /// - /// The annotatable from which to get or add the extension. - /// The extension name. - /// The extension version. - /// - /// The from the . - /// - /// - /// - public static PostgresExtension GetOrAddPostgresExtension( - IMutableAnnotatable annotatable, - string name, - string? version) - => GetOrAddPostgresExtension(annotatable, null, name, version); - - /// - /// Finds a in the , or returns null if not found. - /// - /// The annotatable to search for the extension. - /// The extension schema. The default schema is never used. - /// The extension name. - /// - /// The from the . - /// - /// - /// - /// - public static PostgresExtension? FindPostgresExtension( - IReadOnlyAnnotatable annotatable, - string? schema, - string name) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - var annotationName = BuildAnnotationName(schema, name); - - return annotatable[annotationName] is null ? null : new PostgresExtension(annotatable, annotationName); - } - - internal static string BuildAnnotationName(string? schema, string name) - => schema is not null - ? $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}{schema}.{name}" - : $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}{name}"; - - /// - /// Gets the collection of stored in the . - /// - /// The annotatable to search for annotations. - /// - /// The collection of stored in the . - /// - /// - public static IEnumerable GetPostgresExtensions(IReadOnlyAnnotatable annotatable) - => Check.NotNull(annotatable, nameof(annotatable)) - .GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal)) - .Select(a => new PostgresExtension(annotatable, a.Name)); - - /// - /// The that stores the extension. - /// - public virtual Annotatable Annotatable => (Annotatable)_annotatable; - - /// - /// The extension schema or null to represent the default schema. - /// - public virtual string? Schema => GetData().Schema; - - /// - /// The extension name. - /// - public virtual string Name => GetData().Name!; - - /// - /// The extension version. - /// - public virtual string? Version - { - get => GetData().Version; - set => SetData(value); - } - - private (string? Schema, string? Name, string? Version) GetData() - => Deserialize(Annotatable.FindAnnotation(_annotationName)!); - - private void SetData(string? version) - { - var data = GetData(); - Annotatable[_annotationName] = $"{data.Schema},{data.Name},{version}"; - } - - private static (string? Schema, string? Name, string? Version) Deserialize(IAnnotation? annotation) - { - if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) - { - return (null, null, null); - } - - // TODO: Can't actually use schema and name...they might not be set when this is first called. - var schemaNameValue = value.Split(',').Select(x => x.Trim()).Select(x => x == "" || x == "''" ? null : x).ToArray(); - var schemaAndName = annotation.Name.Substring(NpgsqlAnnotationNames.PostgresExtensionPrefix.Length).Split('.'); - switch (schemaAndName.Length) - { - case 1: - return (null, schemaAndName[0], schemaNameValue[2]); - case 2: - return (schemaAndName[0], schemaAndName[1], schemaNameValue[2]); - default: - throw new ArgumentException($"Cannot parse extension name from annotation: {annotation.Name}"); - } - } -} \ No newline at end of file diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index 1f61e84f7..23eddd0e6 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -1049,21 +1049,25 @@ protected override void Generate( throw new NotSupportedException("PostgreSQL does not support altering the collation on an existing database."); } + foreach (var postgresExtension in PostgresExtension.GetPostgresExtensions(operation).Concat(GetLegacyPostgresExtensions(operation))) + { + GenerateEnsureExtension(postgresExtension, model, builder); + } + + + // TODO: Legacy GenerateCollationStatements(operation, model, builder); GenerateEnumStatements(operation, model, builder); GenerateRangeStatements(operation, model, builder); - foreach (var extension in operation.GetPostgresExtensions()) - { - GenerateCreateExtension(extension, model, builder); - } - builder.EndCommand(); } - /// - protected virtual void GenerateCreateExtension( - PostgresExtension extension, + /// + /// Generates SQL to ensure that a PostgreSQL extension is created in the database. + /// + protected virtual void GenerateEnsureExtension( + IPostgresExtension extension, IModel? model, MigrationCommandListBuilder builder) { @@ -2149,4 +2153,38 @@ public IndexColumn(string name, string? @operator, string? collation, bool isDes } #endregion + + #region Legacy stuff + + private IEnumerable GetLegacyPostgresExtensions(AlterDatabaseOperation operation) + { + foreach (var annotation in operation.GetAnnotations()) + { + if (!annotation.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) + ||annotation.Value is not string value + || string.IsNullOrEmpty(value)) + { + continue; + } + + var schemaNameValue = value.Split(',').Select(x => x.Trim()).Select(x => x == "" || x == "''" ? null : x).ToArray(); + var schemaAndName = annotation.Name.Substring(NpgsqlAnnotationNames.PostgresExtensionPrefix.Length).Split('.'); + switch (schemaAndName.Length) + { + case 1: + yield return new PostgresExtension( + name: schemaAndName[0], schema: null, version: null, model: null, ConfigurationSource.Explicit); + continue; + case 2: + yield return new PostgresExtension( + name: schemaAndName[1], schema: schemaAndName[0], version: schemaNameValue[2], model: null, + ConfigurationSource.Explicit); + continue; + default: + throw new ArgumentException($"Cannot parse extension name from annotation: {annotation.Name}"); + } + } + } + + #endregion LegacyStuff } diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs index 3c2b2f439..477a61386 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs +++ b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs @@ -156,7 +156,7 @@ public static string StoredProcedureResultColumnsNotSupported(object? entityType entityType, sproc); /// - /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support return values; use output parameters instead. + /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with a return value. PostgreSQL stored procedures do not support return values; use an output parameter instead. /// public static string StoredProcedureReturnValueNotSupported(object? entityType, object? sproc) => string.Format( diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.resx b/src/EFCore.PG/Properties/NpgsqlStrings.resx index 22cada8de..4b04801db 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.resx +++ b/src/EFCore.PG/Properties/NpgsqlStrings.resx @@ -238,10 +238,7 @@ The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support result columns; use output parameters instead. - - The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support return values; use output parameters instead. - - The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support return values; use output parameters instead. + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with a return value. PostgreSQL stored procedures do not support return values; use an output parameter instead. \ No newline at end of file diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs index b40361059..51cc7f2cc 100644 --- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs +++ b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs @@ -1048,7 +1048,7 @@ private static void GetExtensions(NpgsqlConnection connection, DatabaseModel dat continue; } - databaseModel.GetOrAddPostgresExtension(schema, name, version); + PostgresExtension.AddPostgresExtension(databaseModel, name, schema, version, ConfigurationSource.Explicit); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRowValueTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRowValueTypeMapping.cs index 8975b1c24..3dbc811da 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRowValueTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRowValueTypeMapping.cs @@ -2,13 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data.Common; +using System.Runtime.CompilerServices; +using System.Text; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// -/// TODO: Update -/// Every node in the SQL tree must have a type mapping, but row values aren't actual values (in the sense that they can be sent as -/// parameters, or have a literal representation). So we have a dummy type mapping for that. +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. /// public class NpgsqlRowValueTypeMapping : RelationalTypeMapping { diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs b/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs index 7bf860cd7..246d229a7 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs @@ -1,5 +1,6 @@ using System.Net.Sockets; using System.Transactions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; @@ -319,7 +320,7 @@ public override void CreateTables() var reloadTypes = operations.OfType() .Any(o => - o.GetPostgresExtensions().Any() || + PostgresExtension.GetPostgresExtensions(o).Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); @@ -365,7 +366,7 @@ public override async Task CreateTablesAsync(CancellationToken cancellationToken var reloadTypes = operations.OfType() .Any(o => - o.GetPostgresExtensions().Any() || + PostgresExtension.GetPostgresExtensions(o).Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index 194b3458c..188c0fdde 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -2716,13 +2716,12 @@ await Test( builder => builder.HasPostgresExtension("citext"), model => { - var citext = Assert.Single(model.GetPostgresExtensions()); + var citext = Assert.Single(PostgresExtension.GetPostgresExtensions(model)); Assert.Equal("citext", citext.Name); Assert.Equal("public", citext.Schema); }); - AssertSql( - @"CREATE EXTENSION IF NOT EXISTS citext;"); + AssertSql("CREATE EXTENSION IF NOT EXISTS citext;"); } [Fact] @@ -2733,20 +2732,24 @@ await Test( builder => builder.HasPostgresExtension("some_schema", "citext"), model => { - var citext = Assert.Single(model.GetPostgresExtensions()); + var citext = Assert.Single(PostgresExtension.GetPostgresExtensions(model)); Assert.Equal("citext", citext.Name); Assert.Equal("some_schema", citext.Schema); }); AssertSql( - @"DO $EF$ +""" +DO $EF$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN CREATE SCHEMA some_schema; END IF; -END $EF$;", +END $EF$; +""", // - @"CREATE EXTENSION IF NOT EXISTS citext SCHEMA some_schema;"); +""" +CREATE EXTENSION IF NOT EXISTS citext SCHEMA some_schema; +"""); } #endregion diff --git a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs index d4c28f65c..f356f30d9 100644 --- a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs @@ -1963,7 +1963,7 @@ public void Postgres_extensions() Enumerable.Empty(), dbModel => { - var extensions = dbModel.GetPostgresExtensions(); + var extensions = PostgresExtension.GetPostgresExtensions(dbModel); Assert.Collection(extensions.OrderBy(e => e.Name), e => { diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs index 47d6e602e..ae0fa15ff 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs @@ -2,6 +2,7 @@ using System.Text; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; @@ -160,7 +161,7 @@ FROM pg_collation coll protected override string BuildCustomSql(DatabaseModel databaseModel) // Some extensions create tables (e.g. PostGIS), so we must drop them first. - => databaseModel.GetPostgresExtensions() + => PostgresExtension.GetPostgresExtensions(databaseModel) .Select(e => _sqlGenerationHelper.DelimitIdentifier(e.Name, e.Schema)) .Aggregate(new StringBuilder(), (builder, s) => builder.Append("DROP EXTENSION ").Append(s).Append(";"),