diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlForeignKeyBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlForeignKeyBuilderExtensions.cs
new file mode 100644
index 000000000..69eac600b
--- /dev/null
+++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlForeignKeyBuilderExtensions.cs
@@ -0,0 +1,72 @@
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore;
+
+///
+/// Npgsql specific extension methods for configuring foreign keys.
+///
+public static class NpgsqlForeignKeyBuilderExtensions
+{
+ ///
+ /// Configure the matching strategy to be used with the foreign key.
+ ///
+ /// The builder for the foreign key being configured.
+ /// The defining the used matching strategy.
+ ///
+ ///
+ ///
+ public static ReferenceReferenceBuilder UsesMatchStrategy(this ReferenceReferenceBuilder builder, PostgresMatchStrategy matchStrategy)
+ {
+ Check.NotNull(builder, nameof(builder));
+ Check.IsDefined(matchStrategy, nameof(matchStrategy));
+ builder.Metadata.SetMatchStrategy(matchStrategy);
+ return builder;
+ }
+
+ ///
+ /// Configure the matching strategy to be used with the foreign key.
+ ///
+ /// The builder for the foreign key being configured.
+ /// The defining the used matching strategy.
+ ///
+ ///
+ ///
+ public static ReferenceReferenceBuilder UsesMatchStrategy(
+ this ReferenceReferenceBuilder builder,
+ PostgresMatchStrategy matchStrategy)
+ where TEntity : class
+ where TRelatedEntity : class
+ => (ReferenceReferenceBuilder)UsesMatchStrategy((ReferenceReferenceBuilder)builder, matchStrategy);
+
+ ///
+ /// Configure the matching strategy to be used with the foreign key.
+ ///
+ /// The builder for the foreign key being configured.
+ /// The defining the used matching strategy.
+ ///
+ ///
+ ///
+ public static ReferenceCollectionBuilder UsesMatchStrategy(this ReferenceCollectionBuilder builder, PostgresMatchStrategy matchStrategy)
+ {
+ Check.NotNull(builder, nameof(builder));
+ Check.IsDefined(matchStrategy, nameof(matchStrategy));
+ builder.Metadata.SetMatchStrategy(matchStrategy);
+ return builder;
+ }
+
+ ///
+ /// Configure the matching strategy to be used with the foreign key.
+ ///
+ /// The builder for the foreign key being configured.
+ /// The defining the used matching strategy.
+ ///
+ ///
+ ///
+ public static ReferenceCollectionBuilder UsesMatchStrategy(
+ this ReferenceCollectionBuilder builder,
+ PostgresMatchStrategy matchStrategy)
+ where TEntity : class
+ where TRelatedEntity : class
+ => (ReferenceCollectionBuilder)UsesMatchStrategy((ReferenceCollectionBuilder)builder, matchStrategy);
+}
diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlForeignKeyExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlForeignKeyExtensions.cs
new file mode 100644
index 000000000..cccc93295
--- /dev/null
+++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlForeignKeyExtensions.cs
@@ -0,0 +1,30 @@
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
+// ReSharper disable once CheckNamespace
+
+namespace Microsoft.EntityFrameworkCore;
+
+///
+/// Npgsql specific extension methods for .
+///
+public static class NpgsqlForeignKeyExtensions
+{
+ ///
+ /// Sets the for a foreign key.
+ ///
+ /// the foreign key being configured.
+ /// the defining the used matching strategy.
+ ///
+ ///
+ ///
+ public static void SetMatchStrategy(this IMutableForeignKey foreignKey, PostgresMatchStrategy matchStrategy)
+ => foreignKey.SetOrRemoveAnnotation(NpgsqlAnnotationNames.MatchStrategy, matchStrategy);
+
+ ///
+ /// Returns the assigned for the provided foreign key
+ ///
+ /// the foreign key
+ /// the if assigned, null otherwise
+ public static PostgresMatchStrategy? GetMatchStrategy(this IReadOnlyForeignKey foreignKey)
+ => (PostgresMatchStrategy?)foreignKey[NpgsqlAnnotationNames.MatchStrategy];
+}
diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
index 0f2dd325e..eac083f2a 100644
--- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
+++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
@@ -271,4 +271,12 @@ public static class NpgsqlAnnotationNames
///
// Replaced by IsDescending in EF Core 7.0
public const string IndexSortOrder = Prefix + "IndexSortOrder";
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string MatchStrategy = Prefix + "MatchStrategy";
}
diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs
index a9493d6ae..6d6c9c14c 100644
--- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs
+++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs
@@ -200,6 +200,28 @@ public override IEnumerable For(ITableIndex index, bool designTime)
}
}
+ ///
+ /// 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 foreignKey, bool designTime)
+ {
+ if (!designTime)
+ {
+ yield break;
+ }
+
+ foreach (var item in foreignKey.MappedForeignKeys)
+ {
+ if (item.GetMatchStrategy() is { } match)
+ {
+ yield return new Annotation(NpgsqlAnnotationNames.MatchStrategy, match);
+ }
+ }
+ }
+
///
/// 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/PostgresMatchStrategy.cs b/src/EFCore.PG/Metadata/PostgresMatchStrategy.cs
new file mode 100644
index 000000000..be2acec83
--- /dev/null
+++ b/src/EFCore.PG/Metadata/PostgresMatchStrategy.cs
@@ -0,0 +1,25 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+///
+/// Matching strategies for a foreign key.
+///
+///
+///
+///
+public enum PostgresMatchStrategy
+{
+ ///
+ /// The default matching strategy, allows any foreign key column to be NULL.
+ ///
+ Simple = 0,
+
+ ///
+ /// Currently not implemented in PostgreSQL.
+ ///
+ Partial = 1,
+
+ ///
+ /// Requires the foreign key to either have all columns to be set or all columns to be NULL.
+ ///
+ Full = 2
+}
diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
index 800941791..3b3ff2fe8 100644
--- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
+++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel;
using System.Globalization;
using System.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
@@ -1447,6 +1448,53 @@ protected virtual void GenerateDropRange(PostgresRange rangeType, IModel? model,
#endregion Range management
+ #region MatchingStrategy management
+
+ ///
+ protected override void ForeignKeyConstraint(AddForeignKeyOperation operation, IModel? model, MigrationCommandListBuilder builder)
+ {
+ if (operation.Name != null)
+ {
+ builder.Append("CONSTRAINT ").Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)).Append(" ");
+ }
+
+ builder.Append("FOREIGN KEY (").Append(ColumnList(operation.Columns)).Append(") REFERENCES ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.PrincipalTable, operation.PrincipalSchema));
+ if (operation.PrincipalColumns != null)
+ {
+ builder.Append(" (").Append(ColumnList(operation.PrincipalColumns)).Append(")");
+ }
+
+ if (operation[NpgsqlAnnotationNames.MatchStrategy] is PostgresMatchStrategy matchStrategy)
+ {
+ builder.Append(" MATCH ")
+ .Append(TranslateMatchStrategy(matchStrategy));
+ }
+
+ if (operation.OnUpdate != 0)
+ {
+ builder.Append(" ON UPDATE ");
+ ForeignKeyAction(operation.OnUpdate, builder);
+ }
+
+ if (operation.OnDelete != 0)
+ {
+ builder.Append(" ON DELETE ");
+ ForeignKeyAction(operation.OnDelete, builder);
+ }
+ }
+
+ private static string TranslateMatchStrategy(PostgresMatchStrategy matchStrategy)
+ => matchStrategy switch
+ {
+ PostgresMatchStrategy.Simple => "SIMPLE",
+ PostgresMatchStrategy.Partial => "PARTIAL",
+ PostgresMatchStrategy.Full => "FULL",
+ _ => throw new InvalidEnumArgumentException(nameof(matchStrategy), (int)matchStrategy, typeof(PostgresMatchStrategy))
+ };
+
+ #endregion MatchingStrategy management
+
///
protected override void Generate(
DropIndexOperation operation,
diff --git a/src/Shared/Check.cs b/src/Shared/Check.cs
index 05c1e3dde..17082029d 100644
--- a/src/Shared/Check.cs
+++ b/src/Shared/Check.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
@@ -127,4 +128,15 @@ public static void DebugAssert([DoesNotReturnIf(false)] bool condition, string m
[DoesNotReturn]
public static void DebugFail(string message)
=> throw new Exception($"Check.DebugFail failed: {message}");
+
+ public static void IsDefined(
+ T value,
+ [InvokerParameterName] string parameterName)
+ where T : struct, Enum
+ {
+ if (!Enum.IsDefined(value))
+ {
+ throw new InvalidEnumArgumentException(parameterName, Convert.ToInt32(value), typeof(T));
+ }
+ }
}
diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs
index 82918f6b8..29db36f7c 100644
--- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs
@@ -1147,7 +1147,8 @@ await Test(
e.Property("Y");
}),
builder => builder.Entity("People").Property("Sum")
- .HasComputedColumnSql("""
+ .HasComputedColumnSql(
+ """
"X" + "Y"
""", stored: true),
builder => builder.Entity("People").Property("Sum"),
@@ -1671,7 +1672,8 @@ await Test(
"People", b =>
{
b.Property("Name");
- b.Property("Name2").HasComputedColumnSql("""
+ b.Property("Name2").HasComputedColumnSql(
+ """
"Name"
""", stored: true);
}),
@@ -1681,7 +1683,8 @@ await Test(
model =>
{
var computedColumn = Assert.Single(Assert.Single(model.Tables).Columns, c => c.Name == "Name2");
- Assert.Equal("""
+ Assert.Equal(
+ """
"Name"
""", computedColumn.ComputedColumnSql);
Assert.Equal(NonDefaultCollation, computedColumn.Collation);
@@ -1938,7 +1941,8 @@ await Test(
_ => { },
builder => builder.Entity("People").HasIndex("Name")
.IncludeProperties("FirstName", "LastName")
- .HasFilter("""
+ .HasFilter(
+ """
"Name" IS NOT NULL
"""),
model =>
@@ -2031,7 +2035,8 @@ await Test(
builder => builder.Entity("People").HasIndex("Name")
.IsUnique()
.IncludeProperties("FirstName", "LastName")
- .HasFilter("""
+ .HasFilter(
+ """
"Name" IS NOT NULL
"""),
model =>
@@ -2460,6 +2465,60 @@ public override async Task Drop_check_constraint()
AssertSql("""ALTER TABLE "People" DROP CONSTRAINT "CK_People_Foo";""");
}
+ [Theory]
+ [InlineData(PostgresMatchStrategy.Simple, "SIMPLE", false)]
+ [InlineData(PostgresMatchStrategy.Partial, "PARTIAL", true)]
+ [InlineData(PostgresMatchStrategy.Full, "FULL", false)]
+ public async Task Add_foreign_key_with_match_strategy(PostgresMatchStrategy strategy, string matchValue, bool throws)
+ {
+ var runningTest = Test(
+ builder =>
+ {
+ builder.Entity(
+ "Customers", delegate(EntityTypeBuilder e)
+ {
+ e.Property("Id");
+ e.HasKey("Id");
+ e.Property("AddressId");
+ });
+ builder.Entity(
+ "Orders", delegate(EntityTypeBuilder e)
+ {
+ e.Property("Id");
+ e.Property("CustomerId");
+ });
+ },
+ _ => { },
+ builder =>
+ {
+ builder.Entity("Orders")
+ .HasOne("Customers")
+ .WithMany()
+ .HasForeignKey("CustomerId")
+ .UsesMatchStrategy(strategy)
+ .HasConstraintName("FK_Foo");
+ },
+ asserter: null);
+
+ if (throws)
+ {
+ await Assert.ThrowsAsync(() => runningTest);
+ }
+ else
+ {
+ await runningTest;
+ }
+
+ AssertSql(
+ """
+CREATE INDEX "IX_Orders_CustomerId" ON "Orders" ("CustomerId");
+""",
+ //
+ $"""
+ALTER TABLE "Orders" ADD CONSTRAINT "FK_Foo" FOREIGN KEY ("CustomerId") REFERENCES "Customers" ("Id") MATCH {matchValue} ON DELETE CASCADE;
+""");
+ }
+
#endregion
#region Sequence
@@ -2727,7 +2786,6 @@ SELECT setval(
""");
}
-
#endregion Data seeding
public override async Task Add_required_primitve_collection_with_custom_default_value_sql_to_existing_table()
@@ -3225,7 +3283,6 @@ public override async Task Add_required_primitve_collection_with_custom_converte
AssertSql("""ALTER TABLE "Customers" ADD "Numbers" text NOT NULL DEFAULT 'some numbers';""");
}
-
protected override string NonDefaultCollation
=> "POSIX";