diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e67486e2..c7b01fa0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: ports: [ "2135:2135", "2136:2136", "8765:8765" ] env: YDB_LOCAL_SURVIVE_RESTART: true - YDB_FEATURE_FLAGS: enable_parameterized_decimal + YDB_FEATURE_FLAGS: enable_parameterized_decimal,enable_table_datetime64 options: '--name ydb-local -h localhost' steps: diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index b44c4670..be5467e2 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,5 @@ +- Feat ADO.NET: Deleted support for `DateTimeOffset` was a mistake. +- Feat ADO.NET: Added support for `Date32`, `Datetime64`, `Timestamp64` and `Interval64` types in YDB. - Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter. - Dev: LogLevel `Warning` -> `Debug` on DeleteSession has been `RpcException`. diff --git a/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs b/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs index 0a525d20..6af6fd05 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs @@ -1,5 +1,3 @@ -using Ydb.Sdk.Ado.YdbType; - namespace Ydb.Sdk.Ado.Internal; internal interface ISqlParam diff --git a/src/Ydb.Sdk/src/Ado/Internal/TimeSpanConsts.cs b/src/Ydb.Sdk/src/Ado/Internal/TimeSpanConsts.cs new file mode 100644 index 00000000..82f508a8 --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/TimeSpanConsts.cs @@ -0,0 +1,6 @@ +namespace Ydb.Sdk.Ado.Internal; + +internal static class TimeSpanUtils +{ + internal const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; +} diff --git a/src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs similarity index 83% rename from src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs rename to src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs index 3ec71829..31223be9 100644 --- a/src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs @@ -1,7 +1,7 @@ using Google.Protobuf; using Google.Protobuf.WellKnownTypes; -namespace Ydb.Sdk.Ado.YdbType; +namespace Ydb.Sdk.Ado.Internal; internal static class YdbTypedValueExtensions { @@ -123,22 +123,39 @@ internal static TypedValue Uuid(this Guid value) internal static TypedValue Date(this DateTime value) => MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Date, new Ydb.Value { Uint32Value = (uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) }); + internal static TypedValue Date32(this DateTime value) => MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Date32, + new Ydb.Value { Int32Value = (int)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) }); + internal static TypedValue Datetime(this DateTime dateTimeValue) => MakePrimitiveTypedValue( - Type.Types.PrimitiveTypeId.Datetime, - new Ydb.Value + Type.Types.PrimitiveTypeId.Datetime, new Ydb.Value { Uint32Value = (uint)(dateTimeValue.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond) } ); + internal static TypedValue Datetime64(this DateTime dateTimeValue) => MakePrimitiveTypedValue( + Type.Types.PrimitiveTypeId.Datetime64, + new Ydb.Value { Int64Value = dateTimeValue.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond } + ); + internal static TypedValue Timestamp(this DateTime dateTimeValue) => MakePrimitiveTypedValue( Type.Types.PrimitiveTypeId.Timestamp, new Ydb.Value { - Uint64Value = (ulong)(dateTimeValue.Ticks - DateTime.UnixEpoch.Ticks) * Duration.NanosecondsPerTick / 1000 + Uint64Value = (ulong)(dateTimeValue.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond } ); + internal static TypedValue Timestamp64(this DateTime dateTimeValue) => MakePrimitiveTypedValue( + Type.Types.PrimitiveTypeId.Timestamp64, new Ydb.Value + { Int64Value = (dateTimeValue.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond } + ); + internal static TypedValue Interval(this TimeSpan timeSpanValue) => MakePrimitiveTypedValue( Type.Types.PrimitiveTypeId.Interval, - new Ydb.Value { Int64Value = timeSpanValue.Ticks * Duration.NanosecondsPerTick / 1000 } + new Ydb.Value { Int64Value = timeSpanValue.Ticks / TimeSpanUtils.TicksPerMicrosecond } + ); + + internal static TypedValue Interval64(this TimeSpan timeSpanValue) => MakePrimitiveTypedValue( + Type.Types.PrimitiveTypeId.Interval64, + new Ydb.Value { Int64Value = timeSpanValue.Ticks / TimeSpanUtils.TicksPerMicrosecond } ); internal static TypedValue List(this IReadOnlyList values) diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs new file mode 100644 index 00000000..c88fd149 --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs @@ -0,0 +1,101 @@ +namespace Ydb.Sdk.Ado.Internal; + +internal static class YdbValueExtensions +{ + private static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified); + + internal static bool IsNull(this Ydb.Value value) => value.ValueCase == Ydb.Value.ValueOneofCase.NullFlagValue; + + internal static bool GetBool(this Ydb.Value value) => value.BoolValue; + + internal static sbyte GetInt8(this Ydb.Value value) => (sbyte)value.Int32Value; + + internal static byte GetUint8(this Ydb.Value value) => (byte)value.Uint32Value; + + internal static short GetInt16(this Ydb.Value value) => (short)value.Int32Value; + + internal static ushort GetUint16(this Ydb.Value value) => (ushort)value.Uint32Value; + + internal static int GetInt32(this Ydb.Value value) => value.Int32Value; + + internal static uint GetUint32(this Ydb.Value value) => value.Uint32Value; + + internal static long GetInt64(this Ydb.Value value) => value.Int64Value; + + internal static ulong GetUint64(this Ydb.Value value) => value.Uint64Value; + + internal static float GetFloat(this Ydb.Value value) => value.FloatValue; + + internal static double GetDouble(this Ydb.Value value) => value.DoubleValue; + + internal static DateTime GetDate(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerDay); + + internal static DateTime GetDate32(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int32Value * TimeSpan.TicksPerDay); + + internal static DateTime GetDatetime(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerSecond); + + internal static DateTime GetDatetime64(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int64Value * TimeSpan.TicksPerSecond); + + internal static DateTime GetTimestamp(this Ydb.Value value) => + UnixEpoch.AddTicks((long)(value.Uint64Value * TimeSpanUtils.TicksPerMicrosecond)); + + internal static DateTime GetTimestamp64(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static TimeSpan GetInterval(this Ydb.Value value) => + TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static TimeSpan GetInterval64(this Ydb.Value value) => + TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static byte[] GetBytes(this Ydb.Value value) => value.BytesValue.ToByteArray(); + + internal static string GetText(this Ydb.Value value) => value.TextValue; + + internal static string GetJson(this Ydb.Value value) => value.TextValue; + + internal static string GetJsonDocument(this Ydb.Value value) => value.TextValue; + + internal static Guid GetUuid(this Ydb.Value value) + { + var high = value.High128; + var low = value.Low128; + + var lowBytes = BitConverter.GetBytes(low); + var highBytes = BitConverter.GetBytes(high); + + var guidBytes = new byte[16]; + Array.Copy(lowBytes, 0, guidBytes, 0, 8); + Array.Copy(highBytes, 0, guidBytes, 8, 8); + + return new Guid(guidBytes); + } + + internal static decimal GetDecimal(this Ydb.Value value, uint scale) + { + var lo = value.Low128; + var hi = value.High128; + var isNegative = (hi & 0x8000_0000_0000_0000UL) != 0; + unchecked + { + if (isNegative) + { + if (lo == 0) + hi--; + + lo--; + lo = ~lo; + hi = ~hi; + } + } + + if (hi >> 32 != 0) + throw new OverflowException("Value does not fit into decimal"); + + return new decimal((int)lo, (int)(lo >> 32), (int)hi, isNegative, (byte)scale); + } +} diff --git a/src/Ydb.Sdk/src/Ado/ThrowHelper.cs b/src/Ydb.Sdk/src/Ado/ThrowHelper.cs deleted file mode 100644 index 4dd739d7..00000000 --- a/src/Ydb.Sdk/src/Ado/ThrowHelper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Ydb.Sdk.Value; - -namespace Ydb.Sdk.Ado; - -internal static class ThrowHelper -{ - internal static T ThrowInvalidCast(YdbValue ydbValue) => - throw new InvalidCastException($"Field YDB type {ydbValue.TypeId} can't be cast to {typeof(T)} type."); - - internal static void ThrowIndexOutOfRangeException(int columnCount) => - throw new IndexOutOfRangeException("Ordinal must be between 0 and " + (columnCount - 1)); - - internal static void ThrowInvalidCastException(string expectedType, string actualType) => - throw new InvalidCastException($"Invalid type of YDB value, expected: {expectedType}, actual: {actualType}."); -} diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index c5a5a9d7..c8618a54 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using Ydb.Sdk.Ado.Internal; -using Ydb.Sdk.Ado.YdbType; namespace Ydb.Sdk.Ado; diff --git a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs index 5bcd0e8f..fa4a7141 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs @@ -3,10 +3,10 @@ using Ydb.Issue; using Ydb.Query; using Ydb.Sdk.Ado.Internal; -using Ydb.Sdk.Value; namespace Ydb.Sdk.Ado; +// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault public sealed class YdbDataReader : DbDataReader, IAsyncEnumerable { private readonly IServerStream _stream; @@ -16,7 +16,7 @@ public sealed class YdbDataReader : DbDataReader, IAsyncEnumerable this switch + private ResultSet CurrentResultSet => this switch { { ReaderState: State.ReadResultSet, _currentRowIndex: >= 0 } => _currentResultSet!, { ReaderState: State.Close } => throw new InvalidOperationException("The reader is closed"), _ => throw new InvalidOperationException("No row is available") }; - private Value.ResultSet.Row CurrentRow => CurrentResultSet.Rows[_currentRowIndex]; + private IReadOnlyList CurrentRow => CurrentResultSet.Rows[_currentRowIndex].Items; private int RowsCount => ReaderMetadata.RowsCount; private enum State @@ -86,14 +86,15 @@ private async Task Init(CancellationToken cancellationToken) ReaderState = State.ReadResultSet; } - public override bool GetBoolean(int ordinal) => GetFieldYdbValue(ordinal).GetBool(); + public override bool GetBoolean(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Bool, ordinal).GetBool(); - public override byte GetByte(int ordinal) => GetFieldYdbValue(ordinal).GetUint8(); + public override byte GetByte(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uint8, ordinal).GetUint8(); - public sbyte GetSByte(int ordinal) => GetFieldYdbValue(ordinal).GetInt8(); + public sbyte GetSByte(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Int8, ordinal).GetInt8(); - // ReSharper disable once MemberCanBePrivate.Global - public byte[] GetBytes(int ordinal) => GetFieldYdbValue(ordinal).GetString(); + public byte[] GetBytes(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.String, ordinal).GetBytes(); public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) { @@ -121,6 +122,7 @@ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int public override char GetChar(int ordinal) { var str = GetString(ordinal); + return str.Length == 0 ? throw new InvalidCastException("Could not read char - string was empty") : str[0]; } @@ -174,30 +176,50 @@ private static void CheckOffsets(long dataOffset, T[]? buffer, int bufferOffs public override DateTime GetDateTime(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Timestamp => ydbValue.GetTimestamp(), - YdbTypeId.Datetime => ydbValue.GetDatetime(), - YdbTypeId.Date => ydbValue.GetDate(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Timestamp => CurrentRow[ordinal].GetTimestamp(), + Type.Types.PrimitiveTypeId.Datetime => CurrentRow[ordinal].GetDatetime(), + Type.Types.PrimitiveTypeId.Date => CurrentRow[ordinal].GetDate(), + Type.Types.PrimitiveTypeId.Timestamp64 => CurrentRow[ordinal].GetTimestamp64(), + Type.Types.PrimitiveTypeId.Datetime64 => CurrentRow[ordinal].GetDatetime64(), + Type.Types.PrimitiveTypeId.Date32 => CurrentRow[ordinal].GetDate32(), + _ => throw InvalidCastException(ordinal) }; } - public TimeSpan GetInterval(int ordinal) => GetFieldYdbValue(ordinal).GetInterval(); + public TimeSpan GetInterval(int ordinal) + { + var type = UnwrapColumnType(ordinal); - public override decimal GetDecimal(int ordinal) => GetFieldYdbValue(ordinal).GetDecimal(); + return type.TypeId switch + { + Type.Types.PrimitiveTypeId.Interval => CurrentRow[ordinal].GetInterval(), + Type.Types.PrimitiveTypeId.Interval64 => CurrentRow[ordinal].GetInterval64(), + _ => throw InvalidCastException(ordinal) + }; + } + + public override decimal GetDecimal(int ordinal) + { + var type = UnwrapColumnType(ordinal); + + return type.TypeCase == Type.TypeOneofCase.DecimalType + ? CurrentRow[ordinal].GetDecimal((byte)type.DecimalType.Scale) + : throw InvalidCastException(Type.TypeOneofCase.DecimalType, ordinal); + } public override double GetDouble(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Float => ydbValue.GetFloat(), - YdbTypeId.Double => ydbValue.GetDouble(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Double => CurrentRow[ordinal].GetDouble(), + Type.Types.PrimitiveTypeId.Float => CurrentRow[ordinal].GetFloat(), + _ => throw InvalidCastException(ordinal) }; } @@ -230,117 +252,126 @@ public override System.Type GetFieldType(int ordinal) type = type.OptionalType.Item; } - var systemType = YdbValue.GetYdbTypeId(type) switch - { - YdbTypeId.Timestamp or YdbTypeId.Datetime or YdbTypeId.Date => typeof(DateTime), - YdbTypeId.Bool => typeof(bool), - YdbTypeId.Int8 => typeof(sbyte), - YdbTypeId.Uint8 => typeof(byte), - YdbTypeId.Int16 => typeof(short), - YdbTypeId.Uint16 => typeof(ushort), - YdbTypeId.Int32 => typeof(int), - YdbTypeId.Uint32 => typeof(uint), - YdbTypeId.Int64 => typeof(long), - YdbTypeId.Uint64 => typeof(ulong), - YdbTypeId.Float => typeof(float), - YdbTypeId.Double => typeof(double), - YdbTypeId.Interval => typeof(TimeSpan), - YdbTypeId.Utf8 or YdbTypeId.JsonDocument or YdbTypeId.Json or YdbTypeId.Yson => - typeof(string), - YdbTypeId.String => typeof(byte[]), - YdbTypeId.DecimalType => typeof(decimal), - YdbTypeId.Uuid => typeof(Guid), + if (type.TypeCase == Type.TypeOneofCase.DecimalType) + { + return typeof(decimal); + } + + return type.TypeId switch + { + Type.Types.PrimitiveTypeId.Date + or Type.Types.PrimitiveTypeId.Date32 + or Type.Types.PrimitiveTypeId.Datetime + or Type.Types.PrimitiveTypeId.Datetime64 + or Type.Types.PrimitiveTypeId.Timestamp + or Type.Types.PrimitiveTypeId.Timestamp64 => typeof(DateTime), + Type.Types.PrimitiveTypeId.Bool => typeof(bool), + Type.Types.PrimitiveTypeId.Int8 => typeof(sbyte), + Type.Types.PrimitiveTypeId.Uint8 => typeof(byte), + Type.Types.PrimitiveTypeId.Int16 => typeof(short), + Type.Types.PrimitiveTypeId.Uint16 => typeof(ushort), + Type.Types.PrimitiveTypeId.Int32 => typeof(int), + Type.Types.PrimitiveTypeId.Uint32 => typeof(uint), + Type.Types.PrimitiveTypeId.Int64 => typeof(long), + Type.Types.PrimitiveTypeId.Uint64 => typeof(ulong), + Type.Types.PrimitiveTypeId.Float => typeof(float), + Type.Types.PrimitiveTypeId.Double => typeof(double), + Type.Types.PrimitiveTypeId.Interval => typeof(TimeSpan), + Type.Types.PrimitiveTypeId.Utf8 + or Type.Types.PrimitiveTypeId.JsonDocument + or Type.Types.PrimitiveTypeId.Json => typeof(string), + Type.Types.PrimitiveTypeId.String => typeof(byte[]), + Type.Types.PrimitiveTypeId.Uuid => typeof(Guid), _ => throw new YdbException($"Unsupported ydb type {type}") }; - - return systemType; } - public override float GetFloat(int ordinal) => GetFieldYdbValue(ordinal).GetFloat(); + public override float GetFloat(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Float, ordinal).GetFloat(); - public override Guid GetGuid(int ordinal) => GetFieldYdbValue(ordinal).GetUuid(); + public override Guid GetGuid(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uuid, ordinal).GetUuid(); public override short GetInt16(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].GetInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].GetInt8(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public ushort GetUint16(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public override int GetInt32(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Int32 => ydbValue.GetInt32(), - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Int32 => CurrentRow[ordinal].GetInt32(), + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].GetInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].GetInt8(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public uint GetUint32(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].GetUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public override long GetInt64(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Int64 => ydbValue.GetInt64(), - YdbTypeId.Int32 => ydbValue.GetInt32(), - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Int64 => CurrentRow[ordinal].GetInt64(), + Type.Types.PrimitiveTypeId.Int32 => CurrentRow[ordinal].GetInt32(), + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].GetInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].GetInt8(), + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].GetUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public ulong GetUint64(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Uint64 => ydbValue.GetUint64(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Uint64 => CurrentRow[ordinal].GetUint64(), + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].GetUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } @@ -356,58 +387,62 @@ public override int GetOrdinal(string name) throw new IndexOutOfRangeException($"Field not found in row: {name}"); } - public override string GetString(int ordinal) => GetFieldYdbValue(ordinal).GetUtf8(); + public override string GetString(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Utf8, ordinal).GetText(); public override TextReader GetTextReader(int ordinal) => new StringReader(GetString(ordinal)); - public string GetJson(int ordinal) => GetFieldYdbValue(ordinal).GetJson(); + public string GetJson(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Json, ordinal).GetJson(); - public string GetJsonDocument(int ordinal) => GetFieldYdbValue(ordinal).GetJsonDocument(); + public string GetJsonDocument(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.JsonDocument, ordinal).GetJsonDocument(); public override object GetValue(int ordinal) { + var type = GetColumnType(ordinal); var ydbValue = CurrentRow[ordinal]; - // ReSharper disable once ConvertIfStatementToSwitchStatement - if (ydbValue.TypeId == YdbTypeId.Null) + if (ydbValue.IsNull()) { return DBNull.Value; } - // ReSharper disable once InvertIf - if (ydbValue.TypeId == YdbTypeId.OptionalType) + if (type.TypeCase == Type.TypeOneofCase.OptionalType) { - if (ydbValue.GetOptional() == null) - { - return DBNull.Value; - } + type = type.OptionalType.Item; + } - ydbValue = ydbValue.GetOptional()!; - } - - return ydbValue.TypeId switch - { - YdbTypeId.Timestamp or YdbTypeId.Datetime or YdbTypeId.Date => GetDateTime(ordinal), - YdbTypeId.Bool => ydbValue.GetBool(), - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Int32 => ydbValue.GetInt32(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - YdbTypeId.Int64 => ydbValue.GetInt64(), - YdbTypeId.Uint64 => ydbValue.GetUint64(), - YdbTypeId.Float => ydbValue.GetFloat(), - YdbTypeId.Double => ydbValue.GetDouble(), - YdbTypeId.Interval => ydbValue.GetInterval(), - YdbTypeId.Utf8 => ydbValue.GetUtf8(), - YdbTypeId.Json => ydbValue.GetJson(), - YdbTypeId.JsonDocument => ydbValue.GetJsonDocument(), - YdbTypeId.Yson => ydbValue.GetYson(), - YdbTypeId.String => ydbValue.GetString(), - YdbTypeId.DecimalType => ydbValue.GetDecimal(), - YdbTypeId.Uuid => ydbValue.GetUuid(), - _ => throw new YdbException($"Unsupported ydb type {ydbValue.TypeId}") + if (type.TypeCase == Type.TypeOneofCase.DecimalType) + { + return ydbValue.GetDecimal(type.DecimalType.Scale); + } + + return type.TypeId switch + { + Type.Types.PrimitiveTypeId.Date => ydbValue.GetDate(), + Type.Types.PrimitiveTypeId.Date32 => ydbValue.GetDate32(), + Type.Types.PrimitiveTypeId.Datetime => ydbValue.GetDatetime(), + Type.Types.PrimitiveTypeId.Datetime64 => ydbValue.GetDatetime64(), + Type.Types.PrimitiveTypeId.Timestamp => ydbValue.GetTimestamp(), + Type.Types.PrimitiveTypeId.Timestamp64 => ydbValue.GetTimestamp64(), + Type.Types.PrimitiveTypeId.Bool => ydbValue.GetBool(), + Type.Types.PrimitiveTypeId.Int8 => ydbValue.GetInt8(), + Type.Types.PrimitiveTypeId.Uint8 => ydbValue.GetUint8(), + Type.Types.PrimitiveTypeId.Int16 => ydbValue.GetInt16(), + Type.Types.PrimitiveTypeId.Uint16 => ydbValue.GetUint16(), + Type.Types.PrimitiveTypeId.Int32 => ydbValue.GetInt32(), + Type.Types.PrimitiveTypeId.Uint32 => ydbValue.GetUint32(), + Type.Types.PrimitiveTypeId.Int64 => ydbValue.GetInt64(), + Type.Types.PrimitiveTypeId.Uint64 => ydbValue.GetUint64(), + Type.Types.PrimitiveTypeId.Float => ydbValue.GetFloat(), + Type.Types.PrimitiveTypeId.Double => ydbValue.GetDouble(), + Type.Types.PrimitiveTypeId.Interval => ydbValue.GetInterval(), + Type.Types.PrimitiveTypeId.Utf8 => ydbValue.GetText(), + Type.Types.PrimitiveTypeId.Json => ydbValue.GetJson(), + Type.Types.PrimitiveTypeId.JsonDocument => ydbValue.GetJsonDocument(), + Type.Types.PrimitiveTypeId.String => ydbValue.GetBytes(), + Type.Types.PrimitiveTypeId.Uuid => ydbValue.GetUuid(), + _ => throw new YdbException($"Unsupported ydb type {GetColumnType(ordinal)}") }; } @@ -416,7 +451,7 @@ public override int GetValues(object[] values) ArgumentNullException.ThrowIfNull(values); if (FieldCount == 0) { - throw new InvalidOperationException(" No resultset is currently being traversed"); + throw new InvalidOperationException("No resultset is currently being traversed"); } var count = Math.Min(FieldCount, values.Length); @@ -425,9 +460,7 @@ public override int GetValues(object[] values) return count; } - public override bool IsDBNull(int ordinal) => - CurrentRow[ordinal].TypeId == YdbTypeId.Null || - (CurrentRow[ordinal].TypeId == YdbTypeId.OptionalType && CurrentRow[ordinal].GetOptional() == null); + public override bool IsDBNull(int ordinal) => CurrentRow[ordinal].IsNull(); public override int FieldCount => ReaderMetadata.FieldCount; public override object this[int ordinal] => GetValue(ordinal); @@ -536,13 +569,24 @@ public override async Task CloseAsync() public override void Close() => CloseAsync().GetAwaiter().GetResult(); - private YdbValue GetFieldYdbValue(int ordinal) + private Type UnwrapColumnType(int ordinal) { + var type = GetColumnType(ordinal); + + if (CurrentRow[ordinal].IsNull()) + throw new InvalidCastException("Field is null."); + + return type.TypeCase == Type.TypeOneofCase.OptionalType ? type.OptionalType.Item : type; + } + + private Type GetColumnType(int ordinal) => ReaderMetadata.GetColumn(ordinal).Type; + + private Ydb.Value GetPrimitiveValue(Type.Types.PrimitiveTypeId primitiveTypeId, int ordinal) + { + var type = UnwrapColumnType(ordinal); var ydbValue = CurrentRow[ordinal]; - return ydbValue.TypeId == YdbTypeId.OptionalType - ? ydbValue.GetOptional() ?? throw new InvalidCastException("Field is null.") - : ydbValue; + return type.TypeId == primitiveTypeId ? ydbValue : throw InvalidCastException(primitiveTypeId, ordinal); } private async ValueTask NextExecPart(CancellationToken cancellationToken) @@ -570,7 +614,7 @@ private async ValueTask NextExecPart(CancellationToken cancellationToken) throw YdbException.FromServer(part.Status, _issueMessagesInStream); } - _currentResultSet = part.ResultSet?.FromProto(); + _currentResultSet = part.ResultSet; ReaderMetadata = _currentResultSet != null ? new Metadata(_currentResultSet) : EmptyMetadata.Instance; if (_ydbTransaction != null && part.TxMeta != null) @@ -631,7 +675,7 @@ private EmptyMetadata() public int FieldCount => 0; public int RowsCount => 0; - public Value.ResultSet.Column GetColumn(int ordinal) => + public Column GetColumn(int ordinal) => throw new InvalidOperationException("No resultset is currently being traversed"); } @@ -649,34 +693,44 @@ private CloseMetadata() public int FieldCount => throw new InvalidOperationException("The reader is closed"); public int RowsCount => 0; - public Value.ResultSet.Column GetColumn(int ordinal) => - throw new InvalidOperationException("The reader is closed"); + public Column GetColumn(int ordinal) => throw new InvalidOperationException("The reader is closed"); } private class Metadata : IMetadata { - private IReadOnlyList Columns { get; } + private IReadOnlyList Columns { get; } public IReadOnlyDictionary ColumnNameToOrdinal { get; } public int FieldCount { get; } public int RowsCount { get; } - public Metadata(Value.ResultSet resultSet) + public Metadata(ResultSet resultSet) { - ColumnNameToOrdinal = resultSet.ColumnNameToOrdinal; Columns = resultSet.Columns; + ColumnNameToOrdinal = ColumnNameToOrdinal = Columns + .Select((c, idx) => (c.Name, Index: idx)) + .ToDictionary(t => t.Name, t => t.Index); RowsCount = resultSet.Rows.Count; FieldCount = resultSet.Columns.Count; } - public Value.ResultSet.Column GetColumn(int ordinal) + public Column GetColumn(int ordinal) { if (ordinal < 0 || ordinal >= FieldCount) { - ThrowHelper.ThrowIndexOutOfRangeException(FieldCount); + throw new IndexOutOfRangeException("Ordinal must be between 0 and " + (FieldCount - 1)); } return Columns[ordinal]; } } + + private InvalidCastException InvalidCastException(int ordinal) => + new($"Field YDB type {GetColumnType(ordinal)} can't be cast to {typeof(T)} type."); + + private InvalidCastException InvalidCastException(Type.Types.PrimitiveTypeId expectedType, int ordinal) => + new($"Invalid type of YDB value, expected primitive typeId: {expectedType}, actual: {GetColumnType(ordinal)}."); + + private InvalidCastException InvalidCastException(Type.TypeOneofCase expectedType, int ordinal) + => new($"Invalid type of YDB value, expected: {expectedType}, actual: {GetColumnType(ordinal)}."); } diff --git a/src/Ydb.Sdk/src/Ado/YdbParameter.cs b/src/Ydb.Sdk/src/Ado/YdbParameter.cs index 78ffbec9..2d4ac898 100644 --- a/src/Ydb.Sdk/src/Ado/YdbParameter.cs +++ b/src/Ydb.Sdk/src/Ado/YdbParameter.cs @@ -2,14 +2,16 @@ using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; +using Ydb.Sdk.Ado.Internal; using Ydb.Sdk.Ado.YdbType; using Ydb.Sdk.Value; +using static Ydb.Sdk.Ado.Internal.YdbTypedValueExtensions; namespace Ydb.Sdk.Ado; public sealed class YdbParameter : DbParameter { - private static readonly TypedValue NullDefaultDecimal = YdbTypedValueExtensions.NullDecimal(22, 9); + private static readonly TypedValue NullDefaultDecimal = NullDecimal(22, 9); private static readonly Dictionary YdbNullByDbType = new() { @@ -32,7 +34,11 @@ public sealed class YdbParameter : DbParameter { YdbDbType.Double, Type.Types.PrimitiveTypeId.Double.Null() }, { YdbDbType.Uuid, Type.Types.PrimitiveTypeId.Uuid.Null() }, { YdbDbType.Json, Type.Types.PrimitiveTypeId.Json.Null() }, - { YdbDbType.JsonDocument, Type.Types.PrimitiveTypeId.JsonDocument.Null() } + { YdbDbType.JsonDocument, Type.Types.PrimitiveTypeId.JsonDocument.Null() }, + { YdbDbType.Date32, Type.Types.PrimitiveTypeId.Date32.Null() }, + { YdbDbType.Datetime64, Type.Types.PrimitiveTypeId.Datetime64.Null() }, + { YdbDbType.Timestamp64, Type.Types.PrimitiveTypeId.Timestamp64.Null() }, + { YdbDbType.Interval64, Type.Types.PrimitiveTypeId.Interval64.Null() } }; private string _parameterName = string.Empty; @@ -153,9 +159,13 @@ internal TypedValue TypedValue YdbDbType.JsonDocument when value is string stringValue => stringValue.JsonDocument(), YdbDbType.Uuid when value is Guid guidValue => guidValue.Uuid(), YdbDbType.Date => MakeDate(value), + YdbDbType.Date32 => MakeDate32(value), YdbDbType.DateTime when value is DateTime dateTimeValue => dateTimeValue.Datetime(), - YdbDbType.Timestamp => MakeTimestamp(value), + YdbDbType.Datetime64 when value is DateTime dateTimeValue => dateTimeValue.Datetime64(), + YdbDbType.Timestamp when value is DateTime dateTimeValue => dateTimeValue.Timestamp(), + YdbDbType.Timestamp64 when value is DateTime dateTimeValue => dateTimeValue.Timestamp64(), YdbDbType.Interval when value is TimeSpan timeSpanValue => timeSpanValue.Interval(), + YdbDbType.Interval64 when value is TimeSpan timeSpanValue => timeSpanValue.Interval64(), YdbDbType.Unspecified => Cast(value), _ => throw ValueTypeNotSupportedException }; @@ -237,10 +247,10 @@ internal TypedValue TypedValue _ => throw ValueTypeNotSupportedException }; - private TypedValue MakeTimestamp(object value) => value switch + private TypedValue MakeDate32(object value) => value switch { - DateTime dateTimeValue => dateTimeValue.Timestamp(), - DateTimeOffset dateTimeOffsetValue => dateTimeOffsetValue.UtcDateTime.Timestamp(), + DateTime dateTimeValue => dateTimeValue.Date32(), + DateOnly dateOnlyValue => dateOnlyValue.ToDateTime(TimeOnly.MinValue).Date32(), _ => throw ValueTypeNotSupportedException }; @@ -261,7 +271,6 @@ internal TypedValue TypedValue decimal decimalValue => Decimal(decimalValue), Guid guidValue => guidValue.Uuid(), DateTime dateTimeValue => dateTimeValue.Timestamp(), - DateTimeOffset dateTimeOffset => dateTimeOffset.UtcDateTime.Timestamp(), DateOnly dateOnlyValue => dateOnlyValue.ToDateTime(TimeOnly.MinValue).Date(), byte[] bytesValue => bytesValue.Bytes(), TimeSpan timeSpanValue => timeSpanValue.Interval(), @@ -271,9 +280,7 @@ internal TypedValue TypedValue }; private TypedValue Decimal(decimal value) => - Precision == 0 && Scale == 0 - ? value.Decimal(22, 9) - : value.Decimal(Precision, Scale); + Precision == 0 && Scale == 0 ? value.Decimal(22, 9) : value.Decimal(Precision, Scale); private TypedValue NullTypedValue() { @@ -286,7 +293,7 @@ private TypedValue NullTypedValue() { return Precision == 0 && Scale == 0 ? NullDefaultDecimal - : YdbTypedValueExtensions.NullDecimal(Precision, Scale); + : NullDecimal(Precision, Scale); } throw new InvalidOperationException( diff --git a/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs b/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs index b170df00..8a4306b1 100644 --- a/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs +++ b/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs @@ -158,7 +158,15 @@ public enum YdbDbType /// Value range: From -136 years to +136 years. Internal representation: Signed 64-bit integer. /// Can't be used in the primary key. /// - Interval + Interval, + + Date32, + + Datetime64, + + Timestamp64, + + Interval64 } internal static class YdbDbTypeExtensions diff --git a/src/Ydb.Sdk/src/Value/ResultSet.cs b/src/Ydb.Sdk/src/Value/ResultSet.cs index 108ab630..1b71c44f 100644 --- a/src/Ydb.Sdk/src/Value/ResultSet.cs +++ b/src/Ydb.Sdk/src/Value/ResultSet.cs @@ -1,6 +1,5 @@ using System.Collections; using Google.Protobuf.Collections; -using Ydb.Sdk.Ado; namespace Ydb.Sdk.Value; @@ -19,7 +18,6 @@ public class ResultSet internal ResultSet(Ydb.ResultSet resultSetProto) { Columns = resultSetProto.Columns.Select(c => new Column(c.Type, c.Name)).ToArray(); - ColumnNameToOrdinal = Columns .Select((c, idx) => (c.Name, Index: idx)) .ToDictionary(t => t.Name, t => t.Index); @@ -116,7 +114,7 @@ public YdbValue this[int columnIndex] { if (columnIndex < 0 || columnIndex >= ColumnCount) { - ThrowHelper.ThrowIndexOutOfRangeException(ColumnCount); + throw new IndexOutOfRangeException("Ordinal must be between 0 and " + (ColumnCount - 1)); } return new YdbValue(_columns[columnIndex].Type, _row.Items[columnIndex]); diff --git a/src/Ydb.Sdk/src/Value/YdbValueParser.cs b/src/Ydb.Sdk/src/Value/YdbValueParser.cs index 51ed43e9..914f7878 100644 --- a/src/Ydb.Sdk/src/Value/YdbValueParser.cs +++ b/src/Ydb.Sdk/src/Value/YdbValueParser.cs @@ -1,5 +1,4 @@ using Google.Protobuf.WellKnownTypes; -using Ydb.Sdk.Ado; namespace Ydb.Sdk.Value; @@ -258,7 +257,8 @@ private void EnsureType(Type.TypeOneofCase expectedType) { if (_protoType.TypeCase != expectedType) { - ThrowHelper.ThrowInvalidCastException(expectedType.ToString(), TypeId.ToString()); + throw new InvalidCastException( + $"Invalid type of YDB value, expected: {expectedType}, actual: {_protoType}."); } } @@ -266,7 +266,8 @@ private void EnsurePrimitiveTypeId(Type.Types.PrimitiveTypeId primitiveTypeId) { if (_protoType.TypeCase != Type.TypeOneofCase.TypeId || _protoType.TypeId != primitiveTypeId) { - ThrowHelper.ThrowInvalidCastException(primitiveTypeId.ToString(), TypeId.ToString()); + throw new InvalidCastException( + $"Invalid type of YDB value, expected: {primitiveTypeId}, actual: {_protoType}."); } } } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index 8d8a1ce4..05ca0c1c 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Data; -using System.Text; using Xunit; using Ydb.Sdk.Value; @@ -60,6 +59,30 @@ public async Task ExecuteScalarAsync_WhenSetYdbParameterThenPrepare_ReturnThisVa Assert.Equal(data.Expected == null ? DBNull.Value : data.Expected, await dbCommand.ExecuteScalarAsync()); } + [Fact] + public async Task ExecuteReaderAsync_WhenSelectNull_ThrowFieldIsNull() + { + await using var connection = await CreateOpenConnectionAsync(); + var dbCommand = connection.CreateCommand(); + dbCommand.CommandText = "SELECT NULL"; + var reader = await dbCommand.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.Equal("Field is null.", Assert.Throws(() => reader.GetFloat(0)).Message); + } + + [Fact] + public async Task ExecuteReaderAsync_WhenOptionalIsNull_ThrowFieldIsNull() + { + await using var connection = await CreateOpenConnectionAsync(); + var dbCommand = connection.CreateCommand(); + dbCommand.CommandText = "SELECT CAST(NULL AS Optional) AS Field"; + var reader = await dbCommand.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.Equal("Field is null.", Assert.Throws(() => reader.GetFloat(0)).Message); + } + [Theory] [ClassData(typeof(TestDataGenerator))] public async Task ExecuteScalarAsync_WhenDbTypeIsObject_ReturnThisValue(Data data) @@ -94,11 +117,9 @@ public async Task ExecuteScalarAsync_WhenNoDbTypeParameter_ReturnThisValue() (YdbValue.MakeJson(simpleJson), simpleJson), (YdbValue.MakeJsonDocument(simpleJson), simpleJson), (YdbValue.MakeInterval(TimeSpan.FromSeconds(5)), TimeSpan.FromSeconds(5)), - (YdbValue.MakeYson("{type=\"yson\"}"u8.ToArray()), "{type=\"yson\"}"u8.ToArray()), (YdbValue.MakeOptionalJson(simpleJson), simpleJson), (YdbValue.MakeOptionalJsonDocument(simpleJson), simpleJson), - (YdbValue.MakeOptionalInterval(TimeSpan.FromSeconds(5)), TimeSpan.FromSeconds(5)), - (YdbValue.MakeOptionalYson("{type=\"yson\"}"u8.ToArray()), "{type=\"yson\"}"u8.ToArray()) + (YdbValue.MakeOptionalInterval(TimeSpan.FromSeconds(5)), TimeSpan.FromSeconds(5)) }; await using var connection = await CreateOpenConnectionAsync(); @@ -185,138 +206,78 @@ public async Task ExecuteScalar_WhenSelectNoRows_ReturnNull() } - public class Data + public class Data(DbType dbType, T expected, bool isNullable = false) { - public Data(DbType dbType, T expected, Func fetchFun, bool isNullable = false) - { - DbType = dbType; - Expected = expected; - IsNullable = isNullable || expected == null; - FetchFun = fetchFun; - } - - public bool IsNullable { get; } - public DbType DbType { get; } - public T Expected { get; } - public Func FetchFun { get; } + public bool IsNullable { get; } = isNullable || expected == null; + public DbType DbType { get; } = dbType; + public T Expected { get; } = expected; } - public class TestDataGenerator : IEnumerable + private class TestDataGenerator : IEnumerable { private readonly List _data = [ - new object[] { new Data(DbType.Boolean, true, value => value.GetBool()) }, - new object[] { new Data(DbType.Boolean, false, value => value.GetBool()) }, - new object[] { new Data(DbType.Boolean, true, value => value.GetBool(), true) }, - new object[] { new Data(DbType.Boolean, false, value => value.GetBool(), true) }, - new object[] { new Data(DbType.Boolean, null, value => value.GetOptionalBool()) }, - new object[] { new Data(DbType.SByte, -1, value => value.GetInt8()) }, - new object[] { new Data(DbType.SByte, -2, value => value.GetInt8(), true) }, - new object[] { new Data(DbType.SByte, null, value => value.GetOptionalInt8()) }, - new object[] { new Data(DbType.Byte, 200, value => value.GetUint8()) }, - new object[] { new Data(DbType.Byte, 228, value => value.GetUint8(), true) }, - new object[] { new Data(DbType.Byte, null, value => value.GetOptionalUint8()) }, - new object[] { new Data(DbType.Int16, 14000, value => value.GetInt16()) }, - new object[] { new Data(DbType.Int16, 14000, value => value.GetInt16(), true) }, - new object[] { new Data(DbType.Int16, null, value => value.GetOptionalInt16()) }, - new object[] { new Data(DbType.UInt16, 40_000, value => value.GetUint16()) }, - new object[] { new Data(DbType.UInt16, 40_000, value => value.GetUint16(), true) }, - new object[] { new Data(DbType.UInt16, null, value => value.GetOptionalUint16()) }, - new object[] { new Data(DbType.Int32, -40_000, value => value.GetInt32()) }, - new object[] { new Data(DbType.Int32, -40_000, value => value.GetInt32(), true) }, - new object[] { new Data(DbType.Int32, null, value => value.GetOptionalInt32()) }, - new object[] { new Data(DbType.UInt32, 4_000_000_000, value => value.GetUint32()) }, - new object[] { new Data(DbType.UInt32, 4_000_000_000, value => value.GetUint32(), true) }, - new object[] { new Data(DbType.UInt32, null, value => value.GetOptionalUint32()) }, - new object[] { new Data(DbType.Int64, -4_000_000_000, value => value.GetInt64()) }, - new object[] { new Data(DbType.Int64, -4_000_000_000, value => value.GetInt64(), true) }, - new object[] { new Data(DbType.Int64, null, value => value.GetOptionalInt64()) }, - new object[] { new Data(DbType.UInt64, 10_000_000_000ul, value => value.GetUint64()) }, - new object[] - { new Data(DbType.UInt64, 10_000_000_000ul, value => value.GetUint64(), true) }, - - new object[] { new Data(DbType.UInt64, null, value => value.GetOptionalUint64()) }, - new object[] { new Data(DbType.Single, -1.7f, value => value.GetFloat()) }, - new object[] { new Data(DbType.Single, -1.7f, value => value.GetFloat(), true) }, - new object[] { new Data(DbType.Single, null, value => value.GetOptionalFloat()) }, - new object[] { new Data(DbType.Double, 123.45, value => value.GetDouble()) }, - new object[] { new Data(DbType.Double, 123.45, value => value.GetDouble(), true) }, - new object[] { new Data(DbType.Double, null, value => value.GetOptionalDouble()) }, - new object[] - { - new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), - value => value.GetUuid()) - }, - - new object[] - { - new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), - value => value.GetUuid(), true) - }, - - new object[] { new Data(DbType.Guid, null, value => value.GetOptionalUuid()) }, - new object[] { new Data(DbType.Date, new DateTime(2021, 08, 21), value => value.GetDate()) }, - new object[] - { - new Data(DbType.Date, new DateTime(2021, 08, 21), value => value.GetDate(), true) - }, - - new object[] { new Data(DbType.Date, null, value => value.GetOptionalDate()) }, - new object[] - { - new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), - value => value.GetDatetime()) - }, - - new object[] - { - new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), - value => value.GetDatetime(), true) - }, - - new object[] { new Data(DbType.DateTime, null, value => value.GetOptionalDatetime()) }, - new object[] - { - new Data(DbType.DateTime2, DateTime.Parse("2029-08-03T06:59:44.8578730Z"), - value => value.GetTimestamp()) - }, - - new object[] - { - new Data(DbType.DateTime2, DateTime.Parse("2029-08-09T17:15:29.6935850Z"), - value => value.GetTimestamp()) - }, - + new object[] { new Data(DbType.Boolean, true) }, + new object[] { new Data(DbType.Boolean, false) }, + new object[] { new Data(DbType.Boolean, true, true) }, + new object[] { new Data(DbType.Boolean, false, true) }, + new object[] { new Data(DbType.Boolean, null) }, + new object[] { new Data(DbType.SByte, -1) }, + new object[] { new Data(DbType.SByte, -2, true) }, + new object[] { new Data(DbType.SByte, null) }, + new object[] { new Data(DbType.Byte, 200) }, + new object[] { new Data(DbType.Byte, 228, true) }, + new object[] { new Data(DbType.Byte, null) }, + new object[] { new Data(DbType.Int16, 14000) }, + new object[] { new Data(DbType.Int16, 14000, true) }, + new object[] { new Data(DbType.Int16, null) }, + new object[] { new Data(DbType.UInt16, 40_000) }, + new object[] { new Data(DbType.UInt16, 40_000, true) }, + new object[] { new Data(DbType.UInt16, null) }, + new object[] { new Data(DbType.Int32, -40_000) }, + new object[] { new Data(DbType.Int32, -40_000, true) }, + new object[] { new Data(DbType.Int32, null) }, + new object[] { new Data(DbType.UInt32, 4_000_000_000) }, + new object[] { new Data(DbType.UInt32, 4_000_000_000, true) }, + new object[] { new Data(DbType.UInt32, null) }, + new object[] { new Data(DbType.Int64, -4_000_000_000) }, + new object[] { new Data(DbType.Int64, -4_000_000_000, true) }, + new object[] { new Data(DbType.Int64, null) }, + new object[] { new Data(DbType.UInt64, 10_000_000_000ul) }, + new object[] { new Data(DbType.UInt64, 10_000_000_000ul, true) }, + new object[] { new Data(DbType.UInt64, null) }, + new object[] { new Data(DbType.Single, -1.7f) }, + new object[] { new Data(DbType.Single, -1.7f, true) }, + new object[] { new Data(DbType.Single, null) }, + new object[] { new Data(DbType.Double, 123.45) }, + new object[] { new Data(DbType.Double, 123.45, true) }, + new object[] { new Data(DbType.Double, null) }, + new object[] { new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B")) }, + new object[] { new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), true) }, + new object[] { new Data(DbType.Guid, null) }, + new object[] { new Data(DbType.Date, new DateTime(2021, 08, 21)) }, + new object[] { new Data(DbType.Date, new DateTime(2021, 08, 21), true) }, + new object[] { new Data(DbType.Date, null) }, + new object[] { new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47)) }, + new object[] { new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), true) }, + new object[] { new Data(DbType.DateTime, null) }, + new object[] { new Data(DbType.DateTime2, DateTime.Parse("2029-08-03T06:59:44.8578730Z")) }, + new object[] { new Data(DbType.DateTime2, DateTime.Parse("2029-08-09T17:15:29.6935850Z")) }, new object[] { new Data(DbType.DateTime2, new DateTime(2021, 08, 21, 23, 30, 47, 581, DateTimeKind.Local), - value => value.GetTimestamp(), true) - }, - - new object[] { new Data(DbType.DateTime2, null, value => value.GetOptionalTimestamp()) }, - new object[] - { - new Data(DbType.Binary, Encoding.ASCII.GetBytes("test str"), - value => value.GetString()) - }, - - new object[] - { - new Data(DbType.Binary, Encoding.ASCII.GetBytes("test str"), - value => value.GetString(), true) + true) }, - - new object[] { new Data(DbType.Binary, null, value => value.GetOptionalString()) }, - new object[] { new Data(DbType.String, "unicode str", value => value.GetUtf8()) }, - new object[] { new Data(DbType.String, "unicode str", value => value.GetUtf8(), true) }, - new object[] { new Data(DbType.String, null, value => value.GetOptionalUtf8()) }, - new object[] { new Data(DbType.Decimal, -18446744073.709551616m, value => value.GetDecimal()) }, - new object[] - { - new Data(DbType.Decimal, -18446744073.709551616m, value => value.GetDecimal(), true) - }, - - new object[] { new Data(DbType.Decimal, null, value => value.GetOptionalDecimal()) } + new object[] { new Data(DbType.DateTime2, null) }, + new object[] { new Data(DbType.Binary, "test str"u8.ToArray()) }, + new object[] { new Data(DbType.Binary, "test str"u8.ToArray(), true) }, + new object[] { new Data(DbType.Binary, null) }, + new object[] { new Data(DbType.String, "unicode str") }, + new object[] { new Data(DbType.String, "unicode str", true) }, + new object[] { new Data(DbType.String, null) }, + new object[] { new Data(DbType.Decimal, -18446744073.709551616m) }, + new object[] { new Data(DbType.Decimal, -18446744073.709551616m, true) }, + new object[] { new Data(DbType.Decimal, null) } ]; public IEnumerator GetEnumerator() => _data.GetEnumerator(); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs index 75be4e27..2ae23c4d 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs @@ -98,45 +98,6 @@ public void YdbParameter_WhenSetDbType_ReturnValueIsConverted() Assert.Equal(1.1f, new YdbParameter("$parameter", DbType.Double) { Value = 1.1f }.TypedValue.Value.DoubleValue); } - [Fact] - public async Task YdbParameter_WhenDateTimeOffset_ReturnTimestamp() - { - var dateTimeOffset = DateTimeOffset.Parse("2029-08-03T06:59:44.8578730Z"); - await using var ydbConnection = await CreateOpenConnectionAsync(); - var tableTableName = $"dateTimeOffset_{Random.Shared.Next()}"; - await new YdbCommand(ydbConnection) - { - CommandText = $"CREATE TABLE {tableTableName} (TimestampField Timestamp, PRIMARY KEY (TimestampField))" - } - .ExecuteNonQueryAsync(); - await new YdbCommand(ydbConnection) - { - CommandText = - $"INSERT INTO {tableTableName}(TimestampField) " + - $"VALUES (@parameter1), (@parameter2), (@parameter3), (@parameter4)", - Parameters = - { - new YdbParameter("$parameter1", dateTimeOffset), - new YdbParameter("$parameter2", DbType.DateTimeOffset, dateTimeOffset.AddHours(1)), - new YdbParameter("$parameter3", DbType.DateTimeOffset, dateTimeOffset.AddHours(2)), - new YdbParameter("$parameter4", YdbDbType.Timestamp, dateTimeOffset.AddHours(3)) - } - } - .ExecuteNonQueryAsync(); - - var ydbDataReader = await new YdbCommand(ydbConnection) { CommandText = $"SELECT * FROM {tableTableName}" } - .ExecuteReaderAsync(); - - var hourCount = 0; - while (ydbDataReader.NextResult()) - { - Assert.True(ydbDataReader.Read()); - Assert.Equal(dateTimeOffset.AddHours(hourCount++).UtcDateTime, ydbDataReader.GetValue(0)); - } - - await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableTableName};" }.ExecuteNonQueryAsync(); - } - [Theory] [InlineData("123e4567-e89b-12d3-a456-426614174000")] [InlineData("2d9e498b-b746-9cfb-084d-de4e1cb4736e")] @@ -261,7 +222,7 @@ public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue() public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNotNull_ReturnsValue() { await using var ydbConnection = await CreateOpenConnectionAsync(); - var tableName = $"NouNull_YdbDbType_{Random.Shared.Next()}"; + var tableName = $"NonNull_YdbDbType_{Random.Shared.Next()}"; await new YdbCommand(ydbConnection) { CommandText = $""" @@ -287,6 +248,10 @@ CustomDecimalColumn Decimal(35, 5) NOT NULL, IntervalColumn Interval NOT NULL, JsonColumn Json NOT NULL, JsonDocumentColumn JsonDocument NOT NULL, + Date32Column Date32 NOT NULL, + Datetime64Column DateTime64 NOT NULL, + Timestamp64Column Timestamp64 NOT NULL, + Interval64Column Interval64 NOT NULL, PRIMARY KEY (Int32Column) ); """ @@ -299,12 +264,14 @@ PRIMARY KEY (Int32Column) Int32Column, BoolColumn, Int64Column, Int16Column, Int8Column, FloatColumn, DoubleColumn, DefaultDecimalColumn, CustomDecimalColumn, Uint8Column, Uint16Column, Uint32Column, Uint64Column, TextColumn, BytesColumn, DateColumn, DatetimeColumn, TimestampColumn, - IntervalColumn, JsonColumn, JsonDocumentColumn + IntervalColumn, JsonColumn, JsonDocumentColumn, Date32Column, Datetime64Column, + Timestamp64Column, Interval64Column ) VALUES ( @Int32Column, @BoolColumn, @Int64Column, @Int16Column, @Int8Column, @FloatColumn, @DoubleColumn, @DefaultDecimalColumn, @CustomDecimalColumn, @Uint8Column, @Uint16Column, @Uint32Column, @Uint64Column, @TextColumn, @BytesColumn, @DateColumn, @DatetimeColumn, - @TimestampColumn, @IntervalColumn, @JsonColumn, @JsonDocumentColumn + @TimestampColumn, @IntervalColumn, @JsonColumn, @JsonDocumentColumn, @Date32Column, + @Datetime64Column, @Timestamp64Column, @Interval64Column ); """, Parameters = @@ -329,7 +296,12 @@ PRIMARY KEY (Int32Column) new YdbParameter("TimestampColumn", YdbDbType.Timestamp, DateTime.UnixEpoch), new YdbParameter("IntervalColumn", YdbDbType.Interval, TimeSpan.Zero), new YdbParameter("JsonColumn", YdbDbType.Json, "{}"), - new YdbParameter("JsonDocumentColumn", YdbDbType.JsonDocument, "{}") + new YdbParameter("JsonDocumentColumn", YdbDbType.JsonDocument, "{}"), + new YdbParameter("Date32Column", YdbDbType.Date32, DateTime.MinValue), + new YdbParameter("Datetime64Column", YdbDbType.Datetime64, DateTime.MinValue), + new YdbParameter("Timestamp64Column", YdbDbType.Timestamp64, DateTime.MinValue), + new YdbParameter("Interval64Column", YdbDbType.Interval64, + TimeSpan.FromMilliseconds(TimeSpan.MinValue.Milliseconds)) } }.ExecuteNonQueryAsync(); @@ -340,7 +312,8 @@ PRIMARY KEY (Int32Column) Int32Column, BoolColumn, Int64Column, Int16Column, Int8Column, FloatColumn, DoubleColumn, DefaultDecimalColumn, CustomDecimalColumn, Uint8Column, Uint16Column, Uint32Column, Uint64Column, TextColumn, BytesColumn, DateColumn, DatetimeColumn, TimestampColumn, - IntervalColumn, JsonColumn, JsonDocumentColumn + IntervalColumn, JsonColumn, JsonDocumentColumn, Date32Column, Datetime64Column, + Timestamp64Column, Interval64Column FROM {tableName}; """ }.ExecuteReaderAsync(); @@ -367,6 +340,10 @@ PRIMARY KEY (Int32Column) Assert.Equal(TimeSpan.Zero, ydbDataReader.GetInterval(18)); Assert.Equal("{}", ydbDataReader.GetJson(19)); Assert.Equal("{}", ydbDataReader.GetJsonDocument(20)); + Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(21)); + Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(22)); + Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(23)); + Assert.Equal(TimeSpan.FromMilliseconds(TimeSpan.MinValue.Milliseconds), ydbDataReader.GetInterval(24)); Assert.False(ydbDataReader.Read()); await ydbDataReader.CloseAsync();