Skip to content

Commit 2a63511

Browse files
committed
Fix to #11502 - Allow to specify constraint name for default values
Adding model builder API to allow specifying explicit constraint name when adding default value (sql) to a column. Also added switch on the model level (opt-in) to switch from system-generated constraints to generating them automatically by name and storing in the model. This is opt-in because we don't want to cause unnecessary migrations. Added de-duplication logic in the finalize model step, in case our generated names happen to clash with ones specified explicitly by user. Model builder APIs are exposed on the SQL Server level, but a lot of piping is in relational so that providers who support named constraints can take advantage of the feature. Also split temporal tables migration tests for SqlServer into a separate file - there were way to many tests in MigrationsSqlServerTest Fixes #11502
1 parent 667c569 commit 2a63511

19 files changed

+10594
-8756
lines changed

src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,15 @@ private void Create(
751751
/// <param name="column">The column to which the annotations are applied.</param>
752752
/// <param name="parameters">Additional parameters used during code generation.</param>
753753
public virtual void Generate(IColumn column, CSharpRuntimeAnnotationCodeGeneratorParameters parameters)
754-
=> GenerateSimpleAnnotations(parameters);
754+
{
755+
if (!parameters.IsRuntime)
756+
{
757+
var annotations = parameters.Annotations;
758+
annotations.Remove(RelationalAnnotationNames.DefaultConstraintName);
759+
}
760+
761+
GenerateSimpleAnnotations(parameters);
762+
}
755763

756764
private void Create(
757765
IViewColumn column,

src/EFCore.Relational/Extensions/RelationalModelExtensions.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,4 +530,48 @@ public static void SetCollation(this IMutableModel model, string? value)
530530
=> model.FindAnnotation(RelationalAnnotationNames.Collation)?.GetConfigurationSource();
531531

532532
#endregion Collation
533+
534+
#region UseNamedDefaultConstraints
535+
536+
/// <summary>
537+
/// Returns the value indicating whether named default constraints should be used.
538+
/// </summary>
539+
/// <param name="model">The model to get the value for.</param>
540+
public static bool? AreNamedDefaultConstraintsUsed(this IReadOnlyModel model)
541+
=> (model is RuntimeModel)
542+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
543+
: (bool?)model[RelationalAnnotationNames.UseNamedDefaultConstraints];
544+
545+
/// <summary>
546+
/// Sets the value indicating whether named default constraints should be used.
547+
/// </summary>
548+
/// <param name="model">The model to get the value for.</param>
549+
/// <param name="value">The value to set.</param>
550+
public static void UseNamedDefaultConstraints(this IMutableModel model, bool value)
551+
=> model.SetOrRemoveAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints, value);
552+
553+
/// <summary>
554+
/// Sets the value indicating whether named default constraints should be used.
555+
/// </summary>
556+
/// <param name="model">The model to get the value for.</param>
557+
/// <param name="value">The value to set.</param>
558+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
559+
public static bool? UseNamedDefaultConstraints(
560+
this IConventionModel model,
561+
bool value,
562+
bool fromDataAnnotation = false)
563+
=> (bool?)model.SetOrRemoveAnnotation(
564+
RelationalAnnotationNames.UseNamedDefaultConstraints,
565+
value,
566+
fromDataAnnotation)?.Value;
567+
568+
/// <summary>
569+
/// Returns the configuration source for the named default constraints setting.
570+
/// </summary>
571+
/// <param name="model">The model to find configuration source for.</param>
572+
/// <returns>The configuration source for the named default constraints setting.</returns>
573+
public static ConfigurationSource? UseNamedDefaultConstraintsConfigurationSource(this IConventionModel model)
574+
=> model.FindAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints)?.GetConfigurationSource();
575+
576+
#endregion
533577
}

src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,4 +2086,60 @@ public static void SetJsonPropertyName(this IMutableProperty property, string? n
20862086
/// <returns>The <see cref="ConfigurationSource" /> for the JSON property name for a given entity property.</returns>
20872087
public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionProperty property)
20882088
=> property.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource();
2089+
2090+
/// <summary>
2091+
/// Gets the default constraint name.
2092+
/// </summary>
2093+
/// <param name="property">The property.</param>
2094+
public static string? GetDefaultConstraintName(this IReadOnlyProperty property)
2095+
=> (property is RuntimeProperty)
2096+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
2097+
: (string?)property[RelationalAnnotationNames.DefaultConstraintName];
2098+
2099+
/// <summary>
2100+
/// Generates the default constraint name based on the table and column name.
2101+
/// </summary>
2102+
/// <param name="property">The property.</param>
2103+
/// <param name="storeObject">The store object identifier to generate the name from.</param>
2104+
public static string GenerateDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
2105+
{
2106+
var candidate = $"DF_{storeObject.Name}_{property.GetColumnName(storeObject)}";
2107+
2108+
return candidate.Length > 120 ? candidate[..120] : candidate;
2109+
}
2110+
2111+
/// <summary>
2112+
/// Sets the default constraint name.
2113+
/// </summary>
2114+
/// <param name="property">The property.</param>
2115+
/// <param name="defaultConstraintName">The name to be used.</param>
2116+
public static void SetDefaultConstraintName(this IMutableProperty property, string? defaultConstraintName)
2117+
=> property.SetAnnotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName);
2118+
2119+
/// <summary>
2120+
/// Sets the default constraint name.
2121+
/// </summary>
2122+
/// <param name="property">The property.</param>
2123+
/// <param name="defaultConstraintName">The name to be used.</param>
2124+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
2125+
public static string? SetDefaultConstraintName(
2126+
this IConventionProperty property,
2127+
string? defaultConstraintName,
2128+
bool fromDataAnnotation = false)
2129+
{
2130+
property.SetAnnotation(
2131+
RelationalAnnotationNames.DefaultConstraintName,
2132+
defaultConstraintName,
2133+
fromDataAnnotation);
2134+
2135+
return defaultConstraintName;
2136+
}
2137+
2138+
/// <summary>
2139+
/// Returns the configuration source for the default constraint name.
2140+
/// </summary>
2141+
/// <param name="property">The property.</param>
2142+
/// <returns>The configuration source for the default constraint name.</returns>
2143+
public static ConfigurationSource? GetDefaultConstraintNameConfigurationSource(this IConventionProperty property)
2144+
=> property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName)?.GetConfigurationSource();
20892145
}

src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ public override ConventionSet CreateConventionSet()
8989
conventionSet.Replace<RuntimeModelConvention>(
9090
new RelationalRuntimeModelConvention(Dependencies, RelationalDependencies));
9191

92+
var defaultConstraintConvention = new RelationalDefaultConstraintConvention(Dependencies, RelationalDependencies);
93+
ConventionSet.AddAfter(
94+
conventionSet.ModelFinalizingConventions,
95+
defaultConstraintConvention,
96+
typeof(SharedTableConvention));
97+
9298
return conventionSet;
9399
}
94100
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
5+
6+
/// <summary>
7+
/// A convention that manipulates names of default constraints to avoid clashes.
8+
/// </summary>
9+
/// <remarks>
10+
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
11+
/// </remarks>
12+
public class RelationalDefaultConstraintConvention : IModelFinalizingConvention
13+
{
14+
/// <summary>
15+
/// Creates a new instance of <see cref="RelationalDefaultConstraintConvention" />.
16+
/// </summary>
17+
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
18+
/// <param name="relationalDependencies">Parameter object containing relational dependencies for this convention.</param>
19+
public RelationalDefaultConstraintConvention(
20+
ProviderConventionSetBuilderDependencies dependencies,
21+
RelationalConventionSetBuilderDependencies relationalDependencies)
22+
{
23+
Dependencies = dependencies;
24+
RelationalDependencies = relationalDependencies;
25+
}
26+
27+
/// <summary>
28+
/// Dependencies for this service.
29+
/// </summary>
30+
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }
31+
32+
/// <summary>
33+
/// Relational provider-specific dependencies for this service.
34+
/// </summary>
35+
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }
36+
37+
/// <inheritdoc />
38+
public void ProcessModelFinalizing(
39+
IConventionModelBuilder modelBuilder,
40+
IConventionContext<IConventionModelBuilder> context)
41+
{
42+
var explicitDefaultConstraintNames = new List<string>();
43+
44+
// store all explicit names first - we don't want to change those in case they conflict with implicit names
45+
foreach (var entity in modelBuilder.Metadata.GetEntityTypes())
46+
{
47+
foreach (var property in entity.GetDeclaredProperties())
48+
{
49+
if (property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName) is IConventionAnnotation annotation
50+
&& annotation.Value is string explicitDefaultConstraintName
51+
&& annotation.GetConfigurationSource() == ConfigurationSource.Explicit)
52+
{
53+
if (property.GetMappedStoreObjects(StoreObjectType.Table).Count() > 1)
54+
{
55+
// for TPC and some entity splitting scenarios (specifically composite key) we end up with multiple tables
56+
// having to define the constraint. Since constraint has to be unique, we can't keep the same name for all
57+
// Disabling this scenario until we have better place to configure the constraint name
58+
// see issue #27970
59+
throw new InvalidOperationException(
60+
RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc(explicitDefaultConstraintName));
61+
}
62+
63+
explicitDefaultConstraintNames.Add(explicitDefaultConstraintName);
64+
}
65+
}
66+
}
67+
68+
var existingDefaultConstraintNames = new List<string>(explicitDefaultConstraintNames);
69+
var useNamedDefaultConstraints = modelBuilder.Metadata.AreNamedDefaultConstraintsUsed() == true;
70+
71+
var suffixCounter = 1;
72+
foreach (var entity in modelBuilder.Metadata.GetEntityTypes())
73+
{
74+
foreach (var property in entity.GetDeclaredProperties())
75+
{
76+
if (property.FindAnnotation(RelationalAnnotationNames.DefaultValue) is IConventionAnnotation defaultValueAnnotation
77+
|| property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql) is IConventionAnnotation defaultValueSqlAnnotation)
78+
{
79+
var defaultConstraintNameAnnotation = property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName);
80+
if (defaultConstraintNameAnnotation != null && defaultConstraintNameAnnotation.GetConfigurationSource() != ConfigurationSource.Convention)
81+
{
82+
// explicit constraint name - we already stored those so nothing to do here
83+
continue;
84+
}
85+
86+
if (useNamedDefaultConstraints)
87+
{
88+
var mappedTables = property.GetMappedStoreObjects(StoreObjectType.Table);
89+
var mappedTablesCount = mappedTables.Count();
90+
91+
if (mappedTablesCount == 0)
92+
{
93+
continue;
94+
}
95+
96+
if (mappedTablesCount == 1)
97+
{
98+
var constraintNameCandidate = property.GenerateDefaultConstraintName(mappedTables.First());
99+
if (!existingDefaultConstraintNames.Contains(constraintNameCandidate))
100+
{
101+
// name that we generate is unique - add it to the list of names but we don't need to store it as annotation
102+
existingDefaultConstraintNames.Add(constraintNameCandidate);
103+
}
104+
else
105+
{
106+
// conflict - generate name that is unique and store is as annotation
107+
while (existingDefaultConstraintNames.Contains(constraintNameCandidate + suffixCounter))
108+
{
109+
suffixCounter++;
110+
}
111+
112+
existingDefaultConstraintNames.Add(constraintNameCandidate + suffixCounter);
113+
property.SetDefaultConstraintName(constraintNameCandidate + suffixCounter);
114+
}
115+
116+
continue;
117+
}
118+
119+
// TPC or entity splitting - when column is mapped to multiple tables, we can deal with them
120+
// as long as there are no name clashes with some other constraints
121+
// by the time we actually need to generate the constraint name (to put it in the annotation for the migration op)
122+
// we will know which store object the property we are processing is mapped to, so can pick the right name based on that
123+
// here though, where we want to uniquefy the name duplicates, we work on the model level so can't pick the right de-duped name
124+
// so in case of conflict, we have to throw
125+
// see issue #27970
126+
var constraintNameCandidates = new List<string>();
127+
foreach (var mappedTable in mappedTables)
128+
{
129+
var constraintNameCandidate = property.GenerateDefaultConstraintName(mappedTable);
130+
if (constraintNameCandidate != null)
131+
{
132+
if (!existingDefaultConstraintNames.Contains(constraintNameCandidate))
133+
{
134+
constraintNameCandidates.Add(constraintNameCandidate);
135+
}
136+
else
137+
{
138+
throw new InvalidOperationException(
139+
RelationalStrings.ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(constraintNameCandidate));
140+
}
141+
}
142+
}
143+
144+
existingDefaultConstraintNames.AddRange(constraintNameCandidates);
145+
}
146+
}
147+
}
148+
}
149+
}
150+
}

src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ public static class RelationalAnnotationNames
5252
/// </summary>
5353
public const string DefaultValue = Prefix + "DefaultValue";
5454

55+
/// <summary>
56+
/// The name for default constraint annotations.
57+
/// </summary>
58+
public const string DefaultConstraintName = Prefix + "DefaultConstraintName";
59+
60+
/// <summary>
61+
/// The name for using named default constraints annotations.
62+
/// </summary>
63+
public const string UseNamedDefaultConstraints = Prefix + "UseNamedDefaultConstraints";
64+
5565
/// <summary>
5666
/// The name for table name annotations.
5767
/// </summary>
@@ -360,6 +370,8 @@ public static class RelationalAnnotationNames
360370
ComputedColumnSql,
361371
IsStored,
362372
DefaultValue,
373+
DefaultConstraintName,
374+
UseNamedDefaultConstraints,
363375
TableName,
364376
Schema,
365377
ViewName,

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@
427427
<data name="ExecuteUpdateSubqueryNotSupportedOverComplexTypes" xml:space="preserve">
428428
<value>ExecuteUpdate is being used over a LINQ operator which isn't natively supported by the database; this cannot be translated because complex type '{complexType}' is projected out. Rewrite your query to project out the containing entity type instead.</value>
429429
</data>
430+
<data name="ExplicitDefaultConstraintNamesNotSupportedForTpc" xml:space="preserve">
431+
<value>Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'.</value>
432+
</data>
430433
<data name="FromSqlMissingColumn" xml:space="preserve">
431434
<value>The required column '{column}' was not present in the results of a 'FromSql' operation.</value>
432435
</data>
@@ -439,6 +442,9 @@
439442
<data name="HasDataNotSupportedForEntitiesMappedToJson" xml:space="preserve">
440443
<value>Can't use HasData for entity type '{entity}'. HasData is not supported for entities mapped to JSON.</value>
441444
</data>
445+
<data name="ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash" xml:space="preserve">
446+
<value>Named default constraints can't be used with TPC or entity splitting if they result in non-unique constraint name. Constraint name: '{constraintNameCandidate}'.</value>
447+
</data>
442448
<data name="IncompatibleTableCommentMismatch" xml:space="preserve">
443449
<value>Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the comment '{comment}' does not match the comment '{otherComment}'.</value>
444450
</data>

0 commit comments

Comments
 (0)