Skip to content

Commit

Permalink
Sugar for sequence-based key value generation on SQL Server
Browse files Browse the repository at this point in the history
Part of #28096

This PR adds the new key generation strategy. A second PR will change what happens by convention on SQL Server.
  • Loading branch information
ajcvickers committed Jul 19, 2022
1 parent 678c0a7 commit 90e9e73
Show file tree
Hide file tree
Showing 51 changed files with 2,250 additions and 835 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2944,8 +2944,8 @@ public static void TpcStoreGeneratedIdentityWarning(
{
definition.Log(
diagnostics,
property.Name,
property.DeclaringEntityType.DisplayName());
property.DeclaringEntityType.DisplayName(),
property.Name);
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
Expand All @@ -2964,8 +2964,8 @@ private static string TpcStoreGeneratedIdentity(EventDefinitionBase definition,
var d = (EventDefinition<string, string>)definition;
var p = (PropertyEventData)payload;
return d.GenerateMessage(
p.Property.Name,
p.Property.DeclaringEntityType.DisplayName());
p.Property.DeclaringEntityType.DisplayName(),
p.Property.Name);
}

/// <summary>
Expand Down
41 changes: 30 additions & 11 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,11 @@ protected override void ValidateInheritanceMapping(
throw new InvalidOperationException(
RelationalStrings.AbstractTpc(entityType.DisplayName(), storeObject));
}

foreach (var key in entityType.GetKeys())
{
ValidateValueGenerationForMappingStrategy(entityType, key, mappingStrategy, logger);
}
}

if (entityType.BaseType != null)
Expand Down Expand Up @@ -1349,17 +1354,8 @@ protected override void ValidateInheritanceMapping(
}
else
{
var primaryKey = entityType.FindPrimaryKey();
if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy)
{
var storeGeneratedProperty = primaryKey?.Properties.FirstOrDefault(p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0);
if (storeGeneratedProperty != null
&& entityType.GetTableName() != null)
{
logger.TpcStoreGeneratedIdentityWarning(storeGeneratedProperty);
}
}
else if (primaryKey == null)
if (mappingStrategy != RelationalAnnotationNames.TpcMappingStrategy
&& entityType.FindPrimaryKey() == null)
{
throw new InvalidOperationException(
RelationalStrings.KeylessMappingStrategy(mappingStrategy ?? RelationalAnnotationNames.TptMappingStrategy, entityType.DisplayName()));
Expand Down Expand Up @@ -1395,6 +1391,29 @@ protected override void ValidateInheritanceMapping(
}
}

/// <summary>
/// Validates the key value generation is valid for the given inheritance mapping strategy.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="key">The key.</param>
/// <param name="mappingStrategy">The inheritance mapping strategy.</param>
/// <param name="logger">The logger to use.</param>
protected virtual void ValidateValueGenerationForMappingStrategy(
IEntityType entityType,
IKey key,
string mappingStrategy,
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
{
if (entityType.GetTableName() != null
&& mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy)
{
foreach (var storeGeneratedProperty in key.Properties.Where(p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0))
{
logger.TpcStoreGeneratedIdentityWarning(storeGeneratedProperty);
}
}
}

/// <summary>
/// Validates that the given mapping strategy is supported
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,21 @@ public sealed record RelationalConventionSetBuilderDependencies
/// the constructor at any point in this process.
/// </remarks>
[EntityFrameworkInternal]
public RelationalConventionSetBuilderDependencies(IRelationalAnnotationProvider relationalAnnotationProvider)
public RelationalConventionSetBuilderDependencies(
IRelationalAnnotationProvider relationalAnnotationProvider,
IUpdateSqlGenerator updateSqlGenerator)
{
RelationalAnnotationProvider = relationalAnnotationProvider;
UpdateSqlGenerator = updateSqlGenerator;
}

/// <summary>
/// The relational annotation provider.
/// </summary>
public IRelationalAnnotationProvider RelationalAnnotationProvider { get; init; }

/// <summary>
/// For generation of SQL.
/// </summary>
public IUpdateSqlGenerator UpdateSqlGenerator { get; init; }
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@
<comment>Debug RelationalEventId.TransactionRollingBack</comment>
</data>
<data name="LogTpcStoreGeneratedIdentity" xml:space="preserve">
<value>The property '{property}' on entity type '{entityType}' is configured with a database-generated default, however the entity type is mapped to the database using table per concrete class strategy. Make sure that the generated values are unique across all the tables, duplicated values could result in errors or data corruption.</value>
<value>The entity type '{entityType}' is using the table per concrete type mapping strategy, but property '{property}' is configured with an incompatible database-generated default. Configure a compatible value generation strategy if available, or use non-generated key values.</value>
<comment>Warning RelationalEventId.TpcStoreGeneratedIdentityWarning string string</comment>
</data>
<data name="LogTransactionError" xml:space="preserve">
Expand Down
24 changes: 22 additions & 2 deletions src/EFCore.Relational/Update/IUpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ namespace Microsoft.EntityFrameworkCore.Update;
public interface IUpdateSqlGenerator
{
/// <summary>
/// Generates SQL that will obtain the next value in the given sequence.
/// Generates SQL that will query for the next value in the given sequence.
/// </summary>
/// <param name="name">The name of the sequence.</param>
/// <param name="schema">The schema that contains the sequence, or <see langword="null" /> to use the default schema.</param>
/// <returns>The SQL.</returns>
string GenerateNextSequenceValueOperation(string name, string? schema);

/// <summary>
/// Generates a SQL fragment that will get the next value from the given sequence and appends it to
/// Generates a SQL fragment that will query for the next value from the given sequence and appends it to
/// the full command being built by the given <see cref="StringBuilder" />.
/// </summary>
/// <param name="commandStringBuilder">The builder to which the SQL fragment should be appended.</param>
Expand All @@ -47,6 +47,26 @@ void AppendNextSequenceValueOperation(
string name,
string? schema);

/// <summary>
/// Generates SQL that will obtain the next value in the given sequence.
/// </summary>
/// <param name="name">The name of the sequence.</param>
/// <param name="schema">The schema that contains the sequence, or <see langword="null" /> to use the default schema.</param>
/// <returns>The SQL.</returns>
string GenerateObtainNextSequenceValueOperation(string name, string? schema);

/// <summary>
/// Generates a SQL fragment that will get the next value from the given sequence and appends it to
/// the full command being built by the given <see cref="StringBuilder" />.
/// </summary>
/// <param name="commandStringBuilder">The builder to which the SQL fragment should be appended.</param>
/// <param name="name">The name of the sequence.</param>
/// <param name="schema">The schema that contains the sequence, or <see langword="null" /> to use the default schema.</param>
void AppendObtainNextSequenceValueOperation(
StringBuilder commandStringBuilder,
string name,
string? schema);

/// <summary>
/// Appends a SQL fragment for the start of a batch to
/// the full command being built by the given <see cref="StringBuilder" />.
Expand Down
32 changes: 18 additions & 14 deletions src/EFCore.Relational/Update/UpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -490,29 +490,33 @@ public virtual void PrependEnsureAutocommit(StringBuilder commandStringBuilder)
{
}

/// <summary>
/// Generates SQL that will obtain the next value in the given sequence.
/// </summary>
/// <param name="name">The name of the sequence.</param>
/// <param name="schema">The schema that contains the sequence, or <see langword="null" /> to use the default schema.</param>
/// <returns>The SQL.</returns>
/// <inheritdoc />
public virtual string GenerateNextSequenceValueOperation(string name, string? schema)
{
var commandStringBuilder = new StringBuilder();
AppendNextSequenceValueOperation(commandStringBuilder, name, schema);
return commandStringBuilder.ToString();
}

/// <summary>
/// Generates a SQL fragment that will get the next value from the given sequence and appends it to
/// the full command being built by the given <see cref="StringBuilder" />.
/// </summary>
/// <param name="commandStringBuilder">The builder to which the SQL fragment should be appended.</param>
/// <param name="name">The name of the sequence.</param>
/// <param name="schema">The schema that contains the sequence, or <see langword="null" /> to use the default schema.</param>
/// <inheritdoc />
public virtual void AppendNextSequenceValueOperation(StringBuilder commandStringBuilder, string name, string? schema)
{
commandStringBuilder.Append("SELECT NEXT VALUE FOR ");
commandStringBuilder.Append("SELECT ");
AppendObtainNextSequenceValueOperation(commandStringBuilder, name, schema);
}

/// <inheritdoc />
public virtual string GenerateObtainNextSequenceValueOperation(string name, string? schema)
{
var commandStringBuilder = new StringBuilder();
AppendObtainNextSequenceValueOperation(commandStringBuilder, name, schema);
return commandStringBuilder.ToString();
}

/// <inheritdoc />
public virtual void AppendObtainNextSequenceValueOperation(StringBuilder commandStringBuilder, string name, string? schema)
{
commandStringBuilder.Append("NEXT VALUE FOR ");
SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, name, schema);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ private static readonly MethodInfo ModelUseHiLoMethodInfo
= typeof(SqlServerModelBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerModelBuilderExtensions.UseHiLo), new[] { typeof(ModelBuilder), typeof(string), typeof(string) })!;

private static readonly MethodInfo ModelUseKeySequenceMethodInfo
= typeof(SqlServerModelBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerModelBuilderExtensions.UseKeySequence), new[] { typeof(ModelBuilder), typeof(string), typeof(string) })!;

private static readonly MethodInfo ModelHasDatabaseMaxSizeMethodInfo
= typeof(SqlServerModelBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerModelBuilderExtensions.HasDatabaseMaxSize), new[] { typeof(ModelBuilder), typeof(string) })!;
Expand Down Expand Up @@ -59,6 +63,10 @@ private static readonly MethodInfo PropertyUseHiLoMethodInfo
= typeof(SqlServerPropertyBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerPropertyBuilderExtensions.UseHiLo), new[] { typeof(PropertyBuilder), typeof(string), typeof(string) })!;

private static readonly MethodInfo PropertyUseKeySequenceMethodInfo
= typeof(SqlServerPropertyBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerPropertyBuilderExtensions.UseKeySequence), new[] { typeof(PropertyBuilder), typeof(string), typeof(string) })!;

private static readonly MethodInfo IndexIsClusteredMethodInfo
= typeof(SqlServerIndexBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerIndexBuilderExtensions.IsClustered), new[] { typeof(IndexBuilder), typeof(bool) })!;
Expand Down Expand Up @@ -378,6 +386,7 @@ protected override bool IsHandledByConvention(IModel model, IAnnotation annotati
});

case SqlServerValueGenerationStrategy.SequenceHiLo:
{
var name = GetAndRemove<string>(annotations, SqlServerAnnotationNames.HiLoSequenceName);
var schema = GetAndRemove<string>(annotations, SqlServerAnnotationNames.HiLoSequenceSchema);
return new MethodCallCodeFragment(
Expand All @@ -388,6 +397,21 @@ protected override bool IsHandledByConvention(IModel model, IAnnotation annotati
(_, null) => new object[] { name },
_ => new object[] { name!, schema }
});
}

case SqlServerValueGenerationStrategy.Sequence:
{
var name = GetAndRemove<string>(annotations, SqlServerAnnotationNames.KeySequenceName);
var schema = GetAndRemove<string>(annotations, SqlServerAnnotationNames.KeySequenceSchema);
return new MethodCallCodeFragment(
onModel ? ModelUseKeySequenceMethodInfo : PropertyUseKeySequenceMethodInfo,
(name, schema) switch
{
(null, null) => Array.Empty<object>(),
(_, null) => new object[] { name },
_ => new object[] { name!, schema }
});
}

case SqlServerValueGenerationStrategy.None:
return new MethodCallCodeFragment(
Expand Down
Loading

0 comments on commit 90e9e73

Please sign in to comment.