diff --git a/Directory.Build.props b/Directory.Build.props index 6c5bd9986..cff76730e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,7 @@ Copyright 2022 © The Npgsql Development Team Npgsql - 7.0.0-preview.7 + 7.0.0-rc.1 true PostgreSQL https://github.com/npgsql/efcore.pg diff --git a/Directory.Packages.props b/Directory.Packages.props index a6fb4bf67..853cccb09 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - 7.0.0-preview.7.22376.2 - 7.0.0-preview.7.22375.6 + 7.0.0-rc.1.22378.4 + 7.0.0-rc.1.22374.4 7.0.0-preview.6 diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index d816c3797..a3606de3b 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -8,96 +8,104 @@ public class NpgsqlAnnotationCodeGenerator : AnnotationCodeGenerator { #region MethodInfos - private static readonly MethodInfo _modelHasPostgresExtensionMethodInfo1 + private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo1 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string)); - private static readonly MethodInfo _modelHasPostgresExtensionMethodInfo2 + private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo2 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string)); - private static readonly MethodInfo _modelHasPostgresEnumMethodInfo1 + private static readonly MethodInfo ModelHasPostgresEnumMethodInfo1 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string[])); - - private static readonly MethodInfo _modelHasPostgresEnumMethodInfo2 + + private static readonly MethodInfo ModelHasPostgresEnumMethodInfo2 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string[])); - private static readonly MethodInfo _modelHasPostgresRangeMethodInfo1 + private static readonly MethodInfo ModelHasPostgresRangeMethodInfo1 = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string)); - - private static readonly MethodInfo _modelHasPostgresRangeMethodInfo2 + + private static readonly MethodInfo ModelHasPostgresRangeMethodInfo2 = 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 ModelUseSerialColumnsMethodInfo = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseSerialColumns), typeof(ModelBuilder)); - private static readonly MethodInfo _modelUseIdentityAlwaysColumnsMethodInfo + private static readonly MethodInfo ModelUseIdentityAlwaysColumnsMethodInfo = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseIdentityAlwaysColumns), typeof(ModelBuilder)); - private static readonly MethodInfo _modelUseIdentityByDefaultColumnsMethodInfo + private static readonly MethodInfo ModelUseIdentityByDefaultColumnsMethodInfo = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns), typeof(ModelBuilder)); - private static readonly MethodInfo _modelUseHiLoMethodInfo + private static readonly MethodInfo ModelUseHiLoMethodInfo = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseHiLo), typeof(ModelBuilder), typeof(string), typeof(string)); - private static readonly MethodInfo _modelHasAnnotationMethodInfo + private static readonly MethodInfo ModelHasAnnotationMethodInfo = typeof(ModelBuilder).GetRequiredRuntimeMethod( nameof(ModelBuilder.HasAnnotation), typeof(string), typeof(object)); - private static readonly MethodInfo _entityTypeIsUnloggedMethodInfo + private static readonly MethodInfo ModelUseKeySequencesMethodInfo + = typeof(NpgsqlModelBuilderExtensions).GetRuntimeMethod( + nameof(NpgsqlModelBuilderExtensions.UseKeySequences), new[] { typeof(ModelBuilder), typeof(string), typeof(string) })!; + + private static readonly MethodInfo EntityTypeIsUnloggedMethodInfo = typeof(NpgsqlEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlEntityTypeBuilderExtensions.IsUnlogged), typeof(EntityTypeBuilder), typeof(bool)); - private static readonly MethodInfo _propertyUseSerialColumnMethodInfo + private static readonly MethodInfo PropertyUseSerialColumnMethodInfo = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseSerialColumn), typeof(PropertyBuilder)); - private static readonly MethodInfo _propertyUseIdentityAlwaysColumnMethodInfo + private static readonly MethodInfo PropertyUseIdentityAlwaysColumnMethodInfo = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn), typeof(PropertyBuilder)); - private static readonly MethodInfo _propertyUseIdentityByDefaultColumnMethodInfo + private static readonly MethodInfo PropertyUseIdentityByDefaultColumnMethodInfo = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn), typeof(PropertyBuilder)); - private static readonly MethodInfo _propertyUseHiLoMethodInfo + private static readonly MethodInfo PropertyUseHiLoMethodInfo = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.UseHiLo), typeof(PropertyBuilder), typeof(string), typeof(string)); - private static readonly MethodInfo _propertyHasIdentityOptionsMethodInfo + private static readonly MethodInfo PropertyHasIdentityOptionsMethodInfo = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlPropertyBuilderExtensions.HasIdentityOptions), typeof(PropertyBuilder), typeof(long?), typeof(long?), typeof(long?), typeof(long?), typeof(bool?), typeof(long?)); - private static readonly MethodInfo _indexUseCollationMethodInfo + private static readonly MethodInfo PropertyUseSequenceMethodInfo + = typeof(NpgsqlPropertyBuilderExtensions).GetRuntimeMethod( + nameof(NpgsqlPropertyBuilderExtensions.UseSequence), new[] { typeof(PropertyBuilder), typeof(string), typeof(string) })!; + + private static readonly MethodInfo IndexUseCollationMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.UseCollation), typeof(IndexBuilder), typeof(string[])); - private static readonly MethodInfo _indexHasMethodMethodInfo + private static readonly MethodInfo IndexHasMethodMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasMethod), typeof(IndexBuilder), typeof(string)); - private static readonly MethodInfo _indexHasOperatorsMethodInfo + private static readonly MethodInfo IndexHasOperatorsMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasOperators), typeof(IndexBuilder), typeof(string[])); - private static readonly MethodInfo _indexHasSortOrderMethodInfo + private static readonly MethodInfo IndexHasSortOrderMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasSortOrder), typeof(IndexBuilder), typeof(SortOrder[])); - private static readonly MethodInfo _indexHasNullSortOrderMethodInfo + private static readonly MethodInfo IndexHasNullSortOrderMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasNullSortOrder), typeof(IndexBuilder), typeof(NullSortOrder[])); - private static readonly MethodInfo _indexIncludePropertiesMethodInfo + private static readonly MethodInfo IndexIncludePropertiesMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.IncludeProperties), typeof(IndexBuilder), typeof(string[])); @@ -161,9 +169,16 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an public override IReadOnlyList GenerateFluentApiCalls( IModel model, IDictionary annotations) - => base.GenerateFluentApiCalls(model, annotations) - .Concat(GenerateValueGenerationStrategy(annotations, onModel: true)) - .ToList(); + { + var fragments = new List(base.GenerateFluentApiCalls(model, annotations)); + + if (GenerateValueGenerationStrategy(annotations, onModel: true) is { } valueGenerationStrategy) + { + fragments.Add(valueGenerationStrategy); + } + + return fragments; + } protected override MethodCallCodeFragment? GenerateFluentApi(IModel model, IAnnotation annotation) { @@ -175,8 +190,8 @@ public override IReadOnlyList GenerateFluentApiCalls( 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); + ? new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo1, extension.Name) + : new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo2, extension.Schema, extension.Name); } if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal)) @@ -184,8 +199,8 @@ public override IReadOnlyList GenerateFluentApiCalls( 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(ModelHasPostgresEnumMethodInfo1, enumTypeDef.Name, enumTypeDef.Labels) + : new MethodCallCodeFragment(ModelHasPostgresEnumMethodInfo2, enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels); } if (annotation.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal)) @@ -198,10 +213,10 @@ rangeTypeDef.SubtypeOpClass is null && rangeTypeDef.Collation is null && rangeTypeDef.SubtypeDiff is null) { - return new MethodCallCodeFragment(_modelHasPostgresRangeMethodInfo1, rangeTypeDef.Name, rangeTypeDef.Subtype); + return new MethodCallCodeFragment(ModelHasPostgresRangeMethodInfo1, rangeTypeDef.Name, rangeTypeDef.Subtype); } - return new MethodCallCodeFragment(_modelHasPostgresRangeMethodInfo2, + return new MethodCallCodeFragment(ModelHasPostgresRangeMethodInfo2, rangeTypeDef.Schema, rangeTypeDef.Name, rangeTypeDef.Subtype, @@ -221,7 +236,7 @@ rangeTypeDef.Collation is null && if (annotation.Name == NpgsqlAnnotationNames.UnloggedTable) { - return new MethodCallCodeFragment(_entityTypeIsUnloggedMethodInfo, annotation.Value); + return new MethodCallCodeFragment(EntityTypeIsUnloggedMethodInfo, annotation.Value); } return null; @@ -230,61 +245,71 @@ rangeTypeDef.Collation is null && public override IReadOnlyList GenerateFluentApiCalls( IProperty property, IDictionary annotations) - => base.GenerateFluentApiCalls(property, annotations) - .Concat(GenerateValueGenerationStrategy(annotations, onModel: false)) - .Concat(GenerateIdentityOptions(annotations)) - .ToList(); + { + var fragments = new List(base.GenerateFluentApiCalls(property, annotations)); - private IReadOnlyList GenerateValueGenerationStrategy( - IDictionary annotations, - bool onModel) + if (GenerateValueGenerationStrategy(annotations, onModel: false) is { } valueGenerationStrategy) + { + fragments.Add(valueGenerationStrategy); + } + + if (GenerateIdentityOptions(annotations) is { } identityOptionsFragment) + { + fragments.Add(identityOptionsFragment); + } + + return fragments; + } + + private MethodCallCodeFragment? GenerateValueGenerationStrategy(IDictionary annotations, bool onModel) { - if (!TryGetAndRemove(annotations, NpgsqlAnnotationNames.ValueGenerationStrategy, - out NpgsqlValueGenerationStrategy strategy)) + if (!TryGetAndRemove(annotations, NpgsqlAnnotationNames.ValueGenerationStrategy, out NpgsqlValueGenerationStrategy strategy)) { - return Array.Empty(); + return null; } switch (strategy) { case NpgsqlValueGenerationStrategy.SerialColumn: - return new List - { - new(onModel ? _modelUseSerialColumnsMethodInfo : _propertyUseSerialColumnMethodInfo) - }; + return new(onModel ? ModelUseSerialColumnsMethodInfo : PropertyUseSerialColumnMethodInfo); case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - return new List - { - new(onModel ? _modelUseIdentityAlwaysColumnsMethodInfo : _propertyUseIdentityAlwaysColumnMethodInfo) - }; + return new(onModel ? ModelUseIdentityAlwaysColumnsMethodInfo : PropertyUseIdentityAlwaysColumnMethodInfo); case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - return new List - { - new(onModel ? _modelUseIdentityByDefaultColumnsMethodInfo : _propertyUseIdentityByDefaultColumnMethodInfo) - }; + return new(onModel ? ModelUseIdentityByDefaultColumnsMethodInfo : PropertyUseIdentityByDefaultColumnMethodInfo); case NpgsqlValueGenerationStrategy.SequenceHiLo: + { var name = GetAndRemove(NpgsqlAnnotationNames.HiLoSequenceName)!; var schema = GetAndRemove(NpgsqlAnnotationNames.HiLoSequenceSchema); - return new List - { - new( - onModel ? _modelUseHiLoMethodInfo : _propertyUseHiLoMethodInfo, - (name, schema) switch - { - (null, null) => Array.Empty(), - (_, null) => new object[] { name }, - _ => new object?[] { name!, schema } - }) - }; + return new( + onModel ? ModelUseHiLoMethodInfo : PropertyUseHiLoMethodInfo, + (name, schema) switch + { + (null, null) => Array.Empty(), + (_, null) => new object[] { name }, + _ => new object?[] { name!, schema } + }); + } + case NpgsqlValueGenerationStrategy.Sequence: + { + var nameOrSuffix = GetAndRemove( + onModel ? NpgsqlAnnotationNames.SequenceNameSuffix : NpgsqlAnnotationNames.SequenceName); + + var schema = GetAndRemove(NpgsqlAnnotationNames.SequenceSchema); + return new MethodCallCodeFragment( + onModel ? ModelUseKeySequencesMethodInfo : PropertyUseSequenceMethodInfo, + (name: nameOrSuffix, schema) switch + { + (null, null) => Array.Empty(), + (_, null) => new object[] { nameOrSuffix }, + _ => new object[] { nameOrSuffix!, schema } + }); + } case NpgsqlValueGenerationStrategy.None: - return new List - { - new(_modelHasAnnotationMethodInfo, NpgsqlAnnotationNames.ValueGenerationStrategy, NpgsqlValueGenerationStrategy.None) - }; + return new(ModelHasAnnotationMethodInfo, NpgsqlAnnotationNames.ValueGenerationStrategy, NpgsqlValueGenerationStrategy.None); default: throw new ArgumentOutOfRangeException(strategy.ToString()); @@ -296,44 +321,41 @@ private IReadOnlyList GenerateValueGenerationStrategy( : default; } - private IReadOnlyList GenerateIdentityOptions(IDictionary annotations) + private MethodCallCodeFragment? GenerateIdentityOptions(IDictionary annotations) { if (!TryGetAndRemove(annotations, NpgsqlAnnotationNames.IdentityOptions, out string? annotationValue)) { - return Array.Empty(); + return null; } var identityOptions = IdentitySequenceOptionsData.Deserialize(annotationValue); - return new List - { - new( - _propertyHasIdentityOptionsMethodInfo, - identityOptions.StartValue, - identityOptions.IncrementBy == 1 ? null : (long?) identityOptions.IncrementBy, - identityOptions.MinValue, - identityOptions.MaxValue, - identityOptions.IsCyclic ? true : null, - identityOptions.NumbersToCache == 1 ? null : (long?) identityOptions.NumbersToCache) - }; + return new( + PropertyHasIdentityOptionsMethodInfo, + identityOptions.StartValue, + identityOptions.IncrementBy == 1 ? null : (long?)identityOptions.IncrementBy, + identityOptions.MinValue, + identityOptions.MaxValue, + identityOptions.IsCyclic ? true : null, + identityOptions.NumbersToCache == 1 ? null : (long?)identityOptions.NumbersToCache); } protected override MethodCallCodeFragment? GenerateFluentApi(IIndex index, IAnnotation annotation) => annotation.Name switch { RelationalAnnotationNames.Collation - => new MethodCallCodeFragment(_indexUseCollationMethodInfo, annotation.Value), + => new MethodCallCodeFragment(IndexUseCollationMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexMethod - => new MethodCallCodeFragment(_indexHasMethodMethodInfo, annotation.Value), + => new MethodCallCodeFragment(IndexHasMethodMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexOperators - => new MethodCallCodeFragment(_indexHasOperatorsMethodInfo, annotation.Value), + => new MethodCallCodeFragment(IndexHasOperatorsMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexSortOrder - => new MethodCallCodeFragment(_indexHasSortOrderMethodInfo, annotation.Value), + => new MethodCallCodeFragment(IndexHasSortOrderMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexNullSortOrder - => new MethodCallCodeFragment(_indexHasNullSortOrderMethodInfo, annotation.Value), + => new MethodCallCodeFragment(IndexHasNullSortOrderMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexInclude - => new MethodCallCodeFragment(_indexIncludePropertiesMethodInfo, annotation.Value), + => new MethodCallCodeFragment(IndexIncludePropertiesMethodInfo, annotation.Value), _ => null }; diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs index 6954df586..1f8a1c99b 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs @@ -38,6 +38,8 @@ public static ModelBuilder UseHiLo(this ModelBuilder modelBuilder, string? name model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); model.SetHiLoSequenceName(name); model.SetHiLoSequenceSchema(schema); + model.SetSequenceNameSuffix(null); + model.SetSequenceSchema(null); return modelBuilder; } @@ -138,11 +140,13 @@ public static ModelBuilder UseIdentityAlwaysColumns(this ModelBuilder modelBuild { Check.NotNull(modelBuilder, nameof(modelBuilder)); - var property = modelBuilder.Model; + var model = modelBuilder.Model; - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn); - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); + model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn); + model.SetSequenceNameSuffix(null); + model.SetSequenceSchema(null); + model.SetHiLoSequenceName(null); + model.SetHiLoSequenceSchema(null); return modelBuilder; } @@ -164,11 +168,13 @@ public static ModelBuilder UseIdentityByDefaultColumns(this ModelBuilder modelBu { Check.NotNull(modelBuilder, nameof(modelBuilder)); - var property = modelBuilder.Model; + var model = modelBuilder.Model; - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); + model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + model.SetSequenceNameSuffix(null); + model.SetSequenceSchema(null); + model.SetHiLoSequenceName(null); + model.SetHiLoSequenceSchema(null); return modelBuilder; } @@ -203,15 +209,25 @@ public static ModelBuilder UseIdentityColumns(this ModelBuilder modelBuilder) NpgsqlValueGenerationStrategy? valueGenerationStrategy, bool fromDataAnnotation = false) { - if (modelBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation)) + if (modelBuilder.CanSetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation)) { modelBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); + if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.SequenceHiLo) { modelBuilder.HasHiLoSequence(null, null, fromDataAnnotation); } + if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.Sequence) + { + if (modelBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix, null) + && modelBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceSchema, null)) + { + modelBuilder.Metadata.SetSequenceNameSuffix(null, fromDataAnnotation); + modelBuilder.Metadata.SetSequenceSchema(null, fromDataAnnotation); + } + } + return modelBuilder; } @@ -238,6 +254,39 @@ public static bool CanSetValueGenerationStrategy( #endregion Identity + #region Sequence + + /// + /// Configures the model to use a sequence per hierarchy to generate values for key properties marked as + /// , when targeting PostgreSQL. + /// + /// The model builder. + /// The name that will suffix the table name for each sequence created automatically. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseKeySequences( + this ModelBuilder modelBuilder, + string? nameSuffix = null, + string? schema = null) + { + Check.NullButNotEmpty(nameSuffix, nameof(nameSuffix)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var model = modelBuilder.Model; + + nameSuffix ??= NpgsqlModelExtensions.DefaultSequenceNameSuffix; + + model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.Sequence); + model.SetSequenceNameSuffix(nameSuffix); + model.SetSequenceSchema(schema); + model.SetHiLoSequenceName(null); + model.SetHiLoSequenceSchema(null); + + return modelBuilder; + } + + #endregion Sequence + #region Extensions /// diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs index e1ad1672c..953c956f2 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs @@ -44,6 +44,8 @@ public static PropertyBuilder UseHiLo( property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); property.SetHiLoSequenceName(name); property.SetHiLoSequenceSchema(schema); + property.SetSequenceName(null); + property.SetSequenceSchema(null); property.RemoveIdentityOptions(); return propertyBuilder; @@ -115,6 +117,106 @@ public static bool CanSetHiLoSequence( #endregion HiLo + #region Sequence + + /// + /// Configures the key property to use a sequence-based key value generation pattern to generate values for new entities, + /// when targeting PostgreSQL. This method sets the property to be . + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseSequence( + this PropertyBuilder propertyBuilder, + string? name = null, + string? schema = null) + { + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var property = propertyBuilder.Metadata; + + property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.Sequence); + property.SetSequenceName(name); + property.SetSequenceSchema(schema); + property.SetHiLoSequenceName(null); + property.SetHiLoSequenceSchema(null); + + return propertyBuilder; + } + + /// + /// Configures the key property to use a sequence-based key value generation pattern to generate values for new entities, + /// when targeting SQL Server. This method sets the property to be . + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseSequence( + this PropertyBuilder propertyBuilder, + string? name = null, + string? schema = null) + => (PropertyBuilder)UseSequence((PropertyBuilder)propertyBuilder, name, schema); + + /// + /// Configures the database sequence used for the key value generation pattern to generate values for the key property, + /// when targeting PostgreSQL. + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the sequence. + public static IConventionSequenceBuilder? HasSequence( + this IConventionPropertyBuilder propertyBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetSequence(name, schema, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetSequenceName(name, fromDataAnnotation); + propertyBuilder.Metadata.SetSequenceSchema(schema, fromDataAnnotation); + + return name == null + ? null + : propertyBuilder.Metadata.DeclaringEntityType.Model.Builder.HasSequence(name, schema, fromDataAnnotation); + } + + /// + /// Returns a value indicating whether the given name and schema can be set for the key value generation sequence. + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given name and schema can be set for the key value generation sequence. + public static bool CanSetSequence( + this IConventionPropertyBuilder propertyBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + return propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceName, name, fromDataAnnotation) + && propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceSchema, schema, fromDataAnnotation); + } + + #endregion Sequence + #region Serial /// @@ -133,8 +235,8 @@ public static PropertyBuilder UseSerialColumn( var property = propertyBuilder.Metadata; property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SerialColumn); - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); + property.SetSequenceName(null); + property.SetSequenceSchema(null); property.RemoveHiLoOptions(); property.RemoveIdentityOptions(); @@ -178,6 +280,8 @@ public static PropertyBuilder UseIdentityAlwaysColumn(this PropertyBuilder prope property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn); property.SetHiLoSequenceName(null); property.SetHiLoSequenceSchema(null); + property.SetSequenceName(null); + property.SetSequenceSchema(null); return propertyBuilder; } @@ -222,6 +326,8 @@ public static PropertyBuilder UseIdentityByDefaultColumn(this PropertyBuilder pr property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); property.SetHiLoSequenceName(null); property.SetHiLoSequenceSchema(null); + property.SetSequenceName(null); + property.SetSequenceSchema(null); return propertyBuilder; } @@ -303,11 +409,17 @@ public static PropertyBuilder UseIdentityColumn( NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation)) { propertyBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); + if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.SequenceHiLo) { propertyBuilder.HasHiLoSequence(null, null, fromDataAnnotation); } + if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.Sequence) + { + propertyBuilder.HasSequence(null, null, fromDataAnnotation); + } + return propertyBuilder; } diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs index 6125932ac..d11bda717 100644 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs +++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs @@ -6,8 +6,16 @@ namespace Microsoft.EntityFrameworkCore; public static class NpgsqlModelExtensions { + /// + /// The default name for the hi-lo sequence. + /// public const string DefaultHiLoSequenceName = "EntityFrameworkHiLoSequence"; + /// + /// The default prefix for sequences applied to properties. + /// + public const string DefaultSequenceNameSuffix = "Sequence"; + #region HiLo /// @@ -102,6 +110,106 @@ public static void SetHiLoSequenceSchema(this IMutableModel model, string? value #endregion + #region Sequence + + /// + /// Returns the suffix to append to the name of automatically created sequences. + /// + /// The model. + /// The name to use for the default key value generation sequence. + public static string GetSequenceNameSuffix(this IReadOnlyModel model) + => (string?)model[NpgsqlAnnotationNames.SequenceNameSuffix] + ?? DefaultSequenceNameSuffix; + + /// + /// Sets the suffix to append to the name of automatically created sequences. + /// + /// The model. + /// The value to set. + public static void SetSequenceNameSuffix(this IMutableModel model, string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix, name); + } + + /// + /// Sets the suffix to append to the name of automatically created sequences. + /// + /// The model. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceNameSuffix( + this IConventionModel model, + string? name, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(name, nameof(name)); + + model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix, name, fromDataAnnotation); + + return name; + } + + /// + /// Returns the for the default value generation sequence name suffix. + /// + /// The model. + /// The for the default key value generation sequence name. + public static ConfigurationSource? GetSequenceNameSuffixConfigurationSource(this IConventionModel model) + => model.FindAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix)?.GetConfigurationSource(); + + /// + /// Returns the schema to use for the default value generation sequence. + /// + /// + /// The model. + /// The schema to use for the default key value generation sequence. + public static string? GetSequenceSchema(this IReadOnlyModel model) + => (string?)model[NpgsqlAnnotationNames.SequenceSchema]; + + /// + /// Sets the schema to use for the default key value generation sequence. + /// + /// The model. + /// The value to set. + public static void SetSequenceSchema(this IMutableModel model, string? value) + { + Check.NullButNotEmpty(value, nameof(value)); + + model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceSchema, value); + } + + /// + /// Sets the schema to use for the default key value generation sequence. + /// + /// The model. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceSchema( + this IConventionModel model, + string? value, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(value, nameof(value)); + + model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceSchema, value, fromDataAnnotation); + + return value; + } + + /// + /// Returns the for the default key value generation sequence schema. + /// + /// The model. + /// The for the default key value generation sequence schema. + public static ConfigurationSource? GetSequenceSchemaConfigurationSource(this IConventionModel model) + => model.FindAnnotation(NpgsqlAnnotationNames.SequenceSchema)?.GetConfigurationSource(); + + #endregion Sequence + #region Value Generation Strategy /// diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs index 3d7bf154a..6c8c50d0b 100644 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs +++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs @@ -211,6 +211,190 @@ public static void RemoveHiLoOptions(this IConventionProperty property) #endregion Hi-lo + #region Sequence + + /// + /// Returns the name to use for the key value generation sequence. + /// + /// The property. + /// The name to use for the key value generation sequence. + public static string? GetSequenceName(this IReadOnlyProperty property) + => (string?)property[NpgsqlAnnotationNames.SequenceName]; + + /// + /// Returns the name to use for the key value generation sequence. + /// + /// The property. + /// The identifier of the store object. + /// The name to use for the key value generation sequence. + public static string? GetSequenceName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var annotation = property.FindAnnotation(NpgsqlAnnotationNames.SequenceName); + if (annotation != null) + { + return (string?)annotation.Value; + } + + return property.FindSharedStoreObjectRootProperty(storeObject)?.GetSequenceName(storeObject); + } + + /// + /// Sets the name to use for the key value generation sequence. + /// + /// The property. + /// The sequence name to use. + public static void SetSequenceName(this IMutableProperty property, string? name) + => property.SetOrRemoveAnnotation( + NpgsqlAnnotationNames.SequenceName, + Check.NullButNotEmpty(name, nameof(name))); + + /// + /// Sets the name to use for the key value generation sequence. + /// + /// The property. + /// The sequence name to use. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceName( + this IConventionProperty property, + string? name, + bool fromDataAnnotation = false) + { + property.SetOrRemoveAnnotation( + NpgsqlAnnotationNames.SequenceName, + Check.NullButNotEmpty(name, nameof(name)), + fromDataAnnotation); + + return name; + } + + /// + /// Returns the for the key value generation sequence name. + /// + /// The property. + /// The for the key value generation sequence name. + public static ConfigurationSource? GetSequenceNameConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(NpgsqlAnnotationNames.SequenceName)?.GetConfigurationSource(); + + /// + /// Returns the schema to use for the key value generation sequence. + /// + /// The property. + /// The schema to use for the key value generation sequence. + public static string? GetSequenceSchema(this IReadOnlyProperty property) + => (string?)property[NpgsqlAnnotationNames.SequenceSchema]; + + /// + /// Returns the schema to use for the key value generation sequence. + /// + /// The property. + /// The identifier of the store object. + /// The schema to use for the key value generation sequence. + public static string? GetSequenceSchema(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var annotation = property.FindAnnotation(NpgsqlAnnotationNames.SequenceSchema); + if (annotation != null) + { + return (string?)annotation.Value; + } + + return property.FindSharedStoreObjectRootProperty(storeObject)?.GetSequenceSchema(storeObject); + } + + /// + /// Sets the schema to use for the key value generation sequence. + /// + /// The property. + /// The schema to use. + public static void SetSequenceSchema(this IMutableProperty property, string? schema) + => property.SetOrRemoveAnnotation( + NpgsqlAnnotationNames.SequenceSchema, + Check.NullButNotEmpty(schema, nameof(schema))); + + /// + /// Sets the schema to use for the key value generation sequence. + /// + /// The property. + /// The schema to use. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceSchema( + this IConventionProperty property, + string? schema, + bool fromDataAnnotation = false) + { + property.SetOrRemoveAnnotation( + NpgsqlAnnotationNames.SequenceSchema, + Check.NullButNotEmpty(schema, nameof(schema)), + fromDataAnnotation); + + return schema; + } + + /// + /// Returns the for the key value generation sequence schema. + /// + /// The property. + /// The for the key value generation sequence schema. + public static ConfigurationSource? GetSequenceSchemaConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(NpgsqlAnnotationNames.SequenceSchema)?.GetConfigurationSource(); + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The sequence to use, or if no sequence exists in the model. + public static IReadOnlySequence? FindSequence(this IReadOnlyProperty property) + { + var model = property.DeclaringEntityType.Model; + + var sequenceName = property.GetSequenceName() + ?? model.GetSequenceNameSuffix(); + + var sequenceSchema = property.GetSequenceSchema() + ?? model.GetSequenceSchema(); + + return model.FindSequence(sequenceName, sequenceSchema); + } + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The identifier of the store object. + /// The sequence to use, or if no sequence exists in the model. + public static IReadOnlySequence? FindSequence(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var model = property.DeclaringEntityType.Model; + + var sequenceName = property.GetSequenceName(storeObject) + ?? model.GetSequenceNameSuffix(); + + var sequenceSchema = property.GetSequenceSchema(storeObject) + ?? model.GetSequenceSchema(); + + return model.FindSequence(sequenceName, sequenceSchema); + } + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The sequence to use, or if no sequence exists in the model. + public static ISequence? FindSequence(this IProperty property) + => (ISequence?)((IReadOnlyProperty)property).FindSequence(); + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The identifier of the store object. + /// The sequence to use, or if no sequence exists in the model. + public static ISequence? FindSequence(this IProperty property, in StoreObjectIdentifier storeObject) + => (ISequence?)((IReadOnlyProperty)property).FindSequence(storeObject); + + #endregion Sequence + #region Value generation /// @@ -228,16 +412,17 @@ public static NpgsqlValueGenerationStrategy GetValueGenerationStrategy(this IRea return (NpgsqlValueGenerationStrategy?)annotation.Value ?? NpgsqlValueGenerationStrategy.None; } + var defaultValueGenerationStrategy = GetDefaultValueGenerationStrategy(property); if (property.ValueGenerated != ValueGenerated.OnAdd || property.IsForeignKey() || property.TryGetDefaultValue(out _) - || property.GetDefaultValueSql() is not null + || (defaultValueGenerationStrategy != NpgsqlValueGenerationStrategy.Sequence && property.GetDefaultValueSql() != null) || property.GetComputedColumnSql() is not null) { return NpgsqlValueGenerationStrategy.None; } - return GetDefaultValueGenerationStrategy(property); + return defaultValueGenerationStrategy; } /// @@ -340,6 +525,7 @@ private static NpgsqlValueGenerationStrategy GetDefaultValueGenerationStrategy(I { case NpgsqlValueGenerationStrategy.SequenceHiLo: case NpgsqlValueGenerationStrategy.SerialColumn: + case NpgsqlValueGenerationStrategy.Sequence: case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: return IsCompatibleWithValueGeneration(property) @@ -363,15 +549,24 @@ private static NpgsqlValueGenerationStrategy GetDefaultValueGenerationStrategy( switch (modelStrategy) { case NpgsqlValueGenerationStrategy.SequenceHiLo: - case NpgsqlValueGenerationStrategy.SerialColumn: - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: return IsCompatibleWithValueGeneration(property, storeObject, typeMappingSource) ? modelStrategy.Value : NpgsqlValueGenerationStrategy.None; + + case NpgsqlValueGenerationStrategy.SerialColumn: + case NpgsqlValueGenerationStrategy.Sequence: + case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: + case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: + return !IsCompatibleWithValueGeneration(property, storeObject, typeMappingSource) + ? NpgsqlValueGenerationStrategy.None + : property.DeclaringEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy + ? NpgsqlValueGenerationStrategy.Sequence + : modelStrategy.Value; + case NpgsqlValueGenerationStrategy.None: case null: return NpgsqlValueGenerationStrategy.None; + default: throw new ArgumentOutOfRangeException(); } diff --git a/src/EFCore.PG/Infrastructure/NpgsqlModelValidator.cs b/src/EFCore.PG/Infrastructure/NpgsqlModelValidator.cs index 8b7ed2d5d..3931aa9f7 100644 --- a/src/EFCore.PG/Infrastructure/NpgsqlModelValidator.cs +++ b/src/EFCore.PG/Infrastructure/NpgsqlModelValidator.cs @@ -65,6 +65,23 @@ protected virtual void ValidateIdentityVersionCompatibility(IModel model) } } + protected override void ValidateValueGeneration( + IEntityType entityType, + IKey key, + IDiagnosticsLogger logger) + { + if (entityType.GetTableName() != null + && (string?)entityType[RelationalAnnotationNames.MappingStrategy] == RelationalAnnotationNames.TpcMappingStrategy) + { + foreach (var storeGeneratedProperty in key.Properties.Where( + p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0 + && p.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.Sequence)) + { + logger.TpcStoreGeneratedIdentityWarning(storeGeneratedProperty); + } + } + } + protected virtual void ValidateIndexIncludeProperties(IModel model) { foreach (var index in model.GetEntityTypes().SelectMany(t => t.GetDeclaredIndexes())) diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs index 1e943ee71..178f764f6 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs @@ -25,7 +25,8 @@ public override ConventionSet CreateConventionSet() { var conventionSet = base.CreateConventionSet(); - var valueGenerationStrategyConvention = new NpgsqlValueGenerationStrategyConvention(Dependencies, _postgresVersion); + var valueGenerationStrategyConvention = + new NpgsqlValueGenerationStrategyConvention(Dependencies, RelationalDependencies, _postgresVersion); conventionSet.ModelInitializedConventions.Add(valueGenerationStrategyConvention); conventionSet.ModelInitializedConventions.Add(new RelationalMaxIdentifierLengthConvention(63, Dependencies, RelationalDependencies)); diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlStoreGenerationConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlStoreGenerationConvention.cs index e8926f618..3d5c074ef 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlStoreGenerationConvention.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlStoreGenerationConvention.cs @@ -55,8 +55,9 @@ public override void ProcessPropertyAnnotationChanged( break; case RelationalAnnotationNames.DefaultValueSql: - if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) is null - && propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) is not null) + if (propertyBuilder.Metadata.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.Sequence + && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) == null + && propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) != null) { context.StopProcessing(); return; @@ -73,10 +74,13 @@ public override void ProcessPropertyAnnotationChanged( break; case NpgsqlAnnotationNames.ValueGenerationStrategy: - if ((propertyBuilder.HasDefaultValue(null, fromDataAnnotation) is null - | propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) is null - | propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) is null) - && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) is not null) + if (((propertyBuilder.Metadata.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.Sequence + && (propertyBuilder.HasDefaultValue(null, fromDataAnnotation) == null + || propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) == null + || propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) == null)) + || (propertyBuilder.HasDefaultValue(null, fromDataAnnotation) == null + || propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) == null)) + && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) != null) { context.StopProcessing(); return; diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs index c99cb83a9..903bb5eb8 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs @@ -60,16 +60,15 @@ public override void ProcessPropertyAnnotationChanged( /// The store value generation strategy to set for the given property. protected override ValueGenerated? GetValueGenerated(IConventionProperty property) { - var tableName = property.DeclaringEntityType.GetTableName(); - if (tableName is null) + var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); + if (declaringTable.Name == null) { return null; } - return GetValueGenerated( - property, - StoreObjectIdentifier.Table(tableName, property.DeclaringEntityType.GetSchema()), - Dependencies.TypeMappingSource); + // If the first mapping can be value generated then we'll consider all mappings to be value generated + // as this is a client-side configuration and can't be specified per-table. + return GetValueGenerated(property, declaringTable, Dependencies.TypeMappingSource); } /// @@ -78,7 +77,7 @@ public override void ProcessPropertyAnnotationChanged( /// The property. /// The identifier of the store object. /// The store value generation strategy to set for the given property. - public new static ValueGenerated? GetValueGenerated(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + public static new ValueGenerated? GetValueGenerated(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) ?? (property.GetValueGenerationStrategy(storeObject) != NpgsqlValueGenerationStrategy.None ? ValueGenerated.OnAdd diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationStrategyConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationStrategyConvention.cs index 7fd09015a..9d5a513b5 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationStrategyConvention.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationStrategyConvention.cs @@ -13,12 +13,15 @@ public class NpgsqlValueGenerationStrategyConvention : IModelInitializedConventi /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. /// The PostgreSQL version being targeted. This affects the default value generation strategy. public NpgsqlValueGenerationStrategyConvention( ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies, Version? postgresVersion) { Dependencies = dependencies; + RelationalDependencies = relationalDependencies; _postgresVersion = postgresVersion; } @@ -27,6 +30,11 @@ public NpgsqlValueGenerationStrategyConvention( /// protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + /// public virtual void ProcessModelInitialized(IConventionModelBuilder modelBuilder, IConventionContext context) => modelBuilder.HasValueGenerationStrategy( @@ -68,9 +76,23 @@ public virtual void ProcessModelFinalizing( } // Needed for the annotation to show up in the model snapshot - if (strategy is not null) + if (strategy != null + && declaringTable.Name != null) { property.Builder.HasValueGenerationStrategy(strategy); + + if (strategy == NpgsqlValueGenerationStrategy.Sequence) + { + var sequence = modelBuilder.HasSequence( + property.GetSequenceName(declaringTable) + ?? entityType.GetRootType().ShortName() + modelBuilder.Metadata.GetSequenceNameSuffix(), + property.GetSequenceSchema(declaringTable) + ?? modelBuilder.Metadata.GetSequenceSchema()).Metadata; + + property.Builder.HasDefaultValueSql( + RelationalDependencies.UpdateSqlGenerator.GenerateObtainNextSequenceValueOperation( + sequence.Name, sequence.Schema)); + } } } } diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs index 7c0b01064..c3c7b5a01 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs @@ -19,6 +19,9 @@ public static class NpgsqlAnnotationNames public const string TsVectorProperties = Prefix + "TsVectorProperties"; public const string UnloggedTable = Prefix + "UnloggedTable"; public const string ValueGenerationStrategy = Prefix + "ValueGenerationStrategy"; + public const string SequenceNameSuffix = Prefix + "SequenceNameSuffix"; + public const string SequenceName = Prefix + "SequenceName"; + public const string SequenceSchema = Prefix + "SequenceSchema"; public const string CollationDefinitionPrefix = Prefix + "CollationDefinition:"; public const string EnumPrefix = Prefix + "Enum:"; diff --git a/src/EFCore.PG/Metadata/NpgsqlValueGenerationStrategy.cs b/src/EFCore.PG/Metadata/NpgsqlValueGenerationStrategy.cs index ed556da78..59859fca1 100644 --- a/src/EFCore.PG/Metadata/NpgsqlValueGenerationStrategy.cs +++ b/src/EFCore.PG/Metadata/NpgsqlValueGenerationStrategy.cs @@ -39,6 +39,11 @@ public enum NpgsqlValueGenerationStrategy /// Available only starting PostgreSQL 10. /// IdentityByDefaultColumn, + + /// + /// A pattern that uses a database sequence to generate values for the column. + /// + Sequence } public static class NpgsqlValueGenerationStrategyExtensions diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index df5b4bcbb..3655829ee 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -2077,7 +2077,7 @@ private static IndexColumn[] GetIndexColumns(CreateIndexOperation operation) var @operator = i < operators?.Length ? operators[i] : null; var collation = i < collations?.Length ? collations[i] : null; var isColumnDescending = isDescendingValues is not null - ? isDescendingValues[i] + ? isDescendingValues.Length == 0 || isDescendingValues[i] : i < legacySortOrders?.Length && legacySortOrders[i] == SortOrder.Descending; var nullSortOrder = i < nullSortOrders?.Length ? nullSortOrders[i] : NullSortOrder.Unspecified; diff --git a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs b/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs index 1e5e84f6e..1693c1fca 100644 --- a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs +++ b/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs @@ -100,9 +100,9 @@ public override ResultSetMapping AppendDeleteOperation( return ResultSetMapping.NoResultSet; } - public override void AppendNextSequenceValueOperation(StringBuilder commandStringBuilder, string name, string? schema) + public override void AppendObtainNextSequenceValueOperation(StringBuilder commandStringBuilder, string name, string? schema) { - commandStringBuilder.Append("SELECT nextval('"); + commandStringBuilder.Append("nextval('"); SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, Check.NotNull(name, nameof(name)), schema); commandStringBuilder.Append("')"); } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs index a8cf575ef..92b0bf67a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs @@ -24,13 +24,13 @@ public Func GetContextCreator() public ISetSource GetExpectedData() => _expectedData ??= new ArrayQueryData(); - public IReadOnlyDictionary GetEntitySorters() + public IReadOnlyDictionary EntitySorters => new Dictionary> { { typeof(ArrayEntity), e => ((ArrayEntity)e)?.Id }, { typeof(ArrayContainerEntity), e => ((ArrayContainerEntity)e)?.Id } }.ToDictionary(e => e.Key, e => (object)e.Value); - public IReadOnlyDictionary GetEntityAsserters() + public IReadOnlyDictionary EntityAsserters => new Dictionary> { { diff --git a/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs index 6e3f6a1e8..9db5faf8b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs @@ -158,11 +158,11 @@ public Func GetContextCreator() public ISetSource GetExpectedData() => _expectedData ??= new BigIntegerData(); - public IReadOnlyDictionary GetEntitySorters() + public IReadOnlyDictionary EntitySorters => new Dictionary> { { typeof(Entity), e => ((Entity)e)?.Id } } .ToDictionary(e => e.Key, e => (object)e.Value); - public IReadOnlyDictionary GetEntityAsserters() + public IReadOnlyDictionary EntityAsserters => new Dictionary> { { diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index 66b6add8c..6ef3cf3fc 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs @@ -306,11 +306,11 @@ public Func GetContextCreator() public ISetSource GetExpectedData() => _expectedData ??= new EnumData(); - public IReadOnlyDictionary GetEntitySorters() + public IReadOnlyDictionary EntitySorters => new Dictionary> { { typeof(SomeEnumEntity), e => ((SomeEnumEntity)e)?.Id } } .ToDictionary(e => e.Key, e => (object)e.Value); - public IReadOnlyDictionary GetEntityAsserters() + public IReadOnlyDictionary EntityAsserters => new Dictionary> { { @@ -383,4 +383,4 @@ public static IReadOnlyList CreateSomeEnumEntities() } #endregion -} \ No newline at end of file +} diff --git a/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs index eb0d636e7..0ea9941b2 100644 --- a/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs @@ -35,6 +35,8 @@ await AssertQuery( public class FunkyDataQueryNpgsqlFixture : FunkyDataQueryFixtureBase { + private FunkyDataData _expectedData; + public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; @@ -47,46 +49,31 @@ public override FunkyDataContext CreateContext() } public override ISetSource GetExpectedData() - => new NpgsqlFunkyDataData(); - - protected override void Seed(FunkyDataContext context) { - context.FunkyCustomers.AddRange(NpgsqlFunkyDataData.CreateFunkyCustomers()); - context.SaveChanges(); - } - - public class NpgsqlFunkyDataData : FunkyDataData - { - public new IReadOnlyList FunkyCustomers { get; } - - public NpgsqlFunkyDataData() - => FunkyCustomers = CreateFunkyCustomers(); - - public override IQueryable Set() - where TEntity : class + if (_expectedData is null) { - if (typeof(TEntity) == typeof(FunkyCustomer)) - { - return (IQueryable)FunkyCustomers.AsQueryable(); - } + _expectedData = (FunkyDataData)base.GetExpectedData(); - return base.Set(); - } + var maxId = _expectedData.FunkyCustomers.Max(c => c.Id); - public new static IReadOnlyList CreateFunkyCustomers() - { - var customers = FunkyDataData.CreateFunkyCustomers(); - var maxId = customers.Max(c => c.Id); + var mutableCustomersOhYeah = (List)_expectedData.FunkyCustomers; - return customers - .Append(new FunkyCustomer + mutableCustomersOhYeah.Add( + new() { Id = maxId + 1, FirstName = "Some\\Guy", LastName = null - }) - .ToList(); + }); } + + return _expectedData; + } + + protected override void Seed(FunkyDataContext context) + { + context.FunkyCustomers.AddRange(GetExpectedData().Set()); + context.SaveChanges(); } } -} \ No newline at end of file +} diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs index fa2cd5806..596e30919 100644 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs @@ -24,7 +24,7 @@ public override ISetSource GetExpectedData() { if (_expectedData is null) { - _expectedData = new GearsOfWarData(); + _expectedData = (GearsOfWarData)base.GetExpectedData(); // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. // Also chop sub-microsecond precision which PostgreSQL does not support. @@ -82,4 +82,4 @@ protected override void Seed(GearsOfWarContext context) context.SaveChanges(); } -} \ No newline at end of file +} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs index af330e4f2..bac8bfc9d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs @@ -24,7 +24,7 @@ public override ISetSource GetExpectedData() { if (_expectedData is null) { - _expectedData = new GearsOfWarData(); + _expectedData = (GearsOfWarData)base.GetExpectedData(); // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. // Also chop sub-microsecond precision which PostgreSQL does not support. diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs index 7090a88a0..2416eca8a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs @@ -23,7 +23,7 @@ public override ISetSource GetExpectedData() { if (_expectedData is null) { - _expectedData = new GearsOfWarData(); + _expectedData = (GearsOfWarData)base.GetExpectedData(); // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. // Also chop sub-microsecond precision which PostgreSQL does not support. @@ -84,4 +84,4 @@ public static void SeedForNpgsql(GearsOfWarContext context) context.SaveChanges(); } -} \ No newline at end of file +} diff --git a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs index 58e00eded..49ed8f931 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs @@ -829,11 +829,11 @@ public Func GetContextCreator() public ISetSource GetExpectedData() => _expectedData ??= new TimestampData(); - public IReadOnlyDictionary GetEntitySorters() + public IReadOnlyDictionary EntitySorters => new Dictionary> { { typeof(Entity), e => ((Entity)e)?.Id } } .ToDictionary(e => e.Key, e => (object)e.Value); - public IReadOnlyDictionary GetEntityAsserters() + public IReadOnlyDictionary EntityAsserters => new Dictionary> { { diff --git a/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs index dcf0a797c..7d7380b20 100644 --- a/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs @@ -12,9 +12,13 @@ public TwoDatabasesNpgsqlTest(NpgsqlFixture fixture) protected new NpgsqlFixture Fixture => (NpgsqlFixture)base.Fixture; protected override DbContextOptionsBuilder CreateTestOptions( - DbContextOptionsBuilder optionsBuilder, bool withConnectionString = false) + DbContextOptionsBuilder optionsBuilder, + bool withConnectionString = false, + bool withNullConnectionString = false) => withConnectionString - ? optionsBuilder.UseNpgsql(DummyConnectionString) + ? withNullConnectionString + ? optionsBuilder.UseNpgsql((string)null) + : optionsBuilder.UseNpgsql(DummyConnectionString) : optionsBuilder.UseNpgsql(); protected override TwoDatabasesWithDataContext CreateBackingContext(string databaseName) diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs index 972534d79..b3a343f9f 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs @@ -1562,11 +1562,11 @@ public Func GetContextCreator() public ISetSource GetExpectedData() => _expectedData ??= new NodaTimeData(); - public IReadOnlyDictionary GetEntitySorters() + public IReadOnlyDictionary EntitySorters => new Dictionary> { { typeof(NodaTimeTypes), e => ((NodaTimeTypes)e)?.Id } } .ToDictionary(e => e.Key, e => (object)e.Value); - public IReadOnlyDictionary GetEntityAsserters() + public IReadOnlyDictionary EntityAsserters => new Dictionary> { {