diff --git a/src/EFCore.Analyzers/EFDiagnostics.cs b/src/EFCore.Analyzers/EFDiagnostics.cs index acedc129899..c3021424089 100644 --- a/src/EFCore.Analyzers/EFDiagnostics.cs +++ b/src/EFCore.Analyzers/EFDiagnostics.cs @@ -18,4 +18,5 @@ public static class EFDiagnostics public const string PrecompiledQueryExperimental = "EF9100"; public const string MetricsExperimental = "EF9101"; public const string PagingExperimental = "EF9102"; + public const string CosmosVectorSearchExperimental = "EF9103"; } diff --git a/src/EFCore.Cosmos/EFCore.Cosmos.csproj b/src/EFCore.Cosmos/EFCore.Cosmos.csproj index 133479d9c1d..4b064e25562 100644 --- a/src/EFCore.Cosmos/EFCore.Cosmos.csproj +++ b/src/EFCore.Cosmos/EFCore.Cosmos.csproj @@ -11,6 +11,7 @@ true $(NoWarn);EF9101 $(NoWarn);EF9102 + $(NoWarn);EF9103 diff --git a/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs index 94a5b2d50af..f562fa59c36 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.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.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Cosmos.Extensions; /// @@ -47,4 +49,145 @@ public static T CoalesceUndefined( T expression1, T expression2) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CoalesceUndefined))); + + /// + /// Returns the distance between two vectors, using the distance function and data type defined using + /// . + /// + /// The instance. + /// The first vector. + /// The second vector. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance(this DbFunctions _, ReadOnlyMemory vector1, ReadOnlyMemory vector2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, given a distance function (aka similarity measure). + /// + /// The instance. + /// The first vector. + /// The second vector. + /// A specifying how the computed value is used in an ORDER BY + /// expression. If , then brute force is used, otherwise any index defined on the vector + /// property is leveraged. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance( + this DbFunctions _, + ReadOnlyMemory vector1, + ReadOnlyMemory vector2, + [NotParameterized] bool useBruteForce) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, given a distance function (aka similarity measure). + /// + /// The instance. + /// The first vector. + /// The second vector. + /// The distance function to use. + /// A specifying how the computed value is used in an ORDER BY + /// expression. If , then brute force is used, otherwise any index defined on the vector + /// property is leveraged. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance( + this DbFunctions _, + ReadOnlyMemory vector1, + ReadOnlyMemory vector2, + [NotParameterized] bool useBruteForce, + [NotParameterized] DistanceFunction distanceFunction) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, using the distance function and data type defined using + /// . + /// + /// The instance. + /// The first vector. + /// The second vector. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance(this DbFunctions _, ReadOnlyMemory vector1, ReadOnlyMemory vector2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, given a distance function (aka similarity measure). + /// + /// The instance. + /// The first vector. + /// The second vector. + /// A specifying how the computed value is used in an ORDER BY + /// expression. If , then brute force is used, otherwise any index defined on the vector + /// property is leveraged. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance( + this DbFunctions _, + ReadOnlyMemory vector1, + ReadOnlyMemory vector2, + [NotParameterized] bool useBruteForce) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, given a distance function (aka similarity measure). + /// + /// The instance. + /// The first vector. + /// The second vector. + /// The distance function to use. + /// A specifying how the computed value is used in an ORDER BY + /// expression. If , then brute force is used, otherwise any index defined on the vector + /// property is leveraged. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance( + this DbFunctions _, + ReadOnlyMemory vector1, + ReadOnlyMemory vector2, + [NotParameterized] bool useBruteForce, + [NotParameterized] DistanceFunction distanceFunction) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, using the distance function and data type defined using + /// . + /// + /// The instance. + /// The first vector. + /// The second vector. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance(this DbFunctions _, ReadOnlyMemory vector1, ReadOnlyMemory vector2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, given a distance function (aka similarity measure). + /// + /// The instance. + /// The first vector. + /// The second vector. + /// A specifying how the computed value is used in an ORDER BY + /// expression. If , then brute force is used, otherwise any index defined on the vector + /// property is leveraged. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance( + this DbFunctions _, + ReadOnlyMemory vector1, + ReadOnlyMemory vector2, + [NotParameterized] bool useBruteForce) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + /// + /// Returns the distance between two vectors, given a distance function (aka similarity measure). + /// + /// The instance. + /// The first vector. + /// The second vector. + /// The distance function to use. + /// A specifying how the computed value is used in an ORDER BY + /// expression. If , then brute force is used, otherwise any index defined on the vector + /// property is leveraged. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static double VectorDistance( + this DbFunctions _, + ReadOnlyMemory vector1, + ReadOnlyMemory vector2, + [NotParameterized] bool useBruteForce, + [NotParameterized] DistanceFunction distanceFunction) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); } diff --git a/src/EFCore.Cosmos/Extensions/CosmosIndexBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosIndexBuilderExtensions.cs new file mode 100644 index 00000000000..ed0a40b8ce7 --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/CosmosIndexBuilderExtensions.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Azure Cosmos DB-specific extension methods for . +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing Azure Cosmos DB with EF Core for more information and examples. +/// +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +public static class CosmosIndexBuilderExtensions +{ + /// + /// Configures the index as a vector index with the given vector index type, such as "flat", "diskANN", or "quantizedFlat". + /// See Vector Search in Azure Cosmos DB for NoSQL for more information. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The builder for the index being configured. + /// The type of vector index to create. + /// A builder to further configure the index. + public static IndexBuilder ForVectors(this IndexBuilder indexBuilder, VectorIndexType? indexType) + { + indexBuilder.Metadata.SetVectorIndexType(indexType); + + return indexBuilder; + } + + /// + /// Configures whether the index as a vector index with the given vector index type, such as "flat", "diskANN", or "quantizedFlat". + /// See Vector Search in Azure Cosmos DB for NoSQL for more information. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The builder for the index being configured. + /// The type of vector index to create. + /// A builder to further configure the index. + public static IndexBuilder ForVectors( + this IndexBuilder indexBuilder, + VectorIndexType? indexType) + => (IndexBuilder)ForVectors((IndexBuilder)indexBuilder, indexType); + + /// + /// Configures whether the index as a vector index with the given vector index type, such as "flat", "diskANN", or "quantizedFlat". + /// See Vector Search in Azure Cosmos DB for NoSQL for more information. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The builder for the index being configured. + /// The type of vector index to create. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionIndexBuilder? ForVectors( + this IConventionIndexBuilder indexBuilder, + VectorIndexType? indexType, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetVectorIndexType(indexType, fromDataAnnotation)) + { + indexBuilder.Metadata.SetVectorIndexType(indexType, fromDataAnnotation); + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the vector index can be configured for vectors. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The builder for the index being configured. + /// The index type to use. + /// Indicates whether the configuration was specified using a data annotation. + /// if the index can be configured for vectors. + public static bool CanSetVectorIndexType( + this IConventionIndexBuilder indexBuilder, + VectorIndexType? indexType, + bool fromDataAnnotation = false) + => indexBuilder.CanSetAnnotation(CosmosAnnotationNames.VectorIndexType, indexType, fromDataAnnotation); +} diff --git a/src/EFCore.Cosmos/Extensions/CosmosIndexExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosIndexExtensions.cs new file mode 100644 index 00000000000..9692a128068 --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/CosmosIndexExtensions.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Index extension methods for Azure Cosmos DB-specific metadata. +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing Azure Cosmos DB with EF Core for more information and examples. +/// +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +public static class CosmosIndexExtensions +{ + /// + /// Returns the vector index type to use, such as "flat", "diskANN", or "quantizedFlat". + /// See Vector Search in Azure Cosmos DB for NoSQL for more information. + /// + /// The index. + /// The index type to use, or if none is set. + public static VectorIndexType? GetVectorIndexType(this IReadOnlyIndex index) + => (index is RuntimeIndex) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (VectorIndexType?)index[CosmosAnnotationNames.VectorIndexType]; + + /// + /// Sets the vector index type to use, such as "flat", "diskANN", or "quantizedFlat". + /// See Vector Search in Azure Cosmos DB for NoSQL for more information. + /// + /// The index. + /// The index type to use. + public static void SetVectorIndexType(this IMutableIndex index, VectorIndexType? indexType) + => index.SetAnnotation(CosmosAnnotationNames.VectorIndexType, indexType); + + /// + /// Sets the vector index type to use, such as "flat", "diskANN", or "quantizedFlat". + /// See Vector Search in Azure Cosmos DB for NoSQL for more information. + /// + /// The index type to use. + /// The index. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetVectorIndexType( + this IConventionIndex index, + VectorIndexType? indexType, + bool fromDataAnnotation = false) + => (string?)index.SetAnnotation( + CosmosAnnotationNames.VectorIndexType, + indexType, + fromDataAnnotation)?.Value; + + /// + /// Returns the for whether the . + /// + /// The property. + /// The for whether the index is clustered. + public static ConfigurationSource? GetVectorIndexTypeConfigurationSource(this IConventionIndex property) + => property.FindAnnotation(CosmosAnnotationNames.VectorIndexType)?.GetConfigurationSource(); +} diff --git a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs index 61da371ef3a..3d8618e3d7f 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs @@ -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 System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -104,6 +105,105 @@ public static bool CanSetJsonProperty( bool fromDataAnnotation = false) => propertyBuilder.CanSetAnnotation(CosmosAnnotationNames.PropertyName, name, fromDataAnnotation); + /// + /// Configures the property as a vector for Azure Cosmos DB. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The distance function for a vector comparisons. + /// The number of dimensions in the vector. + /// The same builder instance so that multiple calls can be chained. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static PropertyBuilder IsVector( + this PropertyBuilder propertyBuilder, + DistanceFunction distanceFunction, + int dimensions) + { + propertyBuilder.Metadata.SetVectorType(CreateVectorType(distanceFunction, dimensions)); + return propertyBuilder; + } + + /// + /// Configures the property as a vector for Azure Cosmos DB. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The distance function for a vector comparisons. + /// The number of dimensions in the vector. + /// The same builder instance so that multiple calls can be chained. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static PropertyBuilder IsVector( + this PropertyBuilder propertyBuilder, + DistanceFunction distanceFunction, + int dimensions) + => (PropertyBuilder)IsVector((PropertyBuilder)propertyBuilder, distanceFunction, dimensions); + + /// + /// Configures the property as a vector for Azure Cosmos DB. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The distance function for a vector comparisons. + /// The number of dimensions in the vector. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static IConventionPropertyBuilder? IsVector( + this IConventionPropertyBuilder propertyBuilder, + DistanceFunction distanceFunction, + int dimensions, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetIsVector(distanceFunction, dimensions, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetVectorType(CreateVectorType(distanceFunction, dimensions), fromDataAnnotation); + + return propertyBuilder; + } + + /// + /// Returns a value indicating whether the vector type can be set. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The distance function for a vector comparisons. + /// The number of dimensions in the vector. + /// Indicates whether the configuration was specified using a data annotation. + /// if the vector type can be set. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static bool CanSetIsVector( + this IConventionPropertyBuilder propertyBuilder, + DistanceFunction distanceFunction, + int dimensions, + bool fromDataAnnotation = false) + => propertyBuilder.CanSetAnnotation( + CosmosAnnotationNames.VectorType, + CreateVectorType(distanceFunction, dimensions), + fromDataAnnotation); + /// /// Configures this property to be the etag concurrency token. /// @@ -136,4 +236,11 @@ public static PropertyBuilder IsETagConcurrency(this PropertyBuilder propertyBui public static PropertyBuilder IsETagConcurrency( this PropertyBuilder propertyBuilder) => (PropertyBuilder)IsETagConcurrency((PropertyBuilder)propertyBuilder); + + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + private static CosmosVectorType CreateVectorType(DistanceFunction distanceFunction, int dimensions) + => Enum.IsDefined(distanceFunction) + ? new CosmosVectorType(distanceFunction, dimensions) + : throw new ArgumentException( + CoreStrings.InvalidEnumValue(distanceFunction, nameof(distanceFunction), typeof(DistanceFunction))); } diff --git a/src/EFCore.Cosmos/Extensions/CosmosPropertyExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosPropertyExtensions.cs index a397598c506..c4e0cb43466 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosPropertyExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosPropertyExtensions.cs @@ -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 System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -73,12 +74,55 @@ public static void SetJsonPropertyName(this IMutableProperty property, string? n fromDataAnnotation)?.Value; /// - /// Gets the the property name that the property is mapped to when targeting Cosmos. + /// Gets the for the property name that the property is mapped to when targeting Cosmos. /// /// The property. /// - /// The the property name that the property is mapped to when targeting Cosmos. + /// The for the property name that the property is mapped to when targeting Cosmos. /// public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionProperty property) => property.FindAnnotation(CosmosAnnotationNames.PropertyName)?.GetConfigurationSource(); + + /// + /// Returns the definition of the vector stored in this property. + /// + /// The property. + /// Returns the definition of the vector stored in this property. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static CosmosVectorType? GetVectorType(this IReadOnlyProperty property) + => (CosmosVectorType?)property[CosmosAnnotationNames.VectorType]; + + /// + /// Sets the definition of the vector stored in this property. + /// + /// The property. + /// The type of vector stored in the property. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static void SetVectorType(this IMutableProperty property, CosmosVectorType? vectorType) + => property.SetOrRemoveAnnotation(CosmosAnnotationNames.VectorType, vectorType); + + /// + /// Sets the definition of the vector stored in this property. + /// + /// The property. + /// The type of vector stored in the property. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static CosmosVectorType? SetVectorType(this IConventionProperty property, CosmosVectorType? vectorType, bool fromDataAnnotation = false) + => (CosmosVectorType?)property.SetOrRemoveAnnotation( + CosmosAnnotationNames.VectorType, + vectorType, + fromDataAnnotation)?.Value; + + /// + /// Gets the for the definition of the vector stored in this property. + /// + /// The property. + /// + /// The for the definition of the vector stored in this property. + /// + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public static ConfigurationSource? GetVectorTypeConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(CosmosAnnotationNames.VectorType)?.GetConfigurationSource(); } diff --git a/src/EFCore.Cosmos/Extensions/DistanceFunction.cs b/src/EFCore.Cosmos/Extensions/DistanceFunction.cs new file mode 100644 index 00000000000..2df582e5ca1 --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/DistanceFunction.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Azure.Cosmos; + +/// +/// Defines the distance function for a vector index specification in the Azure Cosmos DB service. +/// Warning: this type will be replaced by the type from the Cosmos SDK, when it is available. +/// +/// +/// for usage. +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +public enum DistanceFunction +{ + /// + /// Represents the Euclidean distance function. + /// + [EnumMember(Value = "euclidean")] + Euclidean, + + /// + /// Represents the cosine distance function. + /// + [EnumMember(Value = "cosine")] + Cosine, + + /// + /// Represents the dot product distance function. + /// + [EnumMember(Value = "dotproduct")] + DotProduct, +} diff --git a/src/EFCore.Cosmos/Extensions/Embedding.cs b/src/EFCore.Cosmos/Extensions/Embedding.cs new file mode 100644 index 00000000000..f700da1653c --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/Embedding.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Azure.Cosmos; + +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +internal class Embedding : IEquatable +{ + public string? Path { get; set; } + public VectorDataType DataType { get; set; } + public int Dimensions { get; set; } + public DistanceFunction DistanceFunction { get; set; } + public bool Equals(Embedding? that) + => Equals(Path, that?.Path) && Equals(DataType, that?.DataType) && Equals(Dimensions, that.Dimensions); +} diff --git a/src/EFCore.Cosmos/Extensions/VectorDataType.cs b/src/EFCore.Cosmos/Extensions/VectorDataType.cs new file mode 100644 index 00000000000..cb547fd1b3a --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/VectorDataType.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Azure.Cosmos; + +/// +/// Defines the target data type of a vector index specification in the Azure Cosmos DB service. +/// Warning: this type will be replaced by the type from the Cosmos SDK, when it is available. +/// +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +public enum VectorDataType +{ + /// + /// Represents a 16-bit floating point data type. + /// + [EnumMember(Value = "float16")] + Float16, + + /// + /// Represents a 32-bit floating point data type. + /// + [EnumMember(Value = "float32")] + Float32, + + /// + /// Represents an unsigned 8-bit binary data type. + /// + [EnumMember(Value = "uint8")] + Uint8, + + /// + /// Represents a signed 8-bit binary data type. + /// + [EnumMember(Value = "int8")] + Int8, +} diff --git a/src/EFCore.Cosmos/Extensions/VectorIndexPath.cs b/src/EFCore.Cosmos/Extensions/VectorIndexPath.cs new file mode 100644 index 00000000000..8a575c2c40f --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/VectorIndexPath.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Azure.Cosmos; + +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +internal sealed class VectorIndexPath +{ + public string? Path { get; set; } + public VectorIndexType Type { get; set; } +} diff --git a/src/EFCore.Cosmos/Extensions/VectorIndexType.cs b/src/EFCore.Cosmos/Extensions/VectorIndexType.cs new file mode 100644 index 00000000000..88f71d32a5d --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/VectorIndexType.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Azure.Cosmos; + +/// +/// Defines the target index type of the vector index path specification in the Azure Cosmos DB service. +/// Warning: this type will be replaced by the type from the Cosmos SDK, when it is available. +/// +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +public enum VectorIndexType +{ + /// + /// Represents a flat vector index type. + /// + [EnumMember(Value = "flat")] + Flat, + + /// + /// Represents a Disk ANN vector index type. + /// + [EnumMember(Value = "diskANN")] + // ReSharper disable once InconsistentNaming + DiskANN, + + /// + /// Represents a quantized flat vector index type. + /// + [EnumMember(Value = "quantizedFlat")] + QuantizedFlat, +} diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs index 221bf89d345..f02e6b77aa9 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs @@ -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 System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; @@ -120,14 +121,18 @@ protected virtual void ValidateSharedContainerCompatibility( { throw new InvalidOperationException( CosmosStrings.OwnedTypeDifferentContainer( - entityType.DisplayName(), ownership.PrincipalEntityType.DisplayName(), container)); + entityType.DisplayName(), + ownership.PrincipalEntityType.DisplayName(), + container)); } if (entityType.GetContainingPropertyName() != null) { throw new InvalidOperationException( CosmosStrings.ContainerContainingPropertyConflict( - entityType.DisplayName(), container, entityType.GetContainingPropertyName())); + entityType.DisplayName(), + container, + entityType.GetContainingPropertyName())); } if (!containers.TryGetValue(container, out var mappedTypes)) @@ -196,8 +201,12 @@ protected virtual void ValidateSharedContainerCompatibility( { throw new InvalidOperationException( CosmosStrings.PartitionKeyStoreNameMismatch( - firstEntityType.GetPartitionKeyPropertyNames()[i], firstEntityType.DisplayName(), partitionKeyStoreNames[i], - entityType.GetPartitionKeyPropertyNames()[i], entityType.DisplayName(), storeNames[i])); + firstEntityType.GetPartitionKeyPropertyNames()[i], + firstEntityType.DisplayName(), + partitionKeyStoreNames[i], + entityType.GetPartitionKeyPropertyNames()[i], + entityType.DisplayName(), + storeNames[i])); } } } @@ -212,22 +221,23 @@ protected virtual void ValidateSharedContainerCompatibility( { if (entityType.FindDiscriminatorProperty() == null) { - throw new InvalidOperationException( - CosmosStrings.NoDiscriminatorProperty(entityType.DisplayName(), container)); + throw new InvalidOperationException(CosmosStrings.NoDiscriminatorProperty(entityType.DisplayName(), container)); } var discriminatorValue = entityType.GetDiscriminatorValue(); if (discriminatorValue == null) { - throw new InvalidOperationException( - CosmosStrings.NoDiscriminatorValue(entityType.DisplayName(), container)); + throw new InvalidOperationException(CosmosStrings.NoDiscriminatorValue(entityType.DisplayName(), container)); } if (discriminatorValues.TryGetValue(discriminatorValue, out var duplicateEntityType)) { throw new InvalidOperationException( CosmosStrings.DuplicateDiscriminatorValue( - entityType.DisplayName(), discriminatorValue, duplicateEntityType.DisplayName(), container)); + entityType.DisplayName(), + discriminatorValue, + duplicateEntityType.DisplayName(), + container)); } discriminatorValues[discriminatorValue] = entityType; @@ -258,7 +268,10 @@ protected virtual void ValidateSharedContainerCompatibility( var conflictingEntityType = mappedTypes.First(et => et.GetAnalyticalStoreTimeToLive() != null); throw new InvalidOperationException( CosmosStrings.AnalyticalTTLMismatch( - analyticalTtl, conflictingEntityType.DisplayName(), entityType.DisplayName(), currentAnalyticalTtl, + analyticalTtl, + conflictingEntityType.DisplayName(), + entityType.DisplayName(), + currentAnalyticalTtl, container)); } } @@ -275,7 +288,11 @@ protected virtual void ValidateSharedContainerCompatibility( var conflictingEntityType = mappedTypes.First(et => et.GetDefaultTimeToLive() != null); throw new InvalidOperationException( CosmosStrings.DefaultTTLMismatch( - defaultTtl, conflictingEntityType.DisplayName(), entityType.DisplayName(), currentDefaultTtl, container)); + defaultTtl, + conflictingEntityType.DisplayName(), + entityType.DisplayName(), + currentDefaultTtl, + container)); } } @@ -292,8 +309,10 @@ protected virtual void ValidateSharedContainerCompatibility( var conflictingEntityType = mappedTypes.First(et => et.GetThroughput() != null); throw new InvalidOperationException( CosmosStrings.ThroughputMismatch( - throughput.AutoscaleMaxThroughput ?? throughput.Throughput, conflictingEntityType.DisplayName(), - entityType.DisplayName(), currentThroughput.AutoscaleMaxThroughput ?? currentThroughput.Throughput, + throughput.AutoscaleMaxThroughput ?? throughput.Throughput, + conflictingEntityType.DisplayName(), + entityType.DisplayName(), + currentThroughput.AutoscaleMaxThroughput ?? currentThroughput.Throughput, container)); } else if ((throughput.AutoscaleMaxThroughput == null) @@ -308,8 +327,7 @@ protected virtual void ValidateSharedContainerCompatibility( : conflictingEntityType; throw new InvalidOperationException( - CosmosStrings.ThroughputTypeMismatch( - manualType.DisplayName(), autoscaleType.DisplayName(), container)); + CosmosStrings.ThroughputTypeMismatch(manualType.DisplayName(), autoscaleType.DisplayName(), container)); } } } @@ -334,16 +352,14 @@ protected virtual void ValidateOnlyETagConcurrencyToken( var storeName = property.GetJsonPropertyName(); if (storeName != "_etag") { - throw new InvalidOperationException( - CosmosStrings.NonETagConcurrencyToken(entityType.DisplayName(), storeName)); + throw new InvalidOperationException(CosmosStrings.NonETagConcurrencyToken(entityType.DisplayName(), storeName)); } var etagType = property.GetTypeMapping().Converter?.ProviderClrType ?? property.ClrType; if (etagType != typeof(string)) { throw new InvalidOperationException( - CosmosStrings.ETagNonStringStoreType( - property.Name, entityType.DisplayName(), etagType.ShortDisplayName())); + CosmosStrings.ETagNonStringStoreType(property.Name, entityType.DisplayName(), etagType.ShortDisplayName())); } } } @@ -381,8 +397,7 @@ protected virtual void ValidateKeys( if (idType != typeof(string)) { throw new InvalidOperationException( - CosmosStrings.IdNonStringStoreType( - idProperty.Name, entityType.DisplayName(), idType.ShortDisplayName())); + CosmosStrings.IdNonStringStoreType(idProperty.Name, entityType.DisplayName(), idType.ShortDisplayName())); } var partitionKeyPropertyNames = entityType.GetPartitionKeyPropertyNames(); @@ -416,7 +431,9 @@ protected virtual void ValidateKeys( { throw new InvalidOperationException( CosmosStrings.PartitionKeyBadStoreType( - partitionKeyPropertyName, entityType.DisplayName(), partitionKeyType.ShortDisplayName())); + partitionKeyPropertyName, + entityType.DisplayName(), + partitionKeyType.ShortDisplayName())); } } } @@ -535,10 +552,58 @@ protected virtual void ValidateIndexes( { foreach (var index in entityType.GetDeclaredIndexes()) { - throw new InvalidOperationException( - CosmosStrings.IndexesExist( - entityType.DisplayName(), - string.Join(",", index.Properties.Select(e => e.Name)))); + if (index.FindAnnotation(CosmosAnnotationNames.VectorIndexType) != null) + { + if (index.Properties.Count > 1) + { + throw new InvalidOperationException( + CosmosStrings.CompositeVectorIndex( + entityType.DisplayName(), + string.Join(",", index.Properties.Select(e => e.Name)))); + } + + if (index.Properties[0].FindAnnotation(CosmosAnnotationNames.VectorType) == null) + { + throw new InvalidOperationException( + CosmosStrings.VectorIndexOnNonVector( + entityType.DisplayName(), + index.Properties[0].Name)); + } + } + else + { + throw new InvalidOperationException( + CosmosStrings.IndexesExist( + entityType.DisplayName(), + string.Join(",", index.Properties.Select(e => e.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 + /// 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. + /// + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + protected override void ValidatePropertyMapping( + IModel model, + IDiagnosticsLogger logger) + { + base.ValidatePropertyMapping(model, logger); + + foreach (var entityType in model.GetEntityTypes()) + { + foreach (var property in entityType.GetDeclaredProperties()) + { + var cosmosVectorType = property.GetVectorType(); + if (cosmosVectorType is not null) + { + // Will throw if the data type is not set and cannot be inferred. + CosmosVectorType.CreateDefaultVectorDataType(property.ClrType); + } } } } diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs index 02e2af7ff1a..251e2bdd2a3 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.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.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; /// @@ -43,6 +45,24 @@ public static class CosmosAnnotationNames /// public const string PartitionKeyNames = Prefix + "PartitionKeyNames"; + /// + /// 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. + /// + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public const string VectorIndexType = Prefix + "VectorIndexType"; + + /// + /// 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. + /// + [Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] + public const string VectorType = Prefix + "VectorType"; + /// /// 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.Cosmos/Metadata/Internal/CosmosVectorType.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosVectorType.cs new file mode 100644 index 00000000000..f7d22521deb --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosVectorType.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.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. +/// +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +public sealed record class CosmosVectorType(DistanceFunction DistanceFunction, int Dimensions) +{ + /// + /// 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 VectorDataType CreateDefaultVectorDataType(Type clrType) + { + var elementType = clrType.TryGetElementType(typeof(ReadOnlyMemory<>))?.UnwrapNullableType() + ?? clrType.TryGetElementType(typeof(IEnumerable<>))?.UnwrapNullableType(); + + return elementType == typeof(sbyte) + ? VectorDataType.Int8 + : elementType == typeof(byte) + ? VectorDataType.Uint8 + : elementType == typeof(float) + ? VectorDataType.Float32 + : throw new InvalidOperationException(CosmosStrings.BadVectorDataType(clrType.ShortDisplayName())); + } +} diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index 4fecefac02b..8237cb5e734 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -47,12 +47,28 @@ public static string BadDictionaryType(object? givenType, object? dictionaryType GetString("BadDictionaryType", nameof(givenType), nameof(dictionaryType)), givenType, dictionaryType); + /// + /// The type '{clrType}' is being used as a vector, but the vector data type cannot be inferred. Only 'ReadOnlyMemory<byte>, ReadOnlyMemory<sbyte>, ReadOnlyMemory<float>, byte[], sbyte[], and float[] are supported. + /// + public static string BadVectorDataType(object? clrType) + => string.Format( + GetString("BadVectorDataType", nameof(clrType)), + clrType); + /// /// The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'. /// public static string CanConnectNotSupported => GetString("CanConnectNotSupported"); + /// + /// A vector index on '{entityType}' is defined over properties `{properties}`. A vector index can only target a single property. + /// + public static string CompositeVectorIndex(object? entityType, object? properties) + => string.Format( + GetString("CompositeVectorIndex", nameof(entityType), nameof(properties)), + entityType, properties); + /// /// Complex projections in subqueries are currently unsupported. /// @@ -297,6 +313,12 @@ public static string NoReadItemQueryString(object? resourceId, object? partition public static string NoSubqueryPushdown => GetString("NoSubqueryPushdown"); + /// + /// Container configuration for embeddings is not yet supported by the Cosmos SDK. Instead, configure the container manually. See https://aka.ms/ef-cosmos-vectors for more information. + /// + public static string NoVectorContainerConfig + => GetString("NoVectorContainerConfig"); + /// /// The expression '{sqlExpression}' in the SQL tree does not have a type mapping assigned. /// @@ -469,6 +491,20 @@ public static string UpdateStoreException(object? itemId) GetString("UpdateStoreException", nameof(itemId)), itemId); + /// + /// A vector index is defined for `{entityType}.{property}`, but this property has not been configured as a vector. Use 'IsVector()' in 'OnModelCreating' to configure the property as a vector. + /// + public static string VectorIndexOnNonVector(object? entityType, object? property) + => string.Format( + GetString("VectorIndexOnNonVector", nameof(entityType), nameof(property)), + entityType, property); + + /// + /// The 'VectorDistance' function can only be used with a property mapped as a vector. Use 'IsVector()' in 'OnModelCreating' to configure the property as a vector. + /// + public static string VectorSearchRequiresVector + => GetString("VectorSearchRequiresVector"); + /// /// 'VisitChildren' must be overridden in the class deriving from 'SqlExpression'. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index 7e0388b0571..dca83b25d1c 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -126,12 +126,18 @@ The type '{givenType}' cannot be mapped as a dictionary because it does not implement '{dictionaryType}'. + + The type '{clrType}' is being used as a vector, but the vector data type cannot be inferred. Only 'ReadOnlyMemory<byte>, ReadOnlyMemory<sbyte>, ReadOnlyMemory<float>, byte[], sbyte[], and float[] are supported. + The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'. Complex projections in subqueries are currently unsupported. + + A vector index on '{entityType}' is defined over properties `{properties}`. A vector index can only target a single property. + None of connection string, CredentialToken, account key or account endpoint were specified. Specify a set of connection details. @@ -268,6 +274,9 @@ Azure Cosmos DB does not have an appropriate subquery for this translation. + + Container configuration for embeddings is not yet supported by the Cosmos SDK. Instead, configure the container manually. See https://aka.ms/ef-cosmos-vectors for more information. + The expression '{sqlExpression}' in the SQL tree does not have a type mapping assigned. @@ -340,6 +349,12 @@ An error occurred while saving the item with id '{itemId}'. See the inner exception for details. + + A vector index is defined for `{entityType}.{property}`, but this property has not been configured as a vector. Use 'IsVector()' in 'OnModelCreating' to configure the property as a vector. + + + The 'VectorDistance' function can only be used with a property mapped as a vector. Use 'IsVector()' in 'OnModelCreating' to configure the property as a vector. + 'VisitChildren' must be overridden in the class deriving from 'SqlExpression'. diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs index 0d871e9230a..352e3d443e1 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs @@ -22,6 +22,7 @@ public class CosmosMethodCallTranslatorProvider : IMethodCallTranslatorProvider /// public CosmosMethodCallTranslatorProvider( ISqlExpressionFactory sqlExpressionFactory, + ITypeMappingSource typeMappingSource, IEnumerable plugins) { _plugins.AddRange(plugins.SelectMany(p => p.Translators)); @@ -34,7 +35,8 @@ public CosmosMethodCallTranslatorProvider( new CosmosRandomTranslator(sqlExpressionFactory), new CosmosRegexTranslator(sqlExpressionFactory), new CosmosStringMethodTranslator(sqlExpressionFactory), - new CosmosTypeCheckingTranslator(sqlExpressionFactory) + new CosmosTypeCheckingTranslator(sqlExpressionFactory), + new CosmosVectorSearchTranslator(sqlExpressionFactory, typeMappingSource) //new LikeTranslator(sqlExpressionFactory), //new EnumHasFlagTranslator(sqlExpressionFactory), //new GetValueOrDefaultTranslator(sqlExpressionFactory), diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 392b29accb2..6dd2963c6e1 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -626,6 +626,19 @@ protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstant return sqlConstantExpression; } + /// + /// 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 Expression VisitFragment(FragmentExpression fragmentExpression) + { + _sqlBuilder.Append(fragmentExpression.Fragment); + + return fragmentExpression; + } + /// /// 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 @@ -657,11 +670,13 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame if (_sqlParameters.All(sp => sp.Name != parameterName)) { - Check.DebugAssert(sqlParameterExpression.TypeMapping is not null, "SqlParameterExpression without a type mapping"); - var jToken = ((CosmosTypeMapping)sqlParameterExpression.TypeMapping) - .GenerateJToken(_parameterValues[sqlParameterExpression.Name]); + Check.DebugAssert(sqlParameterExpression.TypeMapping is not null, "SqlParameterExpression without a type mapping."); - _sqlParameters.Add(new SqlParameter(parameterName, jToken)); + _sqlParameters.Add( + new SqlParameter( + parameterName, + ((CosmosTypeMapping)sqlParameterExpression.TypeMapping) + .GenerateJToken(_parameterValues[sqlParameterExpression.Name]))); } _sqlBuilder.Append(parameterName); diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs new file mode 100644 index 00000000000..0a14e2f7430 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// An expression that represents a fragment that will be inserted verbatim into the query. +/// +/// +/// 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. +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class FragmentExpression(string fragment) : Expression, IPrintableExpression +{ + /// + /// The fragment. + /// + public virtual string Fragment { get; } = fragment; + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + /// + public virtual void Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Append(Fragment); + + /// + /// 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 bool Equals(FragmentExpression other) + => base.Equals(other) + && Fragment == other.Fragment; + + /// + public override bool Equals(object? obj) + => !ReferenceEquals(null, obj) + && (ReferenceEquals(this, obj) + || obj.GetType() == GetType() + && Equals((FragmentExpression)obj)); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Fragment); +} diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs index d95ec7eb170..22d2b286bb1 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs @@ -34,6 +34,7 @@ ShapedQueryExpression shapedQueryExpression SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression), ObjectBinaryExpression objectBinaryExpression => VisitObjectBinary(objectBinaryExpression), SqlConstantExpression sqlConstantExpression => VisitSqlConstant(sqlConstantExpression), + FragmentExpression jsonFragmentExpression => VisitFragment(jsonFragmentExpression), SqlUnaryExpression sqlUnaryExpression => VisitSqlUnary(sqlUnaryExpression), SqlConditionalExpression sqlConditionalExpression => VisitSqlConditional(sqlConditionalExpression), SqlParameterExpression sqlParameterExpression => VisitSqlParameter(sqlParameterExpression), @@ -172,6 +173,14 @@ ShapedQueryExpression shapedQueryExpression /// protected abstract Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression); + /// + /// 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 abstract Expression VisitFragment(FragmentExpression fragmentExpression); + /// /// 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.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs new file mode 100644 index 00000000000..9c6e62d02a2 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Extensions; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Cosmos.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 CosmosVectorSearchTranslator(ISqlExpressionFactory sqlExpressionFactory, ITypeMappingSource typeMappingSource) + : IMethodCallTranslator +{ + /// + /// 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(CosmosDbFunctionsExtensions) + && method.Name != nameof(CosmosDbFunctionsExtensions.VectorDistance)) + { + return null; + } + + var vectorMapping = arguments[1].TypeMapping as CosmosVectorTypeMapping + ?? arguments[2].TypeMapping as CosmosVectorTypeMapping + ?? throw new InvalidOperationException(CosmosStrings.VectorSearchRequiresVector); + + Check.DebugAssert(arguments.Count is 3 or 4 or 5, "Did you add a parameter?"); + + SqlConstantExpression bruteForce; + if (arguments.Count >= 4) + { + if (arguments[3] is not SqlConstantExpression { Value: bool }) + { + throw new InvalidOperationException( + CoreStrings.ArgumentNotConstant("useBruteForce", nameof(CosmosDbFunctionsExtensions.VectorDistance))); + } + + bruteForce = (SqlConstantExpression)arguments[3]; + } + else + { + bruteForce = (SqlConstantExpression)sqlExpressionFactory.Constant(false); + } + + var vectorType = vectorMapping.VectorType; + if (arguments.Count == 5) + { + if (arguments[4] is not SqlConstantExpression { Value: DistanceFunction distanceFunction }) + { + throw new InvalidOperationException( + CoreStrings.ArgumentNotConstant("distanceFunction", nameof(CosmosDbFunctionsExtensions.VectorDistance))); + } + + vectorType = vectorType with { DistanceFunction = distanceFunction }; + } + + var dataType = CosmosVectorType.CreateDefaultVectorDataType(vectorMapping.ClrType); + + return sqlExpressionFactory.Function( + "VectorDistance", + [ + sqlExpressionFactory.ApplyTypeMapping(arguments[1], vectorMapping), + sqlExpressionFactory.ApplyTypeMapping(arguments[2], vectorMapping), + bruteForce, + new FragmentExpression( + $"{{'distanceFunction':'{vectorType.DistanceFunction.ToString().ToLower()}', 'dataType':'{dataType.ToString().ToLower()}'}}") + ], + typeof(double), + typeMappingSource.FindMapping(typeof(double))!); + } +} diff --git a/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs b/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs index d0761cd9ecb..78965ed3073 100644 --- a/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs +++ b/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.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 Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; /// @@ -9,85 +11,11 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; /// 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 readonly record struct ContainerProperties -{ - /// - /// 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 readonly string Id; - - /// - /// 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 readonly IReadOnlyList PartitionKeyStoreNames; - - /// - /// 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 readonly int? AnalyticalStoreTimeToLiveInSeconds; - - /// - /// 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 readonly int? DefaultTimeToLive; - - /// - /// 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 readonly ThroughputProperties? Throughput; - - /// - /// 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 ContainerProperties( - string containerId, - IReadOnlyList partitionKeyStoreNames, - int? analyticalTtl, - int? defaultTtl, - ThroughputProperties? throughput) - { - Id = containerId; - PartitionKeyStoreNames = partitionKeyStoreNames; - AnalyticalStoreTimeToLiveInSeconds = analyticalTtl; - DefaultTimeToLive = defaultTtl; - Throughput = throughput; - } - - /// - /// 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 void Deconstruct( - out string containerId, - out IReadOnlyList partitionKeyStoreNames, - out int? analyticalTtl, - out int? defaultTtl, - out ThroughputProperties? throughput) - { - containerId = Id; - partitionKeyStoreNames = PartitionKeyStoreNames; - analyticalTtl = AnalyticalStoreTimeToLiveInSeconds; - defaultTtl = DefaultTimeToLive; - throughput = Throughput; - } -} +public readonly record struct ContainerProperties( + string Id, + IReadOnlyList PartitionKeyStoreNames, + int? AnalyticalStoreTimeToLiveInSeconds, + int? DefaultTimeToLive, + ThroughputProperties? Throughput, + IReadOnlyList Indexes, + IReadOnlyList<(IProperty Property, CosmosVectorType VectorType)> Vectors); diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs index 2c6cc8625ef..d72a889c1a3 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs @@ -2,11 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Collections.ObjectModel; using System.Net; using System.Runtime.CompilerServices; using System.Text; using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -234,13 +237,56 @@ private static async Task CreateContainerIfNotExistsOnceAsync( { var (parameters, wrapper) = parametersTuple; var partitionKeyPaths = parameters.PartitionKeyStoreNames.Select(e => "/" + e).ToList(); - var response = await wrapper.Client.GetDatabase(wrapper._databaseId).CreateContainerIfNotExistsAsync( - new Azure.Cosmos.ContainerProperties(parameters.Id, partitionKeyPaths) + + var vectorIndexes = new Collection(); + foreach (var index in parameters.Indexes) + { + var vectorIndexType = (VectorIndexType?)index.FindAnnotation(CosmosAnnotationNames.VectorIndexType)?.Value; + if (vectorIndexType != null) + { + // Model validation will ensure there is only one property. + Check.DebugAssert(index.Properties.Count == 1, "Vector index must have one property."); + + vectorIndexes.Add( + new VectorIndexPath { Path = "/" + index.Properties[0].GetJsonPropertyName(), Type = vectorIndexType.Value }); + } + } + + var embeddings = new Collection(); + foreach (var tuple in parameters.Vectors) + { + embeddings.Add( + new Embedding { - PartitionKeyDefinitionVersion = PartitionKeyDefinitionVersion.V2, - DefaultTimeToLive = parameters.DefaultTimeToLive, - AnalyticalStoreTimeToLiveInSeconds = parameters.AnalyticalStoreTimeToLiveInSeconds - }, + Path = "/" + tuple.Property.GetJsonPropertyName(), + DataType = CosmosVectorType.CreateDefaultVectorDataType(tuple.Property.ClrType), + Dimensions = tuple.VectorType.Dimensions, + DistanceFunction = tuple.VectorType.DistanceFunction + }); + } + + var containerProperties = new Azure.Cosmos.ContainerProperties(parameters.Id, partitionKeyPaths) + { + PartitionKeyDefinitionVersion = PartitionKeyDefinitionVersion.V2, + DefaultTimeToLive = parameters.DefaultTimeToLive, + AnalyticalStoreTimeToLiveInSeconds = parameters.AnalyticalStoreTimeToLiveInSeconds, + }; + + // TODO: Enable these once they are available in the Cosmos SDK. See #33783. + if (embeddings.Any()) + { + throw new InvalidOperationException(CosmosStrings.NoVectorContainerConfig); + //containerProperties.VectorEmbeddingPolicy = new VectorEmbeddingPolicy(embeddings); + } + + if (vectorIndexes.Any()) + { + throw new InvalidOperationException(CosmosStrings.NoVectorContainerConfig); + //containerProperties.IndexingPolicy = new IndexingPolicy { VectorIndexes = vectorIndexes }; + } + + var response = await wrapper.Client.GetDatabase(wrapper._databaseId).CreateContainerIfNotExistsAsync( + containerProperties, throughput: parameters.Throughput?.Throughput, cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index e46852477e3..d55b7a63f9e 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; @@ -112,6 +113,8 @@ private static IEnumerable GetContainersToCreate(IModel mod int? analyticalTtl = null; int? defaultTtl = null; ThroughputProperties? throughput = null; + var indexes = new List(); + var vectors = new List<(IProperty Property, CosmosVectorType VectorType)>(); foreach (var entityType in mappedTypes) { @@ -122,6 +125,15 @@ private static IEnumerable GetContainersToCreate(IModel mod analyticalTtl ??= entityType.GetAnalyticalStoreTimeToLive(); defaultTtl ??= entityType.GetDefaultTimeToLive(); throughput ??= entityType.GetThroughput(); + indexes.AddRange(entityType.GetIndexes()); + + foreach (var property in entityType.GetProperties()) + { + if (property.FindTypeMapping() is CosmosVectorTypeMapping vectorTypeMapping) + { + vectors.Add((property, vectorTypeMapping.VectorType)); + } + } } yield return new ContainerProperties( @@ -129,7 +141,9 @@ private static IEnumerable GetContainersToCreate(IModel mod partitionKeyStoreNames, analyticalTtl, defaultTtl, - throughput); + throughput, + indexes, + vectors); } } @@ -212,11 +226,16 @@ public virtual Task CanConnectAsync(CancellationToken cancellationToken = => throw new NotSupportedException(CosmosStrings.CanConnectNotSupported); /// + /// Returns the store names of the properties that is used to store the partition keys. + /// + /// /// 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. - /// + /// + /// The entity type to get the partition key property names for. + /// The names of the partition key property. private static IReadOnlyList GetPartitionKeyStoreNames(IEntityType entityType) { var properties = entityType.GetPartitionKeyProperties(); diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs index b4055de9e77..7588e248520 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; using Newtonsoft.Json.Linq; @@ -39,6 +39,24 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) }; } + + /// + /// 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 override CoreTypeMapping? FindMapping(IProperty property) + // A provider should typically not override this because using the property directly causes problems with Migrations where + // the property does not exist. However, since the Cosmos provider doesn't have Migrations, it should be okay to use the property + // directly. + => base.FindMapping(property) switch + { + CosmosTypeMapping mapping when property.FindAnnotation(CosmosAnnotationNames.VectorType)?.Value is CosmosVectorType vectorType + => new CosmosVectorTypeMapping(mapping, vectorType), + var other => other + }; + /// /// 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 @@ -61,6 +79,15 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) { var clrType = mappingInfo.ClrType!; + var memoryType = clrType.TryGetElementType(typeof(ReadOnlyMemory<>)); + if (memoryType != null) + { + return new CosmosTypeMapping(clrType) + .WithComposedConverter( + (ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(memoryType))!, + (ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(memoryType))!); + } + return clrType.IsNumeric() || clrType == typeof(bool) || clrType == typeof(DateOnly) @@ -87,12 +114,19 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) // First attempt to resolve this as a primitive collection (e.g. List). This does not handle Dictionary. if (TryFindJsonCollectionMapping( - mappingInfo, clrType, providerClrType: null, ref elementMapping, out var elementComparer, + mappingInfo, + clrType, + providerClrType: null, + ref elementMapping, + out var elementComparer, out var collectionReaderWriter) && elementMapping is not null) { return new CosmosTypeMapping( - clrType, elementComparer, elementMapping: elementMapping, jsonValueReaderWriter: collectionReaderWriter); + clrType, + elementComparer, + elementMapping: elementMapping, + jsonValueReaderWriter: collectionReaderWriter); } // Next, attempt to resolve this as a dictionary (e.g. Dictionary). diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs new file mode 100644 index 00000000000..717e7f92d34 --- /dev/null +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.EntityFrameworkCore.Cosmos.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. +/// +[Experimental(EFDiagnostics.CosmosVectorSearchExperimental)] +public class CosmosVectorTypeMapping : CosmosTypeMapping +{ + /// + /// 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 new CosmosVectorTypeMapping Default { get; } + // Note that this default is not valid because dimensions cannot be zero. But since there is no reasonable + // default dimensions size for a vector type, this is intentionally not valid rather than just being wrong. + // The fundamental problem here is that type mappings are "required" to have some default now. + = new(typeof(byte[]), new CosmosVectorType(DistanceFunction.Cosine, 0)); + + /// + /// 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 CosmosVectorTypeMapping( + Type clrType, + CosmosVectorType vectorType, + ValueComparer? comparer = null, + ValueComparer? keyComparer = null, + CoreTypeMapping? elementMapping = null, + JsonValueReaderWriter? jsonValueReaderWriter = null) + : this( + new CoreTypeMappingParameters( + clrType, + converter: null, + comparer, + keyComparer, + elementMapping: elementMapping, + jsonValueReaderWriter: jsonValueReaderWriter), + vectorType) + { + } + + /// + /// 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 CosmosVectorTypeMapping(CosmosTypeMapping mapping, CosmosVectorType vectorType) + : this( + new CoreTypeMappingParameters( + mapping.ClrType, + converter: mapping.Converter, + mapping.Comparer, + mapping.KeyComparer, + elementMapping: mapping.ElementTypeMapping, + jsonValueReaderWriter: mapping.JsonValueReaderWriter), + vectorType) + { + } + + /// + /// 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 CosmosVectorTypeMapping(CoreTypeMappingParameters parameters, CosmosVectorType vectorType) + : base(parameters) + { + VectorType = vectorType; + } + + /// + /// 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 virtual CosmosVectorType VectorType { get; } + + /// + /// 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 override CoreTypeMapping WithComposedConverter( + ValueConverter? converter, + ValueComparer? comparer = null, + ValueComparer? keyComparer = null, + CoreTypeMapping? elementMapping = null, + JsonValueReaderWriter? jsonValueReaderWriter = null) + => new CosmosVectorTypeMapping( + Parameters.WithComposedConverter(converter, comparer, keyComparer, elementMapping, jsonValueReaderWriter), + VectorType); + + /// + /// 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 CoreTypeMapping Clone(CoreTypeMappingParameters parameters) + => new CosmosVectorTypeMapping(parameters, VectorType); +} diff --git a/src/EFCore.Cosmos/Storage/Internal/ReadOnlyMemoryComparer.cs b/src/EFCore.Cosmos/Storage/Internal/ReadOnlyMemoryComparer.cs new file mode 100644 index 00000000000..04fc97b36cf --- /dev/null +++ b/src/EFCore.Cosmos/Storage/Internal/ReadOnlyMemoryComparer.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Cosmos.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 ReadOnlyMemoryComparer() : ValueComparer>(false); diff --git a/src/EFCore.Cosmos/Storage/Internal/ReadOnlyMemoryConverter.cs b/src/EFCore.Cosmos/Storage/Internal/ReadOnlyMemoryConverter.cs new file mode 100644 index 00000000000..e086b4ccaa0 --- /dev/null +++ b/src/EFCore.Cosmos/Storage/Internal/ReadOnlyMemoryConverter.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Cosmos.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 ReadOnlyMemoryConverter : ValueConverter, T[]> +{ + private static readonly ConverterMappingHints DefaultHints = new(); + + /// + /// 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 ReadOnlyMemoryConverter() + : this(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 ReadOnlyMemoryConverter(ConverterMappingHints? mappingHints) + : base( + v => ToArray(v), + v => ToMemory(v), + DefaultHints.With(mappingHints)) + { + } + + /// + /// 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 T[] ToArray(ReadOnlyMemory memory) + => memory.ToArray(); + + /// + /// 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 ReadOnlyMemory ToMemory(T[] array) + // If the array is empty, then return the default ReadOnlyMemory instance because this will compare the same as other empty + // ReadOnlyMemory instances, while the instance created with an empty array is considered not equal to the default. + => array.Length == 0 ? default : new(array); + + /// + /// 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 ValueConverterInfo DefaultInfo { get; } + = new(typeof(ReadOnlyMemory), typeof(T[]), i => new ReadOnlyMemoryConverter(i.MappingHints), DefaultHints); +} diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index a71d12b602f..1a9d3320fa9 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -114,6 +114,14 @@ public static string AnnotationNotFound(object? annotation, object? annotatable) GetString("AnnotationNotFound", nameof(annotation), nameof(annotatable)), annotation, annotatable); + /// + /// The '{parameter}' value passed to '{methodName}' must be a constant. + /// + public static string ArgumentNotConstant(object? parameter, object? methodName) + => string.Format( + GetString("ArgumentNotConstant", nameof(parameter), nameof(methodName)), + parameter, methodName); + /// /// The property '{property}' of the argument '{argument}' cannot be null. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 34cac8e6dd1..41d62da4122 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -150,6 +150,9 @@ The annotation '{annotation}' was not found. Ensure that the annotation has been added to the object {annotatable} + + The '{parameter}' value passed to '{methodName}' must be a constant. + The property '{property}' of the argument '{argument}' cannot be null. diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs index 8f10c2b7aef..5bfe0266a7a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs @@ -33,7 +33,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+Data", typeof(CompiledModelTestBase.Data), baseEntityType, - propertyCount: 8, + propertyCount: 9, keyCount: 1); var id = runtimeEntityType.AddProperty( @@ -162,20 +162,53 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas byte[] (string v) => Convert.FromBase64String(v)))); blob.AddAnnotation("Cosmos:PropertyName", "JsonBlob"); + var bytes = runtimeEntityType.AddProperty( + "Bytes", + typeof(ReadOnlyMemory)); + bytes.SetAccessors( + ReadOnlyMemory (InternalEntityEntry entry) => entry.ReadShadowValue>(2), + ReadOnlyMemory (InternalEntityEntry entry) => entry.ReadShadowValue>(2), + ReadOnlyMemory (InternalEntityEntry entry) => entry.ReadOriginalValue>(bytes, 3), + ReadOnlyMemory (InternalEntityEntry entry) => entry.GetCurrentValue>(bytes), + object (ValueBuffer valueBuffer) => valueBuffer[3]); + bytes.SetPropertyIndexes( + index: 3, + originalValueIndex: 3, + shadowIndex: 2, + relationshipIndex: -1, + storeGenerationIndex: -1); + bytes.TypeMapping = CosmosTypeMapping.Default.Clone( + comparer: new ValueComparer>( + bool (ReadOnlyMemory v1, ReadOnlyMemory v2) => v1.Equals(v2), + int (ReadOnlyMemory v) => ((object)v).GetHashCode(), + ReadOnlyMemory (ReadOnlyMemory v) => v), + keyComparer: new ValueComparer>( + bool (ReadOnlyMemory v1, ReadOnlyMemory v2) => v1.Equals(v2), + int (ReadOnlyMemory v) => ((object)v).GetHashCode(), + ReadOnlyMemory (ReadOnlyMemory v) => v), + providerValueComparer: new ValueComparer( + bool (byte[] v1, byte[] v2) => StructuralComparisons.StructuralEqualityComparer.Equals(((object)(v1)), ((object)(v2))), + int (byte[] v) => StructuralComparisons.StructuralEqualityComparer.GetHashCode(((object)(v))), + byte[] (byte[] source) => source.ToArray()), + converter: new ValueConverter, byte[]>( + byte[] (ReadOnlyMemory v) => ReadOnlyMemoryConverter.ToArray(v), + ReadOnlyMemory (byte[] v) => ReadOnlyMemoryConverter.ToMemory(v))); + bytes.SetSentinelFromProviderValue(new byte[0]); + var list = runtimeEntityType.AddProperty( "List", typeof(List>), nullable: true); list.SetAccessors( - List> (InternalEntityEntry entry) => entry.ReadShadowValue>>(2), - List> (InternalEntityEntry entry) => entry.ReadShadowValue>>(2), - List> (InternalEntityEntry entry) => entry.ReadOriginalValue>>(list, 3), + List> (InternalEntityEntry entry) => entry.ReadShadowValue>>(3), + List> (InternalEntityEntry entry) => entry.ReadShadowValue>>(3), + List> (InternalEntityEntry entry) => entry.ReadOriginalValue>>(list, 4), List> (InternalEntityEntry entry) => entry.GetCurrentValue>>(list), - object (ValueBuffer valueBuffer) => valueBuffer[3]); + object (ValueBuffer valueBuffer) => valueBuffer[4]); list.SetPropertyIndexes( - index: 3, - originalValueIndex: 3, - shadowIndex: 2, + index: 4, + originalValueIndex: 4, + shadowIndex: 3, relationshipIndex: -1, storeGenerationIndex: -1); list.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -217,15 +250,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas typeof(Dictionary), nullable: true); map.SetAccessors( - Dictionary (InternalEntityEntry entry) => entry.ReadShadowValue>(3), - Dictionary (InternalEntityEntry entry) => entry.ReadShadowValue>(3), - Dictionary (InternalEntityEntry entry) => entry.ReadOriginalValue>(map, 4), + Dictionary (InternalEntityEntry entry) => entry.ReadShadowValue>(4), + Dictionary (InternalEntityEntry entry) => entry.ReadShadowValue>(4), + Dictionary (InternalEntityEntry entry) => entry.ReadOriginalValue>(map, 5), Dictionary (InternalEntityEntry entry) => entry.GetCurrentValue>(map), - object (ValueBuffer valueBuffer) => valueBuffer[4]); + object (ValueBuffer valueBuffer) => valueBuffer[5]); map.SetPropertyIndexes( - index: 4, - originalValueIndex: 4, - shadowIndex: 3, + index: 5, + originalValueIndex: 5, + shadowIndex: 4, relationshipIndex: -1, storeGenerationIndex: -1); map.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -252,15 +285,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas afterSaveBehavior: PropertySaveBehavior.Throw, valueGeneratorFactory: new IdValueGeneratorFactory().Create); __id.SetAccessors( - string (InternalEntityEntry entry) => entry.ReadShadowValue(4), - string (InternalEntityEntry entry) => entry.ReadShadowValue(4), - string (InternalEntityEntry entry) => entry.ReadOriginalValue(__id, 5), + string (InternalEntityEntry entry) => entry.ReadShadowValue(5), + string (InternalEntityEntry entry) => entry.ReadShadowValue(5), + string (InternalEntityEntry entry) => entry.ReadOriginalValue(__id, 6), string (InternalEntityEntry entry) => entry.GetCurrentValue(__id), - object (ValueBuffer valueBuffer) => valueBuffer[5]); + object (ValueBuffer valueBuffer) => valueBuffer[6]); __id.SetPropertyIndexes( - index: 5, - originalValueIndex: 5, - shadowIndex: 4, + index: 6, + originalValueIndex: 6, + shadowIndex: 5, relationshipIndex: -1, storeGenerationIndex: -1); __id.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -288,15 +321,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas beforeSaveBehavior: PropertySaveBehavior.Ignore, afterSaveBehavior: PropertySaveBehavior.Ignore); __jObject.SetAccessors( - JObject (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(6) ? entry.ReadStoreGeneratedValue(0) : (entry.FlaggedAsTemporary(6) && entry.ReadShadowValue(5) == null ? entry.ReadTemporaryValue(0) : entry.ReadShadowValue(5))), - JObject (InternalEntityEntry entry) => entry.ReadShadowValue(5), - JObject (InternalEntityEntry entry) => entry.ReadOriginalValue(__jObject, 6), + JObject (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(7) ? entry.ReadStoreGeneratedValue(0) : (entry.FlaggedAsTemporary(7) && entry.ReadShadowValue(6) == null ? entry.ReadTemporaryValue(0) : entry.ReadShadowValue(6))), + JObject (InternalEntityEntry entry) => entry.ReadShadowValue(6), + JObject (InternalEntityEntry entry) => entry.ReadOriginalValue(__jObject, 7), JObject (InternalEntityEntry entry) => entry.GetCurrentValue(__jObject), - object (ValueBuffer valueBuffer) => valueBuffer[6]); + object (ValueBuffer valueBuffer) => valueBuffer[7]); __jObject.SetPropertyIndexes( - index: 6, - originalValueIndex: 6, - shadowIndex: 5, + index: 7, + originalValueIndex: 7, + shadowIndex: 6, relationshipIndex: -1, storeGenerationIndex: 0); __jObject.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -324,15 +357,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas beforeSaveBehavior: PropertySaveBehavior.Ignore, afterSaveBehavior: PropertySaveBehavior.Ignore); _etag.SetAccessors( - string (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(7) ? entry.ReadStoreGeneratedValue(1) : (entry.FlaggedAsTemporary(7) && entry.ReadShadowValue(6) == null ? entry.ReadTemporaryValue(1) : entry.ReadShadowValue(6))), - string (InternalEntityEntry entry) => entry.ReadShadowValue(6), - string (InternalEntityEntry entry) => entry.ReadOriginalValue(_etag, 7), + string (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(8) ? entry.ReadStoreGeneratedValue(1) : (entry.FlaggedAsTemporary(8) && entry.ReadShadowValue(7) == null ? entry.ReadTemporaryValue(1) : entry.ReadShadowValue(7))), + string (InternalEntityEntry entry) => entry.ReadShadowValue(7), + string (InternalEntityEntry entry) => entry.ReadOriginalValue(_etag, 8), string (InternalEntityEntry entry) => entry.GetCurrentValue(_etag), - object (ValueBuffer valueBuffer) => valueBuffer[7]); + object (ValueBuffer valueBuffer) => valueBuffer[8]); _etag.SetPropertyIndexes( - index: 7, - originalValueIndex: 7, - shadowIndex: 6, + index: 8, + originalValueIndex: 8, + shadowIndex: 7, relationshipIndex: -1, storeGenerationIndex: 1); _etag.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -363,6 +396,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var id = runtimeEntityType.FindProperty("Id")!; var partitionId = runtimeEntityType.FindProperty("PartitionId")!; var blob = runtimeEntityType.FindProperty("Blob")!; + var bytes = runtimeEntityType.FindProperty("Bytes")!; var list = runtimeEntityType.FindProperty("List")!; var map = runtimeEntityType.FindProperty("Map")!; var __id = runtimeEntityType.FindProperty("__id")!; @@ -375,16 +409,16 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) ISnapshot (InternalEntityEntry source) => { var entity = ((CompiledModelTestBase.Data)(source.Entity)); - return ((ISnapshot)(new Snapshot>, Dictionary, string, JObject, string>(((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue(id)), (source.GetCurrentValue(partitionId) == null ? null : ((ValueComparer)(((IProperty)partitionId).GetValueComparer())).Snapshot(source.GetCurrentValue(partitionId))), (source.GetCurrentValue(blob) == null ? null : ((ValueComparer)(((IProperty)blob).GetValueComparer())).Snapshot(source.GetCurrentValue(blob))), (((object)(source.GetCurrentValue>>(list))) == null ? null : ((List>)(((ValueComparer)(((IProperty)list).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>>(list))))))), (((object)(source.GetCurrentValue>(map))) == null ? null : ((Dictionary)(((ValueComparer)(((IProperty)map).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(map))))))), (source.GetCurrentValue(__id) == null ? null : ((ValueComparer)(((IProperty)__id).GetValueComparer())).Snapshot(source.GetCurrentValue(__id))), (source.GetCurrentValue(__jObject) == null ? null : ((ValueComparer)(((IProperty)__jObject).GetValueComparer())).Snapshot(source.GetCurrentValue(__jObject))), (source.GetCurrentValue(_etag) == null ? null : ((ValueComparer)(((IProperty)_etag).GetValueComparer())).Snapshot(source.GetCurrentValue(_etag)))))); + return ((ISnapshot)(new Snapshot, List>, Dictionary, string, JObject, string>(((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue(id)), (source.GetCurrentValue(partitionId) == null ? null : ((ValueComparer)(((IProperty)partitionId).GetValueComparer())).Snapshot(source.GetCurrentValue(partitionId))), (source.GetCurrentValue(blob) == null ? null : ((ValueComparer)(((IProperty)blob).GetValueComparer())).Snapshot(source.GetCurrentValue(blob))), ((ValueComparer>)(((IProperty)bytes).GetValueComparer())).Snapshot(source.GetCurrentValue>(bytes)), (((object)(source.GetCurrentValue>>(list))) == null ? null : ((List>)(((ValueComparer)(((IProperty)list).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>>(list))))))), (((object)(source.GetCurrentValue>(map))) == null ? null : ((Dictionary)(((ValueComparer)(((IProperty)map).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(map))))))), (source.GetCurrentValue(__id) == null ? null : ((ValueComparer)(((IProperty)__id).GetValueComparer())).Snapshot(source.GetCurrentValue(__id))), (source.GetCurrentValue(__jObject) == null ? null : ((ValueComparer)(((IProperty)__jObject).GetValueComparer())).Snapshot(source.GetCurrentValue(__jObject))), (source.GetCurrentValue(_etag) == null ? null : ((ValueComparer)(((IProperty)_etag).GetValueComparer())).Snapshot(source.GetCurrentValue(_etag)))))); }); runtimeEntityType.SetStoreGeneratedValuesFactory( ISnapshot () => ((ISnapshot)(new Snapshot((default(JObject) == null ? null : ((ValueComparer)(((IProperty)__jObject).GetValueComparer())).Snapshot(default(JObject))), (default(string) == null ? null : ((ValueComparer)(((IProperty)_etag).GetValueComparer())).Snapshot(default(string))))))); runtimeEntityType.SetTemporaryValuesFactory( ISnapshot (InternalEntityEntry source) => ((ISnapshot)(new Snapshot(default(JObject), default(string))))); runtimeEntityType.SetShadowValuesFactory( - ISnapshot (IDictionary source) => ((ISnapshot)(new Snapshot>, Dictionary, string, JObject, string>((source.ContainsKey("Id") ? ((int)(source["Id"])) : 0), (source.ContainsKey("PartitionId") ? ((long? )(source["PartitionId"])) : null), (source.ContainsKey("List") ? ((List>)(source["List"])) : null), (source.ContainsKey("Map") ? ((Dictionary)(source["Map"])) : null), (source.ContainsKey("__id") ? ((string)(source["__id"])) : null), (source.ContainsKey("__jObject") ? ((JObject)(source["__jObject"])) : null), (source.ContainsKey("_etag") ? ((string)(source["_etag"])) : null))))); + ISnapshot (IDictionary source) => ((ISnapshot)(new Snapshot, List>, Dictionary, string, JObject, string>((source.ContainsKey("Id") ? ((int)(source["Id"])) : 0), (source.ContainsKey("PartitionId") ? ((long? )(source["PartitionId"])) : null), (source.ContainsKey("Bytes") ? ((ReadOnlyMemory)(source["Bytes"])) : default(ReadOnlyMemory)), (source.ContainsKey("List") ? ((List>)(source["List"])) : null), (source.ContainsKey("Map") ? ((Dictionary)(source["Map"])) : null), (source.ContainsKey("__id") ? ((string)(source["__id"])) : null), (source.ContainsKey("__jObject") ? ((JObject)(source["__jObject"])) : null), (source.ContainsKey("_etag") ? ((string)(source["_etag"])) : null))))); runtimeEntityType.SetEmptyShadowValuesFactory( - ISnapshot () => ((ISnapshot)(new Snapshot>, Dictionary, string, JObject, string>(default(int), default(long? ), default(List>), default(Dictionary), default(string), default(JObject), default(string))))); + ISnapshot () => ((ISnapshot)(new Snapshot, List>, Dictionary, string, JObject, string>(default(int), default(long? ), default(ReadOnlyMemory), default(List>), default(Dictionary), default(string), default(JObject), default(string))))); runtimeEntityType.SetRelationshipSnapshotFactory( ISnapshot (InternalEntityEntry source) => { @@ -392,11 +426,11 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) return ((ISnapshot)(new Snapshot(((ValueComparer)(((IProperty)id).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(id)), (source.GetCurrentValue(partitionId) == null ? null : ((ValueComparer)(((IProperty)partitionId).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(partitionId)))))); }); runtimeEntityType.Counts = new PropertyCounts( - propertyCount: 8, + propertyCount: 9, navigationCount: 0, complexPropertyCount: 0, - originalValueCount: 8, - shadowCount: 7, + originalValueCount: 9, + shadowCount: 8, relationshipCount: 2, storeGeneratedCount: 2); runtimeEntityType.AddAnnotation("Cosmos:ContainerName", "DataContainer"); diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs index 31dc64e2e98..7b2e808bca4 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs @@ -31,6 +31,7 @@ public virtual Task Basic_cosmos_model() eb.ToContainer("DataContainer"); eb.Property>("Map"); eb.Property>>("List"); + eb.Property>("Bytes"); eb.UseETagConcurrency(); eb.HasNoDiscriminator(); eb.Property(d => d.Blob).ToJsonProperty("JsonBlob"); @@ -128,6 +129,22 @@ public virtual Task Basic_cosmos_model() Assert.NotNull(list.GetValueComparer()); Assert.NotNull(list.GetKeyValueComparer()); + var bytes = dataEntity.FindProperty("Bytes")!; + Assert.Equal(typeof(ReadOnlyMemory), bytes.ClrType); + Assert.Null(bytes.PropertyInfo); + Assert.Null(bytes.FieldInfo); + Assert.False(bytes.IsNullable); + Assert.False(bytes.IsConcurrencyToken); + Assert.False(bytes.IsPrimitiveCollection); + Assert.Equal(ValueGenerated.Never, bytes.ValueGenerated); + Assert.Equal(PropertySaveBehavior.Save, bytes.GetAfterSaveBehavior()); + Assert.Equal(PropertySaveBehavior.Save, bytes.GetBeforeSaveBehavior()); + Assert.Equal("Bytes", CosmosPropertyExtensions.GetJsonPropertyName(bytes)); + Assert.Null(bytes.GetValueGeneratorFactory()); + Assert.Null(bytes.GetValueConverter()); + Assert.NotNull(bytes.GetValueComparer()); + Assert.NotNull(bytes.GetKeyValueComparer()); + var eTag = dataEntity.FindProperty("_etag")!; Assert.Equal(typeof(string), eTag.ClrType); Assert.Null(eTag.PropertyInfo); @@ -177,7 +194,7 @@ public virtual Task Basic_cosmos_model() Assert.Equal(1, dataEntity.GetKeys().Count()); - Assert.Equal([id, partitionId, blob, list, map, storeId, jObject, eTag], dataEntity.GetProperties()); + Assert.Equal([id, partitionId, blob, bytes, list, map, storeId, jObject, eTag], dataEntity.GetProperties()); }); protected override void BuildBigModel(ModelBuilder modelBuilder, bool jsonColumns) diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 843686b55c6..1673d662679 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -9,6 +9,7 @@ using Azure.ResourceManager.CosmosDB; using Azure.ResourceManager.CosmosDB.Models; using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -390,12 +391,15 @@ await database.Value.GetCosmosDBSqlContainers().CreateOrUpdateAsync( mappedTypes.Add(entityType); } +#pragma warning disable EF9103 foreach (var (containerName, mappedTypes) in containers) { IReadOnlyList partitionKeyStoreNames = Array.Empty(); int? analyticalTtl = null; int? defaultTtl = null; ThroughputProperties? throughput = null; + var indexes = new List(); + var vectors = new List<(IProperty Property, CosmosVectorType VectorType)>(); foreach (var entityType in mappedTypes) { @@ -406,14 +410,26 @@ await database.Value.GetCosmosDBSqlContainers().CreateOrUpdateAsync( analyticalTtl ??= entityType.GetAnalyticalStoreTimeToLive(); defaultTtl ??= entityType.GetDefaultTimeToLive(); throughput ??= entityType.GetThroughput(); + indexes.AddRange(entityType.GetIndexes()); + + foreach (var property in entityType.GetProperties()) + { + if (property.FindTypeMapping() is CosmosVectorTypeMapping vectorTypeMapping) + { + vectors.Add((property, vectorTypeMapping.VectorType)); + } + } } +#pragma warning restore EF9103 yield return new( containerName, partitionKeyStoreNames, analyticalTtl, defaultTtl, - throughput); + throughput, + indexes, + vectors); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/VectorSearchCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/VectorSearchCosmosTest.cs new file mode 100644 index 00000000000..c67ca8c3cba --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/VectorSearchCosmosTest.cs @@ -0,0 +1,442 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore.Cosmos.Extensions; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + +namespace Microsoft.EntityFrameworkCore; + +#pragma warning disable EF9103 + +// These tests are skipped for now because we cannot create a compatible test container until the SDK supports it. +internal class VectorSearchCosmosTest : IClassFixture +{ + public VectorSearchCosmosTest(VectorSearchFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + _testOutputHelper = testOutputHelper; + fixture.TestSqlLoggerFactory.Clear(); + } + + protected VectorSearchFixture Fixture { get; } + + private readonly ITestOutputHelper _testOutputHelper; + + [ConditionalFact] + public virtual async Task Query_for_vector_distance_sbytes() + { + await using var context = CreateContext(); + var inputVector = new ReadOnlyMemory([2, -1, 4, 3, 5, -2, 5, -7, 3, 1]); + + var booksFromStore = await context + .Set() + .Select(e => EF.Functions.VectorDistance(e.SBytes, inputVector)) + .ToListAsync(); + + Assert.Equal(3, booksFromStore.Count); + Assert.All(booksFromStore, s => Assert.NotEqual(0.0, s)); + + AssertSql( + """ +@__inputVector_1='[2,-1,4,3,5,-2,5,-7,3,1]' + +SELECT VALUE VectorDistance(c["SBytes"], @__inputVector_1, false, {'distanceFunction':'dotproduct', 'dataType':'int8'}) +FROM root c +"""); + } + + [ConditionalFact] + public virtual async Task Query_for_vector_distance_bytes() + { + await using var context = CreateContext(); + var inputVector = new ReadOnlyMemory([2, 1, 4, 3, 5, 2, 5, 7, 3, 1]); + + var booksFromStore = await context + .Set() + .Select(e => EF.Functions.VectorDistance(e.Bytes, inputVector)) + .ToListAsync(); + + Assert.Equal(3, booksFromStore.Count); + Assert.All(booksFromStore, s => Assert.NotEqual(0.0, s)); + + AssertSql( + """ +@__inputVector_1='[2,1,4,3,5,2,5,7,3,1]' + +SELECT VALUE VectorDistance(c["Bytes"], @__inputVector_1, false, {'distanceFunction':'cosine', 'dataType':'uint8'}) +FROM root c +"""); + } + + [ConditionalFact] + public virtual async Task Query_for_vector_distance_singles() + { + await using var context = CreateContext(); + var inputVector = new ReadOnlyMemory([0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f]); + + var booksFromStore = await context + .Set() + .Select( + e => EF.Functions.VectorDistance(e.Singles, inputVector, false, DistanceFunction.DotProduct)) + .ToListAsync(); + + Assert.Equal(3, booksFromStore.Count); + Assert.All(booksFromStore, s => Assert.NotEqual(0.0, s)); + + AssertSql( + """ +@__inputVector_1='[0.33,-0.52,0.45,-0.67,0.89,-0.34,0.86,-0.78,0.86,-0.78]' + +SELECT VALUE VectorDistance(c["Singles"], @__inputVector_1, false, {'distanceFunction':'dotproduct', 'dataType':'float32'}) +FROM root c +"""); + } + + [ConditionalFact] + public virtual async Task Query_for_vector_distance_bytes_array() + { + await using var context = CreateContext(); + var inputVector = new byte[] { 2, 1, 4, 3, 5, 2, 5, 7, 3, 1 }; + + // See Issue #34402 + await Assert.ThrowsAsync( + () => context.Set().Select(e => EF.Functions.VectorDistance(e.BytesArray, inputVector)).ToListAsync()); + + // Assert.Equal(3, booksFromStore.Count); + // Assert.All(booksFromStore, s => Assert.NotEqual(0.0, s)); + + AssertSql( + """ +SELECT VALUE c["BytesArray"] +FROM root c +"""); + } + + [ConditionalFact] + public virtual async Task Query_for_vector_distance_singles_array() + { + await using var context = CreateContext(); + var inputVector = new[] { 0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f }; + + // See Issue #34402 + await Assert.ThrowsAsync( + () => context.Set() + .Select(e => EF.Functions.VectorDistance(e.SinglesArray, inputVector, false, DistanceFunction.DotProduct)).ToListAsync()); + + // Assert.Equal(3, booksFromStore.Count); + // Assert.All(booksFromStore, s => Assert.NotEqual(0.0, s)); + + AssertSql( + """ +SELECT VALUE c["SinglesArray"] +FROM root c +"""); + } + + [ConditionalFact] + public virtual async Task Vector_distance_sbytes_in_OrderBy() + { + await using var context = CreateContext(); + var inputVector = new sbyte[] { 2, 1, 4, 6, 5, 2, 5, 7, 3, 1 }; + + var booksFromStore = await context + .Set() + .OrderBy(e => EF.Functions.VectorDistance(e.SBytes, inputVector, false, DistanceFunction.DotProduct)) + .ToListAsync(); + + Assert.Equal(3, booksFromStore.Count); + + AssertSql( + """ +@__p_1='[2,1,4,6,5,2,5,7,3,1]' + +SELECT VALUE c +FROM root c +ORDER BY VectorDistance(c["SBytes"], @__p_1, false, {'distanceFunction':'dotproduct', 'dataType':'int8'}) +"""); + } + + [ConditionalFact] + public virtual async Task Vector_distance_bytes_in_OrderBy() + { + await using var context = CreateContext(); + var inputVector = new byte[] { 2, 1, 4, 6, 5, 2, 5, 7, 3, 1 }; + + var booksFromStore = await context + .Set() + .OrderBy(e => EF.Functions.VectorDistance(e.Bytes, inputVector)) + .ToListAsync(); + + Assert.Equal(3, booksFromStore.Count); + AssertSql( + """ +@__p_1='[2,1,4,6,5,2,5,7,3,1]' + +SELECT VALUE c +FROM root c +ORDER BY VectorDistance(c["Bytes"], @__p_1, false, {'distanceFunction':'cosine', 'dataType':'uint8'}) +"""); + } + + [ConditionalFact] + public virtual async Task Vector_distance_singles_in_OrderBy() + { + await using var context = CreateContext(); + var inputVector = new[] { 0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f }; + + var booksFromStore = await context + .Set() + .OrderBy(e => EF.Functions.VectorDistance(e.Singles, inputVector)) + .ToListAsync(); + + Assert.Equal(3, booksFromStore.Count); + AssertSql( + """ +@__p_1='[0.33,-0.52,0.45,-0.67,0.89,-0.34,0.86,-0.78]' + +SELECT VALUE c +FROM root c +ORDER BY VectorDistance(c["Singles"], @__p_1, false, {'distanceFunction':'cosine', 'dataType':'float32'}) +"""); + } + + [ConditionalFact] + public virtual async Task Vector_distance_bytes_array_in_OrderBy() + { + await using var context = CreateContext(); + var inputVector = new byte[] { 2, 1, 4, 6, 5, 2, 5, 7, 3, 1 }; + + // See Issue #34402 + await Assert.ThrowsAsync( + () => context.Set().OrderBy(e => EF.Functions.VectorDistance(e.BytesArray, inputVector)).ToListAsync()); + + // Assert.Equal(3, booksFromStore.Count); + + AssertSql( +); + } + + [ConditionalFact] + public virtual async Task Vector_distance_singles_array_in_OrderBy() + { + await using var context = CreateContext(); + var inputVector = new[] { 0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f }; + + // See Issue #34402 + await Assert.ThrowsAsync( + () => context.Set().OrderBy(e => EF.Functions.VectorDistance(e.SinglesArray, inputVector)).ToListAsync()); + + // Assert.Equal(3, booksFromStore.Count); + + AssertSql(); + } + + [ConditionalFact] + public virtual async Task VectorDistance_throws_when_used_on_non_vector() + { + await using var context = CreateContext(); + var inputVector = Array.Empty(); + + Assert.Equal( + CosmosStrings.VectorSearchRequiresVector, + (await Assert.ThrowsAsync( + async () => await context + .Set() + .OrderBy(e => EF.Functions.VectorDistance(e.Isbn, inputVector)) + .ToListAsync())).Message); + + Assert.Equal( + CosmosStrings.VectorSearchRequiresVector, + (await Assert.ThrowsAsync( + async () => await context + .Set() + .OrderBy(e => EF.Functions.VectorDistance(inputVector, e.Isbn)) + .ToListAsync())).Message); + } + + [ConditionalFact] + public virtual async Task VectorDistance_throws_when_used_with_non_const_args() + { + await using var context = CreateContext(); + var inputVector = new ReadOnlyMemory( + [ + 0.33f, + -0.52f, + 0.45f, + -0.67f, + 0.89f, + -0.34f, + 0.86f, + -0.78f + ]); + + Assert.Equal( + CoreStrings.ArgumentNotConstant("useBruteForce", nameof(CosmosDbFunctionsExtensions.VectorDistance)), + (await Assert.ThrowsAsync( + async () => await context + .Set() + .OrderBy(e => EF.Functions.VectorDistance(e.Singles, inputVector, e.IsPublished)) + .ToListAsync())).Message); + + Assert.Equal( + CoreStrings.ArgumentNotConstant("distanceFunction", nameof(CosmosDbFunctionsExtensions.VectorDistance)), + (await Assert.ThrowsAsync( + async () => await context + .Set() + .OrderBy( + e => EF.Functions.VectorDistance(e.Singles, inputVector, false, e.DistanceFunction)) + .ToListAsync())).Message); + } + + private class Book + { + public Guid Id { get; set; } + + public string Publisher { get; set; } = null!; + + public string Title { get; set; } = null!; + + public string Author { get; set; } = null!; + + public ReadOnlyMemory Isbn { get; set; } = null!; + + public bool IsPublished { get; set; } + + public DistanceFunction DistanceFunction { get; set; } // Not meaningful; used for exception testing. + + public ReadOnlyMemory Bytes { get; set; } = null!; + + public ReadOnlyMemory SBytes { get; set; } = null!; + + public ReadOnlyMemory Singles { get; set; } = null!; + + public byte[] BytesArray { get; set; } = null!; + + public float[] SinglesArray { get; set; } = null!; + + public Owned1 OwnedReference { get; set; } = null!; + public List OwnedCollection { get; set; } = null!; + } + + [Owned] + protected class Owned1 + { + public int Prop { get; set; } + public Owned2 NestedOwned { get; set; } = null!; + public List NestedOwnedCollection { get; set; } = null!; + } + + [Owned] + protected class Owned2 + { + public string Prop { get; set; } = null!; + } + + protected DbContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class VectorSearchFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "VectorSearchTest"; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + => modelBuilder.Entity( + b => + { + b.Property(e => e.Id).ValueGeneratedOnAdd(); + b.HasKey(e => e.Id); + b.HasPartitionKey(e => e.Publisher); + + b.HasIndex(e => e.Bytes).ForVectors(VectorIndexType.Flat); + b.HasIndex(e => e.SBytes).ForVectors(VectorIndexType.Flat); + b.HasIndex(e => e.Singles).ForVectors(VectorIndexType.Flat); + b.HasIndex(e => e.BytesArray).ForVectors(VectorIndexType.Flat); + b.HasIndex(e => e.SinglesArray).ForVectors(VectorIndexType.Flat); + + b.Property(e => e.Bytes).IsVector(DistanceFunction.Cosine, 10); + b.Property(e => e.SBytes).IsVector(DistanceFunction.DotProduct, 10); + b.Property(e => e.Singles).IsVector(DistanceFunction.Cosine, 10); + b.Property(e => e.BytesArray).IsVector(DistanceFunction.Cosine, 10); + b.Property(e => e.SinglesArray).IsVector(DistanceFunction.Cosine, 10); + }); + + protected override Task SeedAsync(PoolableDbContext context) + { + var book1 = new Book + { + Publisher = "Manning", + Author = "Jon P Smith", + Title = "Entity Framework Core in Action", + Isbn = new ReadOnlyMemory("978-1617298363"u8.ToArray()), + Bytes = new ReadOnlyMemory([2, 1, 4, 3, 5, 2, 5, 7, 3, 1]), + SBytes = new ReadOnlyMemory([2, -1, 4, 3, 5, -2, 5, -7, 3, 1]), + Singles = new ReadOnlyMemory([0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f]), + BytesArray = [2, 1, 4, 3, 5, 2, 5, 7, 3, 1], + SinglesArray = [0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f], + OwnedReference = new Owned1 + { + Prop = 7, + NestedOwned = new Owned2 { Prop = "7" }, + NestedOwnedCollection = new List { new() { Prop = "71" }, new() { Prop = "72" } } + }, + OwnedCollection = new List { new Owned1 { Prop = 71 }, new Owned1 { Prop = 72 } } + }; + + var book2 = new Book + { + Publisher = "O'Reilly", + Author = "Julie Lerman", + Title = "Programming Entity Framework: DbContext", + Isbn = new ReadOnlyMemory("978-1449312961"u8.ToArray()), + Bytes = new ReadOnlyMemory([2, 1, 4, 3, 5, 2, 5, 7, 3, 1]), + SBytes = new ReadOnlyMemory([2, -1, 4, 3, 5, -2, 5, -7, 3, 1]), + Singles = new ReadOnlyMemory([0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f]), + BytesArray = [2, 1, 4, 3, 5, 2, 5, 7, 3, 1], + SinglesArray = [0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f], + OwnedReference = new Owned1 + { + Prop = 7, + NestedOwned = new Owned2 { Prop = "7" }, + NestedOwnedCollection = new List { new() { Prop = "71" }, new() { Prop = "72" } } + }, + OwnedCollection = new List { new Owned1 { Prop = 71 }, new Owned1 { Prop = 72 } } + }; + + var book3 = new Book + { + Publisher = "O'Reilly", + Author = "Julie Lerman", + Title = "Programming Entity Framework", + Isbn = new ReadOnlyMemory("978-0596807269"u8.ToArray()), + Bytes = new ReadOnlyMemory([2, 1, 4, 3, 5, 2, 5, 7, 3, 1]), + SBytes = new ReadOnlyMemory([2, -1, 4, 3, 5, -2, 5, -7, 3, 1]), + Singles = new ReadOnlyMemory([0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f]), + BytesArray = [2, 1, 4, 3, 5, 2, 5, 7, 3, 1], + SinglesArray = [0.33f, -0.52f, 0.45f, -0.67f, 0.89f, -0.34f, 0.86f, -0.78f, 0.86f, -0.78f], + OwnedReference = new Owned1 + { + Prop = 7, + NestedOwned = new Owned2 { Prop = "7" }, + NestedOwnedCollection = new List { new() { Prop = "71" }, new() { Prop = "72" } } + }, + OwnedCollection = new List { new Owned1 { Prop = 71 }, new Owned1 { Prop = 72 } } + }; + + context.AddRange(book1, book2, book3); + + return context.SaveChangesAsync(); + } + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + } +} +#pragma warning restore EF9103 diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs index ea9900bdef2..0d1f6b00db1 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs @@ -2,13 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using Azure.Core; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.ConferencePlanner; -using Microsoft.EntityFrameworkCore.TestUtilities; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore; diff --git a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs index 344665168fa..37526b77314 100644 --- a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs +++ b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs @@ -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.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Newtonsoft.Json.Linq; @@ -468,28 +469,78 @@ public virtual void Detects_nonString_concurrency_token() VerifyError(CosmosStrings.ETagNonStringStoreType("_etag", nameof(Customer), "int"), modelBuilder); } +#pragma warning disable EF9103 + [ConditionalFact] + public virtual void Detects_multi_property_vector_index() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity( + b => + { + b.HasIndex(e => new { e.Name, e.OtherName }).ForVectors(VectorIndexType.Flat); + }); + + VerifyError(CosmosStrings.CompositeVectorIndex(nameof(Customer), "Name,OtherName"), modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_vector_index_on_non_vector_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity( + b => + { + b.HasIndex(e => new { e.Name }).ForVectors(VectorIndexType.Flat); + }); + + VerifyError(CosmosStrings.VectorIndexOnNonVector(nameof(Customer), "Name"), modelBuilder); + } +#pragma warning restore EF9103 + + + [ConditionalFact] + public virtual void Detects_vector_property_with_unknown_data_type() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity( + b => + { +#pragma warning disable EF9103 + b.Property(e => e.Vector).IsVector(DistanceFunction.Cosine, dimensions: 10); +#pragma warning restore EF9103 + }); + + VerifyError(CosmosStrings.BadVectorDataType("double[]"), modelBuilder); + } + + private class NonVector + { + public Guid Id { get; set; } + public double[] Vector { get; set; } + } + [ConditionalFact] public virtual void Detects_unmappable_property() { var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity>>().ToContainer("Orders"); + modelBuilder.Entity>>().ToContainer("Orders"); VerifyError(CoreStrings.PropertyNotAdded( - typeof(RememberMyName>).ShortDisplayName(), + typeof(RememberMyName>).ShortDisplayName(), nameof(RememberMyName.ForgetMeNot), - typeof(ReadOnlyMemory).ShortDisplayName()), modelBuilder); + typeof(Memory).ShortDisplayName()), modelBuilder); } [ConditionalFact] public virtual void Detects_unmappable_list_property() { var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity[]>>().ToContainer("Orders"); + modelBuilder.Entity[]>>().ToContainer("Orders"); VerifyError(CoreStrings.PropertyNotAdded( - typeof(RememberMyName[]>).ShortDisplayName(), + typeof(RememberMyName[]>).ShortDisplayName(), nameof(RememberMyName.ForgetMeNot), - typeof(ReadOnlyMemory[]).ShortDisplayName()), modelBuilder); + typeof(Memory[]).ShortDisplayName()), modelBuilder); } private class RememberMyName diff --git a/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs b/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs index 66d6f23211e..67e2e2196a0 100644 --- a/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs +++ b/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs @@ -332,8 +332,6 @@ public void Does_not_map_Memory_types_without_converter() { Assert.Null(GetTypeMapping(typeof(Memory))); Assert.Null(GetTypeMapping(typeof(Memory?))); - Assert.Null(GetTypeMapping(typeof(ReadOnlyMemory))); - Assert.Null(GetTypeMapping(typeof(ReadOnlyMemory?))); } private static Type UnwrapNullableType(Type type)