Skip to content

Commit a77e3ce

Browse files
Bump package version to 2.0.5 and enhance complex index handling with improved column name resolution, convention-based naming, and explicit naming support. Add new test cases for complex index behaviors.
1 parent ecdd038 commit a77e3ce

4 files changed

Lines changed: 170 additions & 17 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>2.0.2</Version>
3+
<Version>2.0.5</Version>
44
<Authors>CaffeinatedCoder</Authors>
55
<PackageLicenseExpression>MIT</PackageLicenseExpression>
66
<GenerateDocumentationFile>true</GenerateDocumentationFile>

EFCore.ComplexIndexes.PostgreSQL/EFCore.ComplexIndexes.PostgreSQL.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<ProjectReference Include="..\EFCore.ComplexIndexes\EFCore.ComplexIndexes.csproj" />
1413
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
1514
<PrivateAssets>all</PrivateAssets>
1615
<IncludeAssets>compile</IncludeAssets>
1716
</PackageReference>
1817
</ItemGroup>
1918

2019
<ItemGroup>
20+
<ProjectReference Include="..\EFCore.ComplexIndexes\EFCore.ComplexIndexes.csproj" />
2121
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
2222
</ItemGroup>
2323

EFCore.ComplexIndexes.Tests/MigrationModelDifferTests.cs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.ComponentModel;
21
using Microsoft.Data.Sqlite;
32
using Microsoft.EntityFrameworkCore;
43
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -134,6 +133,90 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
134133
}
135134
}
136135

136+
private enum VacancySource
137+
{
138+
Source1,
139+
Source2,
140+
Source3
141+
}
142+
143+
private class Vacancy
144+
{
145+
public Guid Id { get; set; }
146+
public VacancyOrigin Origin { get; set; } = new();
147+
}
148+
149+
private class VacancyOrigin
150+
{
151+
public VacancySource Source { get; set; }
152+
public string ExternalId { get; set; } = "";
153+
}
154+
155+
private class VacancyCompositeConventionColumnNamesContext(
156+
DbContextOptions<VacancyCompositeConventionColumnNamesContext> options) : DbContext(options)
157+
{
158+
public DbSet<Vacancy> Vacancies => Set<Vacancy>();
159+
160+
protected override void OnModelCreating(ModelBuilder modelBuilder)
161+
{
162+
modelBuilder.Entity<Vacancy>(builder =>
163+
{
164+
builder.ToTable("Vacancies");
165+
builder.HasKey(x => x.Id);
166+
builder.ComplexProperty(x => x.Origin);
167+
168+
builder.HasComplexCompositeIndex(
169+
vacancy => new { vacancy.Origin.Source, vacancy.Origin.ExternalId },
170+
isUnique: true);
171+
});
172+
}
173+
}
174+
175+
private class VacancySingleConventionColumnNameContext(
176+
DbContextOptions<VacancySingleConventionColumnNameContext> options) : DbContext(options)
177+
{
178+
public DbSet<Vacancy> Vacancies => Set<Vacancy>();
179+
180+
protected override void OnModelCreating(ModelBuilder modelBuilder)
181+
{
182+
modelBuilder.Entity<Vacancy>(builder =>
183+
{
184+
builder.ToTable("Vacancies");
185+
builder.HasKey(x => x.Id);
186+
187+
builder.ComplexProperty(x => x.Origin, complex =>
188+
{
189+
complex.Property(x => x.Source).HasComplexIndex(isUnique: true);
190+
});
191+
});
192+
}
193+
}
194+
195+
private class VacancyCompositeExplicitColumnNamesContext(
196+
DbContextOptions<VacancyCompositeExplicitColumnNamesContext> options) : DbContext(options)
197+
{
198+
public DbSet<Vacancy> Vacancies => Set<Vacancy>();
199+
200+
protected override void OnModelCreating(ModelBuilder modelBuilder)
201+
{
202+
modelBuilder.Entity<Vacancy>(builder =>
203+
{
204+
builder.ToTable("Vacancies");
205+
builder.HasKey(x => x.Id);
206+
207+
builder.ComplexProperty(x => x.Origin, complex =>
208+
{
209+
complex.Property(x => x.Source).HasColumnName("vacancy_source");
210+
complex.Property(x => x.ExternalId).HasColumnName("external_vacancy_id");
211+
});
212+
213+
builder.HasComplexCompositeIndex(
214+
vacancy => new { vacancy.Origin.Source, vacancy.Origin.ExternalId },
215+
isUnique: true);
216+
});
217+
}
218+
}
219+
137220
// ── Tests ──
138221

139222
[TestMethod(DisplayName = "Initial migration creates index")]
@@ -195,6 +278,52 @@ public void Composite_index_creates_multi_column()
195278
Assert.IsTrue(createIndex.IsUnique);
196279
}
197280

281+
[TestMethod(DisplayName = "Composite complex index uses convention-based complex column names")]
282+
public void Composite_complex_index_uses_convention_based_complex_column_names()
283+
{
284+
var target = BuildRelationalModel<VacancyCompositeConventionColumnNamesContext>();
285+
var operations = GetDifferences(source: null, target: target);
286+
287+
var createIndex = Assert.ContainsSingle(operations.OfType<CreateIndexOperation>());
288+
289+
string[] expectedColumnNames = ["Origin_Source", "Origin_ExternalId"];
290+
291+
Assert.AreEqual("Vacancies", createIndex.Table);
292+
Assert.IsTrue(createIndex.Columns.SequenceEqual(expectedColumnNames));
293+
Assert.IsTrue(createIndex.IsUnique);
294+
Assert.AreEqual("IX_Vacancies_Origin_Source_Origin_ExternalId", createIndex.Name);
295+
}
296+
297+
[TestMethod(DisplayName = "Single-column complex index uses convention-based complex column name")]
298+
public void Single_column_complex_index_uses_convention_based_complex_column_name()
299+
{
300+
var target = BuildRelationalModel<VacancySingleConventionColumnNameContext>();
301+
var operations = GetDifferences(source: null, target: target);
302+
303+
var createIndex = Assert.ContainsSingle(operations.OfType<CreateIndexOperation>());
304+
305+
Assert.AreEqual("Vacancies", createIndex.Table);
306+
Assert.AreEqual("Origin_Source", Assert.ContainsSingle(createIndex.Columns));
307+
Assert.IsTrue(createIndex.IsUnique);
308+
Assert.AreEqual("IX_Vacancies_Origin_Source", createIndex.Name);
309+
}
310+
311+
[TestMethod(DisplayName = "Composite complex index uses explicit complex column names")]
312+
public void Composite_complex_index_uses_explicit_complex_column_names()
313+
{
314+
var target = BuildRelationalModel<VacancyCompositeExplicitColumnNamesContext>();
315+
var operations = GetDifferences(source: null, target: target);
316+
317+
var createIndex = Assert.ContainsSingle(operations.OfType<CreateIndexOperation>());
318+
319+
string[] expectedColumnNames = ["vacancy_source", "external_vacancy_id"];
320+
321+
Assert.AreEqual("Vacancies", createIndex.Table);
322+
Assert.IsTrue(createIndex.Columns.SequenceEqual(expectedColumnNames));
323+
Assert.IsTrue(createIndex.IsUnique);
324+
Assert.AreEqual("IX_Vacancies_vacancy_source_external_vacancy_id", createIndex.Name);
325+
}
326+
198327
[TestMethod(DisplayName = "Filtered index preserves filter")]
199328
public void Filtered_index_preserves_filter()
200329
{

EFCore.ComplexIndexes/CustomMigrationsModelDiffer.cs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,31 @@ private static void ScanForSingleColumnIndexes(
113113
string? schema,
114114
HashSet<IndexDescriptor> results
115115
)
116+
{
117+
var storeObject = StoreObjectIdentifier.Table(tableName, schema);
118+
119+
ScanForSingleColumnIndexes(typeBase, tableName, schema, storeObject, results);
120+
}
121+
122+
private static void ScanForSingleColumnIndexes(
123+
ITypeBase typeBase,
124+
string tableName,
125+
string? schema,
126+
StoreObjectIdentifier storeObject,
127+
HashSet<IndexDescriptor> results
128+
)
116129
{
117130
foreach (var property in typeBase.GetDeclaredProperties())
118131
{
119132
if (property.FindAnnotation(ComplexIndexAnnotations.IsIndexed)?.Value is not true)
120133
continue;
121134

122-
var columnName = property.GetColumnName();
123-
var isUnique = property.FindAnnotation(ComplexIndexAnnotations.IsUnique)?.Value is true;
124-
var filter = property.FindAnnotation(ComplexIndexAnnotations.Filter)?.Value as string;
135+
var columnName = property.GetColumnName(storeObject);
136+
if (columnName is null)
137+
continue;
138+
139+
var isUnique = property.FindAnnotation(ComplexIndexAnnotations.IsUnique)?.Value is true;
140+
var filter = property.FindAnnotation(ComplexIndexAnnotations.Filter)?.Value as string;
125141
var indexName = property.FindAnnotation(ComplexIndexAnnotations.IndexName)?.Value as string
126142
?? $"IX_{tableName}_{columnName}";
127143

@@ -137,7 +153,7 @@ HashSet<IndexDescriptor> results
137153
}
138154

139155
foreach (var cp in typeBase.GetDeclaredComplexProperties())
140-
ScanForSingleColumnIndexes(cp.ComplexType, tableName, schema, results);
156+
ScanForSingleColumnIndexes(cp.ComplexType, tableName, schema, storeObject, results);
141157
}
142158

143159
private static void ScanForCompositeIndexes(
@@ -153,29 +169,29 @@ HashSet<IndexDescriptor> results
153169
return;
154170

155171
var definitions = CompositeIndexSerializer.Deserialize(json);
172+
var storeObject = StoreObjectIdentifier.Table(tableName, schema);
156173

157174
foreach (var def in definitions)
158175
{
159-
var columnNames = new List<string>(def.PropertyPaths.Count);
160-
var allResolved = true;
176+
var columnNames = new List<string>(def.PropertyPaths.Count);
177+
string? unresolvedPath = null;
161178

162179
foreach (var path in def.PropertyPaths)
163180
{
164-
var col = ResolveColumnName(entityType, path);
181+
var col = ResolveColumnName(entityType, path, storeObject);
165182
if (col is null)
166183
{
167-
allResolved = false;
184+
unresolvedPath = path;
168185
break;
169186
}
170187

171188
columnNames.Add(col);
172189
}
173190

174-
if (!allResolved)
191+
if (unresolvedPath is not null)
175192
{
176193
throw new InvalidOperationException(
177-
$"Could not resolve property path for composite index on entity {entityType.Name}. " +
178-
$"Invalid path: {string.Join(".", def.PropertyPaths)}"
194+
$"Could not resolve property path '{unresolvedPath}' for composite index on entity {entityType.Name}."
179195
);
180196
}
181197

@@ -229,15 +245,19 @@ HashSet<IndexDescriptor> results
229245
};
230246
}
231247

232-
private static string? ResolveColumnName(IEntityType entityType, string dotPath)
248+
private static string? ResolveColumnName(
249+
IEntityType entityType,
250+
string dotPath,
251+
StoreObjectIdentifier storeObject
252+
)
233253
{
234254
var parts = dotPath.Split('.');
235255
ITypeBase current = entityType;
236256

237257
for (var i = 0; i < parts.Length; i++)
238258
{
239259
if (i == parts.Length - 1)
240-
return current.FindProperty(parts[i])?.GetColumnName();
260+
return current.FindProperty(parts[i])?.GetColumnName(storeObject);
241261

242262
var cp = current.FindComplexProperty(parts[i]);
243263
if (cp is null) return null;
@@ -285,10 +305,14 @@ public override int GetHashCode()
285305
var hash = new HashCode();
286306
hash.Add(TableName);
287307
hash.Add(Schema);
288-
foreach (var col in ColumnNames) hash.Add(col);
308+
309+
foreach (var col in ColumnNames)
310+
hash.Add(col);
311+
289312
hash.Add(IndexName);
290313
hash.Add(IsUnique);
291314
hash.Add(Filter);
315+
292316
foreach (var (key, value) in ProviderAnnotations.OrderBy(kv => kv.Key))
293317
{
294318
hash.Add(key);

0 commit comments

Comments
 (0)