Skip to content

Commit

Permalink
Support for SQL Server sparse columns (#23904)
Browse files Browse the repository at this point in the history
Closes #8023
  • Loading branch information
roji authored Jan 22, 2021
1 parent ff0d2e0 commit e64624d
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@ public SqlServerAnnotationCodeGenerator([NotNull] AnnotationCodeGeneratorDepende
public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IModel model,
IDictionary<string, IAnnotation> annotations)
=> base.GenerateFluentApiCalls(model, annotations)
.Concat(GenerateValueGenerationStrategy(annotations, onModel: true))
.ToList();
{
var fragments = new List<MethodCallCodeFragment>(base.GenerateFluentApiCalls(model, annotations));

if (GenerateValueGenerationStrategy(annotations, onModel: true) is MethodCallCodeFragment valueGenerationStrategy)
{
fragments.Add(valueGenerationStrategy);
}

return fragments;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -54,9 +61,24 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IProperty property,
IDictionary<string, IAnnotation> annotations)
=> base.GenerateFluentApiCalls(property, annotations)
.Concat(GenerateValueGenerationStrategy(annotations, onModel: false))
.ToList();
{
var fragments = new List<MethodCallCodeFragment>(base.GenerateFluentApiCalls(property, annotations));

if (GenerateValueGenerationStrategy(annotations, onModel: false) is MethodCallCodeFragment valueGenerationStrategy)
{
fragments.Add(valueGenerationStrategy);
}

if (GetAndRemove<bool?>(annotations, SqlServerAnnotationNames.Sparse) is bool isSparse)
{
fragments.Add(
isSparse
? new(nameof(SqlServerPropertyBuilderExtensions.IsSparse))
: new(nameof(SqlServerPropertyBuilderExtensions.IsSparse), false));
}

return fragments;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -113,7 +135,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IIndex index, IAnnot
_ => null
};

private IReadOnlyList<MethodCallCodeFragment> GenerateValueGenerationStrategy(
private MethodCallCodeFragment GenerateValueGenerationStrategy(
IDictionary<string, IAnnotation> annotations,
bool onModel)
{
Expand All @@ -126,67 +148,58 @@ private IReadOnlyList<MethodCallCodeFragment> GenerateValueGenerationStrategy(
}
else
{
return Array.Empty<MethodCallCodeFragment>();
return null;
}

switch (strategy)
{
case SqlServerValueGenerationStrategy.IdentityColumn:
var seed = GetAndRemove<int?>(SqlServerAnnotationNames.IdentitySeed) ?? 1;
var increment = GetAndRemove<int?>(SqlServerAnnotationNames.IdentityIncrement) ?? 1;
return new List<MethodCallCodeFragment>
{
new(
onModel
? nameof(SqlServerModelBuilderExtensions.UseIdentityColumns)
: nameof(SqlServerPropertyBuilderExtensions.UseIdentityColumn),
(seed, increment) switch
{
(1, 1) => Array.Empty<object>(),
(_, 1) => new object[] { seed },
_ => new object[] { seed, increment }
})
};
var seed = GetAndRemove<int?>(annotations, SqlServerAnnotationNames.IdentitySeed) ?? 1;
var increment = GetAndRemove<int?>(annotations, SqlServerAnnotationNames.IdentityIncrement) ?? 1;
return new(
onModel
? nameof(SqlServerModelBuilderExtensions.UseIdentityColumns)
: nameof(SqlServerPropertyBuilderExtensions.UseIdentityColumn),
(seed, increment) switch
{
(1, 1) => Array.Empty<object>(),
(_, 1) => new object[] { seed },
_ => new object[] { seed, increment }
});

case SqlServerValueGenerationStrategy.SequenceHiLo:
var name = GetAndRemove<string>(SqlServerAnnotationNames.HiLoSequenceName);
var schema = GetAndRemove<string>(SqlServerAnnotationNames.HiLoSequenceSchema);
return new List<MethodCallCodeFragment>
{
new(
nameof(SqlServerModelBuilderExtensions.UseHiLo),
(name, schema) switch
{
(null, null) => Array.Empty<object>(),
(_, null) => new object[] { name },
_ => new object[] { name, schema }
})
};
var name = GetAndRemove<string>(annotations, SqlServerAnnotationNames.HiLoSequenceName);
var schema = GetAndRemove<string>(annotations, SqlServerAnnotationNames.HiLoSequenceSchema);
return new(
nameof(SqlServerModelBuilderExtensions.UseHiLo),
(name, schema) switch
{
(null, null) => Array.Empty<object>(),
(_, null) => new object[] { name },
_ => new object[] { name, schema }
});

case SqlServerValueGenerationStrategy.None:
return new List<MethodCallCodeFragment>
{
new(
nameof(ModelBuilder.HasAnnotation),
SqlServerAnnotationNames.ValueGenerationStrategy,
SqlServerValueGenerationStrategy.None)
};
return new(
nameof(ModelBuilder.HasAnnotation),
SqlServerAnnotationNames.ValueGenerationStrategy,
SqlServerValueGenerationStrategy.None);

default:
throw new ArgumentOutOfRangeException();
}
}

T GetAndRemove<T>(string annotationName)
private static T GetAndRemove<T>(IDictionary<string, IAnnotation> annotations, string annotationName)
{
if (annotations.TryGetValue(annotationName, out var annotation)
&& annotation.Value != null)
{
if (annotations.TryGetValue(annotationName, out var annotation)
&& annotation.Value != null)
{
annotations.Remove(annotationName);
return (T)annotation.Value;
}

return default;
annotations.Remove(annotationName);
return (T)annotation.Value;
}

return default;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -296,5 +296,77 @@ public static bool CanSetValueGenerationStrategy(
&& propertyBuilder.CanSetAnnotation(
SqlServerAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation);
}

/// <summary>
/// Configures whether the property's column is created as sparse when targeting SQL Server.
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="sparse"> A value indicating whether the property's column is created as sparse. </param>
/// <returns> A builder to further configure the property. </returns>
/// <remarks> See https://docs.microsoft.com/sql/relational-databases/tables/use-sparse-columns. </remarks>
public static PropertyBuilder IsSparse([NotNull] this PropertyBuilder propertyBuilder, bool sparse = true)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));

propertyBuilder.Metadata.SetIsSparse(sparse);

return propertyBuilder;
}

/// <summary>
/// Configures whether the property's column is created as sparse when targeting SQL Server.
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="sparse"> A value indicating whether the property's column is created as sparse. </param>
/// <returns> A builder to further configure the property. </returns>
/// <remarks> See https://docs.microsoft.com/sql/relational-databases/tables/use-sparse-columns. </remarks>
public static PropertyBuilder<TProperty> IsSparse<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder,
bool sparse = true)
=> (PropertyBuilder<TProperty>)IsSparse((PropertyBuilder)propertyBuilder, sparse);

/// <summary>
/// Configures whether the property's column is created as sparse when targeting SQL Server.
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="sparse"> A value indicating whether the property's column is created as sparse. </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>
/// <remarks> See https://docs.microsoft.com/sql/relational-databases/tables/use-sparse-columns. </remarks>
public static IConventionPropertyBuilder? IsSparse(
[NotNull] this IConventionPropertyBuilder propertyBuilder,
bool? sparse,
bool fromDataAnnotation = false)
{
if (propertyBuilder.CanSetIsSparse(sparse, fromDataAnnotation))
{
propertyBuilder.Metadata.SetIsSparse(sparse, fromDataAnnotation);

return propertyBuilder;
}

return null;
}

/// <summary>
/// Returns a value indicating whether the property's column can be configured as sparse when targeting SQL Server.
/// </summary>
/// <param name="property"> The builder for the property being configured. </param>
/// <param name="sparse"> A value indicating whether the property's column is created as sparse. </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>
/// <returns>
/// <see langword="true" /> if the property's column can be configured as sparse when targeting SQL Server.
/// </returns>
/// <remarks> See https://docs.microsoft.com/sql/relational-databases/tables/use-sparse-columns. </remarks>
public static bool CanSetIsSparse(
[NotNull] this IConventionPropertyBuilder property,
bool? sparse,
bool fromDataAnnotation = false)
{
Check.NotNull(property, nameof(property));

return property.CanSetAnnotation(SqlServerAnnotationNames.Sparse, sparse, fromDataAnnotation);
}
}
}
44 changes: 44 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,5 +491,49 @@ public static bool IsCompatibleWithValueGeneration([NotNull] IProperty property)
?? property.FindTypeMapping()?.Converter)
== null;
}

/// <summary>
/// Returns a value indicating whether the property's column is sparse.
/// </summary>
/// <param name="property"> The property. </param>
/// <returns> <see langword="true" /> if the property's column is sparse. </returns>
public static bool? IsSparse([NotNull] this IProperty property)
=> (bool?)property[SqlServerAnnotationNames.Sparse];

/// <summary>
/// Sets a value indicating whether the property's column is sparse.
/// </summary>
/// <param name="property"> The property. </param>
/// <param name="sparse"> The value to set. </param>
public static void SetIsSparse([NotNull] this IMutableProperty property, bool? sparse)
=> property.SetOrRemoveAnnotation(SqlServerAnnotationNames.Sparse, sparse);

/// <summary>
/// Sets a value indicating whether the property's column is sparse.
/// </summary>
/// <param name="property"> The property. </param>
/// <param name="sparse"> The value to set. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns> The configured value. </returns>
public static bool? SetIsSparse(
[NotNull] this IConventionProperty property,
bool? sparse,
bool fromDataAnnotation = false)
{
property.SetOrRemoveAnnotation(
SqlServerAnnotationNames.Sparse,
sparse,
fromDataAnnotation);

return sparse;
}

/// <summary>
/// Returns the <see cref="ConfigurationSource" /> for whether the property's column is sparse.
/// </summary>
/// <param name="property"> The property. </param>
/// <returns> The <see cref="ConfigurationSource" /> for whether the property's column is sparse. </returns>
public static ConfigurationSource? GetIsSparseConfigurationSource([NotNull] this IConventionProperty property)
=> property.FindAnnotation(SqlServerAnnotationNames.Sparse)?.GetConfigurationSource();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,18 @@ protected override void ValidateCompatible(
break;
}
}

if (property.IsSparse() != duplicateProperty.IsSparse())
{
throw new InvalidOperationException(
SqlServerStrings.DuplicateColumnSparsenessMismatch(
duplicateProperty.DeclaringEntityType.DisplayName(),
duplicateProperty.Name,
property.DeclaringEntityType.DisplayName(),
property.Name,
columnName,
storeObject.DisplayName()));
}
}

/// <inheritdoc />
Expand Down
Loading

0 comments on commit e64624d

Please sign in to comment.