diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml index 4b28fd7cc2..ff54d18a17 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml @@ -415,7 +415,7 @@ No conversions are performed; therefore. the data retrieved must already be a ch SqlMoney SqlSingle SqlString - SqlVectorFloat32 + SqlVector Stream String TextReader @@ -490,7 +490,7 @@ No conversions are performed; therefore. the data retrieved must already be a ch SqlMoney SqlSingle SqlString - SqlVectorFloat32 + SqlVector Stream String TextReader @@ -964,13 +964,13 @@ The method retur No conversions are performed; therefore, the data retrieved must already be a JSON string, or an exception is generated. - + - Gets the value of the specified column as a . + Gets the value of the specified column as a . - A object representing the column at the given ordinal. + A object representing the column at the given ordinal. The index passed was outside the range of 0 to - 1 @@ -979,12 +979,12 @@ The method retur An attempt was made to read or access columns in a closed . - The retrieved data is not compatible with the type. + The retrieved data is not compatible with the type. No conversions are performed; therefore, the data retrieved must already be a vector value, or an exception is generated. - + The zero-based column ordinal. diff --git a/doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml b/doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml new file mode 100644 index 0000000000..0157a05787 --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml @@ -0,0 +1,49 @@ + + + + + Represents a vector value in SQL Server. + + + + + Constructs a null vector of the given length. SQL Server requires vector arguments to specify their length even when null. + + + Vector length must be non-negative. + + + + + + Constructs a vector with the given values. + + + + + + + + Represents a null instance without any attributes. + + + This property is provided for compatibility with DataTable. + In most cases, prefer using IsNull to check if a SqlVector instance is a null vector. + This is equivalent to null value. + + + + + Returns the number of elements in the vector. + + + + + Returns the number of bytes required to represent this vector when communicating with SQL Server. + + + + Returns the vector values as a memory region. No copies are made. + + + diff --git a/doc/snippets/Microsoft.Data.SqlTypes/SqlVectorFloat32.xml b/doc/snippets/Microsoft.Data.SqlTypes/SqlVectorFloat32.xml deleted file mode 100644 index 2a01f616e4..0000000000 --- a/doc/snippets/Microsoft.Data.SqlTypes/SqlVectorFloat32.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - Represents the 32-bit float vector datatype in SQL Server. - - - - - Constructs a null vector of the given length. SQL Server requires vector arguments to specify their length even when null. - - - Vector column length must be non-negative. - - - - - - Constructs a vector with the given values. - - - - - - - - Represents a null instance of the type without any attributes. - - - This is equivalent to the C# null value. - - - - - Returns the number of elements in the vector. - - - - - Returns the number of bytes required to represent this vector when communicating with SQL Server. - - - - Returns the vector values as a memory region. No copies are made. - An array of float32 values as value. - - - Returns a JSON string representation of the vector. A new string is generated each time you call this method. - A JSON value. - - - Returns the vector values as an array of floats. - An array of values. - - - \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 223fda48ad..e4d29d999c 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -172,7 +172,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlTypes", " ProjectSection(SolutionItems) = preProject ..\doc\snippets\Microsoft.Data.SqlTypes\SqlFileStream.xml = ..\doc\snippets\Microsoft.Data.SqlTypes\SqlFileStream.xml ..\doc\snippets\Microsoft.Data.SqlTypes\SqlJson.xml = ..\doc\snippets\Microsoft.Data.SqlTypes\SqlJson.xml - ..\doc\snippets\Microsoft.Data.SqlTypes\SqlVectorFloat32.xml = ..\doc\snippets\Microsoft.Data.SqlTypes\SqlVectorFloat32.xml + ..\doc\snippets\Microsoft.Data.SqlTypes\SqlVector.xml = ..\doc\snippets\Microsoft.Data.SqlTypes\SqlVector.xml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Data.SqlClient.TestUtilities", "Microsoft.Data.SqlClient\tests\tools\Microsoft.Data.SqlClient.TestUtilities\Microsoft.Data.SqlClient.TestUtilities.csproj", "{89D6D382-9B36-43C9-A912-03802FDA8E36}" diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 0b3eacbe20..55b68fd9b3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -122,27 +122,24 @@ public SqlJson(System.Text.Json.JsonDocument jsonDoc) { } public override string ToString() { throw null; } } - /// - public sealed class SqlVectorFloat32 : System.Data.SqlTypes.INullable - { - /// - public SqlVectorFloat32(int length) { } - /// - public SqlVectorFloat32(System.ReadOnlyMemory values) { } - /// + /// + public sealed class SqlVector : System.Data.SqlTypes.INullable + where T : unmanaged + { + /// + public SqlVector(int length) { } + /// + public SqlVector(System.ReadOnlyMemory memory) { } + /// public bool IsNull => throw null; - /// - public static SqlVectorFloat32 Null => throw null; - /// + /// + public static SqlVector Null => throw null; + /// public int Length { get { throw null; } } - /// + /// public int Size { get { throw null; } } - /// - public System.ReadOnlyMemory Values { get { throw null; } } - /// - public override string ToString() { throw null; } - /// - public float[] ToArray() { throw null; } + /// + public System.ReadOnlyMemory Memory { get { throw null; } } } } namespace Microsoft.Data.SqlClient @@ -1395,8 +1392,8 @@ public override void Close() { } public virtual object GetSqlValue(int i) { throw null; } /// public virtual int GetSqlValues(object[] values) { throw null; } - /// - public virtual Microsoft.Data.SqlTypes.SqlVectorFloat32 GetSqlVectorFloat32(int i) { throw null; } + /// + public virtual Microsoft.Data.SqlTypes.SqlVector GetSqlVector(int i) where T : unmanaged { throw null; } /// public virtual System.Data.SqlTypes.SqlXml GetSqlXml(int i) { throw null; } /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 4d8c185d38..db60c1c927 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -795,8 +795,8 @@ Microsoft\Data\SqlTypes\SqlJson.cs - - Microsoft\Data\SqlTypes\SqlVectorFloat32.cs + + Microsoft\Data\SqlTypes\SqlVector.cs Resources\ResCategoryAttribute.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 8c8d17dacc..d7f280ca33 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -1370,8 +1370,8 @@ public override void Close() { } public virtual object GetSqlValue(int i) { throw null; } /// public virtual int GetSqlValues(object[] values) { throw null; } - /// - public virtual Microsoft.Data.SqlTypes.SqlVectorFloat32 GetSqlVectorFloat32(int i) { throw null; } + /// + public virtual Microsoft.Data.SqlTypes.SqlVector GetSqlVector(int i) where T : unmanaged { throw null; } /// public virtual System.Data.SqlTypes.SqlXml GetSqlXml(int i) { throw null; } /// @@ -2416,26 +2416,23 @@ public SqlJson(System.Text.Json.JsonDocument jsonDoc) { } public override string ToString() { throw null; } } - /// - public sealed class SqlVectorFloat32 : System.Data.SqlTypes.INullable - { - /// - public SqlVectorFloat32(int length) { } - /// - public SqlVectorFloat32(System.ReadOnlyMemory values) { } - /// + /// + public sealed class SqlVector : System.Data.SqlTypes.INullable + where T : unmanaged + { + /// + public SqlVector(int length) { } + /// + public SqlVector(System.ReadOnlyMemory memory) { } + /// public bool IsNull => throw null; - /// - public static SqlVectorFloat32 Null => throw null; - /// + /// + public static SqlVector Null => throw null; + /// public int Length { get { throw null; } } - /// + /// public int Size { get { throw null; } } - /// - public System.ReadOnlyMemory Values { get { throw null; } } - /// - public override string ToString() { throw null; } - /// - public float[] ToArray() { throw null; } + /// + public System.ReadOnlyMemory Memory { get { throw null; } } } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 42751b7d9d..1b1625bbb7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -585,7 +585,7 @@ Microsoft\Data\SqlClient\Server\SqlSer.cs - + Microsoft\Data\SqlClient\ISqlVector.cs @@ -909,8 +909,8 @@ Microsoft\Data\SqlTypes\SqlJson.cs - - Microsoft\Data\SqlTypes\SqlVectorFloat32.cs + + Microsoft\Data\SqlTypes\SqlVector.cs Resources\ResDescriptionAttribute.cs @@ -921,7 +921,10 @@ System\IO\StreamExtensions.netfx.cs - + + System\Runtime\CompilerServices\IsExternalInit.netfx.cs + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index c8747af0f9..470cb8e35e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -1147,12 +1147,6 @@ internal static Exception NullOutputParameterValueForVector(string paramName) internal static ArgumentException InvalidVectorHeader() => Argument(StringsHelper.GetString(Strings.ADP_InvalidVectorHeader)); - internal static ArgumentOutOfRangeException InvalidVectorColumnLength(string paramName) - => ArgumentOutOfRange(paramName, StringsHelper.GetString(Strings.ADP_InvalidVectorColumnLength)); - - internal static ArgumentException EmptyVectorValues(string arrayName) - => Argument(StringsHelper.GetString(Strings.ADP_EmptyVectorValues, arrayName)); - internal static Exception InvalidJsonStringForVector(string value, Exception inner) => InvalidOperation(StringsHelper.GetString(Strings.ADP_InvalidJsonStringForVector, value), inner); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs index 3a30edac44..ca9fe35743 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs @@ -28,7 +28,7 @@ internal interface ISqlVector byte[] VectorPayload { get; } /// - /// Gets the raw vector data formatted for TDS payload. + /// Returns the total size in bytes for sending SqlVector value on TDS. /// int Size { get; } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs index c4fff6b40a..39d2758d62 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs @@ -513,7 +513,7 @@ internal string String switch (elementType) { case MetaType.SqlVectorElementType.Float32: - return SqlVectorFloat32.ToString(); + return GetSqlVector().GetString(); default: throw SQL.VectorTypeNotSupported(elementType.ToString()); } @@ -954,7 +954,14 @@ internal SqlString SqlString { return SqlString.Null; } - return new SqlString(SqlVectorFloat32.ToString()); + var elementType = (MetaType.SqlVectorElementType)_value._vectorInfo._elementType; + switch (elementType) + { + case MetaType.SqlVectorElementType.Float32: + return new SqlString(GetSqlVector().GetString()); + default: + throw SQL.VectorTypeNotSupported(elementType.ToString()); + } } // String and Json storage type are both strings. if (_type is StorageType.String or StorageType.Json) @@ -980,14 +987,18 @@ internal SqlString SqlString internal SqlJson SqlJson => (StorageType.Json == _type) ? (IsNull ? SqlTypes.SqlJson.Null : new SqlJson((string)_object)) : (SqlJson)SqlValue; - internal SqlVectorFloat32 SqlVectorFloat32 => - _type is StorageType.Vector - ? ( - IsNull - ? new SqlVectorFloat32(_value._vectorInfo._elementCount) - : new SqlVectorFloat32(SqlBinary.Value) - ) - : (SqlVectorFloat32)SqlValue; + internal SqlVector GetSqlVector() where T : unmanaged + { + if (_type is StorageType.Vector) + { + if (IsNull) + { + return new SqlVector(_value._vectorInfo._elementCount); + } + return new SqlVector(SqlBinary.Value); + } + return (SqlVector)SqlValue; + } internal object SqlValue { @@ -1028,7 +1039,7 @@ internal object SqlValue switch (elementType) { case MetaType.SqlVectorElementType.Float32: - return SqlVectorFloat32; + return GetSqlVector(); default: throw SQL.VectorTypeNotSupported(elementType.ToString()); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 22c2a2da6a..d1f3f5c1a5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -2835,11 +2835,16 @@ virtual public SqlJson GetSqlJson(int i) return json; } - /// - virtual public SqlVectorFloat32 GetSqlVectorFloat32(int i) + /// + virtual public SqlVector GetSqlVector(int i) where T : unmanaged { + if (typeof(T) != typeof(float)) + { + throw SQL.VectorTypeNotSupported(typeof(T).FullName); + } + ReadColumn(i); - return _data[i].SqlVectorFloat32; + return _data[i].GetSqlVector(); } /// @@ -3101,7 +3106,7 @@ private object GetValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData metaDa switch (metaData.scale) { case (byte)MetaType.SqlVectorElementType.Float32: - return data.SqlVectorFloat32; + return data.GetSqlVector(); default: throw SQL.VectorTypeNotSupported(metaData.scale.ToString()); } @@ -3211,14 +3216,14 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met return (T)(object)data.TimeOnly; } #endif - else if (typeof(T) == typeof(SqlVectorFloat32)) + else if (typeof(T) == typeof(SqlVector)) { MetaType metaType = metaData.metaType; if (metaType.SqlDbType != SqlDbTypeExtensions.Vector) { throw SQL.VectorNotSupportedOnColumnType(metaData.column); } - return (T)(object)data.SqlVectorFloat32; + return (T)(object)data.GetSqlVector(); } else if (typeof(T) == typeof(XmlReader)) { @@ -3363,7 +3368,7 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met if (data.IsNull) return (T)(object)data.String; else - return (T)(object)data.SqlVectorFloat32.ToString(); + return (T)(object)data.GetSqlVector().GetString(); } // the requested type is likely to be one that isn't supported so try the cast and // unless there is a null value conversion then feedback the cast exception with diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs index 903dcdb3ed..27b3eebcd0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs @@ -64,9 +64,12 @@ internal sealed class MetaType internal readonly bool Is100Supported; // SqlVector Element Types + // + // These underlying values must match the vector "dimension type" values + // in the TDS protocol. internal enum SqlVectorElementType : byte { - Float32 = 0x0 + Float32 = 0x00 } public MetaType(byte precision, byte scale, int fixedLength, bool isFixed, bool isLong, bool isPlp, byte tdsType, byte nullableTdsType, string typeName, @@ -377,7 +380,7 @@ private static MetaType GetMetaTypeFromValue(Type dataType, object value, bool i return MetaXml; else if (dataType == typeof(SqlJson)) return s_MetaJson; - else if (dataType == typeof(SqlVectorFloat32)) + else if (dataType == typeof(SqlVector)) return s_MetaVector; else if (dataType == typeof(SqlString)) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs index 3f11139cdf..f9b940fec3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs @@ -773,7 +773,7 @@ private object GetVectorReturnValue() switch (elementType) { case MetaType.SqlVectorElementType.Float32: - return new SqlVectorFloat32(elementCount); + return new SqlVector(elementCount); default: throw SQL.VectorTypeNotSupported(elementType.ToString()); } @@ -781,7 +781,7 @@ private object GetVectorReturnValue() switch (elementType) { case MetaType.SqlVectorElementType.Float32: - return new SqlVectorFloat32((byte[])_sqlBufferReturnValue.Value); + return new SqlVector((byte[])_sqlBufferReturnValue.Value); default: throw SQL.VectorTypeNotSupported(elementType.ToString()); } @@ -2369,7 +2369,7 @@ internal static object CoerceValue(object value, MetaType destinationType, out b value = ((TimeOnly)value).ToTimeSpan(); } #endif - else if (currentType == typeof(SqlVectorFloat32)) + else if (currentType == typeof(SqlVector)) { value = ((ISqlVector)value).VectorPayload; } @@ -2377,7 +2377,7 @@ internal static object CoerceValue(object value, MetaType destinationType, out b { try { - value = (new SqlVectorFloat32(JsonSerializer.Deserialize(value as string)) as ISqlVector).VectorPayload; + value = (new SqlVector(JsonSerializer.Deserialize(value as string)) as ISqlVector).VectorPayload; } catch (Exception ex) when (ex is ArgumentNullException || ex is JsonException) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs new file mode 100644 index 0000000000..e63a5b7462 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -0,0 +1,239 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers.Binary; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.Json; +using Microsoft.Data.Common; +using Microsoft.Data.SqlClient; + +#nullable enable + +namespace Microsoft.Data.SqlTypes; + +/// +public sealed class SqlVector : INullable, ISqlVector +where T : unmanaged +{ + #region Constants + + private const byte VecHeaderMagicNo = 0xA9; + private const byte VecVersionNo = 0x01; + + #endregion + + #region Fields + + private readonly byte _elementType; + private readonly byte _elementSize; + private readonly byte[] _tdsBytes; + + #endregion + + #region Constructors + + /// + public SqlVector(int length) + { + if (length < 0) + { + throw ADP.ArgumentOutOfRange(nameof(length), SQLResource.InvalidArraySizeMessage); + } + + (_elementType, _elementSize) = GetTypeFieldsOrThrow(); + + IsNull = true; + + Length = length; + Size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); + + _tdsBytes = Array.Empty(); + Memory = new(); + } + + /// + public SqlVector(ReadOnlyMemory memory) + { + (_elementType, _elementSize) = GetTypeFieldsOrThrow(); + + IsNull = false; + + Length = memory.Length; + Size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); + + _tdsBytes = MakeTdsBytes(memory); + Memory = memory; + } + + internal SqlVector(byte[] tdsBytes) + { + (_elementType, _elementSize) = GetTypeFieldsOrThrow(); + + (Length, Size) = GetCountsOrThrow(tdsBytes); + + IsNull = false; + + _tdsBytes = tdsBytes; + Memory = new(MakeArray()); + } + + #endregion + + #region Methods + + internal string GetString() + { + if (IsNull) + { + return SQLResource.NullString; + } + return JsonSerializer.Serialize(Memory); + } + + #endregion + + #region Properties + + /// + public bool IsNull { get; init; } + + /// + public static SqlVector? Null => null; + + /// + public int Length { get; init; } + /// + public int Size { get; init; } + + /// + public ReadOnlyMemory Memory { get; init; } + + #endregion + + #region ISqlVector Internal Properties + byte ISqlVector.ElementType => _elementType; + byte ISqlVector.ElementSize => _elementSize; + byte[] ISqlVector.VectorPayload => _tdsBytes; + #endregion + + #region Helpers + + private (byte, byte) GetTypeFieldsOrThrow() + { + byte elementType; + byte elementSize; + + if (typeof(T) == typeof(float)) + { + elementType = (byte)MetaType.SqlVectorElementType.Float32; + elementSize = sizeof(float); + } + else + { + throw SQL.VectorTypeNotSupported(typeof(T).FullName); + } + + return (elementType, elementSize); + } + + private byte[] MakeTdsBytes(ReadOnlyMemory values) + { + //Refer to TDS section 2.2.5.5.7 for vector header format + // +------------------------+-----------------+----------------------+------------------+----------------------------+--------------+ + // | Field | Size (bytes) | Example Value | Description | + // +------------------------+-----------------+----------------------+--------------------------------------------------------------+ + // | Layout Format | 1 | 0xA9 | Magic number indicating vector layout format | + // | Layout Version | 1 | 0x01 | Version of the vector format | + // | Number of Dimensions | 2 | NN | Number of vector elements | + // | Dimension Type | 1 | 0x00 | Element type indicator (e.g. 0x00 for float32) | + // | Reserved | 3 | 0x00 0x00 0x00 | Reserved for future use | + // | Stream of Values | NN * sizeof(T) | [element bytes...] | Raw bytes for vector elements | + // +------------------------+-----------------+----------------------+--------------------------------------------------------------+ + + byte[] result = new byte[Size]; + + // Header Bytes + result[0] = VecHeaderMagicNo; + result[1] = VecVersionNo; + result[2] = (byte)(Length & 0xFF); + result[3] = (byte)((Length >> 8) & 0xFF); + result[4] = _elementType; + result[5] = 0x00; + result[6] = 0x00; + result[7] = 0x00; + +#if NETFRAMEWORK + // Copy data via marshaling. + if (MemoryMarshal.TryGetArray(values, out ArraySegment segment)) + { + Buffer.BlockCopy(segment.Array, segment.Offset * _elementSize, result, TdsEnums.VECTOR_HEADER_SIZE, segment.Count * _elementSize); + } + else + { + Buffer.BlockCopy(values.ToArray(), 0, result, TdsEnums.VECTOR_HEADER_SIZE, values.Length * _elementSize); + } +#else + // Fast span-based copy. + var byteSpan = MemoryMarshal.AsBytes(values.Span); + byteSpan.CopyTo(result.AsSpan(TdsEnums.VECTOR_HEADER_SIZE)); +#endif + return result; + } + + private (int, int) GetCountsOrThrow(byte[] rawBytes) + { + // Validate some of the header fields. + if ( + // Do we have enough bytes for the header? + rawBytes.Length < TdsEnums.VECTOR_HEADER_SIZE || + // Do we have the expected magic number? + rawBytes[0] != VecHeaderMagicNo || + // Do we support the version? + rawBytes[1] != VecVersionNo || + // Do the vector types match? + rawBytes[4] != _elementType) + { + // No, so throw. + throw ADP.InvalidVectorHeader(); + } + + // The vector length is an unsigned 16-bit integer, little-endian. + int length = BinaryPrimitives.ReadUInt16LittleEndian(rawBytes.AsSpan(2)); + + // The vector size is the number of bytes required to represent the vector in TDS. + int size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * length); + + // Are there exactly enough bytes for the vector elements? + if (rawBytes.Length != size) + { + // No, so throw. + throw ADP.InvalidVectorHeader(); + } + + return (length, size); + } + + private T[] MakeArray() + { + if (_tdsBytes.Length == 0) + { + return Array.Empty(); + } + +#if NETFRAMEWORK + // Allocate array and copy bytes into it + T[] result = new T[Length]; + Buffer.BlockCopy(_tdsBytes, 8, result, 0, _elementSize * Length); + return result; +#else + ReadOnlySpan dataSpan = _tdsBytes.AsSpan(8, _elementSize * Length); + return MemoryMarshal.Cast(dataSpan).ToArray(); +#endif + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVectorFloat32.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVectorFloat32.cs deleted file mode 100644 index 895cb2f07a..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVectorFloat32.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Data.SqlTypes; -using System.Text.Json; -using Microsoft.Data.Common; -using Microsoft.Data.SqlClient; - -#nullable enable -namespace Microsoft.Data.SqlTypes -{ - /// - public sealed class SqlVectorFloat32 : INullable, ISqlVector - { - #region Constants - private const byte VecHeaderMagicNo = 0xA9; - private const byte VecVersionNo = 0x01; - private const byte VecTypeFloat32 = 0x00; - - #endregion - - #region Fields - private readonly byte _elementSize = sizeof(float); - private readonly int _elementCount; - private readonly byte[] _rawBytes; - private readonly byte _elementType = VecTypeFloat32; - #endregion - - #region Constructors - private SqlVectorFloat32() - { - _elementCount = 0; - _rawBytes = Array.Empty(); - } - - internal SqlVectorFloat32(byte[] rawBytes) - { - if (!ValidateRawBytes(rawBytes)) - { - throw ADP.InvalidVectorHeader(); - } - _rawBytes = rawBytes; - _elementCount = BinaryPrimitives.ReadUInt16LittleEndian(rawBytes.AsSpan(2)); - var floatArray = new float[_elementCount]; - Buffer.BlockCopy(_rawBytes, 8, floatArray, 0, _elementCount * _elementSize); - Values = new ReadOnlyMemory(floatArray); - } - - /// - public SqlVectorFloat32(int length) - { - if (length < 0) - throw ADP.InvalidVectorColumnLength(nameof(length)); - - _elementCount = length; - _rawBytes = Array.Empty(); - Values = new ReadOnlyMemory(Array.Empty()); - } - - /// - public SqlVectorFloat32(ReadOnlyMemory values) - { - Values = values; - _elementCount = values.Length; - _rawBytes = new byte[TdsEnums.VECTOR_HEADER_SIZE + _elementCount * _elementSize]; - InitializeVectorBytes(values); - } - #endregion - - #region Methods - /// - public override string ToString() - { - if (IsNull || _rawBytes == null) - { - return SQLResource.NullString; - } - return JsonSerializer.Serialize(this.Values); - } - - /// - public float[] ToArray() - { - if (IsNull || _rawBytes == null) - { - return Array.Empty(); - } - return Values.ToArray(); - } - #endregion - - #region Properties - /// - public bool IsNull => _rawBytes.Length == 0; - - /// - public static SqlVectorFloat32? Null => null; - - /// - public int Length => _elementCount; - /// - public int Size => TdsEnums.VECTOR_HEADER_SIZE + _elementCount * _elementSize; - - /// - public ReadOnlyMemory Values { get; } - #endregion - - #region ISqlVectorProperties - byte ISqlVector.ElementType => _elementType; - byte ISqlVector.ElementSize => _elementSize; - byte[] ISqlVector.VectorPayload => _rawBytes; - #endregion - - #region Helpers - private void InitializeVectorBytes(ReadOnlyMemory values) - { - //Refer to TDS section 2.2.5.5.7 for vector header format - // +------------------------+-----------------+----------------------+------------------+----------------------------+--------------+ - // | Field | Size (bytes) | Example Value | Description | - // +------------------------+-----------------+----------------------+--------------------------------------------------------------+ - // | Layout Format | 1 | 0xA9 | Magic number indicating vector layout format | - // | Layout Version | 1 | 0x01 | Version of the vector format | - // | Number of Dimensions | 2 | NN | Number of vector elements | - // | Dimension Type | 1 | 0x00 | Element type indicator (e.g. 0x00 for float32) | - // | Reserved | 3 | 0x00 0x00 0x00 | Reserved for future use | - // | Stream of Values | NN * sizeof(T) | [float bytes...] | Raw bytes for vector elements | - // +------------------------+-----------------+----------------------+--------------------------------------------------------------+ - - _rawBytes[0] = VecHeaderMagicNo; - _rawBytes[1] = VecVersionNo; - BinaryPrimitives.WriteUInt16LittleEndian(_rawBytes.AsSpan(2), (ushort)_elementCount); - _rawBytes[4] = VecTypeFloat32; - _rawBytes[5] = 0x00; - _rawBytes[6] = 0x00; - _rawBytes[7] = 0x00; - - - // Write float data in little-endian format - Span dest = _rawBytes.AsSpan(TdsEnums.VECTOR_HEADER_SIZE); - -#if NETFRAMEWORK - // .NET Framework: Use BitConverter with endianness check - ReadOnlySpan floatSpan = values.Span; - for (int i = 0; i < floatSpan.Length; i++) - { - byte[] bytes = BitConverter.GetBytes(floatSpan[i]); - if (!BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } - bytes.CopyTo(dest.Slice(i * sizeof(float))); - } -#else - // .NET 8.0+: Use BinaryPrimitives for high-performance little-endian writing - ReadOnlySpan floatSpan = values.Span; - for (int i = 0; i < floatSpan.Length; i++) - { - BinaryPrimitives.WriteSingleLittleEndian(dest.Slice(i * sizeof(float)), floatSpan[i]); - } -#endif - } - - private bool ValidateRawBytes(byte[] rawBytes) - { - if (rawBytes.Length == 0 || rawBytes.Length < TdsEnums.VECTOR_HEADER_SIZE) - return false; - - if (rawBytes[0] != VecHeaderMagicNo || rawBytes[1] != VecVersionNo || rawBytes[4] != VecTypeFloat32) - return false; - - int elementCount = BinaryPrimitives.ReadUInt16LittleEndian(rawBytes.AsSpan(2)); - if (rawBytes.Length != TdsEnums.VECTOR_HEADER_SIZE + elementCount * _elementSize) - return false; - - return true; - } - - #endregion - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs index 87bd19f054..b41b331db7 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs @@ -10862,28 +10862,6 @@ internal static string ADP_InvalidVectorHeader } } - /// - /// Looks up a localized string similar to Vector column length must be non-negative.. - /// - internal static string ADP_InvalidVectorColumnLength - { - get - { - return ResourceManager.GetString("ADP_InvalidVectorColumnLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Values {0} cannot be null or empty.. - /// - internal static string ADP_EmptyVectorValues - { - get - { - return ResourceManager.GetString("ADP_EmptyVectorValues", resourceCulture); - } - } - /// /// Looks up a localized string similar to {0} Invalid JSON string for vector... /// @@ -10895,7 +10873,6 @@ internal static string ADP_InvalidJsonStringForVector } } - /// /// Looks up a localized string similar to Expecting argument of type {1}, but received type {0}.. /// diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx index f508014a82..90a157876a 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx @@ -4755,12 +4755,6 @@ Invalid vector header received. - - Vector column length must be non-negative. - - - Values cannot be null or empty. - {0} Invalid JSON string for vector. diff --git a/src/Microsoft.Data.SqlClient/src/System/Runtime/CompilerServices/IsExternalInit.netfx.cs b/src/Microsoft.Data.SqlClient/src/System/Runtime/CompilerServices/IsExternalInit.netfx.cs new file mode 100644 index 0000000000..0d0181ba6d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/System/Runtime/CompilerServices/IsExternalInit.netfx.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NETFRAMEWORK + +using System.ComponentModel; + + +// This class enables the use of the `init` property accessor in .NET framework. +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} + +#endif diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj index 3ab6fed746..56265208b4 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj @@ -65,7 +65,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlVectorFloat32Test.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlVectorFloat32Test.cs deleted file mode 100644 index 0bd08340d7..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlVectorFloat32Test.cs +++ /dev/null @@ -1,91 +0,0 @@ - - -using System; -using Microsoft.Data.SqlTypes; -using Xunit; - -namespace Microsoft.Data.SqlClient.Tests -{ - public class SqlVectorFloat32Test - { - [Fact] - public void Constructor_WithValidLength_ShouldSetLength() - { - var vec = new SqlVectorFloat32(5); - Assert.Equal(5, vec.Length); - Assert.True(vec.IsNull); - } - - [Fact] - public void Constructor_WithNegativeLength_ShouldThrow() - { - Assert.Throws(() => new SqlVectorFloat32(-1)); - } - - [Fact] - public void Constructor_WithEmptyValues_ShouldNotThrow() - { - var vec = new SqlVectorFloat32(values: Array.Empty()); - Assert.Equal(0, vec.Length); - Assert.False(vec.IsNull); - } - - [Fact] - public void Constructor_WithValues_ShouldSetProperties() - { - float[] data = new float[] { 1.1f, 2.2f }; - var vec = new SqlVectorFloat32(values: data); - Assert.Equal(2, vec.Length); - Assert.False(vec.IsNull); - Assert.Equal(data, vec.Values.ToArray()); - } - - [Fact] - public void Constructor_WithReadOnlyMem_ShouldSetProperties() - { - ReadOnlyMemory data = new float[] { 1.1f, 2.2f, 3.3f }; - var vec = new SqlVectorFloat32(values: data); - Assert.Equal(3, vec.Length); - Assert.False(vec.IsNull); - Assert.Equal(data.ToArray(), vec.Values.ToArray()); - } - - [Fact] - public void ToString_ShouldReturnJsonString() - { - float[] data = new float[] { 3.14f }; - var vec = new SqlVectorFloat32(values: data); - string json = vec.ToString(); - Assert.Contains("3.14", json); - } - - [Fact] - public void ToString_ShouldReturnNullString() - { - var vec = new SqlVectorFloat32(0); - string nullStr = vec.ToString(); - Assert.Contains("Null", nullStr); - } - - [Fact] - public void IsNull_WithLengthCtor_ShouldBeTrue() - { - var vec = new SqlVectorFloat32(0); - Assert.True(vec.IsNull); - } - - [Fact] - public void Null_Property_ShouldReturnNull() - { - SqlVectorFloat32 vec = SqlVectorFloat32.Null; - Assert.Null(vec); - } - - [Fact] - public void Values_WhenNull_ShouldReturnEmptyArray() - { - var vec = new SqlVectorFloat32(0); - Assert.Empty(vec.Values.ToArray()); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs index 158bb52aca..8d205cfc9c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs @@ -1,3 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; using System.Data; @@ -18,33 +22,33 @@ public static class VectorFloat32TestData public static int vectorColumnLength = testData.Length; public static IEnumerable GetVectorFloat32TestData() { - // Pattern 1-4 with SqlVectorFloat32(values: testData) - yield return new object[] { 1, new SqlVectorFloat32(testData), testData, sizeInbytes, vectorColumnLength }; - yield return new object[] { 2, new SqlVectorFloat32(testData), testData, sizeInbytes, vectorColumnLength }; - yield return new object[] { 3, new SqlVectorFloat32(testData), testData, sizeInbytes, vectorColumnLength }; - yield return new object[] { 4, new SqlVectorFloat32(testData), testData, sizeInbytes, vectorColumnLength }; - - // Pattern 1–4 with SqlVectorFloat32(n) - yield return new object[] { 1, new SqlVectorFloat32(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 2, new SqlVectorFloat32(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 3, new SqlVectorFloat32(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 4, new SqlVectorFloat32(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; - - // Pattern 1–4 with DBNull + // Pattern 1-4 with SqlVector(values: testData) + yield return new object[] { 1, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; + yield return new object[] { 2, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; + yield return new object[] { 3, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; + yield return new object[] { 4, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; + + // Pattern 1-4 with SqlVector(n) + yield return new object[] { 1, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; + yield return new object[] { 2, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; + yield return new object[] { 3, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; + yield return new object[] { 4, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; + + // Pattern 1-4 with DBNull yield return new object[] { 1, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; yield return new object[] { 2, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; yield return new object[] { 3, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; yield return new object[] { 4, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; - // Pattern 1–4 with SqlVectorFloat32.Null - yield return new object[] { 1, SqlVectorFloat32.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; + // Pattern 1-4 with SqlVector.Null + yield return new object[] { 1, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; // Following scenario is not supported in SqlClient. // This can only be fixed with a behavior change that SqlParameter.Value is internally set to DBNull.Value if it is set to null. - //yield return new object[] { 2, SqlVectorFloat32.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; + //yield return new object[] { 2, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 3, SqlVectorFloat32.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 4, SqlVectorFloat32.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; + yield return new object[] { 3, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; + yield return new object[] { 4, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; } } @@ -97,9 +101,9 @@ public void Dispose() DataTestUtility.DropStoredProcedure(connection, s_storedProcName); } - private void ValidateSqlVectorFloat32Object(bool isNull, SqlVectorFloat32 sqlVectorFloat32, float[] expectedData, int expectedSize, int expectedLength) + private void ValidateSqlVectorFloat32Object(bool isNull, SqlVector sqlVectorFloat32, float[] expectedData, int expectedSize, int expectedLength) { - Assert.Equal(expectedData, sqlVectorFloat32.Values.ToArray()); + Assert.Equal(expectedData, sqlVectorFloat32.Memory.ToArray()); Assert.Equal(expectedSize, sqlVectorFloat32.Size); Assert.Equal(expectedLength, sqlVectorFloat32.Length); if (!isNull) @@ -118,16 +122,16 @@ private void ValidateInsertedData(SqlConnection connection, float[] expectedData using var reader = selectCmd.ExecuteReader(); Assert.True(reader.Read(), "No data found in the table."); - //For both null and non-null cases, validate the SqlVectorFloat32 object - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVectorFloat32)reader.GetSqlVectorFloat32(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), reader.GetFieldValue(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVectorFloat32)reader.GetSqlValue(0), expectedData, expectedSize, expectedLength); + //For both null and non-null cases, validate the SqlVector object + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetSqlVector(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), reader.GetFieldValue>(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetSqlValue(0), expectedData, expectedSize, expectedLength); if (!reader.IsDBNull(0)) { - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVectorFloat32)reader.GetValue(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVectorFloat32)reader[0], expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVectorFloat32)reader["VectorData"], expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetValue(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader[0], expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader["VectorData"], expectedData, expectedSize, expectedLength); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetString(0))); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetSqlString(0).Value)); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetFieldValue(0))); @@ -167,7 +171,7 @@ public void TestSqlVectorFloat32ParameterInsertionAndReads( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVectorFloat32(3).Size) { Value = value }, + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; @@ -184,16 +188,16 @@ private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] e using var reader = await selectCmd.ExecuteReaderAsync(); Assert.True(await reader.ReadAsync(), "No data found in the table."); - //For both null and non-null cases, validate the SqlVectorFloat32 object - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVectorFloat32)reader.GetSqlVectorFloat32(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), await reader.GetFieldValueAsync(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVectorFloat32)reader.GetSqlValue(0), expectedData, expectedSize, expectedLength); + //For both null and non-null cases, validate the SqlVector object + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetSqlVector(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), await reader.GetFieldValueAsync>(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetSqlValue(0), expectedData, expectedSize, expectedLength); if (!await reader.IsDBNullAsync(0)) { - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVectorFloat32)reader.GetValue(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVectorFloat32)reader[0], expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVectorFloat32)reader["VectorData"], expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetValue(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader[0], expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader["VectorData"], expectedData, expectedSize, expectedLength); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetString(0))); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetSqlString(0).Value)); Assert.Equal(expectedData, JsonSerializer.Deserialize(await reader.GetFieldValueAsync(0))); @@ -233,7 +237,7 @@ public async Task TestSqlVectorFloat32ParameterInsertionAndReadsAsync( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVectorFloat32(3).Size) { Value = value }, + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; @@ -273,7 +277,7 @@ public void TestStoredProcParamsForVectorFloat32( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVectorFloat32(3).Size) { Value = value }, + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; command.Parameters.Add(inputParam); @@ -283,7 +287,7 @@ public void TestStoredProcParamsForVectorFloat32( ParameterName = s_outputVectorParamName, SqlDbType = SqlDbTypeExtensions.Vector, Direction = ParameterDirection.Output, - Value = new SqlVectorFloat32(3) + Value = new SqlVector(3) }; command.Parameters.Add(outputParam); @@ -291,13 +295,13 @@ public void TestStoredProcParamsForVectorFloat32( command.ExecuteNonQuery(); // Validate the output parameter - var vector = outputParam.Value as SqlVectorFloat32; + var vector = outputParam.Value as SqlVector; ValidateSqlVectorFloat32Object(vector.IsNull, vector, expectedValues, expectedSize, expectedLength); // Validate error for conventional way of setting output parameters command.Parameters.Clear(); command.Parameters.Add(inputParam); - var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, new SqlVectorFloat32(3).Size) { Direction = ParameterDirection.Output }; + var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Direction = ParameterDirection.Output }; command.Parameters.Add(outputParamWithoutVal); Assert.Throws(() => command.ExecuteNonQuery()); } @@ -331,7 +335,7 @@ public async Task TestStoredProcParamsForVectorFloat32Async( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVectorFloat32(3).Size) { Value = value }, + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; command.Parameters.Add(inputParam); @@ -341,7 +345,7 @@ public async Task TestStoredProcParamsForVectorFloat32Async( ParameterName = s_outputVectorParamName, SqlDbType = SqlDbTypeExtensions.Vector, Direction = ParameterDirection.Output, - Value = new SqlVectorFloat32(3) + Value = new SqlVector(3) }; command.Parameters.Add(outputParam); @@ -349,13 +353,13 @@ public async Task TestStoredProcParamsForVectorFloat32Async( await command.ExecuteNonQueryAsync(); // Validate the output parameter - var vector = outputParam.Value as SqlVectorFloat32; + var vector = outputParam.Value as SqlVector; ValidateSqlVectorFloat32Object(vector.IsNull, vector, expectedValues, expectedSize, expectedLength); // Validate error for conventional way of setting output parameters command.Parameters.Clear(); command.Parameters.Add(inputParam); - var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, new SqlVectorFloat32(3).Size) { Direction = ParameterDirection.Output }; + var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Direction = ParameterDirection.Output }; command.Parameters.Add(outputParamWithoutVal); await Assert.ThrowsAsync(async () => await command.ExecuteNonQueryAsync()); } @@ -377,7 +381,7 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) case 1: // Use SqlServer table as source var insertCmd = new SqlCommand($"insert into {s_bulkCopySrcTableName} values (@VectorData)", sourceConnection); - var vectorParam = new SqlParameter(s_vectorParamName, new SqlVectorFloat32(VectorFloat32TestData.testData)); + var vectorParam = new SqlParameter(s_vectorParamName, new SqlVector(VectorFloat32TestData.testData)); // Insert 2 rows with one non-null and null value insertCmd.Parameters.Add(vectorParam); @@ -391,8 +395,8 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) case 2: table = new DataTable(s_bulkCopySrcTableName); table.Columns.Add("Id", typeof(int)); - table.Columns.Add("VectorData", typeof(SqlVectorFloat32)); - table.Rows.Add(1, new SqlVectorFloat32(VectorFloat32TestData.testData)); + table.Columns.Add("VectorData", typeof(SqlVector)); + table.Rows.Add(1, new SqlVector(VectorFloat32TestData.testData)); table.Rows.Add(2, DBNull.Value); break; default: @@ -447,18 +451,18 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) // Validate first non-null value. Assert.True(!verifyReader.IsDBNull(0), "First row in the table is null."); - Assert.Equal(VectorFloat32TestData.testData, ((SqlVectorFloat32)verifyReader.GetSqlVectorFloat32(0)).Values.ToArray()); - Assert.Equal(VectorFloat32TestData.testData.Length, ((SqlVectorFloat32)verifyReader.GetSqlVectorFloat32(0)).Length); - Assert.Equal(VectorFloat32TestData.sizeInbytes, ((SqlVectorFloat32)verifyReader.GetSqlVectorFloat32(0)).Size); + Assert.Equal(VectorFloat32TestData.testData, ((SqlVector)verifyReader.GetSqlVector(0)).Memory.ToArray()); + Assert.Equal(VectorFloat32TestData.testData.Length, ((SqlVector)verifyReader.GetSqlVector(0)).Length); + Assert.Equal(VectorFloat32TestData.sizeInbytes, ((SqlVector)verifyReader.GetSqlVector(0)).Size); // Verify that we have another row Assert.True(verifyReader.Read(), "Second row not found in the table"); // Verify that we have encountered null. Assert.True(verifyReader.IsDBNull(0)); - Assert.Equal(Array.Empty(), ((SqlVectorFloat32)verifyReader.GetSqlVectorFloat32(0)).Values.ToArray()); - Assert.Equal(VectorFloat32TestData.testData.Length, ((SqlVectorFloat32)verifyReader.GetSqlVectorFloat32(0)).Length); - Assert.Equal(VectorFloat32TestData.sizeInbytes, ((SqlVectorFloat32)verifyReader.GetSqlVectorFloat32(0)).Size); + Assert.Equal(Array.Empty(), ((SqlVector)verifyReader.GetSqlVector(0)).Memory.ToArray()); + Assert.Equal(VectorFloat32TestData.testData.Length, ((SqlVector)verifyReader.GetSqlVector(0)).Length); + Assert.Equal(VectorFloat32TestData.sizeInbytes, ((SqlVector)verifyReader.GetSqlVector(0)).Size); } [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] @@ -479,7 +483,7 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) case 1: // Use SqlServer table as source var insertCmd = new SqlCommand($"insert into {s_bulkCopySrcTableName} values (@VectorData)", sourceConnection); - var vectorParam = new SqlParameter(s_vectorParamName, new SqlVectorFloat32(VectorFloat32TestData.testData)); + var vectorParam = new SqlParameter(s_vectorParamName, new SqlVector(VectorFloat32TestData.testData)); // Insert 2 rows with one non-null and null value insertCmd.Parameters.Add(vectorParam); @@ -493,8 +497,8 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) case 2: table = new DataTable(s_bulkCopySrcTableName); table.Columns.Add("Id", typeof(int)); - table.Columns.Add("VectorData", typeof(SqlVectorFloat32)); - table.Rows.Add(1, new SqlVectorFloat32(VectorFloat32TestData.testData)); + table.Columns.Add("VectorData", typeof(SqlVector)); + table.Rows.Add(1, new SqlVector(VectorFloat32TestData.testData)); table.Rows.Add(2, DBNull.Value); break; default: @@ -547,8 +551,8 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) // Validate first non-null value. Assert.True(!await verifyReader.IsDBNullAsync(0), "First row in the table is null."); - var vector = await verifyReader.GetFieldValueAsync(0); - Assert.Equal(VectorFloat32TestData.testData, vector.Values.ToArray()); + var vector = await verifyReader.GetFieldValueAsync>(0); + Assert.Equal(VectorFloat32TestData.testData, vector.Memory.ToArray()); Assert.Equal(VectorFloat32TestData.testData.Length, vector.Length); Assert.Equal(VectorFloat32TestData.sizeInbytes, vector.Size); @@ -557,8 +561,8 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) // Verify that we have encountered null. Assert.True(await verifyReader.IsDBNullAsync(0)); - vector = await verifyReader.GetFieldValueAsync(0); - Assert.Equal(Array.Empty(), vector.Values.ToArray()); + vector = await verifyReader.GetFieldValueAsync>(0); + Assert.Equal(Array.Empty(), vector.Memory.ToArray()); Assert.Equal(VectorFloat32TestData.testData.Length, vector.Length); Assert.Equal(VectorFloat32TestData.sizeInbytes, vector.Size); } @@ -569,12 +573,12 @@ public void TestInsertVectorsFloat32WithPrepare() SqlConnection conn = new SqlConnection(s_connectionString); conn.Open(); SqlCommand command = new SqlCommand(s_insertCmdString, conn); - SqlParameter vectorParam = new SqlParameter("@VectorData", SqlDbTypeExtensions.Vector, new SqlVectorFloat32(3).Size); + SqlParameter vectorParam = new SqlParameter("@VectorData", SqlDbTypeExtensions.Vector, new SqlVector(3).Size); command.Parameters.Add(vectorParam); command.Prepare(); for (int i = 0; i < 10; i++) { - vectorParam.Value = new SqlVectorFloat32(new float[] { i + 0.1f, i + 0.2f, i + 0.3f }); + vectorParam.Value = new SqlVector(new float[] { i + 0.1f, i + 0.2f, i + 0.3f }); command.ExecuteNonQuery(); } SqlCommand validateCommand = new SqlCommand($"SELECT VectorData FROM {s_tableName}", conn); @@ -583,7 +587,7 @@ public void TestInsertVectorsFloat32WithPrepare() while (reader.Read()) { float[] expectedData = new float[] { rowcnt + 0.1f, rowcnt + 0.2f, rowcnt + 0.3f }; - float[] dbData = reader.GetSqlVectorFloat32(0).Values.ToArray(); + float[] dbData = reader.GetSqlVector(0).Memory.ToArray(); Assert.Equal(expectedData, dbData); rowcnt++; } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs index d4857bf5e2..5fb9cf7625 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs @@ -1,3 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; using System.Data; @@ -271,11 +275,6 @@ public void TestVectorDataReadsAsVarchar() dbData = JsonSerializer.Deserialize(result)!; Assert.Equal(data, dbData); - //Read using GetValue.ToString() - result = reader.GetValue(0).ToString()!; - dbData = JsonSerializer.Deserialize(result)!; - Assert.Equal(data, dbData); - //Read using GetFieldValue result = reader.GetFieldValue(0); dbData = JsonSerializer.Deserialize(result)!; @@ -299,10 +298,6 @@ public void TestVectorDataReadsAsVarchar() //Read using GetSqlString Assert.Throws(() => reader.GetString(0)); - //Read using GetValue.ToString() - result = reader.GetValue(0).ToString(); - Assert.Equal(string.Empty, result); - //Read using GetFieldValue Assert.Throws(() => reader.GetFieldValue(0)); } @@ -337,11 +332,6 @@ public async Task TestVectorDataReadsAsVarcharAsync() dbData = JsonSerializer.Deserialize(result)!; Assert.Equal(data, dbData); - //Read using GetValue.ToString() - result = reader.GetValue(0).ToString()!; - dbData = JsonSerializer.Deserialize(result)!; - Assert.Equal(data, dbData); - //Read using GetFieldValue result = await reader.GetFieldValueAsync(0); dbData = JsonSerializer.Deserialize(result)!; @@ -365,10 +355,6 @@ public async Task TestVectorDataReadsAsVarcharAsync() //Read using GetSqlString Assert.Throws(() => reader2.GetString(0)); - //Read using GetValue.ToString() - result = reader2.GetValue(0).ToString(); - Assert.Equal(string.Empty, result); - //Read using GetFieldValueAsync await Assert.ThrowsAsync(async () => await reader2.GetFieldValueAsync(0)); } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs new file mode 100644 index 0000000000..3390d95c02 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs @@ -0,0 +1,245 @@ + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.SqlTypes; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.Tests; + +public class SqlVectorTest +{ + #region Tests + + [Fact] + public void UnsupportedType() + { + Assert.Throws(() => new SqlVector(5)); + Assert.Throws(() => new SqlVector(5)); + Assert.Throws(() => new SqlVector(5)); + } + + [Fact] + public void Construct_Length_Negative() + { + Assert.Throws(() => new SqlVector(-1)); + } + + [Fact] + public void Construct_Length() + { + var vec = new SqlVector(5); + Assert.True(vec.IsNull); + Assert.Equal(5, vec.Length); + Assert.Equal(28, vec.Size); + // Note that ReadOnlyMemory<> equality checks that both instances point + // to the same memory. We want to check memory content equality, so we + // compare their arrays instead. + Assert.Equal(new ReadOnlyMemory().ToArray(), vec.Memory.ToArray()); + Assert.Equal(SQLResource.NullString, vec.GetString()); + + var ivec = vec as ISqlVector; + Assert.Equal(0x00, ivec.ElementType); + Assert.Equal(0x04, ivec.ElementSize); + Assert.Empty(ivec.VectorPayload); + } + + [Fact] + public void Construct_WithLengthZero() + { + var vec = new SqlVector(0); + Assert.True(vec.IsNull); + Assert.Equal(0, vec.Length); + Assert.Equal(8, vec.Size); + // Note that ReadOnlyMemory<> equality checks that both instances point + // to the same memory. We want to check memory content equality, so we + // compare their arrays instead. + Assert.Equal(new ReadOnlyMemory().ToArray(), vec.Memory.ToArray()); + Assert.Equal(SQLResource.NullString, vec.GetString()); + + var ivec = vec as ISqlVector; + Assert.Equal(0x00, ivec.ElementType); + Assert.Equal(0x04, ivec.ElementSize); + Assert.Empty(ivec.VectorPayload); + } + + [Fact] + public void Construct_Memory_Empty() + { + SqlVector vec = new(new ReadOnlyMemory()); + Assert.False(vec.IsNull); + Assert.Equal(0, vec.Length); + Assert.Equal(8, vec.Size); + Assert.Equal(new ReadOnlyMemory().ToArray(), vec.Memory.ToArray()); + Assert.Equal("[]", vec.GetString()); + + var ivec = vec as ISqlVector; + Assert.Equal(0x00, ivec.ElementType); + Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal( + new byte[] { 0xA9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + ivec.VectorPayload); + } + + [Fact] + public void Construct_Memory() + { + float[] data = [1.1f, 2.2f]; + ReadOnlyMemory memory = new(data); + SqlVector vec = new(memory); + Assert.False(vec.IsNull); + Assert.Equal(2, vec.Length); + Assert.Equal(16, vec.Size); + Assert.Equal(memory.ToArray(), vec.Memory.ToArray()); + Assert.Equal(data, vec.Memory.ToArray()); + #if NETFRAMEWORK + Assert.Equal("[1.10000002,2.20000005]", vec.GetString()); + #else + Assert.Equal("[1.1,2.2]", vec.GetString()); + #endif + var ivec = vec as ISqlVector; + Assert.Equal(0x00, ivec.ElementType); + Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal( + MakeTdsPayload( + new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }, + memory), + ivec.VectorPayload); + } + + [Fact] + public void Construct_Memory_ImplicitConversionFromFloatArray() + { + float[] data = new float[] { 3.3f, 4.4f, 5.5f }; + var vec = new SqlVector(data); + Assert.False(vec.IsNull); + Assert.Equal(3, vec.Length); + Assert.Equal(20, vec.Size); + Assert.Equal(new ReadOnlyMemory(data).ToArray(), vec.Memory.ToArray()); + Assert.Equal(data, vec.Memory.ToArray()); + #if NETFRAMEWORK + Assert.Equal("[3.29999995,4.4000001,5.5]", vec.GetString()); + #else + Assert.Equal("[3.3,4.4,5.5]", vec.GetString()); + #endif + + var ivec = vec as ISqlVector; + Assert.Equal(0x00, ivec.ElementType); + Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal( + MakeTdsPayload( + new byte[] { 0xA9, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }, + data), + ivec.VectorPayload); + } + + [Fact] + public void Construct_Bytes() + { + float[] data = new float[] { 6.6f, 7.7f }; + var bytes = + MakeTdsPayload( + new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }, + data); + + var vec = new SqlVector(bytes); + Assert.False(vec.IsNull); + Assert.Equal(2, vec.Length); + Assert.Equal(16, vec.Size); + Assert.Equal(new ReadOnlyMemory(data).ToArray(), vec.Memory.ToArray()); + Assert.Equal(data, vec.Memory.ToArray()); + #if NETFRAMEWORK + Assert.Equal("[6.5999999,7.69999981]", vec.GetString()); + #else + Assert.Equal("[6.6,7.7]", vec.GetString()); + #endif + + var ivec = vec as ISqlVector; + Assert.Equal(0x00, ivec.ElementType); + Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal(bytes, ivec.VectorPayload); + } + + [Fact] + public void Construct_Bytes_ShortHeader() + { + Assert.Throws(() => + { + new SqlVector(new byte[] { 0xA9, 0x01, 0x00, 0x00 }); + }); + } + + [Fact] + public void Construct_Bytes_UnknownMagic() + { + Assert.Throws(() => + { + new SqlVector( + new byte[] { 0xA8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); + }); + } + + [Fact] + public void Construct_Bytes_UnsupportedVersion() + { + Assert.Throws(() => + { + new SqlVector( + new byte[] { 0xA9, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); + }); + } + + [Fact] + public void Construct_Bytes_TypeMismatch() + { + Assert.Throws(() => + { + new SqlVector( + new byte[] { 0xA9, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00 }); + }); + } + + [Fact] + public void Construct_Bytes_LengthMismatch() + { + // The header indicates 2 elements, but the payload has 3 floats. + var header = new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }; + var bytes = MakeTdsPayload( + header, + new ReadOnlyMemory(new float[] { 1.1f, 2.2f, 3.3f })); + + Assert.Throws(() => + { + new SqlVector(bytes); + }); + } + + [Fact] + public void Null_Property() + { + Assert.Null(SqlVector.Null); + } + + #endregion + + #region Helpers + + private byte[] MakeTdsPayload(byte[] header, ReadOnlyMemory values) + { + int length = header.Length + (values.Length * sizeof(float)); + byte[] payload = new byte[length]; + header.CopyTo(payload, 0); + for (int i = 0; i < values.Length; i++) + { + var offset = header.Length + (i * sizeof(float)); + BitConverter.GetBytes(values.Span[i]).CopyTo(payload, offset); + } + return payload; + } + + #endregion +}