Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ private static readonly MethodInfo IndexAreNullsDistinctMethodInfo
= typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod(
nameof(NpgsqlIndexBuilderExtensions.AreNullsDistinct), typeof(IndexBuilder), typeof(bool));

private static readonly MethodInfo KeyWithoutOverlapsMethodInfo
= typeof(NpgsqlKeyBuilderExtensions).GetRequiredRuntimeMethod(
nameof(NpgsqlKeyBuilderExtensions.WithoutOverlaps), typeof(KeyBuilder), typeof(bool));

#endregion MethodInfos

/// <summary>
Expand Down Expand Up @@ -295,6 +299,25 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
return null;
}

/// <summary>
/// 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.
/// </summary>
protected override MethodCallCodeFragment? GenerateFluentApi(IKey key, IAnnotation annotation)
{
Check.NotNull(key, nameof(key));
Check.NotNull(annotation, nameof(annotation));

if (annotation.Name == NpgsqlAnnotationNames.WithoutOverlaps)
{
return new MethodCallCodeFragment(KeyWithoutOverlapsMethodInfo, annotation.Value);
}

return null;
}

/// <summary>
/// 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Npgsql specific extension methods for <see cref="KeyBuilder" />.
/// </summary>
public static class NpgsqlKeyBuilderExtensions
{
#region WithoutOverlaps

/// <summary>
/// Configures the key to use the PostgreSQL WITHOUT OVERLAPS feature.
/// The last property in the key must be a PostgreSQL range type.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
/// </remarks>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
/// <returns>A builder to further configure the key.</returns>
public static KeyBuilder WithoutOverlaps(this KeyBuilder keyBuilder, bool withoutOverlaps = true)
{
Check.NotNull(keyBuilder, nameof(keyBuilder));

keyBuilder.Metadata.SetWithoutOverlaps(withoutOverlaps);

return keyBuilder;
}

/// <summary>
/// Configures the key to use the PostgreSQL WITHOUT OVERLAPS feature.
/// The last property in the key must be a PostgreSQL range type.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
/// </remarks>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
/// <returns>A builder to further configure the key.</returns>
public static KeyBuilder<TEntity> WithoutOverlaps<TEntity>(this KeyBuilder<TEntity> keyBuilder, bool withoutOverlaps = true)
=> (KeyBuilder<TEntity>)WithoutOverlaps((KeyBuilder)keyBuilder, withoutOverlaps);

/// <summary>
/// Configures the key to use the PostgreSQL WITHOUT OVERLAPS feature.
/// The last property in the key must be a PostgreSQL range type.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
/// </remarks>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>A builder to further configure the key.</returns>
public static IConventionKeyBuilder? WithoutOverlaps(
this IConventionKeyBuilder keyBuilder,
bool? withoutOverlaps = true,
bool fromDataAnnotation = false)
{
if (keyBuilder.CanSetWithoutOverlaps(withoutOverlaps, fromDataAnnotation))
{
keyBuilder.Metadata.SetWithoutOverlaps(withoutOverlaps, fromDataAnnotation);

return keyBuilder;
}

return null;
}

/// <summary>
/// Returns a value indicating whether WITHOUT OVERLAPS can be configured.
/// </summary>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if the key can be configured with WITHOUT OVERLAPS.</returns>
public static bool CanSetWithoutOverlaps(
this IConventionKeyBuilder keyBuilder,
bool? withoutOverlaps = true,
bool fromDataAnnotation = false)
{
Check.NotNull(keyBuilder, nameof(keyBuilder));

return keyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.WithoutOverlaps, withoutOverlaps, fromDataAnnotation);
}

#endregion WithoutOverlaps
}
61 changes: 61 additions & 0 deletions src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlKeyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Extension methods for <see cref="IKey" /> for Npgsql-specific metadata.
/// </summary>
public static class NpgsqlKeyExtensions
{
#region WithoutOverlaps

/// <summary>
/// Returns a value indicating whether the key uses the PostgreSQL WITHOUT OVERLAPS feature.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
/// </remarks>
/// <param name="key">The key.</param>
/// <returns><see langword="true" /> if the key uses WITHOUT OVERLAPS.</returns>
public static bool? GetWithoutOverlaps(this IReadOnlyKey key)
=> (bool?)key[NpgsqlAnnotationNames.WithoutOverlaps];

/// <summary>
/// Sets a value indicating whether the key uses the PostgreSQL WITHOUT OVERLAPS feature.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
/// </remarks>
/// <param name="key">The key.</param>
/// <param name="withoutOverlaps">The value to set.</param>
public static void SetWithoutOverlaps(this IMutableKey key, bool? withoutOverlaps)
=> key.SetOrRemoveAnnotation(NpgsqlAnnotationNames.WithoutOverlaps, withoutOverlaps);

/// <summary>
/// Sets a value indicating whether the key uses the PostgreSQL WITHOUT OVERLAPS feature.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
/// </remarks>
/// <param name="key">The key.</param>
/// <param name="withoutOverlaps">The value to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static bool? SetWithoutOverlaps(this IConventionKey key, bool? withoutOverlaps, bool fromDataAnnotation = false)
{
key.SetOrRemoveAnnotation(NpgsqlAnnotationNames.WithoutOverlaps, withoutOverlaps, fromDataAnnotation);

return withoutOverlaps;
}

/// <summary>
/// Returns the <see cref="ConfigurationSource" /> for whether the key uses WITHOUT OVERLAPS.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The <see cref="ConfigurationSource" />.</returns>
public static ConfigurationSource? GetWithoutOverlapsConfigurationSource(this IConventionKey key)
=> key.FindAnnotation(NpgsqlAnnotationNames.WithoutOverlaps)?.GetConfigurationSource();

#endregion WithoutOverlaps
}
50 changes: 50 additions & 0 deletions src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;

Expand Down Expand Up @@ -43,6 +44,7 @@ public override void Validate(IModel model, IDiagnosticsLogger<DbLoggerCategory.

ValidateIdentityVersionCompatibility(model);
ValidateIndexIncludeProperties(model);
ValidateWithoutOverlaps(model);
}

/// <summary>
Expand Down Expand Up @@ -268,4 +270,52 @@ protected override void ValidateCompatible(
storeObject.DisplayName()));
}
}

/// <summary>
/// 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.
/// </summary>
protected virtual void ValidateWithoutOverlaps(IModel model)
{
foreach (var entityType in model.GetEntityTypes())
{
// Validate primary key and alternate keys
foreach (var key in entityType.GetDeclaredKeys())
{
if (key.GetWithoutOverlaps() == true)
{
ValidateWithoutOverlapsKey(key);
}
}
}
}

private void ValidateWithoutOverlapsKey(IKey key)
{
var keyName = key.IsPrimaryKey() ? "primary key" : $"alternate key {{{string.Join(", ", key.Properties.Select(p => p.Name))}}}";
var entityType = key.DeclaringEntityType;

// Check PostgreSQL version requirement
if (!_postgresVersion.AtLeast(18))
{
throw new InvalidOperationException(
NpgsqlStrings.WithoutOverlapsRequiresPostgres18(keyName, entityType.DisplayName()));
}

// Check that the last property is a range type
var lastProperty = key.Properties.Last();
var typeMapping = lastProperty.FindTypeMapping();

if (typeMapping is not NpgsqlRangeTypeMapping)
{
throw new InvalidOperationException(
NpgsqlStrings.WithoutOverlapsRequiresRangeType(
keyName,
entityType.DisplayName(),
lastProperty.Name,
lastProperty.ClrType.ShortDisplayName()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,30 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder,
ProcessRowVersionProperty(property, typeMapping);
}
}

DiscoverBtreeGistForWithoutOverlaps(entityType, modelBuilder);
}

SetupEnums(modelBuilder);
}

/// <summary>
/// Discovers the btree_gist extension if any keys or indexes use WITHOUT OVERLAPS.
/// </summary>
protected virtual void DiscoverBtreeGistForWithoutOverlaps(
IConventionEntityType entityType,
IConventionModelBuilder modelBuilder)
{
foreach (var key in entityType.GetDeclaredKeys())
{
if (key.GetWithoutOverlaps() == true)
{
modelBuilder.HasPostgresExtension("btree_gist");
return;
}
}
}

/// <summary>
/// Configures the model to create PostgreSQL enums based on the user's enum definitions in the context options.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ public static class NpgsqlAnnotationNames
/// </summary>
public const string UnloggedTable = Prefix + "UnloggedTable";

/// <summary>
/// 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.
/// </summary>
public const string WithoutOverlaps = Prefix + "WithoutOverlaps";

/// <summary>
/// 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
Expand Down
22 changes: 22 additions & 0 deletions src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,28 @@ public override IEnumerable<IAnnotation> For(ITableIndex index, bool designTime)
}
}

/// <summary>
/// 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.
/// </summary>
public override IEnumerable<IAnnotation> For(IUniqueConstraint constraint, bool designTime)
{
if (!designTime)
{
yield break;
}

// Model validation ensures that these facets are the same on all mapped keys
var modelKey = constraint.MappedKeys.First();

if (modelKey.GetWithoutOverlaps() == true)
{
yield return new Annotation(NpgsqlAnnotationNames.WithoutOverlaps, true);
}
}

/// <summary>
/// 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
Expand Down
56 changes: 56 additions & 0 deletions src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,62 @@ protected override void Generate(CreateSequenceOperation operation, IModel? mode
}
}

/// <inheritdoc />
protected override void PrimaryKeyConstraint(
AddPrimaryKeyOperation operation,
IModel? model,
MigrationCommandListBuilder builder)
{
if (operation.Name != null)
{
builder
.Append("CONSTRAINT ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
.Append(" ");
}

builder
.Append("PRIMARY KEY (")
.Append(ColumnList(operation.Columns));

if (operation[NpgsqlAnnotationNames.WithoutOverlaps] is true)
{
builder.Append(" WITHOUT OVERLAPS");
}

builder.Append(")");

IndexOptions(operation, model, builder);
}

/// <inheritdoc />
protected override void UniqueConstraint(
AddUniqueConstraintOperation operation,
IModel? model,
MigrationCommandListBuilder builder)
{
if (operation.Name != null)
{
builder
.Append("CONSTRAINT ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
.Append(" ");
}

builder
.Append("UNIQUE (")
.Append(ColumnList(operation.Columns));

if (operation[NpgsqlAnnotationNames.WithoutOverlaps] is true)
{
builder.Append(" WITHOUT OVERLAPS");
}

builder.Append(")");

IndexOptions(operation, model, builder);
}

#endregion Standard migrations

#region Utilities
Expand Down
Loading