diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index e02711fe8d..2141d65533 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -5861,7 +5861,7 @@ internal static object GetNullSqlValue(SqlBuffer nullVal, case SqlDbTypeExtensions.Vector: nullVal.SetToNullOfType(SqlBuffer.StorageType.Vector); - nullVal.SetVectorInfo(MetaType.GetVectorElementCount(md.length, md.scale), md.scale, true); + nullVal.SetToVectorInfo(MetaType.GetVectorElementCount(md.length, md.scale), md.scale, true); break; default: @@ -6488,7 +6488,7 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, // object from binary payload. int elementCount = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(2)); byte elementType = b[4]; - value.SetVectorInfo(elementCount, elementType, false); + value.SetToVectorInfo(elementCount, elementType, false); break; case TdsEnums.SQLCHAR: diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 0715bd8205..3d40b4a5b6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -6058,7 +6058,7 @@ internal static object GetNullSqlValue(SqlBuffer nullVal, case SqlDbTypeExtensions.Vector: nullVal.SetToNullOfType(SqlBuffer.StorageType.Vector); - nullVal.SetVectorInfo(MetaType.GetVectorElementCount(md.length, md.scale), md.scale, true); + nullVal.SetToVectorInfo(MetaType.GetVectorElementCount(md.length, md.scale), md.scale, true); break; default: @@ -6684,7 +6684,7 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, // object from binary payload. int elementCount = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(2)); byte elementType = b[4]; - value.SetVectorInfo(elementCount, elementType, false); + value.SetToVectorInfo(elementCount, elementType, false); break; case TdsEnums.SQLCHAR: 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 ca9fe35743..cbc8bf099e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs @@ -31,5 +31,10 @@ internal interface ISqlVector /// Returns the total size in bytes for sending SqlVector value on TDS. /// int Size { get; } + + /// + /// Returns a JSON serialized string representation of the current instance. + /// + string GetString(); } } 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 39d2758d62..49f95413df 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs @@ -3,6 +3,7 @@ // 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.Globalization; @@ -13,6 +14,51 @@ namespace Microsoft.Data.SqlClient { internal sealed class SqlBuffer { + #region Constants + // These variables store pre-boxed bool values to be used when returning a boolean as an + // object. If these are not used a new value is boxed each time one is needed which leads + // to a lot of garbage which needs to be collected + private static readonly object True = true; + private static readonly object False = false; + + // These formats work with DateTime stricts + private static readonly string[] Sql2008DateTime2Formats = new[] { + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss.f", + "yyyy-MM-dd HH:mm:ss.ff", + "yyyy-MM-dd HH:mm:ss.fff", + "yyyy-MM-dd HH:mm:ss.ffff", + "yyyy-MM-dd HH:mm:ss.fffff", + "yyyy-MM-dd HH:mm:ss.ffffff", + "yyyy-MM-dd HH:mm:ss.fffffff", + }; + + // These formats work with DateTimeOffset structs + private static readonly string[] Sql2008DateTimeOffsetFormats = new[] { + "yyyy-MM-dd HH:mm:ss zzz", + "yyyy-MM-dd HH:mm:ss.f zzz", + "yyyy-MM-dd HH:mm:ss.ff zzz", + "yyyy-MM-dd HH:mm:ss.fff zzz", + "yyyy-MM-dd HH:mm:ss.ffff zzz", + "yyyy-MM-dd HH:mm:ss.fffff zzz", + "yyyy-MM-dd HH:mm:ss.ffffff zzz", + "yyyy-MM-dd HH:mm:ss.fffffff zzz", + }; + + // These formats only work with TimeSpan structs + private static readonly string[] Sql2008TimeFormats = new[] { + @"hh\:mm\:ss", + @"hh\:mm\:ss\.f", + @"hh\:mm\:ss\.ff", + @"hh\:mm\:ss\.fff", + @"hh\:mm\:ss\.ffff", + @"hh\:mm\:ss\.fffff", + @"hh\:mm\:ss\.ffffff", + @"hh\:mm\:ss\.fffffff", + }; + + #endregion + internal enum StorageType { Empty = 0, @@ -40,466 +86,428 @@ internal enum StorageType Vector, } - internal struct DateTimeInfo - { - // This is used to store DateTime - internal int _daypart; - internal int _timepart; - } - - internal struct NumericInfo - { - // This is used to store Decimal data - internal int _data1; - internal int _data2; - internal int _data3; - internal int _data4; - internal byte _precision; - internal byte _scale; - internal bool _positive; - } - - internal struct TimeInfo - { - internal long _ticks; - internal byte _scale; - } - - internal struct DateTime2Info - { - internal int _date; - internal TimeInfo _timeInfo; - } - - internal struct DateTimeOffsetInfo - { - internal DateTime2Info _dateTime2Info; - internal short _offset; - } - - internal struct VectorInfo - { - internal int _elementCount; - internal byte _elementType; - } - - [StructLayout(LayoutKind.Explicit)] - internal struct Storage - { - [FieldOffset(0)] - internal bool _boolean; - [FieldOffset(0)] - internal byte _byte; - [FieldOffset(0)] - internal DateTimeInfo _dateTimeInfo; - [FieldOffset(0)] - internal double _double; - [FieldOffset(0)] - internal NumericInfo _numericInfo; - [FieldOffset(0)] - internal short _int16; - [FieldOffset(0)] - internal int _int32; - [FieldOffset(0)] - internal long _int64; // also used to store Money, UtcDateTime, Date , and Time - [FieldOffset(0)] - internal Guid _guid; - [FieldOffset(0)] - internal float _single; - [FieldOffset(0)] - internal TimeInfo _timeInfo; - [FieldOffset(0)] - internal DateTime2Info _dateTime2Info; - [FieldOffset(0)] - internal DateTimeOffsetInfo _dateTimeOffsetInfo; - [FieldOffset(0)] - internal VectorInfo _vectorInfo; - } + #region Member Variables - private bool _isNull; + // Storage for reference types, eg, String, SqlBinary, SqlCachedBuffer, SqlGuid. + private object _object; + private StorageType _type; + + // Storage for value types, eg, bool, int32, datetime2. Note that on construction of the + // SqlBuffer object, this will be initialized to an empty Storage instance. private Storage _value; - private object _object; // String, SqlBinary, SqlCachedBuffer, SqlGuid, SqlString, SqlXml + #endregion + + #region Constructors + internal SqlBuffer() { } private SqlBuffer(SqlBuffer value) - { // Clone + { // value types - _isNull = value._isNull; + IsNull = value.IsNull; _type = value._type; + // ref types - should also be read only unless at some point we allow this data // to be mutable, then we will need to copy _value = value._value; _object = value._object; } - internal bool IsEmpty => _type == StorageType.Empty; + #endregion + + #region General Properties - internal bool IsNull => _isNull; + internal Type ClrType + { + get => _type switch + { + StorageType.Boolean => typeof(bool), + StorageType.Byte => typeof(byte), + StorageType.DateTime => typeof(DateTime), + StorageType.DateTime2 => typeof(DateTime), + StorageType.DateTimeOffset => typeof(DateTimeOffset), + StorageType.Decimal => typeof(decimal), + StorageType.Double => typeof(double), + StorageType.Empty => null, + StorageType.Guid => typeof(Guid), + StorageType.Int16 => typeof(short), + StorageType.Int32 => typeof(int), + StorageType.Int64 => typeof(long), + StorageType.Json => typeof(string), + StorageType.Money => typeof(double), + StorageType.Single => typeof(float), + StorageType.SqlBinary => typeof(byte[]), + StorageType.SqlCachedBuffer => typeof(string), + StorageType.SqlGuid => typeof(Guid), + StorageType.SqlXml => typeof(string), + StorageType.String => typeof(string), + StorageType.Vector => typeof(byte[]), + + // @TODO: IF this property is meant to return the type that you'll get back when + // calling Value, then this isn't correct. + #if NET + StorageType.Time => typeof(TimeSpan), + #endif + + _ => null + }; + } + + internal bool IsEmpty => _type == StorageType.Empty; - internal StorageType VariantInternalStorageType => _type; + internal bool IsNull { get; private set; } - internal Storage GetVectorInfo() + internal Type SqlType { - if (_type == StorageType.Vector) + get => _type switch { - return _value; - } - throw new InvalidOperationException(); - } + StorageType.Boolean => typeof(SqlBoolean), + StorageType.Byte => typeof(SqlByte), + StorageType.DateTime => typeof(SqlDateTime), + StorageType.Decimal => typeof(SqlDecimal), + StorageType.Double => typeof(SqlDouble), + StorageType.Empty => null, + StorageType.Guid => typeof(SqlGuid), + StorageType.Int16 => typeof(SqlInt16), + StorageType.Int32 => typeof(SqlInt32), + StorageType.Int64 => typeof(SqlInt64), + StorageType.Json => typeof(SqlJson), + StorageType.Money => typeof(SqlMoney), + StorageType.Single => typeof(SqlSingle), + StorageType.SqlBinary => typeof(object), + StorageType.SqlCachedBuffer => typeof(SqlString), + StorageType.SqlGuid => typeof(SqlGuid), + StorageType.SqlXml => typeof(SqlXml), + StorageType.String => typeof(SqlString), + StorageType.Vector => typeof(object), + _ => null + }; + } + + internal StorageType VariantInternalStorageType => _type; + #endregion + + #region Type Conversion Properties + internal bool Boolean { - get - { - ThrowIfNull(); - - if (StorageType.Boolean == _type) - { - return _value._boolean; - } - return (bool)Value; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._boolean = value; - _type = StorageType.Boolean; - _isNull = false; - } + get => GetValue(StorageType.Boolean, _value._boolean); + set => SetValue(StorageType.Boolean, ref _value._boolean, value); } - + internal byte Byte { - get - { - ThrowIfNull(); - - if (StorageType.Byte == _type) - { - return _value._byte; - } - return (byte)Value; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._byte = value; - _type = StorageType.Byte; - _isNull = false; - } + get => GetValue(StorageType.Byte, _value._byte); + set => SetValue(StorageType.Byte, ref _value._byte, value); } internal byte[] ByteArray { get { - if (_type != StorageType.Vector) + if (_type is not StorageType.Vector) { + // Must be checked here because SqlBinary allows null. ThrowIfNull(); } + return SqlBinary.Value; } } - - internal DateTime DateTime + + #if NET + internal DateOnly DateOnly { get { ThrowIfNull(); - - if (StorageType.Date == _type) - { - return DateTime.MinValue.AddDays(_value._int32); - } - if (StorageType.DateTime2 == _type) - { - return new DateTime(GetTicksFromDateTime2Info(_value._dateTime2Info)); - } - if (StorageType.DateTime == _type) - { - return SqlTypeWorkarounds.SqlDateTimeToDateTime(_value._dateTimeInfo._daypart, _value._dateTimeInfo._timepart); - } - return (DateTime)Value; // anything else we haven't thought of goes through boxing. + return _type == StorageType.Date + ? DateOnly.MinValue.AddDays(_value._int32) + : (DateOnly)Value; } } - - #region Decimal - internal decimal Decimal + #endif + + internal DateTime DateTime { get { ThrowIfNull(); - - if (StorageType.Decimal == _type) - { - if (_value._numericInfo._data4 != 0 || _value._numericInfo._scale > 28) - { - // Only removing trailing zeros from a decimal part won't hit its value! - if (_value._numericInfo._scale > 0) - { - int zeroCnt = FindTrailingZerosAndPrec((uint)_value._numericInfo._data1, (uint)_value._numericInfo._data2, - (uint)_value._numericInfo._data3, (uint)_value._numericInfo._data4, - _value._numericInfo._scale, out int precision); - - int minScale = _value._numericInfo._scale - zeroCnt; // minimum possible sacle after removing the trailing zeros. - - if (zeroCnt > 0 && minScale <= 28 && precision <= 29) - { - SqlDecimal sqlValue = new(_value._numericInfo._precision, _value._numericInfo._scale, _value._numericInfo._positive, - _value._numericInfo._data1, _value._numericInfo._data2, - _value._numericInfo._data3, _value._numericInfo._data4); - - int integral = precision - minScale; - int newPrec = 29; - - if (integral != 1 && precision != 29) - { - newPrec = 28; - } - - try - { - // Precision could be 28 or 29 - // ex: (precision == 29 && scale == 28) - // valid: (+/-)7.1234567890123456789012345678 - // invalid: (+/-)8.1234567890123456789012345678 - return SqlDecimal.ConvertToPrecScale(sqlValue, newPrec, newPrec - integral).Value; - } - catch (OverflowException) - { - throw new OverflowException(SQLResource.ConversionOverflowMessage); - } - } - } - throw new OverflowException(SQLResource.ConversionOverflowMessage); - } - return new decimal(_value._numericInfo._data1, _value._numericInfo._data2, _value._numericInfo._data3, !_value._numericInfo._positive, _value._numericInfo._scale); - } - if (StorageType.Money == _type) - { - long l = _value._int64; - bool isNegative = false; - if (l < 0) - { - isNegative = true; - l = -l; - } - return new decimal((int)(l & 0xffffffff), (int)(l >> 32), 0, isNegative, 4); - } - return (decimal)Value; // anything else we haven't thought of goes through boxing. - } - } - - /// - /// Returns number of trailing zeros using the supplied parameters. - /// - /// An 32-bit unsigned integer which will be combined with data2, data3, and data4 - /// An 32-bit unsigned integer which will be combined with data1, data3, and data4 - /// An 32-bit unsigned integer which will be combined with data1, data2, and data4 - /// An 32-bit unsigned integer which will be combined with data1, data2, and data3 - /// The number of decimal places - /// OUT |The number of digits without trailing zeros - /// Number of trailing zeros - private static int FindTrailingZerosAndPrec(uint data1, uint data2, uint data3, uint data4, byte scale, out int valuablePrecision) - { - // Make local copy of data to avoid modifying input. - Span rgulNumeric = stackalloc uint[4] { data1, data2, data3, data4 }; - int zeroCnt = 0; //Number of trailing zero digits - int precCnt = 0; //Valuable precision - uint uiRem = 0; //Remainder of a division by 10 - int len = 4; // Max possible items - - //Retrieve each digit from the lowest significant digit - while (len > 1 || rgulNumeric[0] != 0) - { - SqlDecimalDivBy(rgulNumeric, ref len, 10, out uiRem); - if (uiRem == 0 && precCnt == 0) - { - zeroCnt++; - } - else + return _type switch { - precCnt++; - } - } - - if (uiRem == 0) - { - zeroCnt = scale; + StorageType.Date => DateTime.MinValue.AddDays(_value._int32), + StorageType.DateTime => _value._dateTimeInfo.ToDateTime(), + StorageType.DateTime2 => _value._dateTime2Info.ToDateTime(), + _ => (DateTime)Value, + }; } - - // if scale of the number has not been reached, pad remaining number with zeros. - if (zeroCnt + precCnt <= scale) - { - precCnt = scale - zeroCnt + 1; - } - valuablePrecision = precCnt; - return zeroCnt; } - /// - /// Multi-precision one super-digit divide in place. - /// U = U / D, - /// R = U % D - /// (Length of U can decrease) - /// - /// InOut | U - /// InOut | Number of items with non-zero value in U between 1 to 4 - /// In | D - /// Out | R - private static void SqlDecimalDivBy(Span data, ref int len, uint divisor, out uint remainder) - { - uint uiCarry = 0; - ulong ulAccum; - ulong ulDivisor = (ulong)divisor; - int iLen = len; - - while (iLen > 0) + internal DateTimeOffset DateTimeOffset + { + get { - iLen--; - ulAccum = (((ulong)uiCarry) << 32) + (ulong)(data[iLen]); - data[iLen] = (uint)(ulAccum / ulDivisor); - uiCarry = (uint)(ulAccum - (ulong)data[iLen] * ulDivisor); // (ULONG) (ulAccum % divisor) + ThrowIfNull(); + return _type == StorageType.DateTimeOffset + ? _value._dateTimeOffsetInfo.ToDateTimeOffset() + : (DateTimeOffset)Value; } - remainder = uiCarry; - - // Normalize multi-precision number - remove leading zeroes - while (len > 1 && data[len - 1] == 0) - { len--; } } - #endregion - internal double Double + internal decimal Decimal { get { ThrowIfNull(); - - if (StorageType.Double == _type) + return _type switch { - return _value._double; - } - return (double)Value; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._double = value; - _type = StorageType.Double; - _isNull = false; + StorageType.Decimal => _value._numericInfo.ToDecimal(), + StorageType.Money => GetSqlMoneyFromLong(_value._int64).ToDecimal(), + _ => (decimal)Value, + }; } } - + + internal double Double + { + get => GetValue(StorageType.Double, _value._double); + set => SetValue(StorageType.Double, ref _value._double, value); + } + internal Guid Guid { get { ThrowIfNull(); - if (StorageType.Guid == _type) - { - return _value._guid; - } - else if (StorageType.SqlGuid == _type) + return _type switch { - return ((SqlGuid)_object).Value; - } - return (Guid)Value; - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _type = StorageType.Guid; - _value._guid = value; - _isNull = false; + StorageType.Guid => _value._guid, + StorageType.SqlGuid => ((SqlGuid)_object).Value, + _ => (Guid)Value, + }; } + set => SetValue(StorageType.Guid, ref _value._guid, value); } - + internal short Int16 + { + get => GetValue(StorageType.Int16, _value._int16); + set => SetValue(StorageType.Int16, ref _value._int16, value); + } + + internal int Int32 + { + get => GetValue(StorageType.Int32, _value._int32); + set => SetValue(StorageType.Int32, ref _value._int32, value); + } + + internal long Int64 + { + get => GetValue(StorageType.Int64, _value._int64); + set => SetValue(StorageType.Int64, ref _value._int64, value); + } + + internal float Single + { + get => GetValue(StorageType.Single, _value._single); + set => SetValue(StorageType.Single, ref _value._single, value); + } + + internal SqlString Sql2008DateTimeSqlString + { + get => _type is StorageType.Date + or StorageType.DateTime2 + or StorageType.DateTimeOffset + or StorageType.Time + ? IsNull ? SqlString.Null : new SqlString(Sql2008DateTimeString) + : (SqlString)SqlValue; + } + + internal string Sql2008DateTimeString { get { ThrowIfNull(); - if (StorageType.Int16 == _type) + string formatString; + switch (_type) { - return _value._int16; + case StorageType.Date: + formatString = "yyyy-MM-dd"; + return DateTime.ToString(formatString, DateTimeFormatInfo.InvariantInfo); + case StorageType.DateTime2: + formatString = Sql2008DateTime2Formats[_value._dateTime2Info._timeInfo._scale]; + return DateTime.ToString(formatString, DateTimeFormatInfo.InvariantInfo); + case StorageType.DateTimeOffset: + formatString = Sql2008DateTimeOffsetFormats[_value._dateTimeOffsetInfo._dateTime2Info._timeInfo._scale]; + return DateTimeOffset.ToString(formatString, DateTimeFormatInfo.InvariantInfo); + case StorageType.Time: + formatString = Sql2008TimeFormats[_value._timeInfo._scale]; + return Time.ToString(formatString, DateTimeFormatInfo.InvariantInfo); + default: + return (string)Value; } - return (short)Value; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._int16 = value; - _type = StorageType.Int16; - _isNull = false; } } + + internal SqlBinary SqlBinary + { + get => _type is StorageType.SqlBinary or StorageType.Vector + ? IsNull ? SqlBinary.Null : (SqlBinary)_object + : (SqlBinary)SqlValue; + set => SetObject(StorageType.SqlBinary, value); + } - internal int Int32 + internal SqlBoolean SqlBoolean { - get - { - ThrowIfNull(); + get => _type == StorageType.Boolean + ? IsNull ? SqlBoolean.Null : new SqlBoolean(_value._boolean) + : (SqlBoolean)SqlValue; + } - if (StorageType.Int32 == _type) - { - return _value._int32; - } - return (int)Value; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._int32 = value; - _type = StorageType.Int32; - _isNull = false; - } + internal SqlByte SqlByte + { + get => _type == StorageType.Byte + ? IsNull ? SqlByte.Null : new SqlByte(_value._byte) + : (SqlByte)SqlValue; } - internal long Int64 + internal SqlCachedBuffer SqlCachedBuffer { - get - { - ThrowIfNull(); + get => _type == StorageType.SqlCachedBuffer + ? IsNull ? SqlCachedBuffer.Null : (SqlCachedBuffer)_object + : (SqlCachedBuffer)SqlValue; + set => SetObject(StorageType.SqlCachedBuffer, value); + } + + internal SqlDateTime SqlDateTime + { + get => _type == StorageType.DateTime + ? IsNull ? SqlDateTime.Null : _value._dateTimeInfo.ToSqlDateTime() + : (SqlDateTime)SqlValue; + } - if (StorageType.Int64 == _type) - { - return _value._int64; - } - return (long)Value; // anything else we haven't thought of goes through boxing. - } - set + internal SqlDecimal SqlDecimal + { + get => _type == StorageType.Decimal + ? IsNull ? SqlDecimal.Null : _value._numericInfo.ToSqlDecimal() + : (SqlDecimal)SqlValue; + } + + internal SqlDouble SqlDouble + { + get => _type == StorageType.Double + ? IsNull ? SqlDouble.Null : new SqlDouble(_value._double) + : (SqlDouble)SqlValue; + } + + internal SqlGuid SqlGuid + { + get => _type switch { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._int64 = value; - _type = StorageType.Int64; - _isNull = false; - } + StorageType.Guid => IsNull ? SqlGuid.Null : new SqlGuid(_value._guid), + StorageType.SqlGuid => IsNull ? SqlGuid.Null : (SqlGuid)_object, + _ => (SqlGuid)SqlValue + }; + set => SetObject(StorageType.SqlGuid, value); + } + + internal SqlInt16 SqlInt16 + { + get => _type == StorageType.Int16 + ? IsNull ? SqlInt16.Null : new SqlInt16(_value._int16) + : (SqlInt16)SqlValue; } - internal float Single + internal SqlInt32 SqlInt32 { - get + get => _type == StorageType.Int32 + ? IsNull ? SqlInt32.Null : new SqlInt32(_value._int32) + : (SqlInt32)SqlValue; + } + + internal SqlInt64 SqlInt64 + { + get => _type == StorageType.Int64 + ? IsNull ? SqlInt64.Null : new SqlInt64(_value._int64) + : (SqlInt64)SqlValue; + } + + internal SqlJson SqlJson + { + get => StorageType.Json == _type + ? IsNull ? SqlJson.Null : new SqlJson((string)_object) + : (SqlJson)SqlValue; + } + + internal SqlMoney SqlMoney + { + get => _type == StorageType.Money + ? IsNull ? SqlMoney.Null : GetSqlMoneyFromLong(_value._int64) + : (SqlMoney)SqlValue; + } + + internal SqlSingle SqlSingle + { + get => _type == StorageType.Single + ? IsNull ? SqlSingle.Null : new SqlSingle(_value._single) + : (SqlSingle)SqlValue; + } + + internal SqlString SqlString + { + get => _type switch { - ThrowIfNull(); + StorageType.Json => IsNull ? SqlString.Null : new SqlString((string)_object), + StorageType.String => IsNull ? SqlString.Null : new SqlString((string)_object), + StorageType.SqlCachedBuffer => IsNull ? SqlString.Null : ((SqlCachedBuffer)_object).ToSqlString(), + StorageType.Vector => IsNull ? SqlString.Null : new SqlString(GetSqlVector().GetString()), + _ => (SqlString)SqlValue + }; + } - if (StorageType.Single == _type) - { - return _value._single; - } - return (float)Value; // anything else we haven't thought of goes through boxing. - } - set + internal object SqlValue + { + get => _type switch { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._single = value; - _type = StorageType.Single; - _isNull = false; - } + StorageType.Boolean => SqlBoolean, + StorageType.Byte => SqlByte, + StorageType.Date => IsNull ? DBNull.Value : DateTime, + StorageType.DateTime => SqlDateTime, + StorageType.DateTime2 => IsNull ? DBNull.Value : DateTime, + StorageType.DateTimeOffset => IsNull ? DBNull.Value : DateTimeOffset, + StorageType.Decimal => SqlDecimal, + StorageType.Double => SqlDouble, + StorageType.Empty => DBNull.Value, + StorageType.Guid => SqlGuid, + StorageType.Int16 => SqlInt16, + StorageType.Int32 => SqlInt32, + StorageType.Int64 => SqlInt64, + StorageType.Json => SqlJson, + StorageType.Money => SqlMoney, + StorageType.Single => SqlSingle, + StorageType.SqlBinary => _object, + StorageType.SqlCachedBuffer => IsNull ? SqlXml.Null : ((SqlCachedBuffer)_object).ToSqlXml(), + StorageType.SqlGuid => _object, + StorageType.SqlXml => IsNull ? SqlXml.Null : (SqlXml)_object, + StorageType.String => SqlString, + StorageType.Time => IsNull ? DBNull.Value : Time, + StorageType.Vector => GetSqlVector(), + _ => null + }; + } + + internal SqlXml SqlXml + { + get => _type == StorageType.SqlXml + ? IsNull ? SqlXml.Null : (SqlXml)_object + : (SqlXml)SqlValue; + set => SetObject(StorageType.SqlXml, value); } internal string String @@ -507,771 +515,102 @@ internal string String get { ThrowIfNull(); - if (_type == StorageType.Vector) + return _type switch { - var elementType = (MetaType.SqlVectorElementType)_value._vectorInfo._elementType; - switch (elementType) - { - case MetaType.SqlVectorElementType.Float32: - return GetSqlVector().GetString(); - default: - throw SQL.VectorTypeNotSupported(elementType.ToString()); - } - } - if (StorageType.String == _type || StorageType.Json == _type) - { - return (string)_object; - } - else if (StorageType.SqlCachedBuffer == _type) - { - return ((SqlCachedBuffer)(_object)).ToString(); - } - return (string)Value; // anything else we haven't thought of goes through boxing. - } - } - - // use static list of format strings indexed by scale for perf - private static readonly string[] s_sql2008DateTimeOffsetFormatByScale = new string[] { - "yyyy-MM-dd HH:mm:ss zzz", - "yyyy-MM-dd HH:mm:ss.f zzz", - "yyyy-MM-dd HH:mm:ss.ff zzz", - "yyyy-MM-dd HH:mm:ss.fff zzz", - "yyyy-MM-dd HH:mm:ss.ffff zzz", - "yyyy-MM-dd HH:mm:ss.fffff zzz", - "yyyy-MM-dd HH:mm:ss.ffffff zzz", - "yyyy-MM-dd HH:mm:ss.fffffff zzz", - }; - - private static readonly string[] s_sql2008DateTime2FormatByScale = new string[] { - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd HH:mm:ss.f", - "yyyy-MM-dd HH:mm:ss.ff", - "yyyy-MM-dd HH:mm:ss.fff", - "yyyy-MM-dd HH:mm:ss.ffff", - "yyyy-MM-dd HH:mm:ss.fffff", - "yyyy-MM-dd HH:mm:ss.ffffff", - "yyyy-MM-dd HH:mm:ss.fffffff", - }; - - private static readonly string[] s_sql2008TimeFormatByScale = new string[] { - "HH:mm:ss", - "HH:mm:ss.f", - "HH:mm:ss.ff", - "HH:mm:ss.fff", - "HH:mm:ss.ffff", - "HH:mm:ss.fffff", - "HH:mm:ss.ffffff", - "HH:mm:ss.fffffff", - }; - - internal string Sql2008DateTimeString - { - get - { - ThrowIfNull(); - - if (StorageType.Date == _type) - { - return DateTime.ToString("yyyy-MM-dd", DateTimeFormatInfo.InvariantInfo); - } - if (StorageType.Time == _type) - { - byte scale = _value._timeInfo._scale; - return new DateTime(_value._timeInfo._ticks).ToString(s_sql2008TimeFormatByScale[scale], DateTimeFormatInfo.InvariantInfo); - } - if (StorageType.DateTime2 == _type) - { - byte scale = _value._dateTime2Info._timeInfo._scale; - return DateTime.ToString(s_sql2008DateTime2FormatByScale[scale], DateTimeFormatInfo.InvariantInfo); - } - if (StorageType.DateTimeOffset == _type) - { - DateTimeOffset dto = DateTimeOffset; - byte scale = _value._dateTimeOffsetInfo._dateTime2Info._timeInfo._scale; - return dto.ToString(s_sql2008DateTimeOffsetFormatByScale[scale], DateTimeFormatInfo.InvariantInfo); - } - return (string)Value; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlString Sql2008DateTimeSqlString - { - get - { - if (StorageType.Date == _type || - StorageType.Time == _type || - StorageType.DateTime2 == _type || - StorageType.DateTimeOffset == _type) - { - if (IsNull) - { - return SqlString.Null; - } - return new SqlString(Sql2008DateTimeString); - } - return (SqlString)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal TimeSpan Time - { - get - { - ThrowIfNull(); - - if (StorageType.Time == _type) - { - return new TimeSpan(_value._timeInfo._ticks); - } - - return (TimeSpan)Value; // anything else we haven't thought of goes through boxing. - } - } - -#if NET - internal TimeOnly TimeOnly - { - get - { - ThrowIfNull(); - - if (StorageType.Time == _type) - { - return new TimeOnly(_value._timeInfo._ticks); - } - - return (TimeOnly)Value; // anything else we haven't thought of goes through boxing. - } - } - - internal DateOnly DateOnly - { - get - { - ThrowIfNull(); - - if (StorageType.Date == _type) - { - return DateOnly.MinValue.AddDays(_value._int32); - } - return (DateOnly)Value; // anything else we haven't thought of goes through boxing. - } - } -#endif - - internal DateTimeOffset DateTimeOffset - { - get - { - ThrowIfNull(); - - if (StorageType.DateTimeOffset == _type) - { - TimeSpan offset = new TimeSpan(0, _value._dateTimeOffsetInfo._offset, 0); - // datetime part presents time in UTC - return new DateTimeOffset(GetTicksFromDateTime2Info(_value._dateTimeOffsetInfo._dateTime2Info) + offset.Ticks, offset); - } - - return (DateTimeOffset)Value; // anything else we haven't thought of goes through boxing. - } - } - - private static long GetTicksFromDateTime2Info(DateTime2Info dateTime2Info) - { - return (dateTime2Info._date * TimeSpan.TicksPerDay + dateTime2Info._timeInfo._ticks); - } - - internal SqlBinary SqlBinary - { - get - { - if (_type is StorageType.SqlBinary or StorageType.Vector) - { - if (IsNull) - { - return SqlBinary.Null; - } - return (SqlBinary)_object; - } - return (SqlBinary)SqlValue; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _object = value; - _type = StorageType.SqlBinary; - _isNull = value.IsNull; - } - } - - internal SqlBoolean SqlBoolean - { - get - { - if (StorageType.Boolean == _type) - { - if (IsNull) - { - return SqlBoolean.Null; - } - return new SqlBoolean(_value._boolean); - } - return (SqlBoolean)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlByte SqlByte - { - get - { - if (StorageType.Byte == _type) - { - if (IsNull) - { - return SqlByte.Null; - } - return new SqlByte(_value._byte); - } - return (SqlByte)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlCachedBuffer SqlCachedBuffer - { - get - { - if (StorageType.SqlCachedBuffer == _type) - { - if (IsNull) - { - return SqlCachedBuffer.Null; - } - return (SqlCachedBuffer)_object; - } - return (SqlCachedBuffer)SqlValue; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _object = value; - _type = StorageType.SqlCachedBuffer; - _isNull = value.IsNull; - } - } - - internal SqlXml SqlXml - { - get - { - if (StorageType.SqlXml == _type) - { - if (IsNull) - { - return SqlXml.Null; - } - return (SqlXml)_object; - } - return (SqlXml)SqlValue; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _object = value; - _type = StorageType.SqlXml; - _isNull = value.IsNull; - } - } - - internal SqlDateTime SqlDateTime - { - get - { - if (StorageType.DateTime == _type) - { - if (IsNull) - { - return SqlDateTime.Null; - } - return new SqlDateTime(_value._dateTimeInfo._daypart, _value._dateTimeInfo._timepart); - } - return (SqlDateTime)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlDecimal SqlDecimal - { - get - { - if (StorageType.Decimal == _type) - { - if (IsNull) - { - return SqlDecimal.Null; - } - return new SqlDecimal(_value._numericInfo._precision, - _value._numericInfo._scale, - _value._numericInfo._positive, - _value._numericInfo._data1, - _value._numericInfo._data2, - _value._numericInfo._data3, - _value._numericInfo._data4 - ); - } - return (SqlDecimal)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlDouble SqlDouble - { - get - { - if (StorageType.Double == _type) - { - if (IsNull) - { - return SqlDouble.Null; - } - return new SqlDouble(_value._double); - } - return (SqlDouble)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlGuid SqlGuid - { - get - { - if (StorageType.Guid == _type) - { - return IsNull ? SqlGuid.Null : new SqlGuid(_value._guid); - } - else if (StorageType.SqlGuid == _type) - { - return IsNull ? SqlGuid.Null : (SqlGuid)_object; - } - return (SqlGuid)SqlValue; // anything else we haven't thought of goes through boxing. - } - set - { - Debug.Assert(IsEmpty, "setting value a second time?"); - _object = value; - _type = StorageType.SqlGuid; - _isNull = value.IsNull; - } - } - - internal SqlInt16 SqlInt16 - { - get - { - if (StorageType.Int16 == _type) - { - if (IsNull) - { - return SqlInt16.Null; - } - return new SqlInt16(_value._int16); - } - return (SqlInt16)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlInt32 SqlInt32 - { - get - { - if (StorageType.Int32 == _type) - { - if (IsNull) - { - return SqlInt32.Null; - } - return new SqlInt32(_value._int32); - } - return (SqlInt32)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlInt64 SqlInt64 - { - get - { - if (StorageType.Int64 == _type) - { - if (IsNull) - { - return SqlInt64.Null; - } - return new SqlInt64(_value._int64); - } - return (SqlInt64)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlMoney SqlMoney - { - get - { - if (StorageType.Money == _type) - { - if (IsNull) - { - return SqlMoney.Null; - } -#if NET - return SqlMoney.FromTdsValue(_value._int64); -#else - return SqlTypeWorkarounds.SqlMoneyCtor(_value._int64, 1/*ignored*/); -#endif - } - return (SqlMoney)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlSingle SqlSingle - { - get - { - if (StorageType.Single == _type) - { - if (IsNull) - { - return SqlSingle.Null; - } - return new SqlSingle(_value._single); - } - return (SqlSingle)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlString SqlString - { - get - { - if (_type is StorageType.Vector) - { - if (IsNull) - { - return SqlString.Null; - } - 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) - { - if (IsNull) - { - return SqlString.Null; - } - return new SqlString((string)_object); - } - else if (StorageType.SqlCachedBuffer == _type) - { - SqlCachedBuffer data = (SqlCachedBuffer)(_object); - if (data.IsNull) - { - return SqlString.Null; - } - return data.ToSqlString(); - } - return (SqlString)SqlValue; // anything else we haven't thought of goes through boxing. - } - } - - internal SqlJson SqlJson => (StorageType.Json == _type) ? (IsNull ? SqlTypes.SqlJson.Null : new SqlJson((string)_object)) : (SqlJson)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 - { - get - { - switch (_type) - { - case StorageType.Empty: - return DBNull.Value; - case StorageType.Boolean: - return SqlBoolean; - case StorageType.Byte: - return SqlByte; - case StorageType.DateTime: - return SqlDateTime; - case StorageType.Decimal: - return SqlDecimal; - case StorageType.Double: - return SqlDouble; - case StorageType.Int16: - return SqlInt16; - case StorageType.Int32: - return SqlInt32; - case StorageType.Int64: - return SqlInt64; - case StorageType.Guid: - return SqlGuid; - case StorageType.Money: - return SqlMoney; - case StorageType.Single: - return SqlSingle; - case StorageType.String: - return SqlString; - case StorageType.Json: - return SqlJson; - case StorageType.Vector: - var elementType = (MetaType.SqlVectorElementType)_value._vectorInfo._elementType; - switch (elementType) - { - case MetaType.SqlVectorElementType.Float32: - return GetSqlVector(); - default: - throw SQL.VectorTypeNotSupported(elementType.ToString()); - } - case StorageType.SqlCachedBuffer: - { - SqlCachedBuffer data = (SqlCachedBuffer)(_object); - if (data.IsNull) - { - return SqlXml.Null; - } - return data.ToSqlXml(); - } - - case StorageType.SqlBinary: - case StorageType.SqlGuid: - return _object; - - case StorageType.SqlXml: - if (_isNull) - { - return SqlXml.Null; - } - Debug.Assert(_object != null); - return (SqlXml)_object; - - case StorageType.Date: - case StorageType.DateTime2: - if (_isNull) - { - return DBNull.Value; - } - return DateTime; - - case StorageType.DateTimeOffset: - if (_isNull) - { - return DBNull.Value; - } - return DateTimeOffset; - - case StorageType.Time: - if (_isNull) - { - return DBNull.Value; - } - return Time; - } - return null; // need to return the value as an object of some SQL type + StorageType.Json => (string)_object, + StorageType.String => (string)_object, + StorageType.SqlCachedBuffer => ((SqlCachedBuffer)_object).ToString(), + StorageType.Vector => GetSqlVector().GetString(), + _ => (string)Value + }; } } - - // these variables store pre-boxed bool values to be used when returning a boolean - // in a object typed location, if these are not used a new value is boxed each time - // one is needed which leads to a lot of garbage which needs to be collected - private static readonly object s_cachedTrueObject = true; - private static readonly object s_cachedFalseObject = false; - - internal object Value + internal TimeSpan Time { get { - if (IsNull) - { - return DBNull.Value; - } - switch (_type) - { - case StorageType.Empty: - return DBNull.Value; - case StorageType.Boolean: - return Boolean ? s_cachedTrueObject : s_cachedFalseObject; // return pre-boxed values for perf - case StorageType.Byte: - return Byte; - case StorageType.DateTime: - return DateTime; - case StorageType.Decimal: - return Decimal; - case StorageType.Double: - return Double; - case StorageType.Int16: - return Int16; - case StorageType.Int32: - return Int32; - case StorageType.Int64: - return Int64; - case StorageType.Guid: - return Guid; - case StorageType.Money: - return Decimal; - case StorageType.Single: - return Single; - case StorageType.String: - return String; - case StorageType.SqlBinary: - case StorageType.Vector: - return ByteArray; - case StorageType.SqlCachedBuffer: - { - // If we have a CachedBuffer, it's because it's an XMLTYPE column - // and we have to return a string when they're asking for the CLS - // value of the column. - return ((SqlCachedBuffer)(_object)).ToString(); - } - case StorageType.SqlGuid: - return Guid; - case StorageType.SqlXml: - { - // XMLTYPE columns must be returned as string when asking for the CLS value - SqlXml data = (SqlXml)_object; - string s = data.Value; - return s; - } - case StorageType.Date: - return DateTime; - case StorageType.DateTime2: - return DateTime; - case StorageType.DateTimeOffset: - return DateTimeOffset; - case StorageType.Time: - return Time; - case StorageType.Json: - return String; - } - return null; // need to return the value as an object of some CLS type + ThrowIfNull(); + return _type == StorageType.Time + ? new TimeSpan(_value._timeInfo._ticks) + : (TimeSpan)Value; } } - - internal Type GetTypeFromStorageType(bool isSqlType) + + #if NET + internal TimeOnly TimeOnly { - if (isSqlType) + get { - switch (_type) - { - case StorageType.Empty: - return null; - case StorageType.Boolean: - return typeof(SqlBoolean); - case StorageType.Byte: - return typeof(SqlByte); - case StorageType.DateTime: - return typeof(SqlDateTime); - case StorageType.Decimal: - return typeof(SqlDecimal); - case StorageType.Double: - return typeof(SqlDouble); - case StorageType.Int16: - return typeof(SqlInt16); - case StorageType.Int32: - return typeof(SqlInt32); - case StorageType.Int64: - return typeof(SqlInt64); - case StorageType.Guid: - return typeof(SqlGuid); - case StorageType.Money: - return typeof(SqlMoney); - case StorageType.Single: - return typeof(SqlSingle); - case StorageType.String: - return typeof(SqlString); - case StorageType.SqlCachedBuffer: - return typeof(SqlString); - case StorageType.SqlBinary: - case StorageType.Vector: - return typeof(object); - case StorageType.SqlGuid: - return typeof(SqlGuid); - case StorageType.SqlXml: - return typeof(SqlXml); - case StorageType.Json: - return typeof(SqlJson); - // Time Date DateTime2 and DateTimeOffset have no direct Sql type to contain them - } + ThrowIfNull(); + return _type == StorageType.Time + ? new TimeOnly(_value._timeInfo._ticks) + : (TimeOnly)Value; } - else - { //Is CLR Type - switch (_type) + } + #endif + + internal object Value + { + get => IsNull + ? DBNull.Value + : _type switch { - case StorageType.Empty: - return null; - case StorageType.Boolean: - return typeof(bool); - case StorageType.Byte: - return typeof(byte); - case StorageType.DateTime: - return typeof(DateTime); - case StorageType.Decimal: - return typeof(decimal); - case StorageType.Double: - return typeof(double); - case StorageType.Int16: - return typeof(short); - case StorageType.Int32: - return typeof(int); - case StorageType.Int64: - return typeof(long); - case StorageType.Guid: - return typeof(Guid); - case StorageType.Money: - return typeof(decimal); - case StorageType.Single: - return typeof(float); - case StorageType.String: - return typeof(string); - case StorageType.SqlBinary: - return typeof(byte[]); - case StorageType.SqlCachedBuffer: - return typeof(string); - case StorageType.SqlGuid: - return typeof(Guid); - case StorageType.SqlXml: - return typeof(string); - case StorageType.Date: - return typeof(DateTime); - case StorageType.DateTime2: - return typeof(DateTime); - case StorageType.DateTimeOffset: - return typeof(DateTimeOffset); - case StorageType.Json: - return typeof(string); - case StorageType.Vector: - return typeof(byte[]); -#if NET - case StorageType.Time: - return typeof(TimeOnly); -#endif + StorageType.Boolean => Boolean ? True : False, // Return pre-boxed values for perf + StorageType.Byte => Byte, + StorageType.Date => DateTime, + StorageType.DateTime => DateTime, + StorageType.DateTime2 => DateTime, + StorageType.DateTimeOffset => DateTimeOffset, + StorageType.Decimal => Decimal, + StorageType.Double => Double, + StorageType.Empty => DBNull.Value, + StorageType.Int16 => Int16, + StorageType.Int32 => Int32, + StorageType.Int64 => Int64, + StorageType.Json => String, + StorageType.Guid => Guid, + StorageType.Money => Decimal, + StorageType.Single => Single, + StorageType.SqlBinary => ByteArray, + StorageType.SqlGuid => Guid, + StorageType.String => String, + StorageType.Time => Time, + StorageType.Vector => ByteArray, + + // @TODO: Verify that these follow the same pattern as other types + // (ie, ClrType => (cast)Value) + // If we have a cached buffer, it's because it's an XMLTYPE column and we have + // to return a string when they're asking for the CLR value of the column. + StorageType.SqlCachedBuffer => ((SqlCachedBuffer)_object).ToString(), + StorageType.SqlXml => ((SqlXml)_object).Value, + + _ => null + }; + } + + #endregion + + #region General Methods + + internal void Clear() + { + IsNull = false; + _type = StorageType.Empty; + _object = null; + } + + internal static void Clear(SqlBuffer[] values) + { + if (values != null) + { + for (int i = 0; i < values.Length; ++i) + { + values[i].Clear(); } } - - return null; // need to return the value as an object of some CLS type } - + internal static SqlBuffer[] CreateBufferArray(int length) { SqlBuffer[] buffers = new SqlBuffer[length]; @@ -1292,263 +631,683 @@ internal static SqlBuffer[] CloneBufferArray(SqlBuffer[] values) return copy; } - internal static void Clear(SqlBuffer[] values) + #endregion + + #region Get Methods + + internal T BooleanAs() => + GetValueAs(_value._boolean); + + internal T ByteAs() => + GetValueAs(_value._byte); + + internal T DoubleAs() => + GetValueAs(_value._double); + + internal T Int16As() => + GetValueAs(_value._int16); + + internal T Int32As() => + GetValueAs(_value._int32); + + internal T Int64As() => + GetValueAs(_value._int64); + + internal T SingleAs() => + GetValueAs(_value._single); + + internal SqlVector GetSqlVector() + where T : unmanaged => + _type is StorageType.Vector + ? IsNull ? _value._vectorInfo.ToNull() : new SqlVector((byte[])_object) + : (SqlVector)SqlValue; + + #endregion + + #region Set Methods + + #if NETFRAMEWORK + internal void SetToDate(DateTime date) => + SetValue(StorageType.Date, ref _value._int32, date.Subtract(DateTime.MinValue).Days); + #endif + + internal void SetToDate(ReadOnlySpan bytes) { - if (values != null) - { - for (int i = 0; i < values.Length; ++i) - { - values[i].Clear(); - } - } + // NOTE: Reordered to optimize JIT generated bounds checks to a single instance, + // review generated asm before changing. + // @TODO: Verify that ^^^ is still accurate/needed + byte thirdByte = bytes[2]; // + int dateValue = bytes[0] + (bytes[1] << 8) + (thirdByte << 16); + SetValue(StorageType.Date, ref _value._int32, dateValue); } - - internal void Clear() + + internal void SetToDateTime(int dayPart, int timePart) { - _isNull = false; - _type = StorageType.Empty; - _object = null; + SetTypeAndIsNull(StorageType.DateTime, false); + _value._dateTimeInfo.FromDateTimeData(dayPart, timePart); } - + #if NETFRAMEWORK - internal void SetToDate(DateTime date) + internal void SetToDateTime2(DateTime dateTime, byte scale) { - Debug.Assert(IsEmpty, "setting value a second time?"); - - _type = StorageType.Date; - _value._int32 = date.Subtract(DateTime.MinValue).Days; - _isNull = false; + SetTypeAndIsNull(StorageType.DateTime2, false); + _value._dateTime2Info.FromDateTimeAndScale(dateTime, scale); } -#endif - - internal void SetVectorInfo(int elementCount, byte elementType, bool isNull) + #endif + + internal void SetToDateTime2(ReadOnlySpan bytes, byte scale, byte denormalizedScale) { - _value._vectorInfo._elementCount = elementCount; - _value._vectorInfo._elementType = elementType; - _type = StorageType.Vector; - _isNull = isNull; + SetTypeAndIsNull(StorageType.DateTime2, false); + _value._dateTime2Info.FromByteArray(bytes, scale, denormalizedScale); } - - internal void SetToDateTime(int daypart, int timepart) + + internal void SetToDateTimeOffset(DateTimeOffset dateTimeOffset, byte scale) { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._dateTimeInfo._daypart = daypart; - _value._dateTimeInfo._timepart = timepart; - _type = StorageType.DateTime; - _isNull = false; + SetTypeAndIsNull(StorageType.DateTimeOffset, false); + _value._dateTimeOffsetInfo.FromDateTimeOffsetAndScale(dateTimeOffset, scale); } - -#if NETFRAMEWORK - internal void SetToDateTime2(DateTime dateTime, byte scale) + + internal void SetToDateTimeOffset(ReadOnlySpan bytes, byte scale, byte denormalizedScale) { - Debug.Assert(IsEmpty, "setting value a second time?"); - - _type = StorageType.DateTime2; - _value._dateTime2Info._timeInfo._ticks = dateTime.TimeOfDay.Ticks; - _value._dateTime2Info._timeInfo._scale = scale; - _value._dateTime2Info._date = dateTime.Subtract(DateTime.MinValue).Days; - _isNull = false; + SetTypeAndIsNull(StorageType.DateTimeOffset, false); + _value._dateTimeOffsetInfo.FromByteArray(bytes, scale, denormalizedScale); } -#endif internal void SetToDecimal(byte precision, byte scale, bool positive, int[] bits) { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._numericInfo._precision = precision; - _value._numericInfo._scale = scale; - _value._numericInfo._positive = positive; - _value._numericInfo._data1 = bits[0]; - _value._numericInfo._data2 = bits[1]; - _value._numericInfo._data3 = bits[2]; - _value._numericInfo._data4 = bits[3]; - _type = StorageType.Decimal; - _isNull = false; + SetTypeAndIsNull(StorageType.Decimal, false); + _value._numericInfo.FromDecimalData(precision, scale, positive, bits); } - internal void SetToMoney(long value) + internal void SetToJson(string value) { - Debug.Assert(IsEmpty, "setting value a second time?"); - _value._int64 = value; - _type = StorageType.Money; - _isNull = false; + SetTypeAndIsNull(StorageType.Json, false); + _object = value; } + + internal void SetToMoney(long value) => + SetValue(StorageType.Money, ref _value._int64, value); internal void SetToNullOfType(StorageType storageType) { - Debug.Assert(IsEmpty, "setting value a second time?"); - _type = storageType; - _isNull = true; + SetTypeAndIsNull(storageType, true); _object = null; } internal void SetToString(string value) { - Debug.Assert(IsEmpty, "setting value a second time?"); + SetTypeAndIsNull(StorageType.String, false); _object = value; - _type = StorageType.String; - _isNull = false; } - internal void SetToJson(string value) + internal void SetToTime(ReadOnlySpan bytes, byte scale, byte denormalizedScale) { - Debug.Assert(IsEmpty, "setting value a second time?"); - _object = value; - _type = StorageType.Json; - _isNull = false; + SetTypeAndIsNull(StorageType.Time, false); + _value._timeInfo.FromByteArray(bytes, scale, denormalizedScale); } - internal void SetToDate(ReadOnlySpan bytes) + internal void SetToTime(TimeSpan timeSpan, byte scale) { - Debug.Assert(IsEmpty, "setting value a second time?"); - - _type = StorageType.Date; - _value._int32 = GetDateFromByteArray(bytes); - _isNull = false; + SetTypeAndIsNull(StorageType.Time, false); + _value._timeInfo.FromTimeSpanAndScale(timeSpan, scale); } - - internal void SetToTime(ReadOnlySpan bytes, byte scale, byte denormalizedScale) + + internal void SetToVectorInfo(int elementCount, byte elementType, bool isNull) { - Debug.Assert(IsEmpty, "setting value a second time?"); + SetTypeAndIsNull(StorageType.Vector, isNull); + _value._vectorInfo._elementCount = elementCount; + _value._vectorInfo._elementType = elementType; + } - _type = StorageType.Time; - FillInTimeInfo(ref _value._timeInfo, bytes, scale, denormalizedScale); - _isNull = false; + #endregion + + #region Private Helpers + + private static SqlMoney GetSqlMoneyFromLong(long value) + { + #if NET + return SqlMoney.FromTdsValue(value); + #else + return SqlTypeWorkarounds.SqlMoneyCtor(value, 1); + #endif } - internal void SetToTime(TimeSpan timeSpan, byte scale) + private ISqlVector GetSqlVector() + { + MetaType.SqlVectorElementType elementType = (MetaType.SqlVectorElementType)_value._vectorInfo._elementType; + switch (elementType) + { + case MetaType.SqlVectorElementType.Float32: + return GetSqlVector(); + default: + throw SQL.VectorTypeNotSupported(elementType.ToString()); + } + } + + private T GetValue(StorageType storageType, T value) { - Debug.Assert(IsEmpty, "setting value a second time?"); + ThrowIfNull(); - _type = StorageType.Time; - _value._timeInfo._ticks = timeSpan.Ticks; - _value._timeInfo._scale = scale; - _isNull = false; + return _type == storageType + ? value + : (T)Value; // Types we cannot directly convert to (ie, everything except for + // `storageType` will need to converted via boxing. + } + + private TOut GetValueAs(TValue value) + { + // [Field]As method explanation: + // these methods are used to bridge generic to non-generic access to value type fields on the storage struct + // 1) where typeof(T) == typeof(field) + // 1) RyuJIT will recognize the pattern of (T)(object)T as being redundant and eliminate + // the T and object casts leaving T, so while this looks like it will put every value type instance in a box the + // generated assembly will be short and direct + // 2) another jit may not recognize the pattern and should emit the code as seen. this will box and then unbox the + // value type which is no worse than the mechanism that this code replaces + // 2) where typeof(T) != typeof(field) + // the jit will emit all the cast operations as written. this will put the value into a box and then attempt to + // cast it, because it is an object no conversions are used and this will generate the desired InvalidCastException + // for example users cannot widen a short to an int preserving external expectations + + ThrowIfNull(); + return (TOut)(object)value; } - internal void SetToDateTime2(ReadOnlySpan bytes, byte scale, byte denormalizedScale) + private void SetObject(StorageType storageType, T value) + where T : INullable { - Debug.Assert(IsEmpty, "setting value a second time?"); - int length = bytes.Length; - _type = StorageType.DateTime2; - FillInTimeInfo(ref _value._dateTime2Info._timeInfo, bytes.Slice(0, length - 3), scale, denormalizedScale); // remaining 3 bytes is for date - _value._dateTime2Info._date = GetDateFromByteArray(bytes.Slice(length - 3)); // 3 bytes for date - _isNull = false; + SetTypeAndIsNull(storageType, value.IsNull); + _object = value; } - - internal void SetToDateTimeOffset(ReadOnlySpan bytes, byte scale, byte denormalizedScale) + + private void SetValue(StorageType storageType, ref T valueField, T value) { - Debug.Assert(IsEmpty, "setting value a second time?"); - int length = bytes.Length; - _type = StorageType.DateTimeOffset; - FillInTimeInfo(ref _value._dateTimeOffsetInfo._dateTime2Info._timeInfo, bytes.Slice(0, length - 5), scale, denormalizedScale); // remaining 5 bytes are for date and offset - _value._dateTimeOffsetInfo._dateTime2Info._date = GetDateFromByteArray(bytes.Slice(length - 5)); // 3 bytes for date - _value._dateTimeOffsetInfo._offset = (short)(bytes[length - 2] + (bytes[length - 1] << 8)); // 2 bytes for offset (Int16) - _isNull = false; + SetTypeAndIsNull(storageType, false); + valueField = value; } - internal void SetToDateTimeOffset(DateTimeOffset dateTimeOffset, byte scale) + private void SetTypeAndIsNull(StorageType storageType, bool isNull) { - Debug.Assert(IsEmpty, "setting value a second time?"); + Debug.Assert(IsEmpty, "Value is being set a second time."); - _type = StorageType.DateTimeOffset; - DateTime utcDateTime = dateTimeOffset.UtcDateTime; // timeInfo stores the utc datetime of a datatimeoffset - _value._dateTimeOffsetInfo._dateTime2Info._timeInfo._ticks = utcDateTime.TimeOfDay.Ticks; - _value._dateTimeOffsetInfo._dateTime2Info._timeInfo._scale = scale; - _value._dateTimeOffsetInfo._dateTime2Info._date = utcDateTime.Subtract(DateTime.MinValue).Days; - _value._dateTimeOffsetInfo._offset = (short)dateTimeOffset.Offset.TotalMinutes; - _isNull = false; + IsNull = isNull; + _type = storageType; } - - private static void FillInTimeInfo(ref TimeInfo timeInfo, ReadOnlySpan timeBytes, byte scale, byte denormalizedScale) + + private void ThrowIfNull() { - int length = timeBytes.Length; - Debug.Assert(3 <= length && length <= 5, "invalid data length for timeInfo: " + length); - Debug.Assert(0 <= scale && scale <= 7, "invalid scale: " + scale); - Debug.Assert(0 <= denormalizedScale && denormalizedScale <= 7, "invalid denormalized scale: " + denormalizedScale); - - long tickUnits = timeBytes[0] + ((long)timeBytes[1] << 8) + ((long)timeBytes[2] << 16); - if (length > 3) + if (IsNull) + { + throw new SqlNullValueException(); + } + } + + #endregion + + #region Private Structs + + /// + /// Used to store DATETIME information. + /// + private struct DateTimeInfo + { + /// + /// Number of days since 1900-00-00 MINUS 53690. + /// + internal int DayPart { get; set; } + + /// + /// Number of SQL ticks since 00:00:00. + /// Note: this is not the same as CLR ticks. + /// + internal int TimePart { get; set; } + + internal void FromDateTimeData(int dayPart, int timePart) { - tickUnits += ((long)timeBytes[3] << 24); + DayPart = dayPart; + TimePart = timePart; } - if (length > 4) + + /// + /// Generates a new DateTime object from the SQL DATETIME information. + /// + internal DateTime ToDateTime() { - tickUnits += ((long)timeBytes[4] << 32); + // SQL DATETIME is represented as two integers, the number of days since 1900-01-01 + // and the number of (SQL) ticks since 00:00:00. + + // Values that come from SqlDateTime + const double SqlTicksPerMillisecond = 0.3; + const int SqlTicksPerSecond = 300; + const int SqlTicksPerMinute = SqlTicksPerSecond * 60; + const int SqlTicksPerHour = SqlTicksPerMinute * 60; + const int SqlTicksPerDay = SqlTicksPerHour * 24; + + // This is added to date to bring negative days up to 0. + const uint MinDays = 53690; + + // 9999-12-31 (max date) is this many days after 1900-01-01 (min date) + const uint MaxDays = 2958463; + const uint OffsetMaxDays = MaxDays + MinDays; + + // Maximum time that can be stored in a DateTime (ie, 23:59:59.997) + const uint MaxTicks = SqlTicksPerDay - 1; + + // Number of ticks to add to a new DateTime to get to 1900-01-01 + const long BaseDateTicks = 599266080000000000L; + + // 1) Check boundaries + // a) Days must be: min_days < days < max_days + // Which can be simplified to: 0 < days+min_days < max_days+min_days + // If days+min_days is still negative, casting to uint will cause it to + // overflow, simplifying to: uint(days+min_days) < max_days+min_days + // b) Time must be: 0 < time < max_time + // If time is negative, casting to uint will cause it to overflow, simplifying + // to: uint(time) < max_time + if ((uint)(DayPart + MinDays) > OffsetMaxDays || + (uint)TimePart > MaxTicks) + { + throw SQL.DateTimeOverflow(); + } + + // 2) Calculate (CLR) ticks in the days + long dayTicks = DayPart * TimeSpan.TicksPerDay; + + // 3) Calculate (CLR) ticks in time part + double timeInMilliseconds = (TimePart / SqlTicksPerMillisecond) + 0.5; + long timeTicks = (long)timeInMilliseconds * TimeSpan.TicksPerMillisecond; + + // 4) Combine ticks and generate DateTime object + long totalTicks = BaseDateTicks + dayTicks + timeTicks; + return new DateTime(totalTicks); } - timeInfo._ticks = tickUnits * TdsEnums.TICKS_FROM_SCALE[scale]; - // Once the deserialization has been completed using the value scale, we need to set the actual denormalized scale, - // coming from the data type, on the original result, so that it has the proper scale setting. - // This only applies for values that got serialized/deserialized for encryption. Otherwise, both scales should be equal. - timeInfo._scale = denormalizedScale; + /// + /// Generates a new SqlDateTime object from the SQL DATETIME information. + /// + internal SqlDateTime ToSqlDateTime() => + new SqlDateTime(DayPart, TimePart); } - - private static int GetDateFromByteArray(ReadOnlySpan buf) + + /// + /// Used to store DATETIME2 information. + /// + private struct DateTime2Info { - byte thirdByte = buf[2]; // reordered to optimize JIT generated bounds checks to a single instance, review generated asm before changing - return buf[0] + (buf[1] << 8) + (thirdByte << 16); - } + // @TODO: Move to properties + + /// + /// Number of days since 0001-01-01. + /// + internal int _date; + + /// + /// Time component of the DATETIME2 value. + /// + internal TimeInfo _timeInfo; - private void ThrowIfNull() - { - if (IsNull) + internal void FromByteArray(ReadOnlySpan bytes, byte scale, byte denormalizedScale) { - throw new SqlNullValueException(); + int length = bytes.Length; + + // Set time from time bytes + ReadOnlySpan timeBytes = bytes.Slice(0, length - 3); + _timeInfo.FromByteArray(timeBytes, scale, denormalizedScale); + + // Set days from day bytes + // NOTE: Reordered to optimize JIT generated bounds checks to a single instance, + // review generated asm before changing. + // @TODO: Verify that ^^^ is still accurate/needed + ReadOnlySpan dateBytes = bytes.Slice(length - 3); + byte thirdByte = dateBytes[2]; + _date = dateBytes[0] + (dateBytes[1] << 8) + (thirdByte << 16); + } + + internal void FromDateTimeAndScale(DateTime dateTime, byte scale) + { + _date = dateTime.Subtract(DateTime.MinValue).Days; + _timeInfo.FromTimeSpanAndScale(dateTime.TimeOfDay, scale); + } + + /// + /// Generates a new DateTime object from the SQL DATETIME2 information. + /// + internal DateTime ToDateTime() + { + long ticks = _date * TimeSpan.TicksPerDay + _timeInfo._ticks; + return new DateTime(ticks); } } - // [Field]As method explanation: - // these methods are used to bridge generic to non-generic access to value type fields on the storage struct - // where typeof(T) == typeof(field) - // 1) RyuJIT will recognize the pattern of (T)(object)T as being redundant and eliminate - // the T and object casts leaving T, so while this looks like it will put every value type instance in a box the - // generated assembly will be short and direct - // 2) another jit may not recognize the pattern and should emit the code as seen. this will box and then unbox the - // value type which is no worse than the mechanism that this code replaces - // where typeof(T) != typeof(field) - // the jit will emit all the cast operations as written. this will put the value into a box and then attempt to - // cast it, because it is an object no conversions are used and this will generate the desired InvalidCastException - // for example users cannot widen a short to an int preserving external expectations - - internal T ByteAs() + + /// + /// Used to store DATETIMEOFFSET information. + /// + private struct DateTimeOffsetInfo { - ThrowIfNull(); - return (T)(object)_value._byte; - } + // @TODO: Move to properties + + /// + /// DateTime component of the DATETIMEOFFSET value. + /// + internal DateTime2Info _dateTime2Info; + + /// + /// Timezone offset component of the DATETIMEOFFSET value, in minutes. + /// + internal short _offset; - internal T BooleanAs() - { - ThrowIfNull(); - return (T)(object)_value._boolean; - } + internal void FromByteArray(ReadOnlySpan bytes, byte scale, byte denormalizedScale) + { + int length = bytes.Length; + + // Set DATETIME2 info from bytes + ReadOnlySpan dateTime2Bytes = bytes.Slice(0, length - 2); + _dateTime2Info.FromByteArray(dateTime2Bytes, scale, denormalizedScale); + + // Set offset from remaining bytes + ReadOnlySpan offsetBytes = bytes.Slice(length - 2); + _offset = BinaryPrimitives.ReadInt16LittleEndian(offsetBytes); + } - internal T Int32As() - { - ThrowIfNull(); - return (T)(object)_value._int32; + internal void FromDateTimeOffsetAndScale(DateTimeOffset dateTimeOffset, byte scale) + { + _dateTime2Info.FromDateTimeAndScale(dateTimeOffset.UtcDateTime, scale); + _offset = (short)dateTimeOffset.Offset.TotalMinutes; + } + + /// + /// Generates a new DateTimeOffset object from the SQL DATETIMEOFFSET information. + /// + internal DateTimeOffset ToDateTimeOffset() + { + DateTime dateTime = _dateTime2Info.ToDateTime(); + TimeSpan offset = new TimeSpan(0, _offset, 0); + return new DateTimeOffset(dateTime, offset); + } } - - internal T Int16As() + + /// + /// Used to store DECIMAL/NUMERIC type information. + /// + private struct NumericInfo { - ThrowIfNull(); - return (T)(object)_value._int16; - } + /// + /// Low 32 bits of the integer value. + /// + internal int _data1; + + /// + /// Middle 32 bits of the integer value. + /// + internal int _data2; + + /// + /// High 32 bits of the integer value. + /// + internal int _data3; + + /// + /// Extended 32 bits of the integer value. + /// + internal int _data4; + + /// + /// Prevision of the value (ie, the number of significant digits to retain). Minimum of + /// 1, maximum of 38. + /// + internal byte _precision; + + /// + /// Scale to apply to the integer value (ie, the integer will be multiplied by 1/10^x). + /// + internal byte _scale; + + /// + /// Whether the number is positive or negative. + /// + internal bool _positive; - internal T Int64As() - { - ThrowIfNull(); - return (T)(object)_value._int64; - } + internal void FromDecimalData(byte precision, byte scale, bool positive, int[] integerBlocks) + { + Debug.Assert(integerBlocks.Length == 4, "Integer blocks must contain low, mid, high, and extended bits"); + + _precision = precision; + _scale = scale; + _positive = positive; + _data1 = integerBlocks[0]; + _data2 = integerBlocks[1]; + _data3 = integerBlocks[2]; + _data4 = integerBlocks[3]; + } + + internal decimal ToDecimal() + { + // SQL DECIMAL/NUMERIC type can store larger numbers than CLR decimal type, if we + // cannot directly represent the number as a CLR decimal, we can try some tricks to + // optimize storage before giving up. + + // 1) Determine if number can fit within a CLR decimal and directly convert if so. + if (_data4 == 0 && _scale <= 28) + { + return new decimal(_data1, _data2, _data3, !_positive, _scale); + } + + // 2) Number cannot fit in a CLR decimal, attempt optimization of the value + if (_scale > 0) + { + // 2.1) Find trailing zeroes and actual precision + (int trailingZeroes, int actualPrecision) = FindTrailingZeroesAndPrecision(); + + // 2.2) Calculate minimum scale after removing trailing zeroes + int minimumScale = _scale - trailingZeroes; + + // 2.3) Check if value fits in CLR decimal after optimization + if (trailingZeroes > 0 && minimumScale <= 28 && actualPrecision <= 29) + { + // We can indeed optimize to fit in CLR decimal! + // 2.3.1) Calculate target precision for conversion + int integerDigits = actualPrecision - minimumScale; + int targetPrecision = 29; // Default to maximum precision + + // 2.3.2) Adjust precision based on integer value size. + if (integerDigits != 1 && actualPrecision != 29) + { + // Integer value is not 1 digit (cannot be zero), and we're not already at + // maximum precision. Use 28 for target precision to allow for potential + // growth. + targetPrecision = 28; + } + + // 2.3.3 Use SqlDecimal to convert to target precision/scale + try + { + int targetScale = targetPrecision - integerDigits; + SqlDecimal sqlValue = ToSqlDecimal(); + return SqlDecimal.ConvertToPrecScale(sqlValue, targetPrecision, targetScale).Value; + } + catch (OverflowException) + { + throw new OverflowException(SQLResource.ConversionOverflowMessage); + } + } + } + + // 3) Optimization was not possible + throw new OverflowException(SQLResource.ConversionOverflowMessage); + } + + internal SqlDecimal ToSqlDecimal() => + new SqlDecimal(_precision, _scale, _positive, _data1, _data2, _data3, _data4); + + private (int trailingZeroes, int valuableDigits) FindTrailingZeroesAndPrecision() + { + // Make local copy of the data so we do not modify the internal data. + Span integerData = stackalloc uint[] { (uint)_data1, (uint)_data2, (uint)_data3, (uint)_data4 }; + + // Repeatedly divide by 10 to determine how many digits are trailing zeroes and how + // many are non-zero + int length = 4; // Number of data blocks that will be used in the calculation + int trailingZeroes = 0; // Number of trailing zeroes + int valuableDigits = 0; // Digits in the number that are valuable (non-zero) + uint remainder = 0; // Remainder after division by 10 + while (length > 1 || integerData[0] != 0) + { + // 1) Divide the number by 10 in-place + uint carry = 0; + for (int i = length - 1; i >= 0; i--) + { + ulong accumulator = ((ulong)carry << 32) + integerData[i]; + integerData[i] = (uint)(accumulator / 10); + carry = (uint)(accumulator - integerData[i] * 10); + } + + remainder = carry; + + // 2) Normalize the multi-precision number, ie, remove the leading zeroes + while (length > 1 && integerData[length - 1] == 0) + { + length--; + } + + // 3) If the working number was divisible by 10, increase trailing zero count. + // Otherwise, increase the number of valuable digits. + if (remainder == 0 && valuableDigits == 0) + { + trailingZeroes++; + } + else + { + valuableDigits++; + } + } + + // Handle case where the number divided down to exactly 0. This means all decimal + // digits are trailing zeroes. + if (remainder == 0) + { + trailingZeroes = _scale; + } + + // Ensure we account for all decimal places defined by the scale. + // If we haven't processed enough digits, the remaining are implied leading zeroes, + // and we need at least one valuable digit to represent the number. + if (trailingZeroes + valuableDigits <= _scale) + { + valuableDigits = _scale - trailingZeroes + 1; + } - internal T DoubleAs() + return (trailingZeroes, valuableDigits); + } + } + + /// + /// This is a hacky way of creating a union type in C#, for optimizing storage of a single + /// object of various types. This struct is specifically used for storing value types. + /// + [StructLayout(LayoutKind.Explicit)] + private struct Storage { - ThrowIfNull(); - return (T)(object)_value._double; + [FieldOffset(0)] + internal bool _boolean; + + [FieldOffset(0)] + internal byte _byte; + + [FieldOffset(0)] + internal DateTimeInfo _dateTimeInfo; + + [FieldOffset(0)] + internal double _double; + + [FieldOffset(0)] + internal NumericInfo _numericInfo; + + [FieldOffset(0)] + internal short _int16; + + [FieldOffset(0)] + internal int _int32; + + [FieldOffset(0)] + internal long _int64; // also used to store Money, UtcDateTime, Date , and Time + + [FieldOffset(0)] + internal Guid _guid; + + [FieldOffset(0)] + internal float _single; + + [FieldOffset(0)] + internal TimeInfo _timeInfo; + + [FieldOffset(0)] + internal DateTime2Info _dateTime2Info; + + [FieldOffset(0)] + internal DateTimeOffsetInfo _dateTimeOffsetInfo; + + [FieldOffset(0)] + internal VectorInfo _vectorInfo; } + + /// + /// Used to store TIME information. + /// + private struct TimeInfo + { + internal long _ticks; + internal byte _scale; - internal T SingleAs() + internal void FromByteArray(ReadOnlySpan timeBytes, byte scale, byte denormalizedScale) + { + // Prefetch the length for guaranteed performance. + int length = timeBytes.Length; + + Debug.Assert(length >= 3 && length <= 5, "typeBytes must have 3-5 items in it."); + Debug.Assert(scale <= 7, "scale must be less than 8"); + Debug.Assert(denormalizedScale <= 7, "denormalizedScale mut be less than 8"); + + // Deserialize the timeBytes into a long + // Note: we cannot use binary primitives here since timeBytes is variable length + // and will never be 8 bytes. + long tickUnits = 0; + for (int i = 0; i < length; i++) + { + tickUnits += (long)timeBytes[i] << (8 * i); + } + + // Calculate true ticks from deserialized value and scale + _ticks = tickUnits * TdsEnums.TICKS_FROM_SCALE[scale]; + + // Once the deserialization has been completed using the value scale, we need to + // set the actual denormalized scale, coming from the data type, on the original + // result, so that it has the proper scale setting. This only applies for values + // that got serialized/deserialized for encryption. Otherwise, both scales should + // be equal. + _scale = denormalizedScale; + } + + internal void FromTimeSpanAndScale(TimeSpan timeSpan, byte scale) + { + _ticks = timeSpan.Ticks; + _scale = scale; + } + } + + private struct VectorInfo { - ThrowIfNull(); - return (T)(object)_value._single; + internal int _elementCount; + internal byte _elementType; + + /// + /// Constructs a new (null contents) SqlVector of the given type. Instance will have + /// count equal to . + /// + internal SqlVector ToNull() + where T : unmanaged => + new SqlVector(_elementCount); } + + #endregion } } 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 d1f3f5c1a5..92d278a3e3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -3161,7 +3161,7 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met // this block of type specific shortcuts uses RyuJIT jit behaviors to achieve fast implementations of the primitive types // RyuJIT will be able to determine at compilation time that the typeof(T)==typeof() options are constant // and be able to remove all implementations which cannot be reached. this will eliminate non-specialized code for - Type dataType = data.GetTypeFromStorageType(false); + Type dataType = data.ClrType; if (typeof(T) == typeof(int) && dataType == typeof(int)) { return data.Int32As(); @@ -3365,10 +3365,7 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met { if (typeof(T) == typeof(string) && metaData.metaType.SqlDbType == SqlDbTypeExtensions.Vector) { - if (data.IsNull) - return (T)(object)data.String; - else - return (T)(object)data.GetSqlVector().GetString(); + return (T)(object)data.String; } // 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/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs index f9b940fec3..b750d7e859 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs @@ -736,18 +736,14 @@ public override object Value { return _value; } - else if (_sqlBufferReturnValue != null) + + if (_sqlBufferReturnValue != null) { - if (ParameterIsSqlType) - { - if (_sqlBufferReturnValue.VariantInternalStorageType == SqlBuffer.StorageType.Vector) - { - return GetVectorReturnValue(); - } - return _sqlBufferReturnValue.SqlValue; - } - return _sqlBufferReturnValue.Value; + return ParameterIsSqlType + ? _sqlBufferReturnValue.SqlValue + : _sqlBufferReturnValue.Value; } + return null; } set @@ -763,30 +759,6 @@ public override object Value } } - private object GetVectorReturnValue() - { - var elementType = (MetaType.SqlVectorElementType)_sqlBufferReturnValue.GetVectorInfo()._vectorInfo._elementType; - int elementCount = _sqlBufferReturnValue.GetVectorInfo()._vectorInfo._elementCount; - - if (IsNull) - { - switch (elementType) - { - case MetaType.SqlVectorElementType.Float32: - return new SqlVector(elementCount); - default: - throw SQL.VectorTypeNotSupported(elementType.ToString()); - } - } - switch (elementType) - { - case MetaType.SqlVectorElementType.Float32: - return new SqlVector((byte[])_sqlBufferReturnValue.Value); - default: - throw SQL.VectorTypeNotSupported(elementType.ToString()); - } - } - /// [ RefreshProperties(RefreshProperties.All), @@ -1953,8 +1925,12 @@ private MetaType GetMetaTypeOnly() return MetaType.GetMetaTypeFromType(valueType); } else if (_sqlBufferReturnValue != null) - { // value came back from the server - Type valueType = _sqlBufferReturnValue.GetTypeFromStorageType(HasFlag(SqlParameterFlags.IsSqlParameterSqlType)); + { + // value came back from the server + Type valueType = HasFlag(SqlParameterFlags.IsSqlParameterSqlType) + ? _sqlBufferReturnValue.SqlType + : _sqlBufferReturnValue.ClrType; + if (valueType != null) { return MetaType.GetMetaTypeFromType(valueType); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs index 853be887dc..8402b400e2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs @@ -52,43 +52,5 @@ internal static XmlReader SqlXmlCreateSqlXmlReader(TextReader textReader, bool c return XmlReader.Create(textReader, settingsToUse); } #endregion - - #region Work around inability to access SqlDateTime.ToDateTime - internal static DateTime SqlDateTimeToDateTime(int daypart, int timepart) - { - // Values need to match those from SqlDateTime - const double SQLTicksPerMillisecond = 0.3; - const int SQLTicksPerSecond = 300; - const int SQLTicksPerMinute = SQLTicksPerSecond * 60; - const int SQLTicksPerHour = SQLTicksPerMinute * 60; - const int SQLTicksPerDay = SQLTicksPerHour * 24; - //const int MinDay = -53690; // Jan 1 1753 - const uint MinDayOffset = 53690; // postive value of MinDay used to pull negative values up to 0 so a single check can be used - const uint MaxDay = 2958463; // Dec 31 9999 is this many days from Jan 1 1900 - const uint MaxTime = SQLTicksPerDay - 1; // = 25919999, 11:59:59:997PM - const long BaseDateTicks = 599266080000000000L;//new DateTime(1900, 1, 1).Ticks; - - // casting to uint wraps negative values to large positive ones above the valid - // ranges so the lower bound doesn't need to be checked - if ((uint)(daypart + MinDayOffset) > (MaxDay + MinDayOffset) || (uint)timepart > MaxTime) - { - ThrowOverflowException(); - } - - long dayticks = daypart * TimeSpan.TicksPerDay; - double timePartPerMs = timepart / SQLTicksPerMillisecond; - timePartPerMs += 0.5; - long timeTicks = ((long)timePartPerMs) * TimeSpan.TicksPerMillisecond; - long totalTicks = BaseDateTicks + dayticks + timeTicks; - return new DateTime(totalTicks); - } - - // this method is split out of SqlDateTimeToDateTime for performance reasons - // it is faster to make a method call than it is to incorporate the asm for this - // method in the calling method. - [MethodImpl(MethodImplOptions.NoInlining)] - private static Exception ThrowOverflowException() => throw SQL.DateTimeOverflow(); - - #endregion } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs index e63a5b7462..e03a24bab4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -85,14 +85,11 @@ internal SqlVector(byte[] tdsBytes) #region Methods - internal string GetString() - { - if (IsNull) - { - return SQLResource.NullString; - } - return JsonSerializer.Serialize(Memory); - } + /// + string ISqlVector.GetString() => + IsNull + ? SQLResource.NullString + : JsonSerializer.Serialize(Memory); #endregion @@ -112,12 +109,15 @@ internal string GetString() /// public ReadOnlyMemory Memory { get; init; } - #endregion + /// + byte ISqlVector.ElementSize => _elementSize; - #region ISqlVector Internal Properties + /// byte ISqlVector.ElementType => _elementType; - byte ISqlVector.ElementSize => _elementSize; + + /// byte[] ISqlVector.VectorPayload => _tdsBytes; + #endregion #region Helpers diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs similarity index 56% rename from src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs rename to src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs index 3390d95c02..d7e118ac87 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs @@ -32,190 +32,240 @@ public void Construct_Length_Negative() [Fact] public void Construct_Length() { + // Act var vec = new SqlVector(5); + + // Assert + // - SqlVector properties/methods 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; + // - ISqlVector properties/methods + ISqlVector ivec = vec; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); Assert.Empty(ivec.VectorPayload); + + Assert.Equal(SQLResource.NullString, ivec.GetString()); } [Fact] public void Construct_WithLengthZero() { + // Act var vec = new SqlVector(0); + + // Assert + // - SqlVector properties/methods 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; + // - ISqlVector properties/methods + ISqlVector ivec = vec; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); Assert.Empty(ivec.VectorPayload); + + Assert.Equal(SQLResource.NullString, ivec.GetString()); } [Fact] public void Construct_Memory_Empty() { - SqlVector vec = new(new ReadOnlyMemory()); + // Act + SqlVector vec = new SqlVector(new ReadOnlyMemory()); + + // Assert + // - SqlVector properties/methods 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; + // - ISqlVector properties/methods + ISqlVector ivec = vec; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); - Assert.Equal( - new byte[] { 0xA9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - ivec.VectorPayload); + Assert.Equal(new byte[] { 0xA9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, ivec.VectorPayload); + + Assert.Equal("[]", ivec.GetString()); } [Fact] public void Construct_Memory() { + // Arrange float[] data = [1.1f, 2.2f]; - ReadOnlyMemory memory = new(data); - SqlVector vec = new(memory); + ReadOnlyMemory memory = new ReadOnlyMemory(data); + + // Act + SqlVector vec = new SqlVector(memory); + + // Assert + // - SqlVector methods/properties 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; + + // - ISqlVector methods/properties + ISqlVector ivec = vec; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); Assert.Equal( - MakeTdsPayload( - new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }, - memory), + MakeTdsPayload(new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }, memory), ivec.VectorPayload); + + #if NETFRAMEWORK + Assert.Equal("[1.10000002,2.20000005]", ivec.GetString()); + #else + Assert.Equal("[1.1,2.2]", ivec.GetString()); + #endif } [Fact] public void Construct_Memory_ImplicitConversionFromFloatArray() { - float[] data = new float[] { 3.3f, 4.4f, 5.5f }; + // Arrange + float[] data = new[] { 3.3f, 4.4f, 5.5f }; + + // Act var vec = new SqlVector(data); + + // Assert + // - SqlVector methods/properties 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; + + // - ISqlVector methods/properties + ISqlVector ivec = vec; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); Assert.Equal( - MakeTdsPayload( - new byte[] { 0xA9, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }, - data), + MakeTdsPayload(new byte[] { 0xA9, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }, data), ivec.VectorPayload); + + #if NETFRAMEWORK + Assert.Equal("[3.29999995,4.4000001,5.5]", ivec.GetString()); + #else + Assert.Equal("[3.3,4.4,5.5]", ivec.GetString()); + #endif } [Fact] public void Construct_Bytes() { + // Arrange + byte[] header = new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }; float[] data = new float[] { 6.6f, 7.7f }; - var bytes = - MakeTdsPayload( - new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }, - data); + byte[] bytes = MakeTdsPayload(header, data); + // Act var vec = new SqlVector(bytes); + + // Assert + // - SqlVector methods/properties 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; + + // - ISqlVector methods/properties + ISqlVector ivec = vec; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); Assert.Equal(bytes, ivec.VectorPayload); + + #if NETFRAMEWORK + Assert.Equal("[6.5999999,7.69999981]", ivec.GetString()); + #else + Assert.Equal("[6.6,7.7]", ivec.GetString()); + #endif } [Fact] public void Construct_Bytes_ShortHeader() { - Assert.Throws(() => - { - new SqlVector(new byte[] { 0xA9, 0x01, 0x00, 0x00 }); - }); + // Arrange + var tdsBytes = new byte[] { 0xA9, 0x01, 0x00, 0x00 }; + + // Act + Action action = () => _ = new SqlVector(tdsBytes); + + // Assert + Assert.Throws(action); } [Fact] public void Construct_Bytes_UnknownMagic() { - Assert.Throws(() => - { - new SqlVector( - new byte[] { 0xA8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); - }); + // Arrange + var tdsBytes = new byte[] { 0xA8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + // Act + Action action = () => _ = new SqlVector(tdsBytes); + + // Assert + Assert.Throws(action); } [Fact] public void Construct_Bytes_UnsupportedVersion() { - Assert.Throws(() => - { - new SqlVector( - new byte[] { 0xA9, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); - }); + // Arrange + var tdsBytes = new byte[] { 0xA9, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + // Act + Action action = () => _ = new SqlVector(tdsBytes); + + // Assert + Assert.Throws(action); } [Fact] public void Construct_Bytes_TypeMismatch() { - Assert.Throws(() => - { - new SqlVector( - new byte[] { 0xA9, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00 }); - }); + // Arrange + var tdsBytes = new byte[] { 0xA9, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00 }; + + // Act + Action action = () => _ = new SqlVector(tdsBytes); + + // Assert + Assert.Throws(action); } [Fact] public void Construct_Bytes_LengthMismatch() { - // The header indicates 2 elements, but the payload has 3 floats. + // Arrange + // - 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 })); + var floats = new ReadOnlyMemory([1.1f, 2.2f, 3.3f]); + var bytes = MakeTdsPayload(header, floats); - Assert.Throws(() => - { - new SqlVector(bytes); - }); + // Act + Action action = () => _ = new SqlVector(bytes); + + // Assert + Assert.Throws(action); } [Fact] @@ -228,7 +278,7 @@ public void Null_Property() #region Helpers - private byte[] MakeTdsPayload(byte[] header, ReadOnlyMemory values) + private static byte[] MakeTdsPayload(byte[] header, ReadOnlyMemory values) { int length = header.Length + (values.Length * sizeof(float)); byte[] payload = new byte[length];