Skip to content

Commit 789f55a

Browse files
authored
Implement support for PG18 WITHOUT OVERLAPS (#3709)
Closes #3708
1 parent 8cbca8b commit 789f55a

13 files changed

Lines changed: 577 additions & 5 deletions

File tree

src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ private static readonly MethodInfo IndexAreNullsDistinctMethodInfo
117117
= typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod(
118118
nameof(NpgsqlIndexBuilderExtensions.AreNullsDistinct), typeof(IndexBuilder), typeof(bool));
119119

120+
private static readonly MethodInfo KeyWithoutOverlapsMethodInfo
121+
= typeof(NpgsqlKeyBuilderExtensions).GetRequiredRuntimeMethod(
122+
nameof(NpgsqlKeyBuilderExtensions.WithoutOverlaps), typeof(KeyBuilder), typeof(bool));
123+
120124
#endregion MethodInfos
121125

122126
/// <summary>
@@ -295,6 +299,25 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
295299
return null;
296300
}
297301

302+
/// <summary>
303+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
304+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
305+
/// any release. You should only use it directly in your code with extreme caution and knowing that
306+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
307+
/// </summary>
308+
protected override MethodCallCodeFragment? GenerateFluentApi(IKey key, IAnnotation annotation)
309+
{
310+
Check.NotNull(key, nameof(key));
311+
Check.NotNull(annotation, nameof(annotation));
312+
313+
if (annotation.Name == NpgsqlAnnotationNames.WithoutOverlaps)
314+
{
315+
return new MethodCallCodeFragment(KeyWithoutOverlapsMethodInfo, annotation.Value);
316+
}
317+
318+
return null;
319+
}
320+
298321
/// <summary>
299322
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
300323
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
2+
3+
// ReSharper disable once CheckNamespace
4+
namespace Microsoft.EntityFrameworkCore;
5+
6+
/// <summary>
7+
/// Npgsql specific extension methods for <see cref="KeyBuilder" />.
8+
/// </summary>
9+
public static class NpgsqlKeyBuilderExtensions
10+
{
11+
#region WithoutOverlaps
12+
13+
/// <summary>
14+
/// Configures the key to use the PostgreSQL WITHOUT OVERLAPS feature.
15+
/// The last property in the key must be a PostgreSQL range type.
16+
/// </summary>
17+
/// <remarks>
18+
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
19+
/// </remarks>
20+
/// <param name="keyBuilder">The builder for the key being configured.</param>
21+
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
22+
/// <returns>A builder to further configure the key.</returns>
23+
public static KeyBuilder WithoutOverlaps(this KeyBuilder keyBuilder, bool withoutOverlaps = true)
24+
{
25+
Check.NotNull(keyBuilder, nameof(keyBuilder));
26+
27+
keyBuilder.Metadata.SetWithoutOverlaps(withoutOverlaps);
28+
29+
return keyBuilder;
30+
}
31+
32+
/// <summary>
33+
/// Configures the key to use the PostgreSQL WITHOUT OVERLAPS feature.
34+
/// The last property in the key must be a PostgreSQL range type.
35+
/// </summary>
36+
/// <remarks>
37+
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
38+
/// </remarks>
39+
/// <param name="keyBuilder">The builder for the key being configured.</param>
40+
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
41+
/// <returns>A builder to further configure the key.</returns>
42+
public static KeyBuilder<TEntity> WithoutOverlaps<TEntity>(this KeyBuilder<TEntity> keyBuilder, bool withoutOverlaps = true)
43+
=> (KeyBuilder<TEntity>)WithoutOverlaps((KeyBuilder)keyBuilder, withoutOverlaps);
44+
45+
/// <summary>
46+
/// Configures the key to use the PostgreSQL WITHOUT OVERLAPS feature.
47+
/// The last property in the key must be a PostgreSQL range type.
48+
/// </summary>
49+
/// <remarks>
50+
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
51+
/// </remarks>
52+
/// <param name="keyBuilder">The builder for the key being configured.</param>
53+
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
54+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
55+
/// <returns>A builder to further configure the key.</returns>
56+
public static IConventionKeyBuilder? WithoutOverlaps(
57+
this IConventionKeyBuilder keyBuilder,
58+
bool? withoutOverlaps = true,
59+
bool fromDataAnnotation = false)
60+
{
61+
if (keyBuilder.CanSetWithoutOverlaps(withoutOverlaps, fromDataAnnotation))
62+
{
63+
keyBuilder.Metadata.SetWithoutOverlaps(withoutOverlaps, fromDataAnnotation);
64+
65+
return keyBuilder;
66+
}
67+
68+
return null;
69+
}
70+
71+
/// <summary>
72+
/// Returns a value indicating whether WITHOUT OVERLAPS can be configured.
73+
/// </summary>
74+
/// <param name="keyBuilder">The builder for the key being configured.</param>
75+
/// <param name="withoutOverlaps">A value indicating whether to use WITHOUT OVERLAPS.</param>
76+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
77+
/// <returns><see langword="true" /> if the key can be configured with WITHOUT OVERLAPS.</returns>
78+
public static bool CanSetWithoutOverlaps(
79+
this IConventionKeyBuilder keyBuilder,
80+
bool? withoutOverlaps = true,
81+
bool fromDataAnnotation = false)
82+
{
83+
Check.NotNull(keyBuilder, nameof(keyBuilder));
84+
85+
return keyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.WithoutOverlaps, withoutOverlaps, fromDataAnnotation);
86+
}
87+
88+
#endregion WithoutOverlaps
89+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
2+
3+
// ReSharper disable once CheckNamespace
4+
namespace Microsoft.EntityFrameworkCore;
5+
6+
/// <summary>
7+
/// Extension methods for <see cref="IKey" /> for Npgsql-specific metadata.
8+
/// </summary>
9+
public static class NpgsqlKeyExtensions
10+
{
11+
#region WithoutOverlaps
12+
13+
/// <summary>
14+
/// Returns a value indicating whether the key uses the PostgreSQL WITHOUT OVERLAPS feature.
15+
/// </summary>
16+
/// <remarks>
17+
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
18+
/// </remarks>
19+
/// <param name="key">The key.</param>
20+
/// <returns><see langword="true" /> if the key uses WITHOUT OVERLAPS.</returns>
21+
public static bool? GetWithoutOverlaps(this IReadOnlyKey key)
22+
=> (bool?)key[NpgsqlAnnotationNames.WithoutOverlaps];
23+
24+
/// <summary>
25+
/// Sets a value indicating whether the key uses the PostgreSQL WITHOUT OVERLAPS feature.
26+
/// </summary>
27+
/// <remarks>
28+
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
29+
/// </remarks>
30+
/// <param name="key">The key.</param>
31+
/// <param name="withoutOverlaps">The value to set.</param>
32+
public static void SetWithoutOverlaps(this IMutableKey key, bool? withoutOverlaps)
33+
=> key.SetOrRemoveAnnotation(NpgsqlAnnotationNames.WithoutOverlaps, withoutOverlaps);
34+
35+
/// <summary>
36+
/// Sets a value indicating whether the key uses the PostgreSQL WITHOUT OVERLAPS feature.
37+
/// </summary>
38+
/// <remarks>
39+
/// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
40+
/// </remarks>
41+
/// <param name="key">The key.</param>
42+
/// <param name="withoutOverlaps">The value to set.</param>
43+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
44+
/// <returns>The configured value.</returns>
45+
public static bool? SetWithoutOverlaps(this IConventionKey key, bool? withoutOverlaps, bool fromDataAnnotation = false)
46+
{
47+
key.SetOrRemoveAnnotation(NpgsqlAnnotationNames.WithoutOverlaps, withoutOverlaps, fromDataAnnotation);
48+
49+
return withoutOverlaps;
50+
}
51+
52+
/// <summary>
53+
/// Returns the <see cref="ConfigurationSource" /> for whether the key uses WITHOUT OVERLAPS.
54+
/// </summary>
55+
/// <param name="key">The key.</param>
56+
/// <returns>The <see cref="ConfigurationSource" />.</returns>
57+
public static ConfigurationSource? GetWithoutOverlapsConfigurationSource(this IConventionKey key)
58+
=> key.FindAnnotation(NpgsqlAnnotationNames.WithoutOverlaps)?.GetConfigurationSource();
59+
60+
#endregion WithoutOverlaps
61+
}

src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
22
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
3+
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
34

45
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
56

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

4445
ValidateIdentityVersionCompatibility(model);
4546
ValidateIndexIncludeProperties(model);
47+
ValidateWithoutOverlaps(model);
4648
}
4749

4850
/// <summary>
@@ -268,4 +270,52 @@ protected override void ValidateCompatible(
268270
storeObject.DisplayName()));
269271
}
270272
}
273+
274+
/// <summary>
275+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
276+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
277+
/// any release. You should only use it directly in your code with extreme caution and knowing that
278+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
279+
/// </summary>
280+
protected virtual void ValidateWithoutOverlaps(IModel model)
281+
{
282+
foreach (var entityType in model.GetEntityTypes())
283+
{
284+
// Validate primary key and alternate keys
285+
foreach (var key in entityType.GetDeclaredKeys())
286+
{
287+
if (key.GetWithoutOverlaps() == true)
288+
{
289+
ValidateWithoutOverlapsKey(key);
290+
}
291+
}
292+
}
293+
}
294+
295+
private void ValidateWithoutOverlapsKey(IKey key)
296+
{
297+
var keyName = key.IsPrimaryKey() ? "primary key" : $"alternate key {{{string.Join(", ", key.Properties.Select(p => p.Name))}}}";
298+
var entityType = key.DeclaringEntityType;
299+
300+
// Check PostgreSQL version requirement
301+
if (!_postgresVersion.AtLeast(18))
302+
{
303+
throw new InvalidOperationException(
304+
NpgsqlStrings.WithoutOverlapsRequiresPostgres18(keyName, entityType.DisplayName()));
305+
}
306+
307+
// Check that the last property is a range type
308+
var lastProperty = key.Properties.Last();
309+
var typeMapping = lastProperty.FindTypeMapping();
310+
311+
if (typeMapping is not NpgsqlRangeTypeMapping)
312+
{
313+
throw new InvalidOperationException(
314+
NpgsqlStrings.WithoutOverlapsRequiresRangeType(
315+
keyName,
316+
entityType.DisplayName(),
317+
lastProperty.Name,
318+
lastProperty.ClrType.ShortDisplayName()));
319+
}
320+
}
271321
}

src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,30 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder,
3030
ProcessRowVersionProperty(property, typeMapping);
3131
}
3232
}
33+
34+
DiscoverBtreeGistForWithoutOverlaps(entityType, modelBuilder);
3335
}
3436

3537
SetupEnums(modelBuilder);
3638
}
3739

40+
/// <summary>
41+
/// Discovers the btree_gist extension if any keys or indexes use WITHOUT OVERLAPS.
42+
/// </summary>
43+
protected virtual void DiscoverBtreeGistForWithoutOverlaps(
44+
IConventionEntityType entityType,
45+
IConventionModelBuilder modelBuilder)
46+
{
47+
foreach (var key in entityType.GetDeclaredKeys())
48+
{
49+
if (key.GetWithoutOverlaps() == true)
50+
{
51+
modelBuilder.HasPostgresExtension("btree_gist");
52+
return;
53+
}
54+
}
55+
}
56+
3857
/// <summary>
3958
/// Configures the model to create PostgreSQL enums based on the user's enum definitions in the context options.
4059
/// </summary>

src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ public static class NpgsqlAnnotationNames
136136
/// </summary>
137137
public const string UnloggedTable = Prefix + "UnloggedTable";
138138

139+
/// <summary>
140+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
141+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
142+
/// any release. You should only use it directly in your code with extreme caution and knowing that
143+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
144+
/// </summary>
145+
public const string WithoutOverlaps = Prefix + "WithoutOverlaps";
146+
139147
/// <summary>
140148
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
141149
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,28 @@ public override IEnumerable<IAnnotation> For(ITableIndex index, bool designTime)
200200
}
201201
}
202202

203+
/// <summary>
204+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
205+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
206+
/// any release. You should only use it directly in your code with extreme caution and knowing that
207+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
208+
/// </summary>
209+
public override IEnumerable<IAnnotation> For(IUniqueConstraint constraint, bool designTime)
210+
{
211+
if (!designTime)
212+
{
213+
yield break;
214+
}
215+
216+
// Model validation ensures that these facets are the same on all mapped keys
217+
var modelKey = constraint.MappedKeys.First();
218+
219+
if (modelKey.GetWithoutOverlaps() == true)
220+
{
221+
yield return new Annotation(NpgsqlAnnotationNames.WithoutOverlaps, true);
222+
}
223+
}
224+
203225
/// <summary>
204226
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
205227
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,62 @@ protected override void Generate(CreateSequenceOperation operation, IModel? mode
15791579
}
15801580
}
15811581

1582+
/// <inheritdoc />
1583+
protected override void PrimaryKeyConstraint(
1584+
AddPrimaryKeyOperation operation,
1585+
IModel? model,
1586+
MigrationCommandListBuilder builder)
1587+
{
1588+
if (operation.Name != null)
1589+
{
1590+
builder
1591+
.Append("CONSTRAINT ")
1592+
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
1593+
.Append(" ");
1594+
}
1595+
1596+
builder
1597+
.Append("PRIMARY KEY (")
1598+
.Append(ColumnList(operation.Columns));
1599+
1600+
if (operation[NpgsqlAnnotationNames.WithoutOverlaps] is true)
1601+
{
1602+
builder.Append(" WITHOUT OVERLAPS");
1603+
}
1604+
1605+
builder.Append(")");
1606+
1607+
IndexOptions(operation, model, builder);
1608+
}
1609+
1610+
/// <inheritdoc />
1611+
protected override void UniqueConstraint(
1612+
AddUniqueConstraintOperation operation,
1613+
IModel? model,
1614+
MigrationCommandListBuilder builder)
1615+
{
1616+
if (operation.Name != null)
1617+
{
1618+
builder
1619+
.Append("CONSTRAINT ")
1620+
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
1621+
.Append(" ");
1622+
}
1623+
1624+
builder
1625+
.Append("UNIQUE (")
1626+
.Append(ColumnList(operation.Columns));
1627+
1628+
if (operation[NpgsqlAnnotationNames.WithoutOverlaps] is true)
1629+
{
1630+
builder.Append(" WITHOUT OVERLAPS");
1631+
}
1632+
1633+
builder.Append(")");
1634+
1635+
IndexOptions(operation, model, builder);
1636+
}
1637+
15821638
#endregion Standard migrations
15831639

15841640
#region Utilities

0 commit comments

Comments
 (0)