Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
32293b1
Initial plan
Copilot Sep 5, 2025
fa6fbfe
Implement first-class SQLite AUTOINCREMENT support
Copilot Sep 5, 2025
a9b7973
Add comprehensive tests for SQLite AUTOINCREMENT first-class support
Copilot Sep 5, 2025
738d27f
Finalize SQLite AUTOINCREMENT first-class implementation
Copilot Sep 5, 2025
a1ab3b0
Fix SQLite AUTOINCREMENT to work with value converters for issues #30…
Copilot Sep 5, 2025
21c588b
Add SqliteStoreGenerationConvention and SqliteAnnotationCodeGenerator…
Copilot Sep 5, 2025
2698227
Address review feedback: move tests, fix value generation convention …
Copilot Sep 5, 2025
e84ae21
Add test for migration consistency between string API and convention …
Copilot Sep 5, 2025
e0087b5
Add tests for SQLite annotation code generator in model snapshots
Copilot Sep 6, 2025
da1ebe3
Address review feedback: fix annotation code generator logic, remove …
Copilot Sep 6, 2025
ca03cda
Address review feedback: rename test class, make GetDefaultValueGener…
Copilot Sep 6, 2025
fdb1536
Address review feedback: remove redundant test, make GetDefaultValueG…
Copilot Sep 6, 2025
665a925
Address review feedback: rename test class, remove comments and empty…
Copilot Sep 6, 2025
219a91c
Address review feedback: fix ambiguous GetValueGenerationStrategy cal…
Copilot Sep 6, 2025
e533351
Fix TryGetAndRemove method in SqliteAnnotationCodeGenerator
Copilot Sep 6, 2025
3769a12
Address review feedback: remove empty lines and add convention-based …
Copilot Sep 6, 2025
28412a4
Address review feedback: rewrite PR description, confirm issue fixes,…
Copilot Sep 6, 2025
2b52a6b
Fix TryGetAndRemove method in SqliteAnnotationCodeGenerator
Copilot Sep 6, 2025
0754086
Address PR review feedback: remove comments, empty lines, move and re…
Copilot Sep 6, 2025
37f6338
Fix compilation issues in moved CSharpMigrationsGeneratorSqliteTest.c…
Copilot Sep 6, 2025
9685d43
Add UseAutoincrement extension method for TestPropertyBuilder and mov…
Copilot Sep 6, 2025
050c685
Extract common beginning and end fragments using AddBoilerPlate metho…
Copilot Sep 6, 2025
1de78ab
Change assertion to expect SqliteValueGenerationStrategy.None for non…
Copilot Sep 6, 2025
b371842
Extract common code to CSharpMigrationsGeneratorTestBase and refactor…
Copilot Sep 6, 2025
a34ef8b
Update baselines
AndriySvyryd Sep 9, 2025
a0a0eb1
Add baselines
AndriySvyryd Sep 9, 2025
744c2c3
Address PR feedback: Add CanSetValueGenerationStrategy, improve valid…
Copilot Sep 9, 2025
eaf05a2
Fix SqliteValueGenerationConvention to inherit from RelationalValueGe…
Copilot Sep 9, 2025
f48a50f
Make autoincrement convention more selective - only apply by conventi…
Copilot Sep 9, 2025
782762c
Make convention fully conservative - no autoincrement by default, onl…
Copilot Sep 9, 2025
a4919ca
Revert last two commits and fix annotation code generator logic for e…
Copilot Sep 9, 2025
388c332
Update annotation code generator to always include autoincrement anno…
Copilot Sep 9, 2025
85b9a3d
Add ConflictingValueGenerationStrategiesWarning for SQLite with strin…
Copilot Sep 10, 2025
f2e7f0f
Address PR feedback: use expression-body syntax and improve property …
Copilot Sep 10, 2025
e337cac
Add TPC mapping strategy check and refactor GetDefaultValueGeneration…
Copilot Sep 10, 2025
b2e736e
Update baselines
AndriySvyryd Sep 10, 2025
c90161c
Address PR feedback: remove unused method, update entity names, fix c…
Copilot Sep 10, 2025
786542b
Fix tests
AndriySvyryd Sep 10, 2025
5e04e23
Add missing methods and cleanup
AndriySvyryd Sep 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,65 @@ namespace Microsoft.EntityFrameworkCore;
/// </remarks>
public static class SqlitePropertyBuilderExtensions
{
/// <summary>
/// Configures the property to use SQLite AUTOINCREMENT feature to generate values for new entities,
/// when targeting SQLite. This method sets the property to be <see cref="ValueGenerated.OnAdd" />.
/// </summary>
/// <remarks>
/// AUTOINCREMENT can only be used on integer primary key columns in SQLite.
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static PropertyBuilder UseAutoincrement(this PropertyBuilder propertyBuilder)
{
var property = propertyBuilder.Metadata;
property.SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);

return propertyBuilder;
}

/// <summary>
/// Configures the property to use SQLite AUTOINCREMENT feature to generate values for new entities,
/// when targeting SQLite. This method sets the property to be <see cref="ValueGenerated.OnAdd" />.
/// </summary>
/// <remarks>
/// AUTOINCREMENT can only be used on integer primary key columns in SQLite.
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see> for more information and examples.
/// </remarks>
/// <typeparam name="TProperty">The type of the property being configured.</typeparam>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static PropertyBuilder<TProperty> UseAutoincrement<TProperty>(
this PropertyBuilder<TProperty> propertyBuilder)
=> (PropertyBuilder<TProperty>)UseAutoincrement((PropertyBuilder)propertyBuilder);

/// <summary>
/// Configures the value generation strategy for the property when targeting SQLite.
/// </summary>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <param name="strategy">The strategy to use.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
public static IConventionPropertyBuilder? HasValueGenerationStrategy(
this IConventionPropertyBuilder propertyBuilder,
SqliteValueGenerationStrategy? strategy,
bool fromDataAnnotation = false)
{
if (propertyBuilder.CanSetAnnotation(
SqliteAnnotationNames.ValueGenerationStrategy, strategy, fromDataAnnotation))
{
propertyBuilder.Metadata.SetValueGenerationStrategy(strategy, fromDataAnnotation);
return propertyBuilder;
}

return null;
}
/// <summary>
/// Configures the SRID of the column that the property maps to when targeting SQLite.
/// </summary>
Expand Down
82 changes: 82 additions & 0 deletions src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,88 @@ namespace Microsoft.EntityFrameworkCore;
/// </remarks>
public static class SqlitePropertyExtensions
{
/// <summary>
/// Returns the <see cref="SqliteValueGenerationStrategy" /> to use for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The strategy to use for the property.</returns>
public static SqliteValueGenerationStrategy GetValueGenerationStrategy(this IReadOnlyProperty property)
{
var annotation = property[SqliteAnnotationNames.ValueGenerationStrategy];
if (annotation != null)
{
return (SqliteValueGenerationStrategy)annotation;
}

return GetDefaultValueGenerationStrategy(property);
}

/// <summary>
/// Returns the <see cref="SqliteValueGenerationStrategy" /> to use for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="storeObject">The identifier of the store object.</param>
/// <returns>The strategy to use for the property.</returns>
public static SqliteValueGenerationStrategy GetValueGenerationStrategy(
this IReadOnlyProperty property,
in StoreObjectIdentifier storeObject)
{
var annotation = property.FindAnnotation(SqliteAnnotationNames.ValueGenerationStrategy);
if (annotation != null)
{
return (SqliteValueGenerationStrategy)annotation.Value!;
}

var sharedProperty = property.FindSharedStoreObjectRootProperty(storeObject);
return sharedProperty != null
? sharedProperty.GetValueGenerationStrategy(storeObject)
: GetDefaultValueGenerationStrategy(property);
}

private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
{
var primaryKey = property.DeclaringType.ContainingEntityType.FindPrimaryKey();
return primaryKey is { Properties.Count: 1 }
&& primaryKey.Properties[0] == property
&& property.ValueGenerated == ValueGenerated.OnAdd
&& property.ClrType.UnwrapNullableType().IsInteger()
? SqliteValueGenerationStrategy.Autoincrement
: SqliteValueGenerationStrategy.None;
}

/// <summary>
/// Sets the <see cref="SqliteValueGenerationStrategy" /> to use for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The strategy to use.</param>
public static void SetValueGenerationStrategy(
this IMutableProperty property,
SqliteValueGenerationStrategy? value)
=> property.SetOrRemoveAnnotation(SqliteAnnotationNames.ValueGenerationStrategy, value);

/// <summary>
/// Sets the <see cref="SqliteValueGenerationStrategy" /> to use for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The strategy to use.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static SqliteValueGenerationStrategy? SetValueGenerationStrategy(
this IConventionProperty property,
SqliteValueGenerationStrategy? value,
bool fromDataAnnotation = false)
=> (SqliteValueGenerationStrategy?)property.SetOrRemoveAnnotation(
SqliteAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the <see cref="ConfigurationSource" /> for the value generation strategy.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The <see cref="ConfigurationSource" /> for the value generation strategy.</returns>
public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource(this IConventionProperty property)
=> property.FindAnnotation(SqliteAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource();


/// <summary>
/// Returns the SRID to use when creating a column for this property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd<IRelationalTypeMappingSource, SqliteTypeMappingSource>()
.TryAdd<ISqlGenerationHelper, SqliteSqlGenerationHelper>()
.TryAdd<IRelationalAnnotationProvider, SqliteAnnotationProvider>()
.TryAdd<IMigrationsAnnotationProvider, SqliteMigrationsAnnotationProvider>()
.TryAdd<IModelValidator, SqliteModelValidator>()
.TryAdd<IProviderConventionSetBuilder, SqliteConventionSetBuilder>()
.TryAdd<IModificationCommandBatchFactory, SqliteModificationCommandBatchFactory>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public override ConventionSet CreateConventionSet()

conventionSet.Replace<SharedTableConvention>(new SqliteSharedTableConvention(Dependencies, RelationalDependencies));
conventionSet.Replace<RuntimeModelConvention>(new SqliteRuntimeModelConvention(Dependencies, RelationalDependencies));
conventionSet.Replace<ValueGenerationConvention>(new SqliteValueGenerationConvention(Dependencies, RelationalDependencies));

return conventionSet;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// ReSharper disable once CheckNamespace

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;

/// <summary>
/// A convention that configures the SQLite value generation strategy for properties.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see>
/// for more information and examples.
/// </remarks>
public class SqliteValueGenerationConvention : ValueGenerationConvention
{
/// <summary>
/// Creates a new instance of <see cref="SqliteValueGenerationConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
/// <param name="relationalDependencies"> Parameter object containing relational dependencies for this convention.</param>
public SqliteValueGenerationConvention(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies)
: base(dependencies)
{
RelationalDependencies = relationalDependencies;
}

/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }

/// <summary>
/// Returns the store value generation strategy to set for the given property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The strategy to set for the property.</returns>
protected override ValueGenerated? GetValueGenerated(IConventionProperty property)
{
var declaringType = property.DeclaringType;

var strategy = GetValueGenerationStrategy(property);
if (strategy == SqliteValueGenerationStrategy.Autoincrement)
{
return ValueGenerated.OnAdd;
}

return base.GetValueGenerated(property);
}

private static SqliteValueGenerationStrategy GetValueGenerationStrategy(IConventionProperty property)
{
var entityType = (IConventionEntityType)property.DeclaringType;
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey is { Properties.Count: 1 }
&& primaryKey.Properties[0] == property
&& property.ClrType.UnwrapNullableType().IsInteger())
{
return SqliteValueGenerationStrategy.Autoincrement;
}

return SqliteValueGenerationStrategy.None;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,12 @@ public static class SqliteAnnotationNames
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public const string UseSqlReturningClause = Prefix + "UseSqlReturningClause";

/// <summary>
/// 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.
/// </summary>
public const string ValueGenerationStrategy = Prefix + "ValueGenerationStrategy";
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,9 @@ public override IEnumerable<IAnnotation> For(IColumn column, bool designTime)

// Model validation ensures that these facets are the same on all mapped properties
var property = column.PropertyMappings.First().Property;
// Only return auto increment for integer single column primary key
var primaryKey = property.DeclaringType.ContainingEntityType.FindPrimaryKey();
if (primaryKey is { Properties.Count: 1 }
&& primaryKey.Properties[0] == property
&& property.ValueGenerated == ValueGenerated.OnAdd
&& property.ClrType.UnwrapNullableType().IsInteger()
&& !HasConverter(property))

// Use the strategy-based approach to determine AUTOINCREMENT
if (property.GetValueGenerationStrategy() == SqliteValueGenerationStrategy.Autoincrement)
{
yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
}
Expand All @@ -82,7 +78,4 @@ public override IEnumerable<IAnnotation> For(IColumn column, bool designTime)
yield return new Annotation(SqliteAnnotationNames.Srid, srid);
}
}

private static bool HasConverter(IProperty property)
=> property.FindTypeMapping()?.Converter != null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// ReSharper disable once CheckNamespace

namespace Microsoft.EntityFrameworkCore.Metadata;

/// <summary>
/// Defines strategies to use across the EF Core stack when generating key values
/// from SQLite database columns.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see>
/// for more information and examples.
/// </remarks>
public enum SqliteValueGenerationStrategy
{
/// <summary>
/// No SQLite-specific strategy
/// </summary>
None,

/// <summary>
/// A pattern that uses SQLite's AUTOINCREMENT feature to generate values for new entities.
/// </summary>
/// <remarks>
/// AUTOINCREMENT can only be used on integer primary key columns in SQLite.
/// </remarks>
Autoincrement
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal;

/// <summary>
/// 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.
/// </summary>
public class SqliteMigrationsAnnotationProvider : MigrationsAnnotationProvider
{
/// <summary>
/// Initializes a new instance of this class.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this service.</param>
#pragma warning disable EF1001 // Internal EF Core API usage.
public SqliteMigrationsAnnotationProvider(MigrationsAnnotationProviderDependencies dependencies)
#pragma warning restore EF1001 // Internal EF Core API usage.
: base(dependencies)
{
}

/// <inheritdoc />
public override IEnumerable<IAnnotation> ForRemove(IColumn column)
{
// Preserve the autoincrement annotation when removing columns for SQLite migrations
if (column[SqliteAnnotationNames.Autoincrement] as bool? == true)
{
yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
}
}

/// <inheritdoc />
public override IEnumerable<IAnnotation> ForRename(IColumn column)
{
// Preserve the autoincrement annotation when renaming columns for SQLite migrations
if (column[SqliteAnnotationNames.Autoincrement] as bool? == true)
{
yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
}
}
}
Loading
Loading