Skip to content

Commit 77a2b2c

Browse files
Fix migration generation for non-nullable SqlVector columns (#38451)
Fixes #37231 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
1 parent 4918757 commit 77a2b2c

6 files changed

Lines changed: 134 additions & 28 deletions

File tree

src/EFCore.Relational/EFCore.Relational.baseline.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17610,6 +17610,9 @@
1761017610
{
1761117611
"Member": "virtual void ConfigureParameter(System.Data.Common.DbParameter parameter);"
1761217612
},
17613+
{
17614+
"Member": "virtual object? GetDefaultProviderValue();"
17615+
},
1761317616
{
1761417617
"Member": "virtual System.Data.Common.DbParameter CreateParameter(System.Data.Common.DbCommand command, string name, object? value, bool? nullable = null, System.Data.ParameterDirection direction = System.Data.ParameterDirection.Input);"
1761517618
},

src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,24 +1230,11 @@ private void Initialize(
12301230
= (valueConverter?.ProviderClrType
12311231
?? typeMapping.ClrType).UnwrapNullableType();
12321232

1233-
if (!column.TryGetDefaultValue(out var defaultValue))
1234-
{
1235-
// for non-nullable collections of primitives that are mapped to JSON we set a default value corresponding to empty JSON collection
1236-
defaultValue = !inline
1237-
&& column is
1238-
{
1239-
IsNullable: false, StoreTypeMapping: { ElementTypeMapping: not null, Converter: { } columnValueConverter }
1240-
}
1241-
&& columnValueConverter.GetType() is { IsGenericType: true } columnValueConverterType
1242-
&& columnValueConverterType.GetGenericTypeDefinition() == typeof(CollectionToJsonStringConverter<>)
1243-
? "[]"
1244-
: null;
1245-
}
1246-
1233+
column.TryGetDefaultValue(out var defaultValue);
12471234
columnOperation.DefaultValue = defaultValue
12481235
?? (inline || isNullable
12491236
? null
1250-
: GetDefaultValue(columnOperation.ClrType));
1237+
: typeMapping.GetDefaultProviderValue());
12511238
columnOperation.DefaultValueSql = column.DefaultValueSql;
12521239
columnOperation.ColumnType = column.StoreType;
12531240
columnOperation.MaxLength = column.MaxLength;
@@ -2555,19 +2542,6 @@ protected virtual IEnumerable<string> GetSchemas(IRelationalModel model)
25552542
.Cast<string>()
25562543
.Distinct();
25572544

2558-
/// <summary>
2559-
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2560-
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
2561-
/// any release. You should only use it directly in your code with extreme caution and knowing that
2562-
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2563-
/// </summary>
2564-
protected virtual object? GetDefaultValue(Type type)
2565-
=> type == typeof(string)
2566-
? string.Empty
2567-
: type.IsArray
2568-
? Array.CreateInstance(type.GetElementType()!, 0)
2569-
: type.UnwrapNullableType().GetDefaultValue();
2570-
25712545
private static ValueConverter? GetValueConverter(IProperty property, RelationalTypeMapping? typeMapping = null)
25722546
=> (property.FindRelationalTypeMapping() ?? typeMapping)?.Converter;
25732547

src/EFCore.Relational/Storage/RelationalTypeMapping.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,32 @@ public virtual string GenerateProviderValueSqlLiteral(object? value)
656656
protected virtual string GenerateNonNullSqlLiteral(object value)
657657
=> string.Format(CultureInfo.InvariantCulture, SqlLiteralFormatString, value);
658658

659+
/// <summary>
660+
/// Creates the provider value used to populate a newly added, required column for existing rows when generating a migration.
661+
/// </summary>
662+
/// <remarks>
663+
/// Type mappings whose facets (such as size) are required to produce a usable value should override this to supply
664+
/// a provider value built from the configured mapping.
665+
/// </remarks>
666+
/// <returns>The default provider value.</returns>
667+
public virtual object? GetDefaultProviderValue()
668+
{
669+
if (ElementTypeMapping is not null
670+
&& Converter?.GetType() is { IsGenericType: true } converterType
671+
&& converterType.GetGenericTypeDefinition() == typeof(CollectionToJsonStringConverter<>))
672+
{
673+
return "[]";
674+
}
675+
676+
var providerType = (Converter?.ProviderClrType ?? ClrType).UnwrapNullableType();
677+
678+
return providerType == typeof(string)
679+
? string.Empty
680+
: providerType.IsArray
681+
? Array.CreateInstance(providerType.GetElementType()!, 0)
682+
: providerType.GetDefaultValue();
683+
}
684+
659685
/// <summary>
660686
/// The method to use when reading values of the given type. The method must be defined
661687
/// on <see cref="DbDataReader" /> or one of its subclasses.

src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Linq.Expressions;
5+
using System.Reflection;
46
using System.Text;
57
using Microsoft.Data.SqlTypes;
68
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
@@ -25,6 +27,15 @@ public class SqlServerVectorTypeMapping : RelationalTypeMapping
2527

2628
private static readonly VectorComparer _comparerInstance = new();
2729

30+
private static readonly MethodInfo _createNullMethod
31+
= typeof(SqlVector<float>).GetMethod(nameof(SqlVector<float>.CreateNull), [typeof(int)])!;
32+
33+
private static readonly ConstructorInfo _constructor
34+
= typeof(SqlVector<float>).GetConstructor([typeof(ReadOnlyMemory<float>)])!;
35+
36+
private static readonly MethodInfo _memoryImplicitOperator
37+
= typeof(ReadOnlyMemory<float>).GetMethod("op_Implicit", [typeof(float[])])!;
38+
2839
// Note that dimensions is mandatory with SQL Server vector.
2940
// However, our scaffolder looks up each type mapping without the facets, to find out whether the scaffolded
3041
// facet happens to be the default (and therefore can be omitted). So we allow constructing a SqlServerVectorTypeMapping
@@ -116,6 +127,38 @@ protected override string GenerateNonNullSqlLiteral(object value)
116127
return builder.ToString();
117128
}
118129

130+
/// <summary>
131+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
132+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
133+
/// any release. You should only use it directly in your code with extreme caution and knowing that
134+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
135+
/// </summary>
136+
public override Expression GenerateCodeLiteral(object value)
137+
{
138+
var vector = (SqlVector<float>)value;
139+
140+
if (vector.IsNull)
141+
{
142+
return Expression.Call(_createNullMethod, Expression.Constant(vector.Length));
143+
}
144+
145+
return Expression.New(
146+
_constructor,
147+
Expression.Convert(
148+
Expression.Constant(vector.Memory.ToArray(), typeof(float[])),
149+
typeof(ReadOnlyMemory<float>),
150+
_memoryImplicitOperator));
151+
}
152+
153+
/// <summary>
154+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
155+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
156+
/// any release. You should only use it directly in your code with extreme caution and knowing that
157+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
158+
/// </summary>
159+
public override object? GetDefaultProviderValue()
160+
=> Size is int dimensions ? new SqlVector<float>(new float[dimensions]) : null;
161+
119162
private sealed class VectorComparer() : ValueComparer<SqlVector<float>>(
120163
(x, y) => CalculateEquality(x, y),
121164
v => CalculateHashCode(v),

test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.Data.SqlTypes;
45
using Microsoft.EntityFrameworkCore.Migrations.Internal;
56
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
67

@@ -174,6 +175,37 @@ public void Add_column_with_dependencies()
174175
Assert.Equal("[FirstName] + [LastName]", columnOperation.ComputedColumnSql);
175176
});
176177

178+
[Fact]
179+
public void Add_required_vector_column_uses_zero_vector_default_value()
180+
=> Execute(
181+
source => source.Entity(
182+
"Cat",
183+
x =>
184+
{
185+
x.Property<int>("Id");
186+
x.ToTable("Cats");
187+
}),
188+
target => target.Entity(
189+
"Cat",
190+
x =>
191+
{
192+
x.Property<int>("Id");
193+
x.ToTable("Cats");
194+
x.Property<SqlVector<float>>("Embedding").HasColumnType("vector(3)");
195+
}),
196+
operations =>
197+
{
198+
Assert.Equal(1, operations.Count);
199+
200+
var operation = Assert.IsType<AddColumnOperation>(operations[0]);
201+
Assert.Equal("Embedding", operation.Name);
202+
203+
var defaultValue = Assert.IsType<SqlVector<float>>(operation.DefaultValue);
204+
Assert.False(defaultValue.IsNull);
205+
Assert.Equal(3, defaultValue.Length);
206+
Assert.True(defaultValue.Memory.Span.TrimStart(0f).IsEmpty);
207+
});
208+
177209
[Fact]
178210
public void Alter_column_identity()
179211
=> Execute(

test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,34 @@ public virtual void Vector_comparer_compares_Memory()
420420
Assert.False(typeMapping.Comparer.Equals(vector1, vector3));
421421
}
422422

423+
[Fact]
424+
public virtual void GenerateCodeLiteral_generates_vector_literal()
425+
=> Test_GenerateCodeLiteral_helper(
426+
new SqlServerVectorTypeMapping(3),
427+
new SqlVector<float>(new float[] { 1, 2, 3 }),
428+
"new Microsoft.Data.SqlTypes.SqlVector<float>(new[] { 1f, 2f, 3f })");
429+
430+
[Fact]
431+
public virtual void GenerateCodeLiteral_generates_null_vector_literal()
432+
=> Test_GenerateCodeLiteral_helper(
433+
new SqlServerVectorTypeMapping(3),
434+
SqlVector<float>.CreateNull(3),
435+
"Microsoft.Data.SqlTypes.SqlVector<float>.CreateNull(3)");
436+
437+
[Fact]
438+
public virtual void Vector_default_provider_value_is_zero_vector_of_configured_dimensions()
439+
{
440+
var value = Assert.IsType<SqlVector<float>>(new SqlServerVectorTypeMapping(3).GetDefaultProviderValue());
441+
442+
Assert.False(value.IsNull);
443+
Assert.Equal(3, value.Length);
444+
Assert.True(value.Memory.Span.TrimStart(0f).IsEmpty);
445+
}
446+
447+
[Fact]
448+
public virtual void Vector_default_provider_value_is_null_without_dimensions()
449+
=> Assert.Null(SqlServerVectorTypeMapping.Default.GetDefaultProviderValue());
450+
423451
#endregion Vector
424452

425453
public static RelationalTypeMapping GetMapping(string type)

0 commit comments

Comments
 (0)