Skip to content

Commit e57c161

Browse files
Add PostgreSQL-specific extensions and enhance complex index support
- Introduced `EFCore.ComplexIndexes.PostgreSQL` package for PostgreSQL-specific features like GIN, GiST, BRIN, and SP-GiST indexes, operator classes, concurrent indexes, and more. - Added `ComplexIndexBuilder` for flexible index configuration. - Enhanced composite index handling to support provider-specific annotations. - Updated metadata for NuGet packaging. - Updated README.md with PostgreSQL details and package table.
1 parent cf3a663 commit e57c161

16 files changed

Lines changed: 431 additions & 52 deletions

Directory.Build.props

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project>
2+
<PropertyGroup>
3+
<Version>2.0.0</Version>
4+
<Authors>CaffeinatedCoder</Authors>
5+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
6+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
7+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
8+
<PackageProjectUrl>https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes</PackageProjectUrl>
9+
<RepositoryUrl>https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes</RepositoryUrl>
10+
</PropertyGroup>
11+
</Project>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<PackageId>EFCore.ComplexIndexes.PostgreSQL</PackageId>
8+
<Description>PostgreSQL provider extensions for EFCore.ComplexIndexes — GIN, GiST, BRIN, SP-GiST, and Hash index support for complex type properties.</Description>
9+
<PackageTags>efcore;entityframework;complextype;index;postgresql;npgsql;gin;gist;brin</PackageTags>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\EFCore.ComplexIndexes\EFCore.ComplexIndexes.csproj" />
14+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
15+
<PrivateAssets>all</PrivateAssets>
16+
<IncludeAssets>compile</IncludeAssets>
17+
</PackageReference>
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<None Include="build/EFCore.ComplexIndexes.PostgreSQL.targets"
26+
Pack="true"
27+
PackagePath="build/" />
28+
<None Include="build/EFCore.ComplexIndexes.PostgreSQL.targets"
29+
Pack="true"
30+
PackagePath="buildTransitive/" />
31+
</ItemGroup>
32+
33+
</Project>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace EFCore.ComplexIndexes.PostgreSQL;
2+
3+
/// <summary>
4+
/// Npgsql annotation key constants for PostgreSQL index features.
5+
/// These mirror <c>NpgsqlAnnotationNames</c> from the Npgsql provider.
6+
/// </summary>
7+
internal static class NpgsqlAnnotations
8+
{
9+
public const string IndexMethod = "Npgsql:IndexMethod";
10+
public const string IndexOperators = "Npgsql:IndexOperators";
11+
public const string IndexInclude = "Npgsql:IndexInclude";
12+
public const string IndexSortOrder = "Npgsql:IndexSortOrder";
13+
public const string IndexNullSortOrder = "Npgsql:IndexNullSortOrder";
14+
public const string CreatedConcurrently = "Npgsql:CreatedConcurrently";
15+
public const string NullsDistinct = "Npgsql:NullsDistinct";
16+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace EFCore.ComplexIndexes.PostgreSQL;
2+
3+
/// <summary>
4+
/// PostgreSQL-specific extension methods for <see cref="ComplexIndexBuilder"/>.
5+
/// </summary>
6+
public static class NpgsqlComplexIndexBuilderExtensions
7+
{
8+
/// <summary>Creates a GIN index — ideal for full-text search, JSONB, and array columns.</summary>
9+
public static ComplexIndexBuilder UseGin(this ComplexIndexBuilder builder)
10+
=> builder.HasAnnotation(NpgsqlAnnotations.IndexMethod, "gin");
11+
12+
/// <summary>Creates a GiST index — ideal for geometric, range, and full-text search columns.</summary>
13+
public static ComplexIndexBuilder UseGist(this ComplexIndexBuilder builder)
14+
=> builder.HasAnnotation(NpgsqlAnnotations.IndexMethod, "gist");
15+
16+
/// <summary>Creates a BRIN index — ideal for large, naturally ordered tables (e.g., time-series data).</summary>
17+
public static ComplexIndexBuilder UseBrin(this ComplexIndexBuilder builder)
18+
=> builder.HasAnnotation(NpgsqlAnnotations.IndexMethod, "brin");
19+
20+
/// <summary>Creates a hash index — useful for simple equality comparisons.</summary>
21+
public static ComplexIndexBuilder UseHash(this ComplexIndexBuilder builder)
22+
=> builder.HasAnnotation(NpgsqlAnnotations.IndexMethod, "hash");
23+
24+
/// <summary>Creates an SP-GiST index — ideal for partitioned search trees (e.g., IP addresses, phone numbers).</summary>
25+
public static ComplexIndexBuilder UseSpGist(this ComplexIndexBuilder builder)
26+
=> builder.HasAnnotation(NpgsqlAnnotations.IndexMethod, "spgist");
27+
28+
/// <summary>
29+
/// Specifies per-column operator classes for the index (e.g., <c>jsonb_path_ops</c>).
30+
/// </summary>
31+
public static ComplexIndexBuilder HasOperators(this ComplexIndexBuilder builder, params string[] operators)
32+
=> builder.HasAnnotation(NpgsqlAnnotations.IndexOperators, operators);
33+
34+
/// <summary>
35+
/// Specifies non-key columns to include in the index (covering index).
36+
/// </summary>
37+
public static ComplexIndexBuilder IncludeProperties(this ComplexIndexBuilder builder, params string[] properties)
38+
=> builder.HasAnnotation(NpgsqlAnnotations.IndexInclude, properties);
39+
40+
/// <summary>
41+
/// Specifies that the index should be created concurrently (non-blocking).
42+
/// </summary>
43+
public static ComplexIndexBuilder IsCreatedConcurrently(this ComplexIndexBuilder builder, bool concurrent = true)
44+
=> builder.HasAnnotation(NpgsqlAnnotations.CreatedConcurrently, concurrent);
45+
46+
/// <summary>
47+
/// Specifies whether null values are considered distinct in a unique index.
48+
/// When <c>false</c>, multiple nulls violate the uniqueness constraint.
49+
/// </summary>
50+
public static ComplexIndexBuilder AreNullsDistinct(this ComplexIndexBuilder builder, bool nullsDistinct = true)
51+
=> builder.HasAnnotation(NpgsqlAnnotations.NullsDistinct, nullsDistinct);
52+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Microsoft.EntityFrameworkCore.Design;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace EFCore.ComplexIndexes.PostgreSQL;
6+
7+
public class NpgsqlComplexIndexDesignTimeServices : IDesignTimeServices
8+
{
9+
public void ConfigureDesignTimeServices(IServiceCollection services)
10+
{
11+
services.AddSingleton<IMigrationsModelDiffer, NpgsqlComplexIndexMigrationsModelDiffer>();
12+
}
13+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Microsoft.EntityFrameworkCore.Metadata;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
using Microsoft.EntityFrameworkCore.Migrations.Operations;
4+
using Microsoft.EntityFrameworkCore.Storage;
5+
using Microsoft.EntityFrameworkCore.Update.Internal;
6+
7+
namespace EFCore.ComplexIndexes.PostgreSQL;
8+
9+
#pragma warning disable EF1001
10+
11+
/// <summary>
12+
/// Extends <see cref="CustomMigrationsModelDiffer"/> to validate that
13+
/// provider annotations on complex index operations use recognized Npgsql keys.
14+
/// </summary>
15+
public class NpgsqlComplexIndexMigrationsModelDiffer(
16+
IRelationalTypeMappingSource typeMappingSource,
17+
IMigrationsAnnotationProvider migrationsAnnotationProvider,
18+
IRelationalAnnotationProvider relationalAnnotationProvider,
19+
IRowIdentityMapFactory rowIdentityMapFactory,
20+
CommandBatchPreparerDependencies commandBatchPreparerDependencies
21+
) : CustomMigrationsModelDiffer(
22+
typeMappingSource,
23+
migrationsAnnotationProvider,
24+
relationalAnnotationProvider,
25+
rowIdentityMapFactory,
26+
commandBatchPreparerDependencies
27+
)
28+
{
29+
private static readonly HashSet<string> SupportedNpgsqlAnnotations =
30+
[
31+
NpgsqlAnnotations.IndexMethod,
32+
NpgsqlAnnotations.IndexOperators,
33+
NpgsqlAnnotations.IndexInclude,
34+
NpgsqlAnnotations.IndexSortOrder,
35+
NpgsqlAnnotations.IndexNullSortOrder,
36+
NpgsqlAnnotations.CreatedConcurrently,
37+
NpgsqlAnnotations.NullsDistinct
38+
];
39+
40+
public override IReadOnlyList<MigrationOperation> GetDifferences(
41+
IRelationalModel? source,
42+
IRelationalModel? target
43+
)
44+
{
45+
var operations = base.GetDifferences(source, target);
46+
47+
foreach (var op in operations.OfType<CreateIndexOperation>())
48+
{
49+
foreach (var annotation in op.GetAnnotations())
50+
{
51+
if (annotation.Name.StartsWith("Npgsql:", StringComparison.Ordinal)
52+
&& !SupportedNpgsqlAnnotations.Contains(annotation.Name))
53+
{
54+
throw new InvalidOperationException(
55+
$"Unrecognized Npgsql index annotation '{annotation.Name}' on index '{op.Name}'. " +
56+
$"Supported annotations: {string.Join(", ", SupportedNpgsqlAnnotations)}."
57+
);
58+
}
59+
}
60+
}
61+
62+
return operations;
63+
}
64+
}
65+
66+
#pragma warning restore EF1001
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project>
2+
<ItemGroup>
3+
<AssemblyAttribute Include="Microsoft.EntityFrameworkCore.Design.DesignTimeServicesReferenceAttribute">
4+
<_Parameter1>EFCore.ComplexIndexes.PostgreSQL.NpgsqlComplexIndexDesignTimeServices, EFCore.ComplexIndexes.PostgreSQL</_Parameter1>
5+
</AssemblyAttribute>
6+
</ItemGroup>
7+
</Project>

EFCore.ComplexIndexes.Tests/CompositeIndexSerializerTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ public void Null_fields_omitted_in_json()
5757

5858
Assert.DoesNotContain("filter", json);
5959
Assert.DoesNotContain("name", json);
60+
Assert.DoesNotContain("props", json);
6061
}
6162
}

EFCore.ComplexIndexes.Tests/IndexDescriptorTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ public class IndexDescriptorTests
66
[TestMethod(DisplayName = "Equal descriptors match")]
77
public void Equal_descriptors_match()
88
{
9-
var a = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["email"], "IX_email", true, "x IS NULL");
10-
var b = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["email"], "IX_email", true, "x IS NULL");
9+
var a = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["email"], "IX_email", true, "x IS NULL", []);
10+
var b = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["email"], "IX_email", true, "x IS NULL", []);
1111

1212
Assert.AreEqual(a, b);
1313
Assert.AreEqual(a.GetHashCode(), b.GetHashCode());
@@ -16,17 +16,17 @@ public void Equal_descriptors_match()
1616
[TestMethod(DisplayName = "Different columns do not match")]
1717
public void Different_columns_do_not_match()
1818
{
19-
var a = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["email"], "IX_email", true, null);
20-
var b = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["name"], "IX_email", true, null);
19+
var a = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["email"], "IX_email", true, null, []);
20+
var b = new CustomMigrationsModelDiffer.IndexDescriptor("person", "public", ["name"], "IX_email", true, null, []);
2121

2222
Assert.AreNotEqual(a, b);
2323
}
2424

2525
[TestMethod(DisplayName = "Column order matters")]
2626
public void Column_order_matters()
2727
{
28-
var a = new CustomMigrationsModelDiffer.IndexDescriptor("person", null, ["a", "b"], "IX_ab", false, null);
29-
var b = new CustomMigrationsModelDiffer.IndexDescriptor("person", null, ["b", "a"], "IX_ab", false, null);
28+
var a = new CustomMigrationsModelDiffer.IndexDescriptor("person", null, ["a", "b"], "IX_ab", false, null, []);
29+
var b = new CustomMigrationsModelDiffer.IndexDescriptor("person", null, ["b", "a"], "IX_ab", false, null, []);
3030

3131
Assert.AreNotEqual(a, b);
3232
}

EFCore.ComplexIndexes.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<Solution>
2+
<Project Path="EFCore.ComplexIndexes.PostgreSQL/EFCore.ComplexIndexes.PostgreSQL.csproj" />
23
<Project Path="EFCore.ComplexIndexes.Tests/EFCore.ComplexIndexes.Tests.csproj" />
34
<Project Path="EFCore.ComplexIndexes/EFCore.ComplexIndexes.csproj" />
45
</Solution>

0 commit comments

Comments
 (0)