Skip to content
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
3 changes: 3 additions & 0 deletions src/EFCore.Relational/EFCore.Relational.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -17582,6 +17582,9 @@
{
"Member": "virtual void ConfigureParameter(System.Data.Common.DbParameter parameter);"
},
{
"Member": "virtual object? GetDefaultProviderValue();"
},
{
"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);"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1230,24 +1230,11 @@ private void Initialize(
= (valueConverter?.ProviderClrType
?? typeMapping.ClrType).UnwrapNullableType();

if (!column.TryGetDefaultValue(out var defaultValue))
{
// for non-nullable collections of primitives that are mapped to JSON we set a default value corresponding to empty JSON collection
defaultValue = !inline
&& column is
{
IsNullable: false, StoreTypeMapping: { ElementTypeMapping: not null, Converter: { } columnValueConverter }
}
&& columnValueConverter.GetType() is { IsGenericType: true } columnValueConverterType
&& columnValueConverterType.GetGenericTypeDefinition() == typeof(CollectionToJsonStringConverter<>)
? "[]"
: null;
}

column.TryGetDefaultValue(out var defaultValue);
columnOperation.DefaultValue = defaultValue
?? (inline || isNullable
? null
: GetDefaultValue(columnOperation.ClrType));
: typeMapping.GetDefaultProviderValue());
columnOperation.DefaultValueSql = column.DefaultValueSql;
columnOperation.ColumnType = column.StoreType;
columnOperation.MaxLength = column.MaxLength;
Expand Down Expand Up @@ -2555,19 +2542,6 @@ protected virtual IEnumerable<string> GetSchemas(IRelationalModel model)
.Cast<string>()
.Distinct();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected virtual object? GetDefaultValue(Type type)
=> type == typeof(string)
? string.Empty
: type.IsArray
? Array.CreateInstance(type.GetElementType()!, 0)
: type.UnwrapNullableType().GetDefaultValue();

private static ValueConverter? GetValueConverter(IProperty property, RelationalTypeMapping? typeMapping = null)
=> (property.FindRelationalTypeMapping() ?? typeMapping)?.Converter;

Expand Down
26 changes: 26 additions & 0 deletions src/EFCore.Relational/Storage/RelationalTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,32 @@ public virtual string GenerateProviderValueSqlLiteral(object? value)
protected virtual string GenerateNonNullSqlLiteral(object value)
=> string.Format(CultureInfo.InvariantCulture, SqlLiteralFormatString, value);

/// <summary>
/// Creates the provider value used to populate a newly added, required column for existing rows when generating a migration.
/// </summary>
/// <remarks>
/// Type mappings whose facets (such as size) are required to produce a usable value should override this to supply
/// a provider value built from the configured mapping.
/// </remarks>
/// <returns>The default provider value.</returns>
public virtual object? GetDefaultProviderValue()
{
if (ElementTypeMapping is not null
&& Converter?.GetType() is { IsGenericType: true } converterType
&& converterType.GetGenericTypeDefinition() == typeof(CollectionToJsonStringConverter<>))
{
return "[]";
}

var providerType = (Converter?.ProviderClrType ?? ClrType).UnwrapNullableType();

return providerType == typeof(string)
? string.Empty
: providerType.IsArray
? Array.CreateInstance(providerType.GetElementType()!, 0)
: providerType.GetDefaultValue();
}

/// <summary>
/// The method to use when reading values of the given type. The method must be defined
/// on <see cref="DbDataReader" /> or one of its subclasses.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Microsoft.Data.SqlTypes;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
Expand All @@ -25,6 +27,15 @@ public class SqlServerVectorTypeMapping : RelationalTypeMapping

private static readonly VectorComparer _comparerInstance = new();

private static readonly MethodInfo _createNullMethod
= typeof(SqlVector<float>).GetMethod(nameof(SqlVector<float>.CreateNull), [typeof(int)])!;

private static readonly ConstructorInfo _constructor
= typeof(SqlVector<float>).GetConstructor([typeof(ReadOnlyMemory<float>)])!;

private static readonly MethodInfo _memoryImplicitOperator
= typeof(ReadOnlyMemory<float>).GetMethod("op_Implicit", [typeof(float[])])!;

// Note that dimensions is mandatory with SQL Server vector.
// However, our scaffolder looks up each type mapping without the facets, to find out whether the scaffolded
// facet happens to be the default (and therefore can be omitted). So we allow constructing a SqlServerVectorTypeMapping
Expand Down Expand Up @@ -116,6 +127,38 @@ protected override string GenerateNonNullSqlLiteral(object value)
return builder.ToString();
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override Expression GenerateCodeLiteral(object value)
{
var vector = (SqlVector<float>)value;

if (vector.IsNull)
{
return Expression.Call(_createNullMethod, Expression.Constant(vector.Length));
}

return Expression.New(
_constructor,
Expression.Convert(
Expression.Constant(vector.Memory.ToArray(), typeof(float[])),
typeof(ReadOnlyMemory<float>),
_memoryImplicitOperator));
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override object? GetDefaultProviderValue()
=> Size is int dimensions ? new SqlVector<float>(new float[dimensions]) : null;

private sealed class VectorComparer() : ValueComparer<SqlVector<float>>(
(x, y) => CalculateEquality(x, y),
v => CalculateHashCode(v),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Data.SqlTypes;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;

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

[Fact]
public void Add_required_vector_column_uses_zero_vector_default_value()
=> Execute(
source => source.Entity(
"Cat",
x =>
{
x.Property<int>("Id");
x.ToTable("Cats");
}),
target => target.Entity(
"Cat",
x =>
{
x.Property<int>("Id");
x.ToTable("Cats");
x.Property<SqlVector<float>>("Embedding").HasColumnType("vector(3)");
}),
operations =>
{
Assert.Equal(1, operations.Count);

var operation = Assert.IsType<AddColumnOperation>(operations[0]);
Assert.Equal("Embedding", operation.Name);

var defaultValue = Assert.IsType<SqlVector<float>>(operation.DefaultValue);
Assert.False(defaultValue.IsNull);
Assert.Equal(3, defaultValue.Length);
Assert.True(defaultValue.Memory.Span.TrimStart(0f).IsEmpty);
});

[Fact]
public void Alter_column_identity()
=> Execute(
Expand Down
28 changes: 28 additions & 0 deletions test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,34 @@ public virtual void Vector_comparer_compares_Memory()
Assert.False(typeMapping.Comparer.Equals(vector1, vector3));
}

[Fact]
public virtual void GenerateCodeLiteral_generates_vector_literal()
=> Test_GenerateCodeLiteral_helper(
new SqlServerVectorTypeMapping(3),
new SqlVector<float>(new float[] { 1, 2, 3 }),
"new Microsoft.Data.SqlTypes.SqlVector<float>(new[] { 1f, 2f, 3f })");

[Fact]
public virtual void GenerateCodeLiteral_generates_null_vector_literal()
=> Test_GenerateCodeLiteral_helper(
new SqlServerVectorTypeMapping(3),
SqlVector<float>.CreateNull(3),
"Microsoft.Data.SqlTypes.SqlVector<float>.CreateNull(3)");

[Fact]
public virtual void Vector_default_provider_value_is_zero_vector_of_configured_dimensions()
{
var value = Assert.IsType<SqlVector<float>>(new SqlServerVectorTypeMapping(3).GetDefaultProviderValue());

Assert.False(value.IsNull);
Assert.Equal(3, value.Length);
Assert.True(value.Memory.Span.TrimStart(0f).IsEmpty);
}

[Fact]
public virtual void Vector_default_provider_value_is_null_without_dimensions()
=> Assert.Null(SqlServerVectorTypeMapping.Default.GetDefaultProviderValue());

#endregion Vector

public static RelationalTypeMapping GetMapping(string type)
Expand Down
Loading