Skip to content

Commit a1ab3b0

Browse files
CopilotAndriySvyryd
andcommitted
Fix SQLite AUTOINCREMENT to work with value converters for issues #30699 and #29519
Co-authored-by: AndriySvyryd <[email protected]>
1 parent 738d27f commit a1ab3b0

File tree

3 files changed

+145
-5
lines changed

3 files changed

+145
-5
lines changed

src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(I
6060
&& primaryKey.Properties[0] == property
6161
&& property.ValueGenerated == ValueGenerated.OnAdd
6262
&& property.ClrType.UnwrapNullableType().IsInteger()
63-
&& property.FindTypeMapping()?.Converter == null
6463
? SqliteValueGenerationStrategy.Autoincrement
6564
: SqliteValueGenerationStrategy.None;
6665
}
@@ -97,8 +96,7 @@ public static void SetValueGenerationStrategy(
9796
public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource(this IConventionProperty property)
9897
=> property.FindAnnotation(SqliteAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource();
9998

100-
private static bool HasConverter(IProperty property)
101-
=> property.FindTypeMapping()?.Converter != null;
99+
102100
/// <summary>
103101
/// Returns the SRID to use when creating a column for this property.
104102
/// </summary>

src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ private static SqliteValueGenerationStrategy GetValueGenerationStrategy(IConvent
5757
var primaryKey = entityType.FindPrimaryKey();
5858
if (primaryKey is { Properties.Count: 1 }
5959
&& primaryKey.Properties[0] == property
60-
&& property.ClrType.UnwrapNullableType().IsInteger()
61-
&& property.FindTypeMapping()?.Converter == null)
60+
&& property.ClrType.UnwrapNullableType().IsInteger())
6261
{
6362
return SqliteValueGenerationStrategy.Autoincrement;
6463
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
5+
6+
namespace Microsoft.EntityFrameworkCore;
7+
8+
/// <summary>
9+
/// Test for SQLite AUTOINCREMENT with value converters, specifically for issues #30699 and #29519.
10+
/// </summary>
11+
public class SqliteAutoincrementWithConverterTest : IClassFixture<SqliteAutoincrementWithConverterTest.SqliteAutoincrementWithConverterFixture>
12+
{
13+
private const string DatabaseName = "AutoincrementWithConverter";
14+
15+
public SqliteAutoincrementWithConverterTest(SqliteAutoincrementWithConverterFixture fixture)
16+
{
17+
Fixture = fixture;
18+
}
19+
20+
protected SqliteAutoincrementWithConverterFixture Fixture { get; }
21+
22+
[ConditionalFact]
23+
public virtual async Task Strongly_typed_id_with_converter_gets_autoincrement()
24+
{
25+
await using var context = (PoolableDbContext)CreateContext();
26+
27+
// Ensure the database is created
28+
await context.Database.EnsureCreatedAsync();
29+
30+
// Check that the SQL contains AUTOINCREMENT for the strongly-typed ID
31+
var sql = context.Database.GenerateCreateScript();
32+
Assert.Contains("\"Id\" INTEGER NOT NULL CONSTRAINT \"PK_Products\" PRIMARY KEY AUTOINCREMENT", sql);
33+
}
34+
35+
[ConditionalFact]
36+
public virtual async Task Insert_with_strongly_typed_id_generates_value()
37+
{
38+
await using var context = (PoolableDbContext)CreateContext();
39+
await context.Database.EnsureCreatedAsync();
40+
41+
// Insert a product with strongly-typed ID
42+
var product = new Product { Name = "Test Product" };
43+
context.Products.Add(product);
44+
await context.SaveChangesAsync();
45+
46+
// The ID should have been generated
47+
Assert.True(product.Id.Value > 0);
48+
49+
// Insert another product
50+
var product2 = new Product { Name = "Test Product 2" };
51+
context.Products.Add(product2);
52+
await context.SaveChangesAsync();
53+
54+
// The second ID should be different
55+
Assert.True(product2.Id.Value > product.Id.Value);
56+
}
57+
58+
[ConditionalFact]
59+
public virtual async Task Migration_consistency_with_value_converter()
60+
{
61+
await using var context = (PoolableDbContext)CreateContext();
62+
63+
// This test ensures that migrations don't generate repeated AlterColumn operations
64+
// by checking that the model annotation is consistent
65+
var property = context.Model.FindEntityType(typeof(Product))!.FindProperty(nameof(Product.Id))!;
66+
var strategy = property.GetValueGenerationStrategy();
67+
68+
Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, strategy);
69+
}
70+
71+
[ConditionalFact]
72+
public virtual async Task Explicit_autoincrement_configuration_is_honored()
73+
{
74+
await using var context = (PoolableDbContext)CreateContext();
75+
76+
// Check that explicitly configured AUTOINCREMENT is honored despite having a converter
77+
var property = context.Model.FindEntityType(typeof(Product))!.FindProperty(nameof(Product.Id))!;
78+
var strategy = property.GetValueGenerationStrategy();
79+
80+
Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, strategy);
81+
82+
// Verify in the actual SQL generation
83+
var sql = context.Database.GenerateCreateScript();
84+
Assert.Contains("AUTOINCREMENT", sql);
85+
}
86+
87+
protected virtual DbContext CreateContext()
88+
=> Fixture.CreateContext();
89+
90+
public class SqliteAutoincrementWithConverterFixture : SharedStoreFixtureBase<SqliteAutoincrementWithConverterTest.PoolableDbContext>
91+
{
92+
protected override string StoreName
93+
=> DatabaseName;
94+
95+
protected override ITestStoreFactory TestStoreFactory
96+
=> SqliteTestStoreFactory.Instance;
97+
98+
protected override IServiceCollection AddServices(IServiceCollection serviceCollection)
99+
=> base.AddServices(serviceCollection);
100+
101+
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
102+
=> base.AddOptions(builder);
103+
104+
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
105+
{
106+
modelBuilder.Entity<Product>(b =>
107+
{
108+
b.Property(e => e.Id).HasConversion(
109+
v => v.Value,
110+
v => new ProductId(v));
111+
b.Property(e => e.Id).UseAutoincrement(); // Explicit configuration
112+
});
113+
114+
modelBuilder.Entity<Category>(); // Standard int ID for comparison
115+
}
116+
}
117+
118+
// Test entities
119+
public record struct ProductId(int Value);
120+
121+
public class Product
122+
{
123+
public ProductId Id { get; set; }
124+
public required string Name { get; set; }
125+
}
126+
127+
public class Category
128+
{
129+
public int Id { get; set; }
130+
public required string Name { get; set; }
131+
}
132+
133+
public class PoolableDbContext : DbContext
134+
{
135+
public PoolableDbContext(DbContextOptions options)
136+
: base(options)
137+
{
138+
}
139+
140+
public DbSet<Product> Products => Set<Product>();
141+
public DbSet<Category> Categories => Set<Category>();
142+
}
143+
}

0 commit comments

Comments
 (0)