Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -807,7 +807,6 @@ internal static SqlServerValueGenerationStrategy GetValueGenerationStrategy(
private static SqlServerValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
{
var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy();

if (modelStrategy is SqlServerValueGenerationStrategy.SequenceHiLo or SqlServerValueGenerationStrategy.Sequence
&& IsCompatibleWithValueGeneration(property))
{
Expand All @@ -826,7 +825,6 @@ private static SqlServerValueGenerationStrategy GetDefaultValueGenerationStrateg
ITypeMappingSource? typeMappingSource)
{
var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy();

if (modelStrategy is SqlServerValueGenerationStrategy.SequenceHiLo or SqlServerValueGenerationStrategy.Sequence
&& IsCompatibleWithValueGeneration(property, storeObject, typeMappingSource))
{
Expand Down Expand Up @@ -979,9 +977,9 @@ private static bool IsCompatibleWithValueGeneration(

var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType();

return (type.IsInteger()
return type.IsInteger()
|| type.IsEnum
|| type == typeof(decimal));
|| type == typeof(decimal);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,27 +96,24 @@ public override void ProcessEntityTypeAnnotationChanged(
/// <returns>The store value generation strategy to set for the given property.</returns>
protected override ValueGenerated? GetValueGenerated(IConventionProperty property)
{
// TODO: move to relational?
if (property.DeclaringType.IsMappedToJson()
var table = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault();
return table.Name != null
? GetValueGenerated(property, table, Dependencies.TypeMappingSource)
: property.DeclaringType.IsMappedToJson()
#pragma warning disable EF1001 // Internal EF Core API usage.
&& property.IsOrdinalKeyProperty()
&& property.IsOrdinalKeyProperty()
#pragma warning restore EF1001 // Internal EF Core API usage.
&& (property.DeclaringType as IReadOnlyEntityType)?.FindOwnership()!.IsUnique == false)
{
return ValueGenerated.OnAdd;
}

var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault();
if (declaringTable.Name == null)
{
return null;
}

// If the first mapping can be value generated then we'll consider all mappings to be value generated
// as this is a client-side configuration and can't be specified per-table.
return GetValueGenerated(property, declaringTable, Dependencies.TypeMappingSource);
&& (property.DeclaringType as IReadOnlyEntityType)?.FindOwnership()!.IsUnique == false
? ValueGenerated.OnAddOrUpdate
: property.GetMappedStoreObjects(StoreObjectType.InsertStoredProcedure).Any()
? GetValueGenerated((IReadOnlyProperty)property)
: null;
}

/// <inheritdoc/>
protected override bool MappingStrategyAllowsValueGeneration(IConventionProperty property, string? mappingStrategy)
=> true;

/// <summary>
/// Returns the store value generation strategy to set for the given property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Sqlite.Design.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 SqliteAnnotationCodeGenerator : AnnotationCodeGenerator
{
#region MethodInfos

private static readonly MethodInfo PropertyUseAutoincrementMethodInfo
= typeof(SqlitePropertyBuilderExtensions).GetRuntimeMethod(
nameof(SqlitePropertyBuilderExtensions.UseAutoincrement), [typeof(PropertyBuilder)])!;

private static readonly MethodInfo ComplexTypePropertyUseAutoincrementMethodInfo
= typeof(SqliteComplexTypePropertyBuilderExtensions).GetRuntimeMethod(
nameof(SqliteComplexTypePropertyBuilderExtensions.UseAutoincrement), [typeof(ComplexTypePropertyBuilder)])!;

#endregion MethodInfos

/// <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 SqliteAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies)
: base(dependencies)
{
}

/// <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 override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IProperty property,
IDictionary<string, IAnnotation> annotations)
{
var fragments = new List<MethodCallCodeFragment>(base.GenerateFluentApiCalls(property, annotations));

if (TryGetAndRemove<SqliteValueGenerationStrategy>(annotations, SqliteAnnotationNames.ValueGenerationStrategy, out var strategy)
&& strategy == SqliteValueGenerationStrategy.Autoincrement)
{
var methodInfo = property.DeclaringType is IComplexType
? ComplexTypePropertyUseAutoincrementMethodInfo
: PropertyUseAutoincrementMethodInfo;
fragments.Add(new MethodCallCodeFragment(methodInfo));
}

return fragments;
}

/// <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>
protected override bool IsHandledByConvention(IProperty property, IAnnotation annotation)
{
if (annotation.Name == SqliteAnnotationNames.ValueGenerationStrategy)
{
return (SqliteValueGenerationStrategy)annotation.Value! == property.GetDefaultValueGenerationStrategy();
}

return base.IsHandledByConvention(property, annotation);
}

private static bool TryGetAndRemove<T>(
IDictionary<string, IAnnotation> annotations,
string annotationName,
[NotNullWhen(true)] out T? annotationValue)
{
if (annotations.TryGetValue(annotationName, out var annotation)
&& annotation.Value != null)
{
annotations.Remove(annotationName);
annotationValue = (T)annotation.Value;
return true;
}

annotationValue = default;
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollec
serviceCollection.AddEntityFrameworkSqlite();
#pragma warning disable EF1001 // Internal EF Core API usage.
new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
.TryAdd<IAnnotationCodeGenerator, SqliteAnnotationCodeGenerator>()
.TryAdd<ICSharpRuntimeAnnotationCodeGenerator, SqliteCSharpRuntimeAnnotationCodeGenerator>()
#pragma warning restore EF1001 // Internal EF Core API usage.
.TryAdd<IDatabaseModelFactory, SqliteDatabaseModelFactory>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Diagnostics;

/// <summary>
/// A <see cref="DiagnosticSource" /> event payload class for events that have
/// conflicting value generation strategies.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</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 ConflictingValueGenerationStrategiesEventData : EventData
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we really need a whole new heavy event + event data here? IIUC this is only for the case where the user misconfigures their model - we usually just do a simple warning (or even just throw), shouldn't we do this here? Is there a case where it makes sense for the user to actually configure a property with conflicting value generation strategies (and suppress this)?

(I can see that SQL Server has this too, but I'm trying to understand why we need it)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is mostly to avoid breaking changes.

{
/// <summary>
/// Constructs the event payload.
/// </summary>
/// <param name="eventDefinition">The event definition.</param>
/// <param name="messageGenerator">A delegate that generates a log message for this event.</param>
/// <param name="sqliteValueGenerationStrategy">The SQLite value generation strategy.</param>
/// <param name="otherValueGenerationStrategy">The other value generation strategy.</param>
/// <param name="property">The property.</param>
public ConflictingValueGenerationStrategiesEventData(
EventDefinitionBase eventDefinition,
Func<EventDefinitionBase, EventData, string> messageGenerator,
SqliteValueGenerationStrategy sqliteValueGenerationStrategy,
string otherValueGenerationStrategy,
IReadOnlyProperty property)
: base(eventDefinition, messageGenerator)
{
SqliteValueGenerationStrategy = sqliteValueGenerationStrategy;
OtherValueGenerationStrategy = otherValueGenerationStrategy;
Property = property;
}

/// <summary>
/// The SQLite value generation strategy.
/// </summary>
public virtual SqliteValueGenerationStrategy SqliteValueGenerationStrategy { get; }

/// <summary>
/// The other value generation strategy.
/// </summary>
public virtual string OtherValueGenerationStrategy { get; }

/// <summary>
/// The property.
/// </summary>
public virtual IReadOnlyProperty Property { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ public class SqliteLoggingDefinitions : RelationalLoggingDefinitions
/// </summary>
public EventDefinitionBase? LogCompositeKeyWithValueGeneration;

/// <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 EventDefinitionBase? LogConflictingValueGenerationStrategies;

/// <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
Expand Down
15 changes: 15 additions & 0 deletions src/EFCore.Sqlite.Core/Diagnostics/SqliteEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ private enum Id
SchemaConfiguredWarning = CoreEventId.ProviderBaseId,
SequenceConfiguredWarning,
CompositeKeyWithValueGeneration,
ConflictingValueGenerationStrategiesWarning,

// Infrastructure events
UnexpectedConnectionTypeWarning = CoreEventId.ProviderBaseId + 100,
Expand Down Expand Up @@ -98,6 +99,20 @@ private static EventId MakeValidationId(Id id)
/// </remarks>
public static readonly EventId CompositeKeyWithValueGeneration = MakeValidationId(Id.CompositeKeyWithValueGeneration);

/// <summary>
/// Both the SqliteValueGenerationStrategy and another value generation configuration have been set on a property.
/// Configuring two strategies is usually unintentional and will likely result in a database error.
/// </summary>
/// <remarks>
/// <para>
/// This event is in the <see cref="DbLoggerCategory.Model.Validation" /> category.
/// </para>
/// <para>
/// This event uses the <see cref="ConflictingValueGenerationStrategiesEventData" /> payload when used with a <see cref="DiagnosticSource" />.
/// </para>
/// </remarks>
public static readonly EventId ConflictingValueGenerationStrategiesWarning = MakeValidationId(Id.ConflictingValueGenerationStrategiesWarning);

private static readonly string InfraPrefix = DbLoggerCategory.Infrastructure.Name + ".";

private static EventId MakeInfraId(Id id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,51 @@ private static string CompositeKeyWithValueGeneration(EventDefinitionBase defini
p.Key.Properties.Format());
}

/// <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 static void ConflictingValueGenerationStrategiesWarning(
this IDiagnosticsLogger<DbLoggerCategory.Model.Validation> diagnostics,
SqliteValueGenerationStrategy sqliteValueGenerationStrategy,
string otherValueGenerationStrategy,
IReadOnlyProperty property)
{
var definition = SqliteResources.LogConflictingValueGenerationStrategies(diagnostics);

if (diagnostics.ShouldLog(definition))
{
definition.Log(
diagnostics, sqliteValueGenerationStrategy.ToString(), otherValueGenerationStrategy,
property.Name, property.DeclaringType.DisplayName());
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = new ConflictingValueGenerationStrategiesEventData(
definition,
ConflictingValueGenerationStrategiesWarning,
sqliteValueGenerationStrategy,
otherValueGenerationStrategy,
property);

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
}
}

private static string ConflictingValueGenerationStrategiesWarning(EventDefinitionBase definition, EventData payload)
{
var d = (EventDefinition<string, string, string, string>)definition;
var p = (ConflictingValueGenerationStrategiesEventData)payload;
return d.GenerateMessage(
p.SqliteValueGenerationStrategy.ToString(),
p.OtherValueGenerationStrategy,
p.Property.Name,
p.Property.DeclaringType.DisplayName());
}

/// <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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ namespace Microsoft.EntityFrameworkCore;
/// </remarks>
public static class SqliteComplexTypePropertyBuilderExtensions
{
/// <summary>
/// Configures the property to use the SQLite AUTOINCREMENT feature to generate values for new entities,
/// when targeting SQLite. This method sets the property's value generation strategy to <see cref="SqliteValueGenerationStrategy.Autoincrement" />.
/// </summary>
/// <remarks>
/// 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 ComplexTypePropertyBuilder UseAutoincrement(this ComplexTypePropertyBuilder propertyBuilder)
{
propertyBuilder.Metadata.SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);

return propertyBuilder;
}

/// <summary>
/// Configures the property to use the SQLite AUTOINCREMENT feature to generate values for new entities,
/// when targeting SQLite. This method sets the property's value generation strategy to <see cref="SqliteValueGenerationStrategy.Autoincrement" />.
/// </summary>
/// <remarks>
/// 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 ComplexTypePropertyBuilder<TProperty> UseAutoincrement<TProperty>(
this ComplexTypePropertyBuilder<TProperty> propertyBuilder)
=> (ComplexTypePropertyBuilder<TProperty>)UseAutoincrement((ComplexTypePropertyBuilder)propertyBuilder);

/// <summary>
/// Configures the SRID of the column that the property maps to when targeting SQLite.
/// </summary>
Expand Down
Loading
Loading