Skip to content

Fix to #11502 - Allow to specify constraint name for default values #36067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -751,7 +751,15 @@ private void Create(
/// <param name="column">The column to which the annotations are applied.</param>
/// <param name="parameters">Additional parameters used during code generation.</param>
public virtual void Generate(IColumn column, CSharpRuntimeAnnotationCodeGeneratorParameters parameters)
=> GenerateSimpleAnnotations(parameters);
{
if (!parameters.IsRuntime)
{
var annotations = parameters.Annotations;
annotations.Remove(RelationalAnnotationNames.DefaultConstraintName);
}

GenerateSimpleAnnotations(parameters);
}

private void Create(
IViewColumn column,
Expand Down
44 changes: 44 additions & 0 deletions src/EFCore.Relational/Extensions/RelationalModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,4 +530,48 @@ public static void SetCollation(this IMutableModel model, string? value)
=> model.FindAnnotation(RelationalAnnotationNames.Collation)?.GetConfigurationSource();

#endregion Collation

#region UseNamedDefaultConstraints

/// <summary>
/// Returns the value indicating whether named default constraints should be used.
/// </summary>
/// <param name="model">The model to get the value for.</param>
public static bool AreNamedDefaultConstraintsUsed(this IReadOnlyModel model)
=> (model is RuntimeModel)
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
: (bool?)model[RelationalAnnotationNames.UseNamedDefaultConstraints] ?? false;

/// <summary>
/// Sets the value indicating whether named default constraints should be used.
/// </summary>
/// <param name="model">The model to get the value for.</param>
/// <param name="value">The value to set.</param>
public static void UseNamedDefaultConstraints(this IMutableModel model, bool value)
=> model.SetOrRemoveAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints, value);

/// <summary>
/// Sets the value indicating whether named default constraints should be used.
/// </summary>
/// <param name="model">The model to get the value for.</param>
/// <param name="value">The value to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
public static bool? UseNamedDefaultConstraints(
this IConventionModel model,
bool? value,
bool fromDataAnnotation = false)
=> (bool?)model.SetOrRemoveAnnotation(
RelationalAnnotationNames.UseNamedDefaultConstraints,
value,
fromDataAnnotation)?.Value;

/// <summary>
/// Returns the configuration source for the named default constraints setting.
/// </summary>
/// <param name="model">The model to find configuration source for.</param>
/// <returns>The configuration source for the named default constraints setting.</returns>
public static ConfigurationSource? UseNamedDefaultConstraintsConfigurationSource(this IConventionModel model)
=> model.FindAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints)?.GetConfigurationSource();

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -2086,4 +2086,82 @@ public static void SetJsonPropertyName(this IMutableProperty property, string? n
/// <returns>The <see cref="ConfigurationSource" /> for the JSON property name for a given entity property.</returns>
public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionProperty property)
=> property.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource();

/// <summary>
/// Gets the default constraint name.
/// </summary>
/// <param name="property">The property.</param>
public static string? GetDefaultConstraintName(this IReadOnlyProperty property)
=> property is RuntimeProperty
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
: (string?)property[RelationalAnnotationNames.DefaultConstraintName]
?? (ShouldHaveDefaultConstraintName(property)
&& StoreObjectIdentifier.Create(property.DeclaringType, StoreObjectType.Table) is StoreObjectIdentifier table
? property.GenerateDefaultConstraintName(table)
: null);

/// <summary>
/// Gets the default constraint name.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="storeObject">The store object identifier to generate the name for.</param>
public static string? GetDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
=> property is RuntimeProperty
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
: (string?)property[RelationalAnnotationNames.DefaultConstraintName]
?? (ShouldHaveDefaultConstraintName(property)
? property.GenerateDefaultConstraintName(storeObject)
: null);

private static bool ShouldHaveDefaultConstraintName(IReadOnlyProperty property)
=> property.DeclaringType.Model.AreNamedDefaultConstraintsUsed()
&& (property[RelationalAnnotationNames.DefaultValue] is not null
|| property[RelationalAnnotationNames.DefaultValueSql] is not null);

/// <summary>
/// Generates the default constraint name based on the table and column name.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="storeObject">The store object identifier to generate the name for.</param>
public static string GenerateDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
{
var candidate = $"DF_{storeObject.Name}_{property.GetColumnName(storeObject)}";

return Uniquifier.Truncate(candidate, property.DeclaringType.Model.GetMaxIdentifierLength());
}

/// <summary>
/// Sets the default constraint name.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="defaultConstraintName">The name to be used.</param>
public static void SetDefaultConstraintName(this IMutableProperty property, string? defaultConstraintName)
=> property.SetAnnotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName);

/// <summary>
/// Sets the default constraint name.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="defaultConstraintName">The name to be used.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
public static string? SetDefaultConstraintName(
this IConventionProperty property,
string? defaultConstraintName,
bool fromDataAnnotation = false)
{
property.SetAnnotation(
RelationalAnnotationNames.DefaultConstraintName,
defaultConstraintName,
fromDataAnnotation);

return defaultConstraintName;
}

/// <summary>
/// Returns the configuration source for the default constraint name.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The configuration source for the default constraint name.</returns>
public static ConfigurationSource? GetDefaultConstraintNameConfigurationSource(this IConventionProperty property)
=> property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName)?.GetConfigurationSource();
}
117 changes: 115 additions & 2 deletions src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public virtual void ProcessModelFinalizing(
var foreignKeys = new Dictionary<string, (IConventionForeignKey, StoreObjectIdentifier)>();
var indexes = new Dictionary<string, (IConventionIndex, StoreObjectIdentifier)>();
var checkConstraints = new Dictionary<(string, string?), (IConventionCheckConstraint, StoreObjectIdentifier)>();
var defaultConstraints = new Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)>();
var triggers = new Dictionary<string, (IConventionTrigger, StoreObjectIdentifier)>();
foreach (var ((tableName, schema), conventionEntityTypes) in tables)
{
Expand All @@ -76,6 +77,11 @@ public virtual void ProcessModelFinalizing(
checkConstraints.Clear();
}

if (!DefaultConstraintsUniqueAcrossTables)
{
defaultConstraints.Clear();
}

if (!TriggersUniqueAcrossTables)
{
triggers.Clear();
Expand All @@ -89,6 +95,7 @@ public virtual void ProcessModelFinalizing(
UniquifyForeignKeyNames(entityType, foreignKeys, storeObject, maxLength);
UniquifyIndexNames(entityType, indexes, storeObject, maxLength);
UniquifyCheckConstraintNames(entityType, checkConstraints, storeObject, maxLength);
UniquifyDefaultConstraintNames(entityType, defaultConstraints, storeObject, maxLength);
UniquifyTriggerNames(entityType, triggers, storeObject, maxLength);
}
}
Expand Down Expand Up @@ -124,14 +131,19 @@ protected virtual bool CheckConstraintsUniqueAcrossTables
protected virtual bool TriggersUniqueAcrossTables
=> true;

/// <summary>
/// Gets a value indicating whether default constraint names should be unique across tables.
/// </summary>
protected virtual bool DefaultConstraintsUniqueAcrossTables
=> false;

private static void TryUniquifyTableNames(
IConventionModel model,
Dictionary<(string Name, string? Schema), List<IConventionEntityType>> tables,
int maxLength)
{
Dictionary<(string Name, string? Schema), Dictionary<(string Name, string? Schema), List<IConventionEntityType>>>?
clashingTables
= null;
clashingTables = null;
foreach (var entityType in model.GetEntityTypes())
{
var tableName = entityType.GetTableName();
Expand Down Expand Up @@ -646,6 +658,107 @@ protected virtual bool AreCompatible(
return null;
}

private void UniquifyDefaultConstraintNames(
IConventionEntityType entityType,
Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)> defaultConstraints,
in StoreObjectIdentifier storeObject,
int maxLength)
{
foreach (var property in entityType.GetProperties())
{
var constraintName = property.GetDefaultConstraintName(storeObject);
if (constraintName == null)
{
continue;
}

var columnName = property.GetColumnName(storeObject);
if (columnName == null)
{
continue;
}

if (!defaultConstraints.TryGetValue((constraintName, storeObject.Schema), out var otherPropertyPair))
{
defaultConstraints[(constraintName, storeObject.Schema)] = (property, storeObject);
continue;
}

var (otherProperty, otherStoreObject) = otherPropertyPair;
if (storeObject == otherStoreObject
&& columnName == otherProperty.GetColumnName(storeObject)
&& AreCompatibleDefaultConstraints(property, otherProperty, storeObject))
{
continue;
}

var newConstraintName = TryUniquifyDefaultConstraint(property, constraintName, storeObject.Schema, defaultConstraints, storeObject, maxLength);
if (newConstraintName != null)
{
defaultConstraints[(newConstraintName, storeObject.Schema)] = (property, storeObject);
continue;
}

var newOtherConstraintName = TryUniquifyDefaultConstraint(otherProperty, constraintName, storeObject.Schema, defaultConstraints, otherStoreObject, maxLength);
if (newOtherConstraintName != null)
{
defaultConstraints[(constraintName, storeObject.Schema)] = (property, storeObject);
defaultConstraints[(newOtherConstraintName, otherStoreObject.Schema)] = otherPropertyPair;
}
}
}

/// <summary>
/// Gets a value indicating whether two default constraints with the same name are compatible.
/// </summary>
/// <param name="property">A property with a default constraint.</param>
/// <param name="duplicateProperty">Another property with a default constraint.</param>
/// <param name="storeObject">The identifier of the store object.</param>
/// <returns><see langword="true" /> if compatible</returns>
protected virtual bool AreCompatibleDefaultConstraints(
IReadOnlyProperty property,
IReadOnlyProperty duplicateProperty,
in StoreObjectIdentifier storeObject)
=> property.GetDefaultValue(storeObject) == duplicateProperty.GetDefaultValue(storeObject)
&& property.GetDefaultValueSql(storeObject) == duplicateProperty.GetDefaultValueSql(storeObject);

private static string? TryUniquifyDefaultConstraint(
IConventionProperty property,
string constraintName,
string? schema,
Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)> defaultConstraints,
in StoreObjectIdentifier storeObject,
int maxLength)
{
var mappedTables = property.GetMappedStoreObjects(StoreObjectType.Table);
if (mappedTables.Count() > 1)
{
// For TPC and some entity splitting scenarios we end up with multiple tables having to define the constraint.
// Since constraint name has to be unique, we can't keep the same name for all
// Disabling this scenario until we have better way to configure the constraint name
// see issue #27970
if (property.GetDefaultConstraintNameConfigurationSource() == null)
{
throw new InvalidOperationException(
RelationalStrings.ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(constraintName));
}
else
{
throw new InvalidOperationException(
RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc(constraintName));
}
}

if (property.Builder.CanSetAnnotation(RelationalAnnotationNames.DefaultConstraintName, null))
{
constraintName = Uniquifier.Uniquify(constraintName, defaultConstraints, n => (n, schema), maxLength);
property.Builder.HasAnnotation(RelationalAnnotationNames.DefaultConstraintName, constraintName);
return constraintName;
}

return null;
}

private void UniquifyTriggerNames(
IConventionEntityType entityType,
Dictionary<string, (IConventionTrigger, StoreObjectIdentifier)> triggers,
Expand Down
12 changes: 12 additions & 0 deletions src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ public static class RelationalAnnotationNames
/// </summary>
public const string DefaultValue = Prefix + "DefaultValue";

/// <summary>
/// The name for default constraint annotations.
/// </summary>
public const string DefaultConstraintName = Prefix + "DefaultConstraintName";

/// <summary>
/// The name for using named default constraints annotations.
/// </summary>
public const string UseNamedDefaultConstraints = Prefix + "UseNamedDefaultConstraints";

/// <summary>
/// The name for table name annotations.
/// </summary>
Expand Down Expand Up @@ -360,6 +370,8 @@ public static class RelationalAnnotationNames
ComputedColumnSql,
IsStored,
DefaultValue,
DefaultConstraintName,
UseNamedDefaultConstraints,
TableName,
Schema,
ViewName,
Expand Down
16 changes: 16 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

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

6 changes: 6 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@
<data name="ExecuteUpdateSubqueryNotSupportedOverComplexTypes" xml:space="preserve">
<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>
</data>
<data name="ExplicitDefaultConstraintNamesNotSupportedForTpc" xml:space="preserve">
<value>Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'.</value>
</data>
<data name="FromSqlMissingColumn" xml:space="preserve">
<value>The required column '{column}' was not present in the results of a 'FromSql' operation.</value>
</data>
Expand All @@ -439,6 +442,9 @@
<data name="HasDataNotSupportedForEntitiesMappedToJson" xml:space="preserve">
<value>Can't use HasData for entity type '{entity}'. HasData is not supported for entities mapped to JSON.</value>
</data>
<data name="ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash" xml:space="preserve">
<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>
</data>
<data name="IncompatibleTableCommentMismatch" xml:space="preserve">
<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>
</data>
Expand Down
Loading