diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlForeignKeyBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlForeignKeyBuilderExtensions.cs
new file mode 100644
index 000000000..40c961c3a
--- /dev/null
+++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlForeignKeyBuilderExtensions.cs
@@ -0,0 +1,191 @@
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore;
+
+///
+/// Npgsql specific extension methods for relationship builders.
+///
+public static class NpgsqlForeignKeyBuilderExtensions
+{
+ #region Period
+
+ ///
+ /// Configures the foreign key to use the PostgreSQL PERIOD feature for temporal foreign keys.
+ /// The last column in the foreign key must be a PostgreSQL range type, and the referenced
+ /// principal key must have WITHOUT OVERLAPS configured.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ReferenceCollectionBuilder WithPeriod(
+ this ReferenceCollectionBuilder referenceCollectionBuilder,
+ bool withPeriod = true)
+ {
+ Check.NotNull(referenceCollectionBuilder, nameof(referenceCollectionBuilder));
+
+ referenceCollectionBuilder.Metadata.SetPeriod(withPeriod);
+
+ return referenceCollectionBuilder;
+ }
+
+ ///
+ /// Configures the foreign key to use the PostgreSQL PERIOD feature for temporal foreign keys.
+ /// The last column in the foreign key must be a PostgreSQL range type, and the referenced
+ /// principal key must have WITHOUT OVERLAPS configured.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// The principal entity type in this relationship.
+ /// The dependent entity type in this relationship.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ReferenceCollectionBuilder WithPeriod(
+ this ReferenceCollectionBuilder referenceCollectionBuilder,
+ bool withPeriod = true)
+ where TEntity : class
+ where TRelatedEntity : class
+ => (ReferenceCollectionBuilder)WithPeriod(
+ (ReferenceCollectionBuilder)referenceCollectionBuilder, withPeriod);
+
+ ///
+ /// Configures the foreign key to use the PostgreSQL PERIOD feature for temporal foreign keys.
+ /// The last column in the foreign key must be a PostgreSQL range type, and the referenced
+ /// principal key must have WITHOUT OVERLAPS configured.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ReferenceReferenceBuilder WithPeriod(
+ this ReferenceReferenceBuilder referenceReferenceBuilder,
+ bool withPeriod = true)
+ {
+ Check.NotNull(referenceReferenceBuilder, nameof(referenceReferenceBuilder));
+
+ referenceReferenceBuilder.Metadata.SetPeriod(withPeriod);
+
+ return referenceReferenceBuilder;
+ }
+
+ ///
+ /// Configures the foreign key to use the PostgreSQL PERIOD feature for temporal foreign keys.
+ /// The last column in the foreign key must be a PostgreSQL range type, and the referenced
+ /// principal key must have WITHOUT OVERLAPS configured.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// The entity type on one end of the relationship.
+ /// The entity type on the other end of the relationship.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ReferenceReferenceBuilder WithPeriod(
+ this ReferenceReferenceBuilder referenceReferenceBuilder,
+ bool withPeriod = true)
+ where TEntity : class
+ where TRelatedEntity : class
+ => (ReferenceReferenceBuilder)WithPeriod(
+ (ReferenceReferenceBuilder)referenceReferenceBuilder, withPeriod);
+
+ ///
+ /// Configures the foreign key to use the PostgreSQL PERIOD feature for temporal foreign keys.
+ /// The last column in the foreign key must be a PostgreSQL range type, and the referenced
+ /// principal key must have WITHOUT OVERLAPS configured.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// The same builder instance so that multiple calls can be chained.
+ public static OwnershipBuilder WithPeriod(
+ this OwnershipBuilder ownershipBuilder,
+ bool withPeriod = true)
+ {
+ Check.NotNull(ownershipBuilder, nameof(ownershipBuilder));
+
+ ownershipBuilder.Metadata.SetPeriod(withPeriod);
+
+ return ownershipBuilder;
+ }
+
+ ///
+ /// Configures the foreign key to use the PostgreSQL PERIOD feature for temporal foreign keys.
+ /// The last column in the foreign key must be a PostgreSQL range type, and the referenced
+ /// principal key must have WITHOUT OVERLAPS configured.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// The entity type on one end of the relationship.
+ /// The entity type on the other end of the relationship.
+ /// The same builder instance so that multiple calls can be chained.
+ public static OwnershipBuilder WithPeriod(
+ this OwnershipBuilder ownershipBuilder,
+ bool withPeriod = true)
+ where TEntity : class
+ where TDependentEntity : class
+ => (OwnershipBuilder)WithPeriod(
+ (OwnershipBuilder)ownershipBuilder, withPeriod);
+
+ ///
+ /// Configures the foreign key to use the PostgreSQL PERIOD feature for temporal foreign keys.
+ /// The last column in the foreign key must be a PostgreSQL range type, and the referenced
+ /// principal key must have WITHOUT OVERLAPS configured.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionForeignKeyBuilder? WithPeriod(
+ this IConventionForeignKeyBuilder foreignKeyBuilder,
+ bool? withPeriod = true,
+ bool fromDataAnnotation = false)
+ {
+ if (foreignKeyBuilder.CanSetPeriod(withPeriod, fromDataAnnotation))
+ {
+ foreignKeyBuilder.Metadata.SetPeriod(withPeriod, fromDataAnnotation);
+
+ return foreignKeyBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether PERIOD can be configured.
+ ///
+ /// The builder being used to configure the relationship.
+ /// A value indicating whether to use PERIOD.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the foreign key can be configured with PERIOD.
+ public static bool CanSetPeriod(
+ this IConventionForeignKeyBuilder foreignKeyBuilder,
+ bool? withPeriod = true,
+ bool fromDataAnnotation = false)
+ {
+ Check.NotNull(foreignKeyBuilder, nameof(foreignKeyBuilder));
+
+ return foreignKeyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.Period, withPeriod, fromDataAnnotation);
+ }
+
+ #endregion Period
+}
diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlForeignKeyExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlForeignKeyExtensions.cs
new file mode 100644
index 000000000..7a5140fe1
--- /dev/null
+++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlForeignKeyExtensions.cs
@@ -0,0 +1,61 @@
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore;
+
+///
+/// Extension methods for for Npgsql-specific metadata.
+///
+public static class NpgsqlForeignKeyExtensions
+{
+ #region Period
+
+ ///
+ /// Returns a value indicating whether the foreign key uses the PostgreSQL PERIOD feature for temporal foreign keys.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The foreign key.
+ /// if the foreign key uses PERIOD.
+ public static bool? GetPeriod(this IReadOnlyForeignKey foreignKey)
+ => (bool?)foreignKey[NpgsqlAnnotationNames.Period];
+
+ ///
+ /// Sets a value indicating whether the foreign key uses the PostgreSQL PERIOD feature for temporal foreign keys.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The foreign key.
+ /// The value to set.
+ public static void SetPeriod(this IMutableForeignKey foreignKey, bool? period)
+ => foreignKey.SetOrRemoveAnnotation(NpgsqlAnnotationNames.Period, period);
+
+ ///
+ /// Sets a value indicating whether the foreign key uses the PostgreSQL PERIOD feature for temporal foreign keys.
+ ///
+ ///
+ /// See https://www.postgresql.org/docs/current/sql-createtable.html for more information.
+ ///
+ /// The foreign key.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static bool? SetPeriod(this IConventionForeignKey foreignKey, bool? period, bool fromDataAnnotation = false)
+ {
+ foreignKey.SetOrRemoveAnnotation(NpgsqlAnnotationNames.Period, period, fromDataAnnotation);
+
+ return period;
+ }
+
+ ///
+ /// Returns the for whether the foreign key uses PERIOD.
+ ///
+ /// The foreign key.
+ /// The .
+ public static ConfigurationSource? GetPeriodConfigurationSource(this IConventionForeignKey foreignKey)
+ => foreignKey.FindAnnotation(NpgsqlAnnotationNames.Period)?.GetConfigurationSource();
+
+ #endregion Period
+}
diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs
index 9cabc02ad..23791956c 100644
--- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs
@@ -45,6 +45,7 @@ public override void Validate(IModel model, IDiagnosticsLogger
@@ -294,7 +295,7 @@ protected virtual void ValidateWithoutOverlaps(IModel model)
private void ValidateWithoutOverlapsKey(IKey key)
{
- var keyName = key.IsPrimaryKey() ? "primary key" : $"alternate key {{{string.Join(", ", key.Properties.Select(p => p.Name))}}}";
+ var keyName = key.IsPrimaryKey() ? "primary key" : $"alternate key {key.Properties.Format()}";
var entityType = key.DeclaringEntityType;
// Check PostgreSQL version requirement
@@ -305,7 +306,7 @@ private void ValidateWithoutOverlapsKey(IKey key)
}
// Check that the last property is a range type
- var lastProperty = key.Properties.Last();
+ var lastProperty = key.Properties[^1];
var typeMapping = lastProperty.FindTypeMapping();
if (typeMapping is not NpgsqlRangeTypeMapping)
@@ -318,4 +319,65 @@ private void ValidateWithoutOverlapsKey(IKey key)
lastProperty.ClrType.ShortDisplayName()));
}
}
+
+ ///
+ /// 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.
+ ///
+ protected virtual void ValidatePeriod(IModel model)
+ {
+ foreach (var entityType in model.GetEntityTypes())
+ {
+ foreach (var foreignKey in entityType.GetDeclaredForeignKeys())
+ {
+ if (foreignKey.GetPeriod() == true)
+ {
+ ValidatePeriodForeignKey(foreignKey);
+ }
+ }
+ }
+ }
+
+ private void ValidatePeriodForeignKey(IForeignKey foreignKey)
+ {
+ var entityType = foreignKey.DeclaringEntityType;
+ var fkName = foreignKey.Properties.Format();
+ var principalKey = foreignKey.PrincipalKey;
+ var principalEntityType = principalKey.DeclaringEntityType;
+
+ if (!_postgresVersion.AtLeast(18))
+ {
+ throw new InvalidOperationException(
+ NpgsqlStrings.PeriodRequiresPostgres18(fkName, entityType.DisplayName()));
+ }
+
+ // Check that the principal key has WITHOUT OVERLAPS (check this before range type)
+ if (principalKey.GetWithoutOverlaps() != true)
+ {
+ throw new InvalidOperationException(
+ NpgsqlStrings.PeriodRequiresWithoutOverlapsOnPrincipal(
+ fkName,
+ entityType.DisplayName(),
+ principalKey.IsPrimaryKey()
+ ? "primary key"
+ : $"alternate key {principalKey.Properties.Format()}",
+ principalEntityType.DisplayName()));
+ }
+
+ // Check that the last property is a range type
+ var lastProperty = foreignKey.Properties[^1];
+ var typeMapping = lastProperty.FindTypeMapping();
+
+ if (typeMapping is not NpgsqlRangeTypeMapping)
+ {
+ throw new InvalidOperationException(
+ NpgsqlStrings.PeriodRequiresRangeType(
+ fkName,
+ entityType.DisplayName(),
+ lastProperty.Name,
+ lastProperty.ClrType.ShortDisplayName()));
+ }
+ }
}
diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs
index 7bbdf9ed6..0370454b2 100644
--- a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs
+++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs
@@ -64,6 +64,9 @@ public override ConventionSet CreateConventionSet()
ReplaceConvention(conventionSet.ForeignKeyRemovedConventions, valueGenerationConvention);
+ conventionSet.ForeignKeyAnnotationChangedConventions.Add(
+ new NpgsqlPeriodConvention(Dependencies, RelationalDependencies));
+
var storeGenerationConvention =
new NpgsqlStoreGenerationConvention(Dependencies, RelationalDependencies);
ReplaceConvention(conventionSet.PropertyAnnotationChangedConventions, storeGenerationConvention);
diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlPeriodConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlPeriodConvention.cs
new file mode 100644
index 000000000..a01791b82
--- /dev/null
+++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlPeriodConvention.cs
@@ -0,0 +1,39 @@
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
+
+///
+/// A convention that sets the delete behavior to for foreign keys with PERIOD,
+/// since PostgreSQL does not support cascading deletes for temporal foreign keys.
+///
+/// Parameter object containing dependencies for this convention.
+/// Parameter object containing relational dependencies for this convention.
+public class NpgsqlPeriodConvention(
+ ProviderConventionSetBuilderDependencies dependencies,
+ RelationalConventionSetBuilderDependencies relationalDependencies)
+ : IForeignKeyAnnotationChangedConvention
+{
+ ///
+ /// Dependencies for this service.
+ ///
+ protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } = dependencies;
+
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } = relationalDependencies;
+
+ ///
+ public virtual void ProcessForeignKeyAnnotationChanged(
+ IConventionForeignKeyBuilder relationshipBuilder,
+ string name,
+ IConventionAnnotation? annotation,
+ IConventionAnnotation? oldAnnotation,
+ IConventionContext context)
+ {
+ if (name == NpgsqlAnnotationNames.Period && annotation?.Value is true)
+ {
+ relationshipBuilder.OnDelete(DeleteBehavior.NoAction);
+ }
+ }
+}
diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
index 32d1ffb86..42519abd5 100644
--- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
+++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
@@ -144,6 +144,14 @@ public static class NpgsqlAnnotationNames
///
public const string WithoutOverlaps = Prefix + "WithoutOverlaps";
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string Period = Prefix + "Period";
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs
index f42deda95..f92bb721c 100644
--- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs
+++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs
@@ -222,6 +222,28 @@ public override IEnumerable For(IUniqueConstraint constraint, bool
}
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override IEnumerable For(IForeignKeyConstraint constraint, bool designTime)
+ {
+ if (!designTime)
+ {
+ yield break;
+ }
+
+ // Model validation ensures that these facets are the same on all mapped foreign keys
+ var modelForeignKey = constraint.MappedForeignKeys.First();
+
+ if (modelForeignKey.GetPeriod() == true)
+ {
+ yield return new Annotation(NpgsqlAnnotationNames.Period, true);
+ }
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
index c4011dcdc..c5478e4cf 100644
--- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
+++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
@@ -1635,6 +1635,70 @@ protected override void UniqueConstraint(
IndexOptions(operation, model, builder);
}
+ ///
+ protected override void ForeignKeyConstraint(
+ AddForeignKeyOperation operation,
+ IModel? model,
+ MigrationCommandListBuilder builder)
+ {
+ if (operation.Name != null)
+ {
+ builder
+ .Append("CONSTRAINT ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
+ .Append(" ");
+ }
+
+ var withPeriod = operation[NpgsqlAnnotationNames.Period] is true;
+
+ builder.Append("FOREIGN KEY (");
+ if (withPeriod)
+ {
+ builder
+ .Append(ColumnList(operation.Columns[..^1]))
+ .Append(", PERIOD ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Columns[^1]));
+ }
+ else
+ {
+ builder.Append(ColumnList(operation.Columns));
+ }
+
+ builder
+ .Append(") REFERENCES ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.PrincipalTable, operation.PrincipalSchema));
+
+ if (operation.PrincipalColumns != null)
+ {
+ builder.Append(" (");
+ if (withPeriod)
+ {
+ builder
+ .Append(ColumnList(operation.PrincipalColumns[..^1]))
+ .Append(", PERIOD ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.PrincipalColumns[^1]));
+ }
+ else
+ {
+ builder.Append(ColumnList(operation.PrincipalColumns));
+ }
+
+ builder.Append(")");
+ }
+
+ if (operation.OnUpdate != ReferentialAction.NoAction)
+ {
+ builder.Append(" ON UPDATE ");
+ ForeignKeyAction(operation.OnUpdate, builder);
+ }
+
+ if (operation.OnDelete != ReferentialAction.NoAction)
+ {
+ builder.Append(" ON DELETE ");
+ ForeignKeyAction(operation.OnDelete, builder);
+ }
+ }
+
#endregion Standard migrations
#region Utilities
diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs
index 581714698..230f25fe0 100644
--- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs
+++ b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs
@@ -183,6 +183,30 @@ public static string TwoDataSourcesInSameServiceProvider(object? useInternalServ
public static string TransientExceptionDetected
=> GetString("TransientExceptionDetected");
+ ///
+ /// PERIOD on foreign key '{foreignKeyName}' in entity type '{entityType}' requires PostgreSQL 18.0 or later. If you're targeting an older version, remove the `WithPeriod()` configuration call. Otherwise, set PostgreSQL compatibility mode by calling 'optionsBuilder.UseNpgsql(..., o => o.SetPostgresVersion(18, 0))' in your model's OnConfiguring.
+ ///
+ public static string PeriodRequiresPostgres18(object? foreignKeyName, object? entityType)
+ => string.Format(
+ GetString("PeriodRequiresPostgres18", nameof(foreignKeyName), nameof(entityType)),
+ foreignKeyName, entityType);
+
+ ///
+ /// PERIOD on foreign key '{foreignKeyName}' in entity type '{entityType}' requires the last column to be a PostgreSQL range type (e.g. daterange, tsrange, tstzrange), but property '{property}' has type '{propertyType}'.
+ ///
+ public static string PeriodRequiresRangeType(object? foreignKeyName, object? entityType, object? property, object? propertyType)
+ => string.Format(
+ GetString("PeriodRequiresRangeType", nameof(foreignKeyName), nameof(entityType), nameof(property), nameof(propertyType)),
+ foreignKeyName, entityType, property, propertyType);
+
+ ///
+ /// PERIOD on foreign key '{foreignKeyName}' in entity type '{entityType}' requires the referenced {principalKeyName} in entity type '{principalEntityType}' to be configured with WITHOUT OVERLAPS.
+ ///
+ public static string PeriodRequiresWithoutOverlapsOnPrincipal(object? foreignKeyName, object? entityType, object? principalKeyName, object? principalEntityType)
+ => string.Format(
+ GetString("PeriodRequiresWithoutOverlapsOnPrincipal", nameof(foreignKeyName), nameof(entityType), nameof(principalKeyName), nameof(principalEntityType)),
+ foreignKeyName, entityType, principalKeyName, principalEntityType);
+
///
/// WITHOUT OVERLAPS on {keyOrIndexName} in entity type '{entityType}' requires PostgreSQL 18.0 or later. If you're targeting an older version, remove the `WithoutOverlaps()` configuration call. Otherwise, set PostgreSQL compatibility mode by calling 'optionsBuilder.UseNpgsql(..., o => o.SetPostgresVersion(18, 0))' in your model's OnConfiguring.
///
diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.resx b/src/EFCore.PG/Properties/NpgsqlStrings.resx
index 87f970af6..cce1d432f 100644
--- a/src/EFCore.PG/Properties/NpgsqlStrings.resx
+++ b/src/EFCore.PG/Properties/NpgsqlStrings.resx
@@ -1,17 +1,17 @@
-
@@ -250,6 +250,15 @@
Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'.
+
+ PERIOD on foreign key '{foreignKeyName}' in entity type '{entityType}' requires PostgreSQL 18.0 or later. If you're targeting an older version, remove the `WithPeriod()` configuration call. Otherwise, set PostgreSQL compatibility mode by calling 'optionsBuilder.UseNpgsql(..., o => o.SetPostgresVersion(18, 0))' in your model's OnConfiguring.
+
+
+ PERIOD on foreign key '{foreignKeyName}' in entity type '{entityType}' requires the last column to be a PostgreSQL range type (e.g. daterange, tsrange, tstzrange), but property '{property}' has type '{propertyType}'.
+
+
+ PERIOD on foreign key '{foreignKeyName}' in entity type '{entityType}' requires the referenced {principalKeyName} in entity type '{principalEntityType}' to be configured with WITHOUT OVERLAPS.
+
WITHOUT OVERLAPS on {keyOrIndexName} in entity type '{entityType}' requires PostgreSQL 18.0 or later. If you're targeting an older version, remove the `WithoutOverlaps()` configuration call. Otherwise, set PostgreSQL compatibility mode by calling 'optionsBuilder.UseNpgsql(..., o => o.SetPostgresVersion(18, 0))' in your model's OnConfiguring.
diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs
index 01cc11bfc..87c1533f5 100644
--- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs
+++ b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs
@@ -955,6 +955,12 @@ deptype IN ('e', 'x')
foreignKey.PrincipalColumns.Add(foreignKeyPrincipalColumn);
}
+ // PERIOD (PostgreSQL 18+)
+ if (foreignKeyRecord.GetFieldValue("conperiod"))
+ {
+ foreignKey[NpgsqlAnnotationNames.Period] = true;
+ }
+
table.ForeignKeys.Add(foreignKey);
ForeignKeyEnd: ;
}
diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs
index 146c28fe1..bff25bffe 100644
--- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs
@@ -2524,6 +2524,69 @@ await Test(
"""ALTER TABLE "Reservations" ADD CONSTRAINT "AK_Reservations_RoomId_During" UNIQUE ("RoomId", "During");""");
}
+ [ConditionalFact, MinimumPostgresVersion(18, 0)]
+ public virtual async Task Create_table_with_foreign_key_with_period()
+ {
+ await Test(
+ _ => { },
+ builder =>
+ {
+ builder.Entity(
+ "Rooms", e =>
+ {
+ e.Property("Id");
+ e.Property>("During");
+ e.HasKey("Id", "During").WithoutOverlaps();
+ });
+ builder.Entity(
+ "Reservations", e =>
+ {
+ e.Property("Id");
+ e.Property("RoomId");
+ e.Property>("During");
+ e.HasKey("Id");
+ e.HasOne("Rooms").WithMany().HasForeignKey("RoomId", "During")
+ .HasPrincipalKey("Id", "During")
+ .WithPeriod();
+ });
+ },
+ model =>
+ {
+ var table = model.Tables.Single(t => t.Name == "Reservations");
+ var foreignKey = Assert.Single(table.ForeignKeys);
+ Assert.Equal(true, foreignKey[NpgsqlAnnotationNames.Period]);
+ });
+
+ AssertSql(
+ """CREATE EXTENSION IF NOT EXISTS btree_gist CASCADE;""",
+ //
+ """
+CREATE TABLE "Rooms" (
+ "Id" integer NOT NULL,
+ "During" tstzrange NOT NULL,
+ CONSTRAINT "PK_Rooms" PRIMARY KEY ("Id", "During" WITHOUT OVERLAPS)
+);
+""",
+ //
+ """
+CREATE TABLE "Reservations" (
+ "Id" integer GENERATED BY DEFAULT AS IDENTITY,
+ "During" tstzrange NOT NULL,
+ "RoomId" integer NOT NULL,
+ CONSTRAINT "PK_Reservations" PRIMARY KEY ("Id"),
+ CONSTRAINT "FK_Reservations_Rooms_RoomId_During" FOREIGN KEY ("RoomId", PERIOD "During") REFERENCES "Rooms" ("Id", PERIOD "During")
+);
+""",
+ //
+ """
+CREATE INDEX "IX_Reservations_RoomId_During" ON "Reservations" ("RoomId", "During");
+""");
+ }
+
+ // Note: We don't have Alter_foreign_key_add_period and Alter_foreign_key_remove_period because PostgreSQL requires foreign keys
+ // to use PERIOD when referencing a primary key with WITHOUT OVERLAPS.
+ // It's not possible to have a non-PERIOD FK pointing to a WITHOUT OVERLAPS PK, so these migrations aren't valid.
+
#endregion
#region Sequence
diff --git a/test/EFCore.PG.Tests/Infrastructure/NpgsqlModelValidatorTest.cs b/test/EFCore.PG.Tests/Infrastructure/NpgsqlModelValidatorTest.cs
index 2ed9b0822..252bebade 100644
--- a/test/EFCore.PG.Tests/Infrastructure/NpgsqlModelValidatorTest.cs
+++ b/test/EFCore.PG.Tests/Infrastructure/NpgsqlModelValidatorTest.cs
@@ -37,7 +37,7 @@ public void Throws_for_WithoutOverlaps_on_alternate_key_without_range_type()
});
VerifyError(
- "WITHOUT OVERLAPS on alternate key {Name, Period} in entity type 'EntityWithIntPeriod' requires the last column to be a PostgreSQL range type (e.g. daterange, tsrange, tstzrange), but property 'Period' has type 'int'.",
+ "WITHOUT OVERLAPS on alternate key {'Name', 'Period'} in entity type 'EntityWithIntPeriod' requires the last column to be a PostgreSQL range type (e.g. daterange, tsrange, tstzrange), but property 'Period' has type 'int'.",
modelBuilder);
}
@@ -57,6 +57,37 @@ public void Throws_for_WithoutOverlaps_on_primary_key_below_postgres_18()
modelBuilder);
}
+ [ConditionalFact]
+ public void Throws_for_Period_on_foreign_key_without_without_overlaps_on_principal()
+ {
+ // Configure PG 18 so version check passes
+ var modelBuilder = CreateConventionModelBuilder(
+ o => o.UseNpgsql("Host=localhost", npgsqlOptions => npgsqlOptions.SetPostgresVersion(18, 0)));
+
+ // Use int for Period to avoid EF Core's base validation issues with NpgsqlRange
+ // Principal key does NOT have WithoutOverlaps configured
+ modelBuilder.Entity(
+ b =>
+ {
+ b.HasKey(e => new { e.Id, e.Period });
+ });
+ modelBuilder.Entity(
+ b =>
+ {
+ b.HasKey(e => e.Id);
+ b.HasOne().WithMany()
+ .HasForeignKey(e => new { e.PrincipalId, e.Period })
+ .HasPrincipalKey(e => new { e.Id, e.Period })
+ .WithPeriod();
+ });
+
+ // The PERIOD validation should fail because principal key doesn't have WITHOUT OVERLAPS
+ // Note: We would also fail the range type check, but the WITHOUT OVERLAPS check comes first
+ VerifyError(
+ "PERIOD on foreign key '{'PrincipalId', 'Period'}' in entity type 'DependentWithIntPeriod' requires the referenced primary key in entity type 'PrincipalWithIntPeriod' to be configured with WITHOUT OVERLAPS.",
+ modelBuilder);
+ }
+
private class EntityWithIntPeriod
{
public int Id { get; set; }
@@ -64,6 +95,19 @@ private class EntityWithIntPeriod
public int Period { get; set; }
}
+ private class PrincipalWithIntPeriod
+ {
+ public int Id { get; set; }
+ public int Period { get; set; }
+ }
+
+ private class DependentWithIntPeriod
+ {
+ public int Id { get; set; }
+ public int PrincipalId { get; set; }
+ public int Period { get; set; }
+ }
+
protected virtual TestHelpers.TestModelBuilder CreateConventionModelBuilder(
Func configureContext = null)
=> NpgsqlTestHelpers.Instance.CreateConventionBuilder(configureContext: configureContext);