Skip to content

Commit

Permalink
SQLite RevEng: Include computed columns
Browse files Browse the repository at this point in the history
Note, we can't get their actual SQL without parsing the entire CREATE TABLE statement, so we just generate HasComputedColumnSql without any arguments to at least make them usable.

Fixes #21557
  • Loading branch information
bricelam committed Jul 16, 2020
1 parent 743f846 commit a5f68d8
Show file tree
Hide file tree
Showing 15 changed files with 497 additions and 53 deletions.
3 changes: 1 addition & 2 deletions src/EFCore.Design/Design/Internal/CSharpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -787,8 +787,7 @@ internal static IReadOnlyCollection<Enum> GetFlags(Enum flags)
/// </summary>
public virtual string UnknownLiteral(object value)
{
if (value == null
|| value == DBNull.Value)
if (value == null)
{
return "null";
}
Expand Down
24 changes: 17 additions & 7 deletions src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1290,18 +1290,28 @@ protected virtual void GenerateFluentApiForDefaultValue(
[NotNull] IProperty property,
[NotNull] IndentedStringBuilder stringBuilder)
{
if (property.GetDefaultValue() is object defaultValue)
var defaultValue = property.GetDefaultValue();
if (defaultValue == null)
{
return;
}

stringBuilder
.AppendLine()
.Append(".")
.Append(nameof(RelationalPropertyBuilderExtensions.HasDefaultValue))
.Append("(");

if (defaultValue != DBNull.Value)
{
stringBuilder
.AppendLine()
.Append(".")
.Append(nameof(RelationalPropertyBuilderExtensions.HasDefaultValue))
.Append("(")
.Append(Code.UnknownLiteral(FindValueConverter(property) is ValueConverter valueConverter
? valueConverter.ConvertToProvider(defaultValue)
: defaultValue))
.Append(")");
: defaultValue));
}

stringBuilder
.Append(")");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,11 +626,17 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations)
$"({(property.IsUnicode() == false ? "false" : "")})");
}

if (property.GetDefaultValue() != null)
var defaultValue = property.GetDefaultValue();
if (defaultValue == DBNull.Value)
{
lines.Add($".{nameof(RelationalPropertyBuilderExtensions.HasDefaultValue)}()");
annotations.Remove(RelationalAnnotationNames.DefaultValue);
}
else if (defaultValue != null)
{
lines.Add(
$".{nameof(RelationalPropertyBuilderExtensions.HasDefaultValue)}" +
$"({_code.UnknownLiteral(property.GetDefaultValue())})");
$"({_code.UnknownLiteral(defaultValue)})");
annotations.Remove(RelationalAnnotationNames.DefaultValue);
}

Expand Down
21 changes: 15 additions & 6 deletions src/EFCore.Relational/Design/AnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,24 @@ public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
annotations,
RelationalAnnotationNames.ColumnName, nameof(RelationalPropertyBuilderExtensions.HasColumnName), methodCallCodeFragments);

GenerateSimpleFluentApiCall(
annotations,
RelationalAnnotationNames.DefaultValueSql, nameof(RelationalPropertyBuilderExtensions.HasDefaultValueSql),
methodCallCodeFragments);
if (TryGetAndRemove(annotations, RelationalAnnotationNames.DefaultValueSql, out string defaultValueSql))
{
methodCallCodeFragments.Add(
defaultValueSql?.Length == 0
? new MethodCallCodeFragment(
nameof(RelationalPropertyBuilderExtensions.HasDefaultValueSql))
: new MethodCallCodeFragment(
nameof(RelationalPropertyBuilderExtensions.HasDefaultValueSql),
defaultValueSql));
}

if (TryGetAndRemove(annotations, RelationalAnnotationNames.ComputedColumnSql, out object computedColumnSql))
if (TryGetAndRemove(annotations, RelationalAnnotationNames.ComputedColumnSql, out string computedColumnSql))
{
methodCallCodeFragments.Add(
TryGetAndRemove(annotations, RelationalAnnotationNames.IsStored, out bool isStored)
computedColumnSql?.Length == 0
? new MethodCallCodeFragment(
nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql))
: TryGetAndRemove(annotations, RelationalAnnotationNames.IsStored, out bool isStored)
? new MethodCallCodeFragment(
nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql),
computedColumnSql,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,28 @@ public static bool CanSetIsFixedLength(
bool fromDataAnnotation = false)
=> propertyBuilder.CanSetAnnotation(RelationalAnnotationNames.IsFixedLength, fixedLength, fromDataAnnotation);

/// <summary>
/// <para>
/// Configures the default value expression for the column that the property maps to when targeting a
/// relational database.
/// </para>
/// <para>
/// When called with no argument, this method tells EF that a column has a default value constraint of
/// some sort without needing to specify exactly what it is. This can be useful when mapping EF to an
/// existing database.
/// </para>
/// </summary>
/// <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 PropertyBuilder HasDefaultValueSql([NotNull] this PropertyBuilder propertyBuilder)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));

propertyBuilder.Metadata.SetDefaultValueSql(string.Empty);

return propertyBuilder;
}

/// <summary>
/// Configures the default value expression for the column that the property maps to when targeting a relational database.
/// </summary>
Expand All @@ -284,6 +306,24 @@ public static PropertyBuilder HasDefaultValueSql(
return propertyBuilder;
}

/// <summary>
/// <para>
/// Configures the default value expression for the column that the property maps to when targeting a
/// relational database.
/// </para>
/// <para>
/// When called with no argument, this method tells EF that a column has a default value constraint of
/// some sort without needing to specify exactly what it is. This can be useful when mapping EF to an
/// existing database.
/// </para>
/// </summary>
/// <typeparam name="TProperty"> The type of the property being configured. </typeparam>
/// <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 PropertyBuilder<TProperty> HasDefaultValueSql<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder)
=> (PropertyBuilder<TProperty>)HasDefaultValueSql((PropertyBuilder)propertyBuilder);

/// <summary>
/// Configures the default value expression for the column that the property maps to when targeting a relational database.
/// </summary>
Expand Down Expand Up @@ -333,9 +373,30 @@ public static bool CanSetDefaultValueSql(
bool fromDataAnnotation = false)
=> propertyBuilder.CanSetAnnotation(
RelationalAnnotationNames.DefaultValueSql,
Check.NullButNotEmpty(sql, nameof(sql)),
sql,
fromDataAnnotation);

/// <summary>
/// <para>
/// Configures the property to map to a computed column when targeting a relational database.
/// </para>
/// <para>
/// When called with no arguments, this method tells EF that a column is computed without needing to
/// specify the actual SQL used to computed it. This can be useful when mapping EF to an existing
/// database.
/// </para>
/// </summary>
/// <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 PropertyBuilder HasComputedColumnSql([NotNull] this PropertyBuilder propertyBuilder)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));

propertyBuilder.Metadata.SetComputedColumnSql(string.Empty);

return propertyBuilder;
}

/// <summary>
/// Configures the property to map to a computed column when targeting a relational database.
/// </summary>
Expand Down Expand Up @@ -365,6 +426,23 @@ public static PropertyBuilder HasComputedColumnSql(
return propertyBuilder;
}

/// <summary>
/// <para>
/// Configures the property to map to a computed column when targeting a relational database.
/// </para>
/// <para>
/// When called with no arguments, this method tells EF that a column is computed without needing to
/// specify the actual SQL used to computed it. This can be useful when mapping EF to an existing
/// database.
/// </para>
/// </summary>
/// <typeparam name="TProperty"> The type of the property being configured. </typeparam>
/// <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 PropertyBuilder<TProperty> HasComputedColumnSql<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder)
=> (PropertyBuilder<TProperty>)HasComputedColumnSql((PropertyBuilder)propertyBuilder);

/// <summary>
/// Configures the property to map to a computed column when targeting a relational database.
/// </summary>
Expand Down Expand Up @@ -447,7 +525,7 @@ public static bool CanSetComputedColumnSql(
bool fromDataAnnotation = false)
=> propertyBuilder.CanSetAnnotation(
RelationalAnnotationNames.ComputedColumnSql,
Check.NullButNotEmpty(sql, nameof(sql)),
sql,
fromDataAnnotation);

/// <summary>
Expand Down Expand Up @@ -482,15 +560,30 @@ public static bool CanSetIsStoredComputedColumn(
/// </para>
/// </summary>
/// <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 PropertyBuilder HasDefaultValue([NotNull] this PropertyBuilder propertyBuilder)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));

propertyBuilder.Metadata.SetDefaultValue(DBNull.Value);

return propertyBuilder;
}

/// <summary>
/// Configures the default value for the column that the property maps
/// to when targeting a relational database.
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="value"> The default value of the column. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static PropertyBuilder HasDefaultValue(
[NotNull] this PropertyBuilder propertyBuilder,
[CanBeNull] object value = null)
[CanBeNull] object value)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));

propertyBuilder.Metadata.SetDefaultValue(value ?? DBNull.Value);
propertyBuilder.Metadata.SetDefaultValue(value);

return propertyBuilder;
}
Expand All @@ -508,11 +601,22 @@ public static PropertyBuilder HasDefaultValue(
/// </summary>
/// <typeparam name="TProperty"> The type of the property being configured. </typeparam>
/// <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 PropertyBuilder<TProperty> HasDefaultValue<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder)
=> (PropertyBuilder<TProperty>)HasDefaultValue((PropertyBuilder)propertyBuilder);

/// <summary>
/// Configures the default value for the column that the property maps
/// to when targeting a relational database.
/// </summary>
/// <typeparam name="TProperty"> The type of the property being configured. </typeparam>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="value"> The default value of the column. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static PropertyBuilder<TProperty> HasDefaultValue<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder,
[CanBeNull] object value = null)
[CanBeNull] object value)
=> (PropertyBuilder<TProperty>)HasDefaultValue((PropertyBuilder)propertyBuilder, value);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ public static string GetDefaultValueSql([NotNull] this IProperty property, Store
public static void SetDefaultValueSql([NotNull] this IMutableProperty property, [CanBeNull] string value)
=> property.SetOrRemoveAnnotation(
RelationalAnnotationNames.DefaultValueSql,
Check.NullButNotEmpty(value, nameof(value)));
value);

/// <summary>
/// Sets the SQL expression that is used as the default value for the column this property is mapped to.
Expand All @@ -414,7 +414,7 @@ public static string SetDefaultValueSql(
{
property.SetOrRemoveAnnotation(
RelationalAnnotationNames.DefaultValueSql,
Check.NullButNotEmpty(value, nameof(value)),
value,
fromDataAnnotation);

return value;
Expand Down Expand Up @@ -475,7 +475,7 @@ public static string GetComputedColumnSql([NotNull] this IProperty property, Sto
public static void SetComputedColumnSql([NotNull] this IMutableProperty property, [CanBeNull] string value)
=> property.SetOrRemoveAnnotation(
RelationalAnnotationNames.ComputedColumnSql,
Check.NullButNotEmpty(value, nameof(value)));
value);

/// <summary>
/// Sets the SQL expression that is used as the computed value for the column this property is mapped to.
Expand All @@ -489,7 +489,7 @@ public static string SetComputedColumnSql(
{
property.SetOrRemoveAnnotation(
RelationalAnnotationNames.ComputedColumnSql,
Check.NullButNotEmpty(value, nameof(value)),
value,
fromDataAnnotation);

return value;
Expand Down
29 changes: 25 additions & 4 deletions src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
Expand Down Expand Up @@ -1078,6 +1080,28 @@ private void Initialize(
IEnumerable<IAnnotation> migrationsAnnotations,
bool inline = false)
{
if (column.DefaultValue == DBNull.Value)
{
throw new InvalidOperationException(
RelationalStrings.DefaultValueUnspecified(
column.Name,
(column.Table.Name, column.Table.Schema).FormatTable()));
}
if (column.DefaultValueSql?.Length == 0)
{
throw new InvalidOperationException(
RelationalStrings.DefaultValueSqlUnspecified(
column.Name,
(column.Table.Name, column.Table.Schema).FormatTable()));
}
if (column.ComputedColumnSql?.Length == 0)
{
throw new InvalidOperationException(
RelationalStrings.ComputedColumnSqlUnspecified(
column.Name,
(column.Table.Name, column.Table.Schema).FormatTable()));
}

var property = column.PropertyMappings.First().Property;
var valueConverter = GetValueConverter(property, typeMapping);
columnOperation.ClrType
Expand All @@ -1092,13 +1116,10 @@ private void Initialize(
columnOperation.IsFixedLength = column.IsFixedLength;
columnOperation.IsRowVersion = column.IsRowVersion;
columnOperation.IsNullable = isNullable;

var defaultValue = column.DefaultValue;
columnOperation.DefaultValue = (defaultValue == DBNull.Value ? null : defaultValue)
columnOperation.DefaultValue = column.DefaultValue
?? (inline || isNullable
? null
: GetDefaultValue(columnOperation.ClrType));

columnOperation.DefaultValueSql = column.DefaultValueSql;
columnOperation.ComputedColumnSql = column.ComputedColumnSql;
columnOperation.IsStored = column.IsStored;
Expand Down
24 changes: 24 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.

Loading

0 comments on commit a5f68d8

Please sign in to comment.