diff --git a/Directory.Packages.props b/Directory.Packages.props index 086bb8e4c66..c0abbe61659 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,8 @@ - - + + diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.csproj b/src/EFCore.SqlServer/EFCore.SqlServer.csproj index fff89e95202..64c3f917d64 100644 --- a/src/EFCore.SqlServer/EFCore.SqlServer.csproj +++ b/src/EFCore.SqlServer/EFCore.SqlServer.csproj @@ -50,6 +50,7 @@ + diff --git a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs index 94b3b3aca77..baa97ffb632 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs @@ -3,6 +3,8 @@ // ReSharper disable once CheckNamespace +using Microsoft.Data.SqlTypes; + namespace Microsoft.EntityFrameworkCore; /// @@ -2452,4 +2454,30 @@ public static long PatIndex( => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); #endregion Population variance + + #region Vector functions + + /// + /// Calculates the distance between two vectors using a specified distance metric. + /// + /// The instance. + /// + /// A string with the name of the distance metric to use to calculate the distance between the two given vectors. The following distance metrics are supported: cosine, euclidean or dot. + /// + /// The first vector. + /// The second vector. + /// + /// Vector distance is always exact and doesn't use any vector index, even if available. + /// + /// SQL Server documentation for VECTOR_DISTANCE. + /// Vectors in the SQL Database Engine. + public static double VectorDistance( + this DbFunctions _, + [NotParameterized] string distanceMetric, + SqlVector vector1, + SqlVector vector2) + where T : unmanaged + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + #endregion Vector functions } diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs index 02e49289a8e..a40aea55bfe 100644 --- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Extensions.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; @@ -43,6 +45,7 @@ public override void Validate(IModel model, IDiagnosticsLogger + /// 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. + /// + protected virtual void ValidateVectorColumns( + IModel model, + IDiagnosticsLogger logger) + { + foreach (IConventionProperty property in model.GetEntityTypes() + .SelectMany(t => t.GetDeclaredProperties()) + .Where(p => p.ClrType.UnwrapNullableType() == typeof(SqlVector))) + { + if (property.GetTypeMapping() is not SqlServerVectorTypeMapping { Size: not null } vectorTypeMapping) + { + throw new InvalidOperationException(SqlServerStrings.VectorDimensionsMissing(property.DeclaringType.DisplayName(), property.Name)); + } + + if (property.DeclaringType.IsMappedToJson()) + { + throw new InvalidOperationException(SqlServerStrings.VectorPropertiesNotSupportedInJson(property.DeclaringType.DisplayName(), property.Name)); + } + } + } + /// /// 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 diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index 60215b821cb..32e9b4c4278 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -417,6 +417,28 @@ public static string TemporalSetOperationOnMismatchedSources(object? entityType) public static string TransientExceptionDetected => GetString("TransientExceptionDetected"); + /// + /// Vector properties require a positive size (number of dimensions). + /// + public static string VectorDimensionsInvalid + => GetString("VectorDimensionsInvalid"); + + /// + /// Vector property '{structuralType}.{propertyName}' was not configured with the number of dimensions. Set the column type to 'vector(x)' with the desired number of dimensions, or use the 'MaxLength' APIs. + /// + public static string VectorDimensionsMissing(object? structuralType, object? propertyName) + => string.Format( + GetString("VectorDimensionsMissing", nameof(structuralType), nameof(propertyName)), + structuralType, propertyName); + + /// + /// Vector property '{propertyName}' is on '{structuralType}' which is mapped to JSON. Vector properties are not supported within JSON documents. + /// + public static string VectorPropertiesNotSupportedInJson(object? structuralType, object? propertyName) + => string.Format( + GetString("VectorPropertiesNotSupportedInJson", nameof(structuralType), nameof(propertyName)), + structuralType, propertyName); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index dc00eb238cb..4d581755f60 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -369,4 +369,13 @@ An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. + + Vector properties require a positive size (number of dimensions). + + + Vector property '{structuralType}.{propertyName}' was not configured with the number of dimensions. Set the column type to 'vector(x)' with the desired number of dimensions, or use the 'MaxLength' APIs. + + + Vector property '{propertyName}' is on '{structuralType}' which is mapped to JSON. Vector properties are not supported within JSON documents. + \ No newline at end of file diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs index f397ef5e767..a85edd1d63e 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs @@ -30,7 +30,8 @@ public SqlServerMemberTranslatorProvider( new SqlServerDateTimeMemberTranslator(sqlExpressionFactory, typeMappingSource), new SqlServerStringMemberTranslator(sqlExpressionFactory), new SqlServerTimeSpanMemberTranslator(sqlExpressionFactory), - new SqlServerTimeOnlyMemberTranslator(sqlExpressionFactory) + new SqlServerTimeOnlyMemberTranslator(sqlExpressionFactory), + new SqlServerVectorTranslator(sqlExpressionFactory, typeMappingSource) ]); } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs index 79a99b8c437..d9952444e5a 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs @@ -42,7 +42,8 @@ public SqlServerMethodCallTranslatorProvider( new SqlServerNewGuidTranslator(sqlExpressionFactory), new SqlServerObjectToStringTranslator(sqlExpressionFactory, typeMappingSource), new SqlServerStringMethodTranslator(sqlExpressionFactory, sqlServerSingletonOptions), - new SqlServerTimeOnlyMethodTranslator(sqlExpressionFactory) + new SqlServerTimeOnlyMethodTranslator(sqlExpressionFactory), + new SqlServerVectorTranslator(sqlExpressionFactory, typeMappingSource) ]); } } diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerVectorTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerVectorTranslator.cs new file mode 100644 index 00000000000..3e53d8baacf --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerVectorTranslator.cs @@ -0,0 +1,97 @@ +// 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.Query.SqlExpressions; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// 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. +/// +public class SqlServerVectorTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + : IMethodCallTranslator, IMemberTranslator +{ + /// + /// 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. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(SqlServerDbFunctionsExtensions)) + { + switch (method.Name) + { + case nameof(SqlServerDbFunctionsExtensions.VectorDistance) + when arguments is [_, var distanceMetric, var vector1, var vector2]: + { + var vectorTypeMapping = vector1.TypeMapping ?? vector2.TypeMapping + ?? throw new InvalidOperationException( + "One of the arguments to EF.Functions.VectorDistance must be a vector column."); + + return sqlExpressionFactory.Function( + "VECTOR_DISTANCE", + [ + sqlExpressionFactory.ApplyTypeMapping(distanceMetric, typeMappingSource.FindMapping("varchar(max)")), + sqlExpressionFactory.ApplyTypeMapping(vector1, vectorTypeMapping), + sqlExpressionFactory.ApplyTypeMapping(vector2, vectorTypeMapping) + ], + nullable: true, + argumentsPropagateNullability: [true, true, true], + typeof(double), + typeMappingSource.FindMapping(typeof(double))); + } + } + } + + return null; + } + + /// + /// 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. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + if (member.DeclaringType == typeof(SqlVector)) + { + switch (member.Name) + { + case nameof(SqlVector<>.Length) when instance is not null: + { + return sqlExpressionFactory.Function( + "VECTORPROPERTY", + [ + instance, + sqlExpressionFactory.Constant("Dimensions", typeMappingSource.FindMapping("varchar(max)")) + ], + nullable: true, + argumentsPropagateNullability: [true, true], + typeof(int), + typeMappingSource.FindMapping(typeof(int))); + } + } + } + + return null; + } +} + diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs index 4b6fde64fdf..78de96ff183 100644 --- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs @@ -395,7 +395,7 @@ FROM [sys].[types] AS [t] var precision = reader.GetValueOrDefault("precision"); var scale = reader.GetValueOrDefault("scale"); - var storeType = GetStoreType(systemType, maxLength, precision, scale); + var storeType = GetStoreType(systemType, maxLength, precision, scale, vectorDimensions: 0); _logger.TypeAliasFound(DisplayName(schema, userType), storeType); @@ -472,7 +472,7 @@ FROM [sys].[sequences] AS [s] storeType = value.storeType; } - storeType = GetStoreType(storeType, maxLength: 0, precision: precision, scale: scale); + storeType = GetStoreType(storeType, maxLength: 0, precision, scale, vectorDimensions: 0); _logger.SequenceFound(DisplayName(schema, name), storeType, cyclic, incrementBy, startValue, minValue, maxValue); @@ -730,6 +730,7 @@ private void GetColumns( CAST([c].[max_length] AS int) AS [max_length], CAST([c].[precision] AS int) AS [precision], CAST([c].[scale] AS int) AS [scale], + {(_compatibilityLevel is >= 170 ? "[c].[vector_dimensions]" : "NULL as [vector_dimensions]")}, [c].[is_nullable], [c].[is_identity], [dc].[definition] AS [default_sql], @@ -801,6 +802,7 @@ FROM [sys].[views] v var maxLength = dataRecord.GetValueOrDefault("max_length"); var precision = dataRecord.GetValueOrDefault("precision"); var scale = dataRecord.GetValueOrDefault("scale"); + var vectorDimensions = dataRecord.GetValueOrDefault("vector_dimensions"); var nullable = dataRecord.GetValueOrDefault("is_nullable"); var isIdentity = dataRecord.GetValueOrDefault("is_identity"); var defaultValueSql = dataRecord.GetValueOrDefault("default_sql"); @@ -835,15 +837,19 @@ FROM [sys].[views] v string storeType; string systemTypeName; - // Swap store type if type alias is used - if (typeAliases.TryGetValue($"[{dataTypeSchemaName}].[{dataTypeName}]", out var value)) + // If the store type is in our loaded aliases dictionary, resolve to the canonical type. + // Note that the vector type is implemented as an alias for varbinary, but we do not want + // to scaffold vectors as varbinary. + var fullQualifiedTypeName = $"[{dataTypeSchemaName}].[{dataTypeName}]"; + if (fullQualifiedTypeName is not "[sys].[vector]" + && typeAliases.TryGetValue(fullQualifiedTypeName, out var value)) { storeType = value.storeType; systemTypeName = value.typeName; } else { - storeType = GetStoreType(dataTypeName, maxLength, precision, scale); + storeType = GetStoreType(dataTypeName, maxLength, precision, scale, vectorDimensions); systemTypeName = dataTypeName; } @@ -995,16 +1001,16 @@ void Unwrap() } } - private static string GetStoreType(string dataTypeName, int maxLength, int precision, int scale) + private static string GetStoreType(string dataTypeName, int maxLength, int precision, int scale, int vectorDimensions) { - if (dataTypeName == "timestamp") + switch (dataTypeName) { - return "rowversion"; - } - - if (dataTypeName is "decimal" or "numeric") - { - return $"{dataTypeName}({precision}, {scale})"; + case "timestamp": + return "rowversion"; + case "decimal" or "numeric": + return $"{dataTypeName}({precision}, {scale})"; + case "vector": + return $"vector({vectorDimensions})"; } if (DateTimePrecisionTypes.Contains(dataTypeName) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index b89cfbc9f60..1b0bbdb08a8 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -3,6 +3,8 @@ using System.Collections; using System.Data; +using Microsoft.Data.SqlTypes; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -205,6 +207,7 @@ static SqlServerTypeMappingSource() { "varbinary(max)", [VariableLengthMaxBinary] }, { "varchar", [SqlServerStringTypeMapping.Default] }, { "varchar(max)", [VariableLengthMaxAnsiString] }, + { "vector", [SqlServerVectorTypeMapping.Default] }, { "xml", [Xml] } }; // ReSharper restore CoVariantArrayConversion @@ -304,62 +307,54 @@ public SqlServerTypeMappingSource( return mapping; } - if (clrType == typeof(ulong) && mappingInfo.IsRowVersion == true) + switch (clrType) { - return UlongRowversion; - } + case Type t when t == typeof(ulong) && mappingInfo.IsRowVersion is true: + return UlongRowversion; - if (clrType == typeof(long) && mappingInfo.IsRowVersion == true) - { - return LongRowversion; - } + case Type t when t == typeof(long) && mappingInfo.IsRowVersion is true: + return LongRowversion; - if (clrType == typeof(string)) - { - if (storeTypeName == "json") - { - return SqlServerStringTypeMapping.JsonTypeDefault; - } + case Type t when t == typeof(byte[]) && mappingInfo.IsRowVersion is true: + return Rowversion; - var isAnsi = mappingInfo.IsUnicode == false; - var isFixedLength = mappingInfo.IsFixedLength == true; - var maxSize = isAnsi ? 8000 : 4000; + case Type t when t == typeof(string) && storeTypeName == "json": + return SqlServerStringTypeMapping.JsonTypeDefault; - var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? isAnsi ? 900 : 450 : null); - if (size < 0 || size > maxSize) + case Type t when t == typeof(string): { - size = isFixedLength ? maxSize : null; - } + var isAnsi = mappingInfo.IsUnicode == false; + var isFixedLength = mappingInfo.IsFixedLength == true; + var maxSize = isAnsi ? 8000 : 4000; - if (size == null - && storeTypeName == null - && !mappingInfo.IsKeyOrIndex) - { - return isAnsi - ? isFixedLength - ? FixedLengthAnsiString - : VariableLengthMaxAnsiString - : isFixedLength - ? FixedLengthUnicodeString - : VariableLengthMaxUnicodeString; - } + var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? isAnsi ? 900 : 450 : null); + if (size < 0 || size > maxSize) + { + size = isFixedLength ? maxSize : null; + } - return new SqlServerStringTypeMapping( - unicode: !isAnsi, - size: size, - fixedLength: isFixedLength, - storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None, - useKeyComparison: mappingInfo.IsKey); - } + if (size == null + && storeTypeName == null + && !mappingInfo.IsKeyOrIndex) + { + return isAnsi + ? isFixedLength + ? FixedLengthAnsiString + : VariableLengthMaxAnsiString + : isFixedLength + ? FixedLengthUnicodeString + : VariableLengthMaxUnicodeString; + } - if (clrType == typeof(byte[])) - { - if (mappingInfo.IsRowVersion == true) - { - return Rowversion; + return new SqlServerStringTypeMapping( + unicode: !isAnsi, + size: size, + fixedLength: isFixedLength, + storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None, + useKeyComparison: mappingInfo.IsKey); } - if (mappingInfo.ElementTypeMapping == null) + case Type t when t == typeof(byte[]) && mappingInfo.ElementTypeMapping is null: { var isFixedLength = mappingInfo.IsFixedLength == true; @@ -376,6 +371,9 @@ public SqlServerTypeMappingSource( fixedLength: isFixedLength, storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None); } + + case Type t when t == typeof(SqlVector): + return new SqlServerVectorTypeMapping(mappingInfo.Size); } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs new file mode 100644 index 00000000000..b3c627f39a9 --- /dev/null +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Data.SqlTypes; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; + +/// +/// 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. +/// +public class SqlServerVectorTypeMapping : RelationalTypeMapping +{ + /// + /// 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. + /// + public static SqlServerVectorTypeMapping Default { get; } = new(dimensions: null); + + private static readonly VectorComparer _comparerInstance = new(); + + // 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 + // without dimensions, and validate against it in SqlServerModelValidator. + + /// + /// 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. + /// + public SqlServerVectorTypeMapping(int? dimensions) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(SqlVector), comparer: _comparerInstance), + "vector", + StoreTypePostfix.Size, + size: dimensions)) + { + if (dimensions is <= 0) + { + throw new InvalidOperationException(SqlServerStrings.VectorDimensionsInvalid); + } + } + + /// + /// 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. + /// + protected SqlServerVectorTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + if (parameters.Size is <= 0) + { + throw new InvalidOperationException(SqlServerStrings.VectorDimensionsInvalid); + } + } + + /// + /// 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. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SqlServerVectorTypeMapping(parameters); + + /// + /// 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. + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + Check.DebugAssert(Size is not null); + + var sqlVector = (SqlVector)value; + + if (sqlVector.IsNull) + { + return "NULL"; + } + + // SQL Server has an implicit cast from JSON arrays (as strings or as the json type) to vector - + // that's the literal representation (though use-cases are probably mostly contrived/testing-only). + var builder = new StringBuilder(); + var floats = sqlVector.Memory.Span; + + builder.Append("CAST('["); + + for (var i = 0; i < floats.Length; i++) + { + if (i > 0) + { + builder.Append(','); + } + + builder.Append(floats[i]); + } + + builder + .Append("]' AS VECTOR(") + .Append(Size) + .Append("))"); + + return builder.ToString(); + } + + private sealed class VectorComparer() : ValueComparer>( + (x, y) => CalculateEquality(x, y), + v => CalculateHashCode(v), + v => v) + { + // Note that we do not perform value comparison here, only checking that the SqlVector wraps the same memory. + // This is because vectors are basically immutable, and it's better to have more efficient change tracking + // equality checks. + private static bool CalculateEquality(SqlVector? x, SqlVector? y) + => x is null + ? y is null + : y is not null && (x.IsNull + ? y.IsNull + : !y.IsNull && x.Memory.Span == y.Memory.Span); + + private static int CalculateHashCode(SqlVector vector) + { + if (vector.IsNull) + { + return 0; + } + + var hash = new HashCode(); + + foreach (var value in vector.Memory.Span) + { + hash.Add(value); + } + + return hash.ToHashCode(); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/VectorTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/VectorTranslationsSqlServerTest.cs new file mode 100644 index 00000000000..d795d499ef2 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/VectorTranslationsSqlServerTest.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.Data.SqlTypes; + +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +[SqlServerCondition(SqlServerCondition.SupportsVectorType)] +public class VectorTranslationsSqlServerTest : IClassFixture +{ + private VectorQueryFixture Fixture { get; } + + public VectorTranslationsSqlServerTest(VectorQueryFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public async Task VectorDistance_with_parameter() + { + using var ctx = CreateContext(); + + var vector = new SqlVector(new float[] { 1, 2, 100 }); + var results = await ctx.VectorEntities + .OrderBy(v => EF.Functions.VectorDistance("cosine", v.Vector, vector)) + .Take(1) + .ToListAsync(); + + Assert.Equal(2, results.Single().Id); + + AssertSql( + """ +@p='1' +@vector='Microsoft.Data.SqlTypes.SqlVector`1[System.Single]' (Size = 20) (DbType = Binary) + +SELECT TOP(@p) [v].[Id], [v].[Vector] +FROM [VectorEntities] AS [v] +ORDER BY VECTOR_DISTANCE('cosine', [v].[Vector], @vector) +"""); + } + + [ConditionalFact] + public async Task VectorDistance_with_constant() + { + using var ctx = CreateContext(); + + var results = await ctx.VectorEntities + .OrderBy(v => EF.Functions.VectorDistance("cosine", v.Vector, new SqlVector(new float[] { 1, 2, 100 }))) + .Take(1) + .ToListAsync(); + + Assert.Equal(2, results.Single().Id); + + AssertSql( + """ +@p='1' + +SELECT TOP(@p) [v].[Id], [v].[Vector] +FROM [VectorEntities] AS [v] +ORDER BY VECTOR_DISTANCE('cosine', [v].[Vector], CAST('[1,2,100]' AS VECTOR(3))) +"""); + } + + [ConditionalFact] + public async Task Length() + { + using var ctx = CreateContext(); + + var count = await ctx.VectorEntities + .Where(v => v.Vector.Length == 3) + .CountAsync(); + + using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + { + Assert.Equal(await ctx.VectorEntities.CountAsync(), count); + } + + AssertSql( + """ +SELECT COUNT(*) +FROM [VectorEntities] AS [v] +WHERE VECTORPROPERTY([v].[Vector], 'Dimensions') = 3 +"""); + } + + protected VectorQueryContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class VectorQueryContext(DbContextOptions options) : PoolableDbContext(options) + { + public DbSet VectorEntities { get; set; } = null!; + + public static async Task SeedAsync(VectorQueryContext context) + { + var vectorEntities = new VectorEntity[] + { + new() { Id = 1, Vector = new(new float[] { 1, 2, 3 }) }, + new() { Id = 2, Vector = new(new float[] { 1, 2, 100 }) }, + new() { Id = 3, Vector = new(new float[] { 1, 2, 1000 }) } + }; + + context.VectorEntities.AddRange(vectorEntities); + await context.SaveChangesAsync(); + } + } + + public class VectorEntity + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + [Column(TypeName = "vector(3)")] + public SqlVector Vector { get; set; } = null!; + } + + public class VectorQueryFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "VectorQueryTest"; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override Task SeedAsync(VectorQueryContext context) + => VectorQueryContext.SeedAsync(context); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs index 0be424ecf12..499bc3faa52 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; // ReSharper disable InconsistentNaming @@ -5726,6 +5727,32 @@ FOREIGN KEY (ForeignKeyId) REFERENCES PrincipalTable(Id) ON DELETE SET NULL, #endregion + #region Types + + [ConditionalFact] + [SqlServerCondition(SqlServerCondition.SupportsVectorType)] + public void Vector_type() + => Test( + "CREATE TABLE [dbo].[VectorTable] (vector VECTOR(3))", + tables: [], + schemas: [], + (dbModel, scaffoldingFactory) => + { + var table = Assert.Single(dbModel.Tables); + var column = Assert.Single(table.Columns); + Assert.Equal("vector", column.Name); + Assert.Equal("vector(3)", column.StoreType); + + var model = scaffoldingFactory.Create(dbModel, new ModelReverseEngineerOptions()); + var entityType = Assert.Single(model.GetEntityTypes()); + var property = Assert.Single(entityType.GetProperties()); + Assert.Equal("Vector", property.Name); + Assert.True(property.GetTypeMapping() is SqlServerVectorTypeMapping { Size: 3 }); + }, + "DROP TABLE [dbo].[VectorTable]"); + + #endregion + #region Warnings [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs index 24b17c653ef..791720b9abf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs @@ -22,4 +22,5 @@ public enum SqlServerCondition SupportsFunctions2019 = 1 << 13, SupportsFunctions2022 = 1 << 14, SupportsJsonType = 1 << 15, + SupportsVectorType = 1 << 15, } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs index f2c4c635fb0..1c5ec540b8f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs @@ -97,6 +97,11 @@ public ValueTask IsMetAsync() isMet &= TestEnvironment.IsJsonTypeSupported; } + if (Conditions.HasFlag(SqlServerCondition.SupportsVectorType)) + { + isMet &= TestEnvironment.IsVectorTypeSupported; + } + return ValueTask.FromResult(isMet); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs index 34e7e76ab28..f31640344ab 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs @@ -43,6 +43,8 @@ public static class TestEnvironment private static bool? _supportsJsonPathExpressions; + private static bool? _isVectorTypeSupported; + private static bool? _supportsFunctions2017; private static bool? _supportsFunctions2019; @@ -402,6 +404,33 @@ public static bool IsFunctions2022Supported public static bool IsJsonTypeSupported => false; + public static bool IsVectorTypeSupported + { + get + { + if (!IsConfigured) + { + return false; + } + + if (_isVectorTypeSupported.HasValue) + { + return _isVectorTypeSupported.Value; + } + + try + { + _isVectorTypeSupported = GetProductMajorVersion() >= 17 || IsSqlAzure; + } + catch (PlatformNotSupportedException) + { + _isVectorTypeSupported = false; + } + + return _isVectorTypeSupported.Value; + } + } + public static byte SqlServerMajorVersion => GetProductMajorVersion(); diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 6660e3f221a..0fa763c3ad9 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -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.ComponentModel.DataAnnotations.Schema; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Internal; @@ -965,6 +967,8 @@ public void DefaultValue_with_implicit_constraint_name_throws_for_TPC() modelBuilder); } + #region Temporal tables + [ConditionalFact] public void Temporal_can_only_be_specified_on_root_entities() { @@ -1158,6 +1162,57 @@ public void Temporal_table_with_owned_with_explicit_precision_on_period_columns_ Assert.Equal(2, ownedEntity.FindProperty("End").GetPrecision()); } + #endregion Temporal tables + + #region Vector + + [ConditionalFact] + public virtual void Throws_for_vector_property_without_dimensions() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(); + + VerifyError( + SqlServerStrings.VectorDimensionsMissing(nameof(VectorWithoutDimensionsEntity), nameof(VectorWithoutDimensionsEntity.Vector)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Throws_for_vector_property_inside_JSON() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity().OwnsOne(v => v.VectorContainer, n => + { + n.ToJson(); + n.Property(v => v.Vector).HasMaxLength(3); + }); + + VerifyError( + SqlServerStrings.VectorPropertiesNotSupportedInJson(nameof(VectorContainer), nameof(VectorContainer.Vector)), + modelBuilder); + } + + public class VectorWithoutDimensionsEntity + { + public int Id { get; set; } + public SqlVector Vector { get; set; } + } + + public class VectorInsideJsonEntity + { + public int Id { get; set; } + public VectorContainer VectorContainer { get; set; } + } + + public class VectorContainer + { + public SqlVector Vector { get; set; } + } + + #endregion Vector + public class Human { public int Id { get; set; } diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs index ce3ef9ee209..28427209481 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; // ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore; +namespace Microsoft.EntityFrameworkCore.Storage; public class SqlServerTypeMappingSourceTest : RelationalTypeMappingSourceTestBase { @@ -1329,6 +1331,7 @@ public void Throws_for_unrecognized_property_types() [InlineData("varchar(333)", typeof(string), 333, false, false)] [InlineData("varchar(max)", typeof(string), -1, false, false)] [InlineData("VARCHAR(max)", typeof(string), -1, false, false, "VARCHAR(max)")] + [InlineData("vector(3)", typeof(SqlVector), 3, false, false)] public void Can_map_by_store_type(string storeType, Type type, int? size, bool unicode, bool fixedLength, string expectedType = null) { var mapping = CreateRelationalTypeMappingSource(CreateModel()).FindMapping(storeType); @@ -1346,6 +1349,7 @@ public void Can_map_by_store_type(string storeType, Type type, int? size, bool u [InlineData(typeof(DateTime), "date")] [InlineData(typeof(TimeOnly), "time")] [InlineData(typeof(TimeSpan), "time")] + [InlineData(typeof(SqlVector), "vector(3)")] public void Can_map_by_clr_and_store_types(Type clrType, string storeType) { var mapping = CreateRelationalTypeMappingSource(CreateModel()).FindMapping(clrType, storeType); @@ -1762,6 +1766,37 @@ public void String_FK_unicode_is_preferred_if_specified() mapper.GetMapping(model.FindEntityType(typeof(MyRelatedType4)).FindProperty("Relationship2Id")).StoreType); } + #region Vector + + [ConditionalFact] + public void Vector_is_properly_mapped() + { + var typeMapping = GetTypeMapping(typeof(SqlVector), maxLength: 3); + + Assert.Null(typeMapping.DbType); + Assert.Equal("vector(3)", typeMapping.StoreType); + Assert.Equal(3, typeMapping.Size); + Assert.Null(typeMapping.Precision); + Assert.Null(typeMapping.Scale); + } + + [ConditionalFact] + public void Vector_requires_positive_dimensions() + { + var exception = Assert.Throws(() => GetTypeMapping(typeof(SqlVector), maxLength: 0)); + Assert.Equal(SqlServerStrings.VectorDimensionsInvalid, exception.Message); + + exception = Assert.Throws(() => GetTypeMapping(typeof(SqlVector), maxLength: -1)); + Assert.Equal(SqlServerStrings.VectorDimensionsInvalid, exception.Message); + + // We do allow constructing a vector type mapping with no dimensions, since the scaffolder requires it + // (see comment in SqlServerVectorTypeMapping) + var typeMapping = GetTypeMapping(typeof(SqlVector)); + Assert.Null(typeMapping.Size); + } + + #endregion Vector + [ConditionalFact] public void Plugins_can_override_builtin_mappings() { @@ -1769,7 +1804,7 @@ public void Plugins_can_override_builtin_mappings() TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create() with { - Plugins = new[] { new FakeTypeMappingSourcePlugin() } + Plugins = [new FakeTypeMappingSourcePlugin()] }); Assert.Equal("String", typeMappingSource.GetMapping("datetime2").ClrType.Name); diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs index e7c0d89a840..e357123ecea 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; -using System.Globalization; using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -408,6 +408,24 @@ public virtual void TimeOnly_code_literal_generated_correctly() "new TimeOnly(12, 30, 10, 500).Add(TimeSpan.FromTicks(10))"); } + #region Vector + + [ConditionalFact] + public virtual void Vector_comparer_compares_Memory() + { + var typeMapping = new SqlServerVectorTypeMapping(3); + + float[] array = [1, 2, 3]; + var vector1 = new SqlVector(array); + var vector2 = new SqlVector(array); + var vector3 = new SqlVector(new float[] { 1, 2, 3 }); + + Assert.True(typeMapping.Comparer.Equals(vector1, vector2)); + Assert.False(typeMapping.Comparer.Equals(vector1, vector3)); + } + + #endregion Vector + public static RelationalTypeMapping GetMapping(string type) => GetTypeMappingSource().FindMapping(type); @@ -430,146 +448,6 @@ protected virtual void Test_GenerateCodeLiteral_helper( Assert.Equal(expectedCode, csharpHelper.UnknownLiteral(value)); } - private class FakeType(string fullName) : Type - { - public override object[] GetCustomAttributes(bool inherit) - => throw new NotImplementedException(); - - public override bool IsDefined(Type attributeType, bool inherit) - => throw new NotImplementedException(); - - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type GetInterface(string name, bool ignoreCase) - => throw new NotImplementedException(); - - public override Type[] GetInterfaces() - => throw new NotImplementedException(); - - public override EventInfo GetEvent(string name, BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type GetNestedType(string name, BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type GetElementType() - => throw new NotImplementedException(); - - protected override bool HasElementTypeImpl() - => throw new NotImplementedException(); - - protected override PropertyInfo GetPropertyImpl( - string name, - BindingFlags bindingAttr, - Binder binder, - Type returnType, - Type[] types, - ParameterModifier[] modifiers) - => throw new NotImplementedException(); - - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - protected override MethodInfo GetMethodImpl( - string name, - BindingFlags bindingAttr, - Binder binder, - CallingConventions callConvention, - Type[] types, - ParameterModifier[] modifiers) - => throw new NotImplementedException(); - - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override FieldInfo GetField(string name, BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - protected override TypeAttributes GetAttributeFlagsImpl() - => throw new NotImplementedException(); - - protected override bool IsArrayImpl() - => throw new NotImplementedException(); - - protected override bool IsByRefImpl() - => throw new NotImplementedException(); - - protected override bool IsPointerImpl() - => throw new NotImplementedException(); - - protected override bool IsPrimitiveImpl() - => throw new NotImplementedException(); - - protected override bool IsCOMObjectImpl() - => throw new NotImplementedException(); - - public override object InvokeMember( - string name, - BindingFlags invokeAttr, - Binder binder, - object target, - object[] args, - ParameterModifier[] modifiers, - CultureInfo culture, - string[] namedParameters) - => throw new NotImplementedException(); - - public override Type UnderlyingSystemType { get; } - - protected override ConstructorInfo GetConstructorImpl( - BindingFlags bindingAttr, - Binder binder, - CallingConventions callConvention, - Type[] types, - ParameterModifier[] modifiers) - => throw new NotImplementedException(); - - public override string Name - => throw new NotImplementedException(); - - public override Guid GUID - => throw new NotImplementedException(); - - public override Module Module - => throw new NotImplementedException(); - - public override Assembly Assembly - => throw new NotImplementedException(); - - public override string Namespace - => throw new NotImplementedException(); - - public override string AssemblyQualifiedName - => throw new NotImplementedException(); - - public override Type BaseType - => throw new NotImplementedException(); - - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => throw new NotImplementedException(); - - public override string FullName { get; } = fullName; - - public override int GetHashCode() - => FullName.GetHashCode(); - - public override bool Equals(object o) - => ReferenceEquals(this, o); - } - protected override DbContextOptions ContextOptions { get; } = new DbContextOptionsBuilder() .UseInternalServiceProvider(SqlServerFixture.DefaultServiceProvider) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy