diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 61cb173e..57881d6b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -96,3 +96,4 @@ jobs: EmptyStatement, ObjectCreationAsStatement, ParameterOnlyUsedForPreconditionCheck.Local + BitwiseOperatorOnEnumWithoutFlags diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index b2e167df..77001a65 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,13 @@ +- **Breaking Change**: Renamed `YdbDbType` enum members to match the [ydb.tech](https://ydb.tech/docs/en/yql/reference/types/primitive) naming: + - `UInt8` -> `Uint8`. + - `UInt16` -> `Uint16`. + - `UInt32` -> `Uint32`. + - `Uint64` -> `Uint64`. + - `DateTime` -> `Datetime`. +- **Breaking Change**: Removed unused methods `GetJson`, `GetJsonDocument`, and `GetYson` from YdbDataReader. +- Feat ADO.NET: Add support for reading `Yson` from `YdbDataReader.GetBytes`. +- Feat ADO.NET: Add support for reading `Json` and `JsonDocument` from `YdbDataReader.GetString`. +- Feat ADO.NET: Added type checking in the parameter list in sql statement `IN (@id1, @id2)`. - **Breaking Change**: `Ydb.Sdk.Services.Topic` moved to `Ydb.Sdk.Topic`. ## v0.24.0 diff --git a/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs b/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs index 6af6fd05..ef22e01b 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs @@ -31,7 +31,7 @@ public ListPrimitiveParam(IReadOnlyList paramNames, int globalNumber) public bool IsNative => false; public TypedValue YdbValueFetch(Dictionary ydbParameters) => - _paramNames.Select(ydbParameters.Get).ToArray().List(); + _paramNames.Select(ydbParameters.Get).List(); } internal static class YdbParametersExtension diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbPrimitiveTypeInfo.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbPrimitiveTypeInfo.cs new file mode 100644 index 00000000..f0d90cf4 --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbPrimitiveTypeInfo.cs @@ -0,0 +1,158 @@ +namespace Ydb.Sdk.Ado.Internal; + +using static YdbValueExtensions; + +internal class YdbPrimitiveTypeInfo +{ + internal static readonly YdbPrimitiveTypeInfo + Bool = new(Type.Types.PrimitiveTypeId.Bool, TryPack(PackBool)), + Int8 = new(Type.Types.PrimitiveTypeId.Int8, TryPack(PackInt8)), + Int16 = new(Type.Types.PrimitiveTypeId.Int16, TryPackInt16), + Int32 = new(Type.Types.PrimitiveTypeId.Int32, TryPackInt32), + Int64 = new(Type.Types.PrimitiveTypeId.Int64, TryPackInt64), + Uint8 = new(Type.Types.PrimitiveTypeId.Uint8, TryPack(PackUint8)), + Uint16 = new(Type.Types.PrimitiveTypeId.Uint16, TryPackUint16), + Uint32 = new(Type.Types.PrimitiveTypeId.Uint32, TryPackUint32), + Uint64 = new(Type.Types.PrimitiveTypeId.Uint64, TryPackUint64), + Float = new(Type.Types.PrimitiveTypeId.Float, TryPack(PackFloat)), + Double = new(Type.Types.PrimitiveTypeId.Double, TryPackDouble), + Bytes = new(Type.Types.PrimitiveTypeId.String, TryPackBytes), + Text = new(Type.Types.PrimitiveTypeId.Utf8, TryPack(PackText)), + Json = new(Type.Types.PrimitiveTypeId.Json, TryPack(PackText)), + JsonDocument = new(Type.Types.PrimitiveTypeId.JsonDocument, TryPack(PackText)), + Yson = new(Type.Types.PrimitiveTypeId.Yson, TryPackBytes), + Uuid = new(Type.Types.PrimitiveTypeId.Uuid, TryPack(PackUuid)), + Date = new(Type.Types.PrimitiveTypeId.Date, TryPackDate), + Date32 = new(Type.Types.PrimitiveTypeId.Date32, TryPackDate32), + Datetime = new(Type.Types.PrimitiveTypeId.Datetime, TryPack(PackDatetime)), + Datetime64 = new(Type.Types.PrimitiveTypeId.Datetime64, TryPack(PackDatetime64)), + Timestamp = new(Type.Types.PrimitiveTypeId.Timestamp, TryPack(PackTimestamp)), + Timestamp64 = new(Type.Types.PrimitiveTypeId.Timestamp64, TryPack(PackTimestamp64)), + Interval = new(Type.Types.PrimitiveTypeId.Interval, TryPack(PackInterval)), + Interval64 = new(Type.Types.PrimitiveTypeId.Interval64, TryPack(PackInterval64)); + + private YdbPrimitiveTypeInfo(Type.Types.PrimitiveTypeId primitiveTypeId, Func pack) + { + YdbType = new Type { TypeId = primitiveTypeId }; + NullValue = new TypedValue + { Type = new Type { OptionalType = new OptionalType { Item = YdbType } }, Value = YdbValueNull }; + Pack = pack; + } + + internal Type YdbType { get; } + internal TypedValue NullValue { get; } + internal Func Pack { get; } + + internal static YdbPrimitiveTypeInfo? TryResolve(System.Type type) + { + if (type == typeof(bool)) return Bool; + + if (type == typeof(sbyte)) return Int8; + if (type == typeof(short)) return Int16; + if (type == typeof(int)) return Int32; + if (type == typeof(long)) return Int64; + + if (type == typeof(byte)) return Uint8; + if (type == typeof(ushort)) return Uint16; + if (type == typeof(uint)) return Uint32; + if (type == typeof(ulong)) return Uint64; + + if (type == typeof(float)) return Float; + if (type == typeof(double)) return Double; + + if (type == typeof(byte[]) || type == typeof(MemoryStream)) return Bytes; + if (type == typeof(string)) return Text; + if (type == typeof(Guid)) return Uuid; + + if (type == typeof(DateOnly)) return Date; + if (type == typeof(DateTime)) return Timestamp; + if (type == typeof(TimeSpan)) return Interval; + + return null; + } + + private static Func TryPack(Func pack) => + value => value is T valueT ? pack(valueT) : null; + + private static Ydb.Value? TryPackInt16(object value) => value switch + { + short shortValue => PackInt16(shortValue), + sbyte sbyteValue => PackInt16(sbyteValue), + byte byteValue => PackInt16(byteValue), + _ => null + }; + + private static Ydb.Value? TryPackInt32(object value) => value switch + { + int intValue => PackInt32(intValue), + sbyte sbyteValue => PackInt32(sbyteValue), + byte byteValue => PackInt32(byteValue), + short shortValue => PackInt32(shortValue), + ushort ushortValue => PackInt32(ushortValue), + _ => null + }; + + private static Ydb.Value? TryPackInt64(object value) => value switch + { + long longValue => PackInt64(longValue), + sbyte sbyteValue => PackInt64(sbyteValue), + byte byteValue => PackInt64(byteValue), + short shortValue => PackInt64(shortValue), + ushort ushortValue => PackInt64(ushortValue), + int intValue => PackInt64(intValue), + uint uintValue => PackInt64(uintValue), + _ => null + }; + + private static Ydb.Value? TryPackUint16(object value) => value switch + { + ushort shortValue => PackUint16(shortValue), + byte byteValue => PackUint16(byteValue), + _ => null + }; + + private static Ydb.Value? TryPackUint32(object value) => value switch + { + uint intValue => PackUint32(intValue), + byte byteValue => PackUint32(byteValue), + ushort ushortValue => PackUint32(ushortValue), + _ => null + }; + + private static Ydb.Value? TryPackUint64(object value) => value switch + { + ulong longValue => PackUint64(longValue), + byte byteValue => PackUint64(byteValue), + ushort ushortValue => PackUint64(ushortValue), + uint uintValue => PackUint64(uintValue), + _ => null + }; + + private static Ydb.Value? TryPackDouble(object value) => value switch + { + double doubleValue => PackDouble(doubleValue), + float floatValue => PackDouble(floatValue), + _ => null + }; + + private static Ydb.Value? TryPackBytes(object value) => value switch + { + byte[] bytesValue => PackBytes(bytesValue), + MemoryStream memoryStream => PackBytes(memoryStream.ToArray()), + _ => null + }; + + private static Ydb.Value? TryPackDate(object value) => value switch + { + DateTime dateTimeValue => PackDate(dateTimeValue), + DateOnly dateOnlyValue => PackDate(dateOnlyValue.ToDateTime(TimeOnly.MinValue)), + _ => null + }; + + private static Ydb.Value? TryPackDate32(object value) => value switch + { + DateTime dateTimeValue => PackDate32(dateTimeValue), + DateOnly dateOnlyValue => PackDate32(dateOnlyValue.ToDateTime(TimeOnly.MinValue)), + _ => null + }; +} diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbTypeExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbTypeExtensions.cs new file mode 100644 index 00000000..964f3331 --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbTypeExtensions.cs @@ -0,0 +1,17 @@ +namespace Ydb.Sdk.Ado.Internal; + +internal static class YdbTypeExtensions +{ + internal const byte DefaultDecimalPrecision = 22; + internal const byte DefaultDecimalScale = 9; + + private static readonly Type DefaultDecimalType = DecimalType(DefaultDecimalPrecision, DefaultDecimalScale); + + internal static Type DecimalType(byte precision, byte scale) => precision == 0 && scale == 0 + ? DefaultDecimalType + : new Type { DecimalType = new DecimalType { Precision = precision, Scale = scale } }; + + internal static Type ListType(this Type type) => new() { ListType = new ListType { Item = type } }; + + internal static Type OptionalType(this Type type) => new() { OptionalType = new OptionalType { Item = type } }; +} diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs index 29bd7271..d0d5d3f5 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs @@ -1,26 +1,11 @@ -using Google.Protobuf; -using Google.Protobuf.WellKnownTypes; +using static Ydb.Sdk.Ado.Internal.YdbTypeExtensions; +using static Ydb.Sdk.Ado.Internal.YdbValueExtensions; namespace Ydb.Sdk.Ado.Internal; internal static class YdbTypedValueExtensions { - private const byte MaxPrecisionDecimal = 29; - private static readonly decimal[] Pow10 = CreatePow10(); - - private static decimal[] CreatePow10() - { - var a = new decimal[29]; - a[0] = 1m; - for (var i = 1; i < a.Length; i++) a[i] = a[i - 1] * 10m; // 1..1e28 - return a; - } - - internal static TypedValue Null(this Type.Types.PrimitiveTypeId primitiveTypeId) => new() - { - Type = new Type { OptionalType = new OptionalType { Item = new Type { TypeId = primitiveTypeId } } }, - Value = new Ydb.Value { NullFlagValue = NullValue.NullValue } - }; + private static readonly TypedValue DecimalDefaultNull = DecimalNull(DefaultDecimalPrecision, DefaultDecimalScale); internal static string ToYql(this TypedValue typedValue) => ToYql(typedValue.Type); @@ -35,166 +20,36 @@ private static string ToYql(Type type) => _ => "Unknown" }; - internal static TypedValue NullDecimal(byte precision, byte scale) => new() - { - Type = new Type - { - OptionalType = new OptionalType - { - Item = new Type { DecimalType = new DecimalType { Precision = precision, Scale = scale } } - } - }, - Value = new Ydb.Value { NullFlagValue = NullValue.NullValue } - }; - - internal static TypedValue Text(this string value) => MakeText(Type.Types.PrimitiveTypeId.Utf8, value); - - internal static TypedValue Bool(this bool value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Bool, new Ydb.Value { BoolValue = value }); - - internal static TypedValue Int8(this sbyte value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Int8, new Ydb.Value { Int32Value = value }); - - internal static TypedValue Int16(this short value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Int16, new Ydb.Value { Int32Value = value }); - - internal static TypedValue Int32(this int value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Int32, new Ydb.Value { Int32Value = value }); - - internal static TypedValue Int64(this long value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Int64, new Ydb.Value { Int64Value = value }); - - internal static TypedValue Uint8(this byte value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Uint8, new Ydb.Value { Uint32Value = value }); - - internal static TypedValue Uint16(this ushort value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Uint16, new Ydb.Value { Uint32Value = value }); - - internal static TypedValue Uint32(this uint value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Uint32, new Ydb.Value { Uint32Value = value }); - - internal static TypedValue Uint64(this ulong value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Uint64, new Ydb.Value { Uint64Value = value }); - - internal static TypedValue Float(this float value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Float, new Ydb.Value { FloatValue = value }); - - internal static TypedValue Double(this double value) => - MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Double, new Ydb.Value { DoubleValue = value }); - - internal static TypedValue Decimal(this decimal value, byte precision, byte scale) + internal static TypedValue List(this IEnumerable values) { - if (scale > precision) - throw new ArgumentOutOfRangeException(nameof(scale), "Scale cannot exceed precision"); - - var origScale = (decimal.GetBits(value)[3] >> 16) & 0xFF; - - if (origScale > scale || (precision < MaxPrecisionDecimal && Pow10[precision - scale] <= Math.Abs(value))) - { - throw new OverflowException($"Value {value} does not fit Decimal({precision}, {scale})"); - } - - value *= 1.0000000000000000000000000000m; // 28 zeros, max supported by c# decimal - value = Math.Round(value, scale); - var bits = decimal.GetBits(value); - var low = ((ulong)(uint)bits[1] << 32) | (uint)bits[0]; - var high = (ulong)(uint)bits[2]; - var isNegative = bits[3] < 0; + TypedValue? first = null; + var value = new Ydb.Value(); - unchecked + foreach (var v in values) { - if (isNegative) + first ??= v; + if (!first.Type.Equals(v.Type)) { - low = ~low + 1UL; - high = ~high + (low == 0 ? 1UL : 0UL); + throw new ArgumentException("All elements in the list must have the same type. " + + $"Expected: {first.Type}, actual: {v.Type}"); } - } - - return new TypedValue - { - Type = new Type { DecimalType = new DecimalType { Precision = precision, Scale = scale } }, - Value = new Ydb.Value { Low128 = low, High128 = high } - }; - } - - internal static TypedValue Bytes(this byte[] value) => MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.String, - new Ydb.Value { BytesValue = ByteString.CopyFrom(value) }); - - internal static TypedValue Yson(this byte[] value) => MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Yson, - new Ydb.Value { BytesValue = ByteString.CopyFrom(value) }); - - internal static TypedValue Json(this string value) => MakeText(Type.Types.PrimitiveTypeId.Json, value); - - internal static TypedValue JsonDocument(this string value) => - MakeText(Type.Types.PrimitiveTypeId.JsonDocument, value); - - internal static TypedValue Uuid(this Guid value) - { - var bytes = value.ToByteArray(); - var low = BitConverter.ToUInt64(bytes, 0); - var high = BitConverter.ToUInt64(bytes, 8); - - return MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Uuid, new Ydb.Value { Low128 = low, High128 = high }); - } - - 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 - { 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) / TimeSpanUtils.TicksPerMicrosecond + value.Items.Add(v.Value); } - ); - - 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 / 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) - { - if (values.Count == 0) - { - throw new ArgumentOutOfRangeException(nameof(values)); - } + if (first is null) throw new ArgumentException("The list must contain at least one element"); - var value = new Ydb.Value(); - value.Items.Add(values.Select(v => v.Value)); + return new TypedValue { Type = first.Type.ListType(), Value = value }; + } - return new TypedValue + internal static TypedValue DecimalNull(byte precision, byte scale) => precision == 0 && scale == 0 + ? DecimalDefaultNull + : new TypedValue { - Type = new Type { ListType = new ListType { Item = values[0].Type } }, - Value = value + Type = new Type { OptionalType = new OptionalType { Item = DecimalType(precision, scale) } }, + Value = YdbValueNull }; - } - - private static TypedValue MakeText(Type.Types.PrimitiveTypeId primitiveTypeId, string textValue) => - MakePrimitiveTypedValue(primitiveTypeId, new Ydb.Value { TextValue = textValue }); - private static TypedValue MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId primitiveTypeId, Ydb.Value value) => - new() { Type = new Type { TypeId = primitiveTypeId }, Value = value }; + internal static TypedValue ListNull(Type type) => + new() { Type = type.ListType().OptionalType(), Value = YdbValueNull }; } diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs index 4694ea00..9ad5c1b5 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs @@ -1,68 +1,133 @@ +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; + namespace Ydb.Sdk.Ado.Internal; internal static class YdbValueExtensions { + private const byte MaxPrecisionDecimal = 29; + private static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified); + private static readonly decimal[] Pow10 = CreatePow10(); + + private static decimal[] CreatePow10() + { + var a = new decimal[29]; + a[0] = 1m; + for (var i = 1; i < a.Length; i++) a[i] = a[i - 1] * 10m; // 1..1e28 + return a; + } + + internal static readonly Ydb.Value YdbValueNull = new() { NullFlagValue = NullValue.NullValue }; 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 Ydb.Value PackBool(bool value) => new() { BoolValue = value }; + internal static bool UnpackBool(this Ydb.Value value) => value.BoolValue; - internal static sbyte GetInt8(this Ydb.Value value) => (sbyte)value.Int32Value; + internal static Ydb.Value PackInt8(sbyte value) => new() { Int32Value = value }; + internal static sbyte UnpackInt8(this Ydb.Value value) => (sbyte)value.Int32Value; - internal static byte GetUint8(this Ydb.Value value) => (byte)value.Uint32Value; + internal static Ydb.Value PackInt16(short value) => new() { Int32Value = value }; + internal static short UnpackInt16(this Ydb.Value value) => (short)value.Int32Value; - internal static short GetInt16(this Ydb.Value value) => (short)value.Int32Value; + internal static Ydb.Value PackInt32(int value) => new() { Int32Value = value }; + internal static int UnpackInt32(this Ydb.Value value) => value.Int32Value; - internal static ushort GetUint16(this Ydb.Value value) => (ushort)value.Uint32Value; + internal static Ydb.Value PackInt64(long value) => new() { Int64Value = value }; + internal static long UnpackInt64(this Ydb.Value value) => value.Int64Value; - internal static int GetInt32(this Ydb.Value value) => value.Int32Value; + internal static Ydb.Value PackUint8(byte value) => new() { Uint32Value = value }; + internal static byte UnpackUint8(this Ydb.Value value) => (byte)value.Uint32Value; - internal static uint GetUint32(this Ydb.Value value) => value.Uint32Value; + internal static Ydb.Value PackUint16(ushort value) => new() { Uint32Value = value }; + internal static ushort UnpackUint16(this Ydb.Value value) => (ushort)value.Uint32Value; - internal static long GetInt64(this Ydb.Value value) => value.Int64Value; + internal static Ydb.Value PackUint32(uint value) => new() { Uint32Value = value }; + internal static uint UnpackUint32(this Ydb.Value value) => value.Uint32Value; - internal static ulong GetUint64(this Ydb.Value value) => value.Uint64Value; + internal static Ydb.Value PackUint64(ulong value) => new() { Uint64Value = value }; + internal static ulong UnpackUint64(this Ydb.Value value) => value.Uint64Value; - internal static float GetFloat(this Ydb.Value value) => value.FloatValue; + internal static Ydb.Value PackFloat(float value) => new() { FloatValue = value }; + internal static float UnpackFloat(this Ydb.Value value) => value.FloatValue; - internal static double GetDouble(this Ydb.Value value) => value.DoubleValue; + internal static Ydb.Value PackDouble(double value) => new() { DoubleValue = value }; + internal static double UnpackDouble(this Ydb.Value value) => value.DoubleValue; - internal static DateTime GetDate(this Ydb.Value value) => - UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerDay); + internal static Ydb.Value PackDecimal(this decimal value, byte precision, byte scale) + { + if (precision == 0 && scale == 0) + { + precision = YdbTypeExtensions.DefaultDecimalPrecision; + scale = YdbTypeExtensions.DefaultDecimalScale; + } - internal static DateTime GetDate32(this Ydb.Value value) => - UnixEpoch.AddTicks(value.Int32Value * TimeSpan.TicksPerDay); + if (scale > precision) + throw new ArgumentOutOfRangeException(nameof(scale), "Scale cannot exceed precision"); - internal static DateTime GetDatetime(this Ydb.Value value) => - UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerSecond); + var origScale = (decimal.GetBits(value)[3] >> 16) & 0xFF; - internal static DateTime GetDatetime64(this Ydb.Value value) => - UnixEpoch.AddTicks(value.Int64Value * TimeSpan.TicksPerSecond); + if (origScale > scale || (precision < MaxPrecisionDecimal && Pow10[precision - scale] <= Math.Abs(value))) + { + throw new OverflowException($"Value {value} does not fit Decimal({precision}, {scale})"); + } - internal static DateTime GetTimestamp(this Ydb.Value value) => - UnixEpoch.AddTicks((long)(value.Uint64Value * TimeSpanUtils.TicksPerMicrosecond)); + value *= 1.0000000000000000000000000000m; // 28 zeros, max supported by c# decimal + value = Math.Round(value, scale); + var bits = decimal.GetBits(value); + var low = ((ulong)(uint)bits[1] << 32) | (uint)bits[0]; + var high = (ulong)(uint)bits[2]; + var isNegative = bits[3] < 0; - internal static DateTime GetTimestamp64(this Ydb.Value value) => - UnixEpoch.AddTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + unchecked + { + if (isNegative) + { + low = ~low + 1UL; + high = ~high + (low == 0 ? 1UL : 0UL); + } + } - internal static TimeSpan GetInterval(this Ydb.Value value) => - TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + return new Ydb.Value { Low128 = low, High128 = high }; + } - internal static TimeSpan GetInterval64(this Ydb.Value value) => - TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + internal static decimal UnpackDecimal(this Ydb.Value value, uint scale) + { + var low = value.Low128; + var high = value.High128; + var isNegative = (high & 0x8000_0000_0000_0000UL) != 0; + unchecked + { + if (isNegative) + { + low = ~low + 1UL; + high = ~high + (low == 0 ? 1UL : 0UL); + } + } + + if (high >> 32 != 0) + throw new OverflowException("Value does not fit into decimal"); - internal static byte[] GetBytes(this Ydb.Value value) => value.BytesValue.ToByteArray(); + return new decimal((int)low, (int)(low >> 32), (int)high, isNegative, (byte)scale); + } - internal static byte[] GetYson(this Ydb.Value value) => value.BytesValue.ToByteArray(); + internal static Ydb.Value PackBytes(byte[] value) => new() { BytesValue = ByteString.CopyFrom(value) }; + internal static byte[] UnpackBytes(this Ydb.Value value) => value.BytesValue.ToByteArray(); - internal static string GetText(this Ydb.Value value) => value.TextValue; + internal static Ydb.Value PackText(string value) => new() { TextValue = value }; + internal static string UnpackText(this Ydb.Value value) => value.TextValue; - internal static string GetJson(this Ydb.Value value) => value.TextValue; + internal static Ydb.Value PackUuid(Guid value) + { + var bytes = value.ToByteArray(); + var low = BitConverter.ToUInt64(bytes, 0); + var high = BitConverter.ToUInt64(bytes, 8); - internal static string GetJsonDocument(this Ydb.Value value) => value.TextValue; + return new Ydb.Value { Low128 = low, High128 = high }; + } - internal static Guid GetUuid(this Ydb.Value value) + internal static Guid UnpackUuid(this Ydb.Value value) { var high = value.High128; var low = value.Low128; @@ -77,23 +142,51 @@ internal static Guid GetUuid(this Ydb.Value value) return new Guid(guidBytes); } - internal static decimal GetDecimal(this Ydb.Value value, uint scale) - { - var low = value.Low128; - var high = value.High128; - var isNegative = (high & 0x8000_0000_0000_0000UL) != 0; - unchecked - { - if (isNegative) - { - low = ~low + 1UL; - high = ~high + (low == 0 ? 1UL : 0UL); - } - } + internal static Ydb.Value PackDate(DateTime value) => + new() { Uint32Value = (uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) }; - if (high >> 32 != 0) - throw new OverflowException("Value does not fit into decimal"); + internal static DateTime UnpackDate(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerDay); - return new decimal((int)low, (int)(low >> 32), (int)high, isNegative, (byte)scale); - } + internal static Ydb.Value PackDate32(DateTime value) => new() + { Int32Value = (int)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) }; + + internal static DateTime UnpackDate32(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int32Value * TimeSpan.TicksPerDay); + + internal static Ydb.Value PackDatetime(DateTime value) => new() + { Uint32Value = (uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond) }; + + internal static DateTime UnpackDatetime(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerSecond); + + internal static Ydb.Value PackDatetime64(DateTime value) => new() + { Int64Value = value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond }; + + internal static DateTime UnpackDatetime64(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int64Value * TimeSpan.TicksPerSecond); + + internal static Ydb.Value PackTimestamp(DateTime value) => new() + { Uint64Value = (ulong)(value.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond }; + + internal static DateTime UnpackTimestamp(this Ydb.Value value) => + UnixEpoch.AddTicks((long)(value.Uint64Value * TimeSpanUtils.TicksPerMicrosecond)); + + internal static Ydb.Value PackTimestamp64(DateTime value) => new() + { Int64Value = (value.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond }; + + internal static DateTime UnpackTimestamp64(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static Ydb.Value PackInterval(TimeSpan value) => new() + { Int64Value = value.Ticks / TimeSpanUtils.TicksPerMicrosecond }; + + internal static TimeSpan UnpackInterval(this Ydb.Value value) => + TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static Ydb.Value PackInterval64(TimeSpan value) => new() + { Int64Value = value.Ticks / TimeSpanUtils.TicksPerMicrosecond }; + + internal static TimeSpan UnpackInterval64(this Ydb.Value value) => + TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); } diff --git a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs index ae8aaa54..0ebb94b6 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs @@ -108,7 +108,7 @@ private async Task Init(CancellationToken cancellationToken) /// The zero-based column ordinal. /// The value of the specified column. public override bool GetBoolean(int ordinal) => - GetPrimitiveValue(Type.Types.PrimitiveTypeId.Bool, ordinal).GetBool(); + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Bool, ordinal).UnpackBool(); /// /// Gets the value of the specified column as a byte. @@ -116,23 +116,28 @@ public override bool GetBoolean(int ordinal) => /// The zero-based column ordinal. /// The value of the specified column. public override byte GetByte(int ordinal) => - GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uint8, ordinal).GetUint8(); + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uint8, ordinal).UnpackUint8(); /// /// Gets the value of the specified column as a signed byte. /// /// The zero-based column ordinal. /// The value of the specified column as a signed byte. - public sbyte GetSByte(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Int8, ordinal).GetInt8(); + public sbyte GetSByte(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Int8, ordinal).UnpackInt8(); /// /// Gets the value of the specified column as a byte array. /// /// The zero-based column ordinal. /// The value of the specified column as a byte array. - public byte[] GetBytes(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.String, ordinal).GetBytes(); + public byte[] GetBytes(int ordinal) + { + var type = UnwrapColumnType(ordinal); - public byte[] GetYson(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Yson, ordinal).GetYson(); + return type.TypeId is Type.Types.PrimitiveTypeId.String or Type.Types.PrimitiveTypeId.Yson + ? CurrentRow[ordinal].UnpackBytes() + : throw InvalidCastException(ordinal); + } /// /// Reads a stream of bytes from the specified column offset into the buffer as an array. @@ -256,12 +261,12 @@ public override DateTime GetDateTime(int ordinal) return type.TypeId switch { - 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(), + Type.Types.PrimitiveTypeId.Timestamp => CurrentRow[ordinal].UnpackTimestamp(), + Type.Types.PrimitiveTypeId.Datetime => CurrentRow[ordinal].UnpackDatetime(), + Type.Types.PrimitiveTypeId.Date => CurrentRow[ordinal].UnpackDate(), + Type.Types.PrimitiveTypeId.Timestamp64 => CurrentRow[ordinal].UnpackTimestamp64(), + Type.Types.PrimitiveTypeId.Datetime64 => CurrentRow[ordinal].UnpackDatetime64(), + Type.Types.PrimitiveTypeId.Date32 => CurrentRow[ordinal].UnpackDate32(), _ => throw InvalidCastException(ordinal) }; } @@ -277,8 +282,8 @@ public TimeSpan GetInterval(int ordinal) return type.TypeId switch { - Type.Types.PrimitiveTypeId.Interval => CurrentRow[ordinal].GetInterval(), - Type.Types.PrimitiveTypeId.Interval64 => CurrentRow[ordinal].GetInterval64(), + Type.Types.PrimitiveTypeId.Interval => CurrentRow[ordinal].UnpackInterval(), + Type.Types.PrimitiveTypeId.Interval64 => CurrentRow[ordinal].UnpackInterval64(), _ => throw InvalidCastException(ordinal) }; } @@ -293,7 +298,7 @@ public override decimal GetDecimal(int ordinal) var type = UnwrapColumnType(ordinal); return type.TypeCase == Type.TypeOneofCase.DecimalType - ? CurrentRow[ordinal].GetDecimal((byte)type.DecimalType.Scale) + ? CurrentRow[ordinal].UnpackDecimal((byte)type.DecimalType.Scale) : throw InvalidCastException(Type.TypeOneofCase.DecimalType, ordinal); } @@ -308,8 +313,8 @@ public override double GetDouble(int ordinal) return type.TypeId switch { - Type.Types.PrimitiveTypeId.Double => CurrentRow[ordinal].GetDouble(), - Type.Types.PrimitiveTypeId.Float => CurrentRow[ordinal].GetFloat(), + Type.Types.PrimitiveTypeId.Double => CurrentRow[ordinal].UnpackDouble(), + Type.Types.PrimitiveTypeId.Float => CurrentRow[ordinal].UnpackFloat(), _ => throw InvalidCastException(ordinal) }; } @@ -395,14 +400,15 @@ or Type.Types.PrimitiveTypeId.JsonDocument /// The zero-based column ordinal. /// The value of the specified column. public override float GetFloat(int ordinal) => - GetPrimitiveValue(Type.Types.PrimitiveTypeId.Float, ordinal).GetFloat(); + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Float, ordinal).UnpackFloat(); /// /// Gets the value of the specified column as a globally unique identifier (GUID). /// /// The zero-based column ordinal. /// The value of the specified column. - public override Guid GetGuid(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uuid, ordinal).GetUuid(); + public override Guid GetGuid(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uuid, ordinal).UnpackUuid(); /// /// Gets the value of the specified column as a 16-bit signed integer. @@ -415,9 +421,9 @@ public override short GetInt16(int ordinal) return type.TypeId switch { - Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].GetInt16(), - Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].GetInt8(), - Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].UnpackInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].UnpackInt8(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(), _ => throw InvalidCastException(ordinal) }; } @@ -433,8 +439,8 @@ public ushort GetUint16(int ordinal) return type.TypeId switch { - Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), - Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].UnpackUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(), _ => throw InvalidCastException(ordinal) }; } @@ -450,11 +456,11 @@ public override int GetInt32(int ordinal) return type.TypeId switch { - 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(), + Type.Types.PrimitiveTypeId.Int32 => CurrentRow[ordinal].UnpackInt32(), + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].UnpackInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].UnpackInt8(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].UnpackUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(), _ => throw InvalidCastException(ordinal) }; } @@ -470,9 +476,9 @@ public uint GetUint32(int ordinal) return type.TypeId switch { - Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].GetUint32(), - Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), - Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].UnpackUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].UnpackUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(), _ => throw InvalidCastException(ordinal) }; } @@ -488,13 +494,13 @@ public override long GetInt64(int ordinal) return type.TypeId switch { - 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(), + Type.Types.PrimitiveTypeId.Int64 => CurrentRow[ordinal].UnpackInt64(), + Type.Types.PrimitiveTypeId.Int32 => CurrentRow[ordinal].UnpackInt32(), + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].UnpackInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].UnpackInt8(), + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].UnpackUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].UnpackUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(), _ => throw InvalidCastException(ordinal) }; } @@ -510,10 +516,10 @@ public ulong GetUint64(int ordinal) return type.TypeId switch { - 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(), + Type.Types.PrimitiveTypeId.Uint64 => CurrentRow[ordinal].UnpackUint64(), + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].UnpackUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].UnpackUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(), _ => throw InvalidCastException(ordinal) }; } @@ -546,8 +552,15 @@ public override int GetOrdinal(string name) /// /// The zero-based column ordinal. /// The value of the specified column. - public override string GetString(int ordinal) => - GetPrimitiveValue(Type.Types.PrimitiveTypeId.Utf8, ordinal).GetText(); + public override string GetString(int ordinal) + { + var type = UnwrapColumnType(ordinal); + + return type.TypeId is Type.Types.PrimitiveTypeId.Utf8 or Type.Types.PrimitiveTypeId.Json + or Type.Types.PrimitiveTypeId.JsonDocument + ? CurrentRow[ordinal].UnpackText() + : throw InvalidCastException(ordinal); + } /// /// Gets the value of the specified column as a TextReader. @@ -556,21 +569,6 @@ public override string GetString(int ordinal) => /// A TextReader containing the column value. public override TextReader GetTextReader(int ordinal) => new StringReader(GetString(ordinal)); - /// - /// Gets the value of the specified column as a JSON string. - /// - /// The zero-based column ordinal. - /// The value of the specified column as JSON. - public string GetJson(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Json, ordinal).GetJson(); - - /// - /// Gets the value of the specified column as a JSON document string. - /// - /// The zero-based column ordinal. - /// The value of the specified column as a JSON document. - public string GetJsonDocument(int ordinal) => - GetPrimitiveValue(Type.Types.PrimitiveTypeId.JsonDocument, ordinal).GetJsonDocument(); - /// /// Gets the value of the specified column in its native format. /// @@ -593,35 +591,34 @@ public override object GetValue(int ordinal) if (type.TypeCase == Type.TypeOneofCase.DecimalType) { - return ydbValue.GetDecimal(type.DecimalType.Scale); + return ydbValue.UnpackDecimal(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.Yson => ydbValue.GetYson(), - Type.Types.PrimitiveTypeId.String => ydbValue.GetBytes(), - Type.Types.PrimitiveTypeId.Uuid => ydbValue.GetUuid(), + Type.Types.PrimitiveTypeId.Date => ydbValue.UnpackDate(), + Type.Types.PrimitiveTypeId.Date32 => ydbValue.UnpackDate32(), + Type.Types.PrimitiveTypeId.Datetime => ydbValue.UnpackDatetime(), + Type.Types.PrimitiveTypeId.Datetime64 => ydbValue.UnpackDatetime64(), + Type.Types.PrimitiveTypeId.Timestamp => ydbValue.UnpackTimestamp(), + Type.Types.PrimitiveTypeId.Timestamp64 => ydbValue.UnpackTimestamp64(), + Type.Types.PrimitiveTypeId.Bool => ydbValue.UnpackBool(), + Type.Types.PrimitiveTypeId.Int8 => ydbValue.UnpackInt8(), + Type.Types.PrimitiveTypeId.Uint8 => ydbValue.UnpackUint8(), + Type.Types.PrimitiveTypeId.Int16 => ydbValue.UnpackInt16(), + Type.Types.PrimitiveTypeId.Uint16 => ydbValue.UnpackUint16(), + Type.Types.PrimitiveTypeId.Int32 => ydbValue.UnpackInt32(), + Type.Types.PrimitiveTypeId.Uint32 => ydbValue.UnpackUint32(), + Type.Types.PrimitiveTypeId.Int64 => ydbValue.UnpackInt64(), + Type.Types.PrimitiveTypeId.Uint64 => ydbValue.UnpackUint64(), + Type.Types.PrimitiveTypeId.Float => ydbValue.UnpackFloat(), + Type.Types.PrimitiveTypeId.Double => ydbValue.UnpackDouble(), + Type.Types.PrimitiveTypeId.Interval => ydbValue.UnpackInterval(), + Type.Types.PrimitiveTypeId.Utf8 + or Type.Types.PrimitiveTypeId.Json + or Type.Types.PrimitiveTypeId.JsonDocument => ydbValue.UnpackText(), + Type.Types.PrimitiveTypeId.Yson or Type.Types.PrimitiveTypeId.String => ydbValue.UnpackBytes(), + Type.Types.PrimitiveTypeId.Uuid => ydbValue.UnpackUuid(), _ => throw new YdbException($"Unsupported ydb type {GetColumnType(ordinal)}") }; } diff --git a/src/Ydb.Sdk/src/Ado/YdbDataRecord.cs b/src/Ydb.Sdk/src/Ado/YdbDataRecord.cs index 2225cf71..d9b17a5f 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataRecord.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataRecord.cs @@ -71,9 +71,5 @@ public override long GetChars(int i, long dataIndex, char[]? buffer, int bufferI public ulong GetUint64(int i) => _ydbDataReader.GetUint64(i); - public string GetJson(int i) => _ydbDataReader.GetJson(i); - - public string GetJsonDocument(int i) => _ydbDataReader.GetJsonDocument(i); - public TimeSpan GetInterval(int i) => _ydbDataReader.GetInterval(i); } diff --git a/src/Ydb.Sdk/src/Ado/YdbParameter.cs b/src/Ydb.Sdk/src/Ado/YdbParameter.cs index 2647a1e7..fde203e3 100644 --- a/src/Ydb.Sdk/src/Ado/YdbParameter.cs +++ b/src/Ydb.Sdk/src/Ado/YdbParameter.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.ComponentModel; using System.Data; using System.Data.Common; @@ -6,6 +7,8 @@ using Ydb.Sdk.Ado.YdbType; using Ydb.Sdk.Value; using static Ydb.Sdk.Ado.Internal.YdbTypedValueExtensions; +using static Ydb.Sdk.Ado.Internal.YdbTypeExtensions; +using static Ydb.Sdk.Ado.Internal.YdbValueExtensions; namespace Ydb.Sdk.Ado; @@ -19,37 +22,8 @@ namespace Ydb.Sdk.Ado; /// public sealed class YdbParameter : DbParameter { - private static readonly TypedValue NullDefaultDecimal = NullDecimal(22, 9); - - private static readonly Dictionary YdbNullByDbType = new() - { - { YdbDbType.Text, Type.Types.PrimitiveTypeId.Utf8.Null() }, - { YdbDbType.Bytes, Type.Types.PrimitiveTypeId.String.Null() }, - { YdbDbType.Bool, Type.Types.PrimitiveTypeId.Bool.Null() }, - { YdbDbType.Int8, Type.Types.PrimitiveTypeId.Int8.Null() }, - { YdbDbType.Int16, Type.Types.PrimitiveTypeId.Int16.Null() }, - { YdbDbType.Int32, Type.Types.PrimitiveTypeId.Int32.Null() }, - { YdbDbType.Int64, Type.Types.PrimitiveTypeId.Int64.Null() }, - { YdbDbType.UInt8, Type.Types.PrimitiveTypeId.Uint8.Null() }, - { YdbDbType.UInt16, Type.Types.PrimitiveTypeId.Uint16.Null() }, - { YdbDbType.UInt32, Type.Types.PrimitiveTypeId.Uint32.Null() }, - { YdbDbType.UInt64, Type.Types.PrimitiveTypeId.Uint64.Null() }, - { YdbDbType.Date, Type.Types.PrimitiveTypeId.Date.Null() }, - { YdbDbType.DateTime, Type.Types.PrimitiveTypeId.Datetime.Null() }, - { YdbDbType.Timestamp, Type.Types.PrimitiveTypeId.Timestamp.Null() }, - { YdbDbType.Interval, Type.Types.PrimitiveTypeId.Interval.Null() }, - { YdbDbType.Float, Type.Types.PrimitiveTypeId.Float.Null() }, - { YdbDbType.Double, Type.Types.PrimitiveTypeId.Double.Null() }, - { YdbDbType.Uuid, Type.Types.PrimitiveTypeId.Uuid.Null() }, - { YdbDbType.Yson, Type.Types.PrimitiveTypeId.Yson.Null() }, - { YdbDbType.Json, Type.Types.PrimitiveTypeId.Json.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 YdbPrimitiveTypeInfo? _ydbPrimitiveTypeInfo; + private YdbDbType _ydbDbType = YdbDbType.Unspecified; private string _parameterName = string.Empty; /// @@ -118,7 +92,23 @@ public override void ResetDbType() /// YdbDbType provides YDB-specific data types that may not have direct equivalents in standard DbType. /// When set, this property automatically updates the corresponding DbType value. /// - public YdbDbType YdbDbType { get; set; } = YdbDbType.Unspecified; + public YdbDbType YdbDbType + { + get => _ydbDbType; + set + { + _ydbPrimitiveTypeInfo = value.PrimitiveTypeInfo(); + if (value == YdbDbType.List) + { + throw new ArgumentOutOfRangeException(nameof(value), + "Cannot set YdbDbType to just List. " + + "Use Binary-Or with the element type (e.g. Array of dates is YdbDbType.List | YdbDbType.Date)." + ); + } + + _ydbDbType = value; + } + } private DbType _dbType = DbType.Object; @@ -227,179 +217,155 @@ internal TypedValue TypedValue if (value == null || value == DBNull.Value) { - return NullTypedValue(); + return _ydbPrimitiveTypeInfo?.NullValue ?? + (_ydbDbType == YdbDbType.Decimal + ? DecimalNull(Precision, Scale) + : _ydbDbType.HasFlag(YdbDbType.List) + ? ListNull((~YdbDbType.List & _ydbDbType).PrimitiveTypeInfo()?.YdbType ?? + DecimalType(Precision, Scale) /* only decimal is possible */) + : throw new InvalidOperationException( + "Writing value of 'null' is not supported without explicit mapping to the YdbDbType") + ); } - return YdbDbType switch + return _ydbDbType switch { - YdbDbType.Text when value is string stringValue => stringValue.Text(), - YdbDbType.Bool when value is bool boolValue => boolValue.Bool(), - YdbDbType.Int8 when value is sbyte sbyteValue => sbyteValue.Int8(), - YdbDbType.Int16 => MakeInt16(value), - YdbDbType.Int32 => MakeInt32(value), - YdbDbType.Int64 => MakeInt64(value), - YdbDbType.UInt8 when value is byte byteValue => byteValue.Uint8(), - YdbDbType.UInt16 => MakeUint16(value), - YdbDbType.UInt32 => MakeUint32(value), - YdbDbType.UInt64 => MakeUint64(value), - YdbDbType.Float when value is float floatValue => floatValue.Float(), - YdbDbType.Double => MakeDouble(value), - YdbDbType.Decimal when value is decimal decimalValue => Decimal(decimalValue), - YdbDbType.Bytes => MakeBytes(value), - YdbDbType.Yson => MakeYson(value), - YdbDbType.Json when value is string stringValue => stringValue.Json(), - 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.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), + _ when _ydbPrimitiveTypeInfo != null => new TypedValue + { + Type = _ydbPrimitiveTypeInfo.YdbType, + Value = _ydbPrimitiveTypeInfo.Pack(value) ?? throw ValueTypeNotSupportedException + }, + YdbDbType.Decimal when value is decimal decimalValue => PackDecimal(decimalValue), + YdbDbType.Unspecified => PackObject(value), + _ when YdbDbType.HasFlag(YdbDbType.List) && value is IList itemsValue => + PackList(itemsValue, ~YdbDbType.List & _ydbDbType), _ => throw ValueTypeNotSupportedException }; } } - private TypedValue MakeInt16(object value) => value switch + private TypedValue PackObject(object value) => value switch { - short shortValue => shortValue.Int16(), - sbyte sbyteValue => YdbTypedValueExtensions.Int16(sbyteValue), - byte byteValue => YdbTypedValueExtensions.Int16(byteValue), - _ => throw ValueTypeNotSupportedException + bool boolValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Bool.YdbType, Value = PackBool(boolValue) }, + sbyte sbyteValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Int8.YdbType, Value = PackInt8(sbyteValue) }, + short shortValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Int16.YdbType, Value = PackInt16(shortValue) }, + int intValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Int32.YdbType, Value = PackInt32(intValue) }, + long longValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Int64.YdbType, Value = PackInt64(longValue) }, + byte byteValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Uint8.YdbType, Value = PackUint8(byteValue) }, + ushort ushortValue => new TypedValue + { Type = YdbPrimitiveTypeInfo.Uint16.YdbType, Value = PackUint16(ushortValue) }, + uint uintValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Uint32.YdbType, Value = PackUint32(uintValue) }, + ulong ulongValue => new TypedValue + { Type = YdbPrimitiveTypeInfo.Uint64.YdbType, Value = PackUint64(ulongValue) }, + float floatValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Float.YdbType, Value = PackFloat(floatValue) }, + double doubleValue => new TypedValue + { Type = YdbPrimitiveTypeInfo.Double.YdbType, Value = PackDouble(doubleValue) }, + decimal decimalValue => PackDecimal(decimalValue), + Guid guidValue => new TypedValue { Type = YdbPrimitiveTypeInfo.Uuid.YdbType, Value = PackUuid(guidValue) }, + DateTime dateTimeValue => new TypedValue + { Type = YdbPrimitiveTypeInfo.Timestamp.YdbType, Value = PackTimestamp(dateTimeValue) }, + DateOnly dateOnlyValue => new TypedValue + { Type = YdbPrimitiveTypeInfo.Date.YdbType, Value = PackDate(dateOnlyValue.ToDateTime(TimeOnly.MinValue)) }, + byte[] bytesValue when value.GetType().GetElementType() == typeof(byte) /* array covariance */ => new TypedValue + { Type = YdbPrimitiveTypeInfo.Bytes.YdbType, Value = PackBytes(bytesValue) }, + string stringValue => new TypedValue + { Type = YdbPrimitiveTypeInfo.Text.YdbType, Value = PackText(stringValue) }, + TimeSpan timeSpanValue => new TypedValue + { Type = YdbPrimitiveTypeInfo.Interval.YdbType, Value = PackInterval(timeSpanValue) }, + MemoryStream memoryStream => new TypedValue + { Type = YdbPrimitiveTypeInfo.Bytes.YdbType, Value = PackBytes(memoryStream.ToArray()) }, + IList itemsValue => PackList(itemsValue), + _ => throw new InvalidOperationException( + $"Writing value of '{value.GetType()}' is not supported without explicit mapping to the YdbDbType") }; - private TypedValue MakeInt32(object value) => value switch - { - int intValue => intValue.Int32(), - sbyte sbyteValue => YdbTypedValueExtensions.Int32(sbyteValue), - byte byteValue => YdbTypedValueExtensions.Int32(byteValue), - short shortValue => YdbTypedValueExtensions.Int32(shortValue), - ushort ushortValue => YdbTypedValueExtensions.Int32(ushortValue), - _ => throw ValueTypeNotSupportedException - }; + private TypedValue PackDecimal(decimal value) => new() + { Type = DecimalType(Precision, Scale), Value = value.PackDecimal(Precision, Scale) }; - private TypedValue MakeInt64(object value) => value switch + private TypedValue PackList(IList items, YdbDbType ydbDbType = YdbDbType.Unspecified) { - long longValue => longValue.Int64(), - sbyte sbyteValue => YdbTypedValueExtensions.Int64(sbyteValue), - byte byteValue => YdbTypedValueExtensions.Int64(byteValue), - short shortValue => YdbTypedValueExtensions.Int64(shortValue), - ushort ushortValue => YdbTypedValueExtensions.Int64(ushortValue), - int intValue => YdbTypedValueExtensions.Int64(intValue), - uint uintValue => YdbTypedValueExtensions.Int64(uintValue), - _ => throw ValueTypeNotSupportedException - }; + var elementType = GetElementType(items) ?? throw ValueTypeNotSupportedException; + elementType = Nullable.GetUnderlyingType(elementType) ?? elementType; + var primitiveTypeInfo = ydbDbType.PrimitiveTypeInfo() ?? YdbPrimitiveTypeInfo.TryResolve(elementType); - private TypedValue MakeUint16(object value) => value switch - { - ushort shortValue => shortValue.Uint16(), - byte byteValue => YdbTypedValueExtensions.Uint16(byteValue), - _ => throw ValueTypeNotSupportedException - }; + if (primitiveTypeInfo != null) + { + var value = new Ydb.Value(); + var isOptional = false; - private TypedValue MakeUint32(object value) => value switch - { - uint intValue => intValue.Uint32(), - byte byteValue => YdbTypedValueExtensions.Uint32(byteValue), - ushort ushortValue => YdbTypedValueExtensions.Uint32(ushortValue), - _ => throw ValueTypeNotSupportedException - }; + foreach (var item in items) + { + if (item == null) + { + isOptional = true; + value.Items.Add(YdbValueNull); + } + else + { + value.Items.Add(primitiveTypeInfo.Pack(item) ?? throw ValueTypeNotSupportedException); + } + } - private TypedValue MakeUint64(object value) => value switch - { - ulong longValue => longValue.Uint64(), - byte byteValue => YdbTypedValueExtensions.Uint64(byteValue), - ushort ushortValue => YdbTypedValueExtensions.Uint64(ushortValue), - uint uintValue => YdbTypedValueExtensions.Uint64(uintValue), - _ => throw ValueTypeNotSupportedException - }; + var type = primitiveTypeInfo.YdbType; + if (isOptional) + { + type = type.OptionalType(); + } - private TypedValue MakeDouble(object value) => value switch - { - double doubleValue => doubleValue.Double(), - float floatValue => YdbTypedValueExtensions.Double(floatValue), - _ => throw ValueTypeNotSupportedException - }; + return new TypedValue { Type = type.ListType(), Value = value }; + } - private TypedValue MakeBytes(object value) => value switch - { - byte[] bytesValue => bytesValue.Bytes(), - MemoryStream memoryStream => memoryStream.ToArray().Bytes(), - _ => throw ValueTypeNotSupportedException - }; + if (ydbDbType == YdbDbType.Decimal || elementType == typeof(decimal)) + { + var value = new Ydb.Value(); + var isOptional = false; - private TypedValue MakeYson(object value) => value switch - { - byte[] bytesValue => bytesValue.Yson(), - MemoryStream memoryStream => memoryStream.ToArray().Yson(), - _ => throw ValueTypeNotSupportedException - }; + foreach (var item in items) + { + if (item == null) + { + isOptional = true; + value.Items.Add(YdbValueNull); + } + else + { + value.Items.Add(item is decimal decimalValue + ? decimalValue.PackDecimal(Precision, Scale) + : throw ValueTypeNotSupportedException); + } + } - private TypedValue MakeDate(object value) => value switch - { - DateTime dateTimeValue => dateTimeValue.Date(), - DateOnly dateOnlyValue => dateOnlyValue.ToDateTime(TimeOnly.MinValue).Date(), - _ => throw ValueTypeNotSupportedException - }; + var type = DecimalType(Precision, Scale); + if (isOptional) + { + type = type.OptionalType(); + } - private TypedValue MakeDate32(object value) => value switch - { - DateTime dateTimeValue => dateTimeValue.Date32(), - DateOnly dateOnlyValue => dateOnlyValue.ToDateTime(TimeOnly.MinValue).Date32(), - _ => throw ValueTypeNotSupportedException - }; + return new TypedValue { Type = type.ListType(), Value = value }; + } - private TypedValue Cast(object value) => value switch - { - string stringValue => stringValue.Text(), - bool boolValue => boolValue.Bool(), - sbyte sbyteValue => sbyteValue.Int8(), - short shortValue => shortValue.Int16(), - int intValue => intValue.Int32(), - long longValue => longValue.Int64(), - byte byteValue => byteValue.Uint8(), - ushort ushortValue => ushortValue.Uint16(), - uint uintValue => uintValue.Uint32(), - ulong ulongValue => ulongValue.Uint64(), - float floatValue => floatValue.Float(), - double doubleValue => doubleValue.Double(), - decimal decimalValue => Decimal(decimalValue), - Guid guidValue => guidValue.Uuid(), - DateTime dateTimeValue => dateTimeValue.Timestamp(), - DateOnly dateOnlyValue => dateOnlyValue.ToDateTime(TimeOnly.MinValue).Date(), - byte[] bytesValue => bytesValue.Bytes(), - TimeSpan timeSpanValue => timeSpanValue.Interval(), - MemoryStream memoryStream => memoryStream.ToArray().Bytes(), - _ => throw new InvalidOperationException( - $"Writing value of '{value.GetType()}' is not supported without explicit mapping to the YdbDbType") - }; + return (from object? item in items + select PackObject(item ?? throw new ArgumentException( + $"Collection of type '{items.GetType()}' contains null. " + + $"Specify YdbDbType (e.g. YdbDbType.List | YdbDbType.) or use a strongly-typed collection (e.g., List).")) + ).ToArray().List(); + } - private TypedValue Decimal(decimal value) => - Precision == 0 && Scale == 0 ? value.Decimal(22, 9) : value.Decimal(Precision, Scale); + private InvalidOperationException ValueTypeNotSupportedException => + new($"Writing value of '{Value!.GetType()}' is not supported" + + $" for parameters having YdbDbType '{YdbDbType.ToYdbTypeName()}'"); - private TypedValue NullTypedValue() + private static System.Type? GetElementType(IList value) { - if (YdbNullByDbType.TryGetValue(YdbDbType, out var value)) - { - return value; - } + var typeValue = value.GetType(); - if (YdbDbType == YdbDbType.Decimal) + if (typeValue.IsArray) { - return Precision == 0 && Scale == 0 - ? NullDefaultDecimal - : NullDecimal(Precision, Scale); + return typeValue.GetElementType(); } - throw new InvalidOperationException( - "Writing value of 'null' is not supported without explicit mapping to the YdbDbType" - ); + return typeValue.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>))? + .GetGenericArguments()[0]; } - - private InvalidOperationException ValueTypeNotSupportedException => - new($"Writing value of '{Value!.GetType()}' is not supported for parameters having YdbDbType '{YdbDbType}'"); } diff --git a/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs b/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs index dd2a1f27..5aac9cb8 100644 --- a/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs +++ b/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs @@ -1,4 +1,5 @@ using System.Data; +using Ydb.Sdk.Ado.Internal; namespace Ydb.Sdk.Ado.YdbType; @@ -59,25 +60,25 @@ public enum YdbDbType /// An unsigned integer. /// Acceptable values: from 0 to 2 ^ 16 – 1. /// - UInt8, + Uint8, /// /// An unsigned integer. /// Acceptable values: from 0 to 2 ^ 16 – 1. /// - UInt16, + Uint16, /// /// An unsigned integer. /// Acceptable values: from 0 to 2 ^ 32 – 1. /// - UInt32, + Uint32, /// /// An unsigned integer. /// Acceptable values: from 0 to 2 ^ 64 – 1. /// - UInt64, + Uint64, /// /// A real number with variable precision, 4 bytes in size. @@ -167,7 +168,7 @@ public enum YdbDbType /// /// Internal representation: Unsigned 32-bit integer. /// - DateTime, + Datetime, /// /// Date/time, precision to the microsecond. @@ -220,7 +221,14 @@ public enum YdbDbType /// Extended range interval type that supports larger time intervals /// beyond the standard Interval range. /// - Interval64 + Interval64, + + /// + /// Corresponds to the YDB container "List" type, a variable-length multidimensional array of + /// another type. This value must be combined with another value from + /// via a bit OR (e.g. YdbDbType.List | YdbDbType.Int32) + /// + List = int.MinValue } internal static class YdbDbTypeExtensions @@ -236,19 +244,53 @@ DbType.AnsiStringFixedLength or DbType.Int32 => YdbDbType.Int32, DbType.Int16 => YdbDbType.Int16, DbType.SByte => YdbDbType.Int8, - DbType.Byte => YdbDbType.UInt8, - DbType.UInt16 => YdbDbType.UInt16, - DbType.UInt32 => YdbDbType.UInt32, - DbType.UInt64 => YdbDbType.UInt64, + DbType.Byte => YdbDbType.Uint8, + DbType.UInt16 => YdbDbType.Uint16, + DbType.UInt32 => YdbDbType.Uint32, + DbType.UInt64 => YdbDbType.Uint64, DbType.Single => YdbDbType.Float, DbType.Double => YdbDbType.Double, DbType.Decimal or DbType.Currency => YdbDbType.Decimal, DbType.Date => YdbDbType.Date, - DbType.DateTime => YdbDbType.DateTime, + DbType.DateTime => YdbDbType.Datetime, DbType.DateTime2 or DbType.DateTimeOffset => YdbDbType.Timestamp, DbType.Guid => YdbDbType.Uuid, DbType.Binary => YdbDbType.Bytes, DbType.Object => YdbDbType.Unspecified, _ => throw new NotSupportedException($"Ydb don't supported this DbType: {dbType}") }; + + internal static YdbPrimitiveTypeInfo? PrimitiveTypeInfo(this YdbDbType ydbDbType) => ydbDbType switch + { + YdbDbType.Bool => YdbPrimitiveTypeInfo.Bool, + YdbDbType.Int8 => YdbPrimitiveTypeInfo.Int8, + YdbDbType.Int16 => YdbPrimitiveTypeInfo.Int16, + YdbDbType.Int32 => YdbPrimitiveTypeInfo.Int32, + YdbDbType.Int64 => YdbPrimitiveTypeInfo.Int64, + YdbDbType.Uint8 => YdbPrimitiveTypeInfo.Uint8, + YdbDbType.Uint16 => YdbPrimitiveTypeInfo.Uint16, + YdbDbType.Uint32 => YdbPrimitiveTypeInfo.Uint32, + YdbDbType.Uint64 => YdbPrimitiveTypeInfo.Uint64, + YdbDbType.Float => YdbPrimitiveTypeInfo.Float, + YdbDbType.Double => YdbPrimitiveTypeInfo.Double, + YdbDbType.Bytes => YdbPrimitiveTypeInfo.Bytes, + YdbDbType.Text => YdbPrimitiveTypeInfo.Text, + YdbDbType.Yson => YdbPrimitiveTypeInfo.Yson, + YdbDbType.Json => YdbPrimitiveTypeInfo.Json, + YdbDbType.JsonDocument => YdbPrimitiveTypeInfo.JsonDocument, + YdbDbType.Uuid => YdbPrimitiveTypeInfo.Uuid, + YdbDbType.Date => YdbPrimitiveTypeInfo.Date, + YdbDbType.Date32 => YdbPrimitiveTypeInfo.Date32, + YdbDbType.Datetime => YdbPrimitiveTypeInfo.Datetime, + YdbDbType.Datetime64 => YdbPrimitiveTypeInfo.Datetime64, + YdbDbType.Timestamp => YdbPrimitiveTypeInfo.Timestamp, + YdbDbType.Timestamp64 => YdbPrimitiveTypeInfo.Timestamp64, + YdbDbType.Interval => YdbPrimitiveTypeInfo.Interval, + YdbDbType.Interval64 => YdbPrimitiveTypeInfo.Interval64, + _ => null + }; + + internal static string ToYdbTypeName(this YdbDbType ydbDbType) => ydbDbType.HasFlag(YdbDbType.List) + ? $"List<{~YdbDbType.List & ydbDbType}>" + : ydbDbType.ToString(); } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs index f5011633..4f5e0556 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs @@ -14,41 +14,23 @@ public class DapperIntegrationTests : TestBase [Fact] public async Task DapperYqlTutorialTests() { - SqlMapper.SetTypeMap( - typeof(Episode), - new CustomPropertyTypeMap( - typeof(Episode), - (type, columnName) => - type.GetProperties().FirstOrDefault(prop => - prop.GetCustomAttributes(false) - .OfType() - .Any(attr => attr.Name == columnName)) ?? throw new InvalidOperationException())); + SqlMapper.SetTypeMap(typeof(Episode), new CustomPropertyTypeMap(typeof(Episode), (type, columnName) => + type.GetProperties().FirstOrDefault(prop => prop.GetCustomAttributes(false) + .OfType() + .Any(attr => attr.Name == columnName)) ?? throw new InvalidOperationException())); await using var connection = await CreateOpenConnectionAsync(); await connection.ExecuteAsync(Tables.CreateTables); // create tables await connection.ExecuteAsync(Tables.UpsertData); // adding data to table - var selectedEpisodes = (await connection.QueryAsync($@" -SELECT - series_id, - season_id, - episode_id, - title, - air_date - -FROM {Tables.Episodes} -WHERE - series_id = @series_id -- List of conditions to build the result - AND season_id > @season_id -- Logical AND is used for complex conditions - -ORDER BY -- Sorting the results. - series_id, -- ORDER BY sorts the values by one or multiple - season_id, -- columns. Columns are separated by commas. - episode_id - -LIMIT 3 -- LIMIT N after ORDER BY means - -- ""get top N"" or ""get bottom N"" results, -; -- depending on sort order. - ", new { series_id = 1, season_id = 1 })).ToArray(); + var selectedEpisodes = (await connection.QueryAsync( + $""" + SELECT series_id, season_id, episode_id, title, air_date + FROM {Tables.Episodes} + WHERE series_id = @series_id AND season_id > @season_id + ORDER BY series_id, season_id, episode_id + LIMIT 3; + """, new { series_id = 1, season_id = 1 })) + .ToArray(); Assert.Equal( new[] @@ -71,24 +53,14 @@ LIMIT 3 -- LIMIT N after ORDER BY means }, selectedEpisodes); - var selectedTitlesSeasonAndSeries = (await connection.QueryAsync($@" -SELECT - sa.title AS season_title, -- sa and sr are ""join names"", - sr.title AS series_title, -- table aliases declared below using AS. - sr.series_id, -- They are used to avoid - sa.season_id -- ambiguity in the column names used. - -FROM - {Tables.Seasons} AS sa -INNER JOIN - {Tables.Series} AS sr -ON sa.series_id = sr.series_id -WHERE sa.series_id = @series_id -ORDER BY -- Sorting of the results. - sr.series_id, - sa.season_id -- ORDER BY sorts the values by one column -; -- or multiple columns. - -- Columns are separated by commas.", new { series_id = 1 })).ToArray(); + var selectedTitlesSeasonAndSeries = (await connection.QueryAsync( + $""" + SELECT sa.title AS season_title, sr.title AS series_title, sr.series_id, sa.season_id + FROM {Tables.Seasons} AS sa + INNER JOIN {Tables.Series} AS sr + ON sa.series_id = sr.series_id + WHERE sa.series_id = @series_id ORDER BY sr.series_id, sa.season_id; + """, new { series_id = 1 })).ToArray(); for (var i = 0; i < selectedTitlesSeasonAndSeries.Length; i++) { @@ -111,24 +83,11 @@ ORDER BY -- Sorting of the results. parameters1.Add("title", episode1.Title, DbType.String); parameters1.Add("air_date", episode1.AirDate, DbType.Date); - await connection.ExecuteAsync($@" -UPSERT INTO {Tables.Episodes} -( - series_id, - season_id, - episode_id, - title, - air_date -) -VALUES -( - @series_id, - @season_id, - @episode_id, - @title, - @air_date -); -;", parameters1, transaction); + await connection.ExecuteAsync( + $""" + UPSERT INTO {Tables.Episodes} (series_id, season_id, episode_id, title, air_date) + VALUES (@series_id, @season_id, @episode_id, @title, @air_date ); + """, parameters1, transaction); await using (var otherConn = await CreateOpenConnectionAsync()) { Assert.Null(await otherConn.QuerySingleOrDefaultAsync( @@ -164,32 +123,12 @@ UPSERT INTO {Tables.Episodes} await transaction.CommitAsync(); var rollbackTransaction = connection.BeginTransaction(); - await connection.ExecuteAsync($@" -INSERT INTO {Tables.Episodes} -( - series_id, - season_id, - episode_id, - title, - air_date -) -VALUES -( - 2, - 5, - 21, - ""Test 21"", - Date(""2018-08-27"") -), -- Rows are separated by commas. -( - 2, - 5, - 22, - ""Test 22"", - Date(""2018-08-27"") -) -; -;", transaction: rollbackTransaction); + await connection.ExecuteAsync( + $""" + INSERT INTO {Tables.Episodes} (series_id, season_id, episode_id, title, air_date) VALUES + (2, 5, 21, "Test 21", Date("2018-08-27")), + (2, 5, 22, "Test 22", Date("2018-08-27")); + """, transaction: rollbackTransaction); await rollbackTransaction.RollbackAsync(); Assert.Equal((ulong)72, await connection.ExecuteScalarAsync($"SELECT COUNT(*) FROM {Tables.Episodes}")); @@ -203,37 +142,38 @@ public async Task NullableFieldSupported() var tableName = "DapperNullableTypes_" + Random.Shared.Next(); await using var connection = await CreateOpenConnectionAsync(); - await connection.ExecuteAsync(@$" -CREATE TABLE {tableName} ( - Id INT32, - BoolColumn BOOL, - LongColumn INT64, - ShortColumn INT16, - SbyteColumn INT8, - FloatColumn FLOAT, - DoubleColumn DOUBLE, - DecimalColumn DECIMAL(22,9), - ByteColumn UINT8, - UshortColumn UINT16, - UintColumn UINT32, - UlongColumn UINT64, - TextColumn TEXT, - BytesColumn BYTES, - TimestampColumn TIMESTAMP, - PRIMARY KEY (Id) -) -"); + await connection.ExecuteAsync($""" + CREATE TABLE {tableName} ( + Id INT32, + BoolColumn BOOL, + LongColumn INT64, + ShortColumn INT16, + SbyteColumn INT8, + FloatColumn FLOAT, + DoubleColumn DOUBLE, + DecimalColumn DECIMAL(22,9), + ByteColumn UINT8, + UshortColumn UINT16, + UintColumn UINT32, + UlongColumn UINT64, + TextColumn TEXT, + BytesColumn BYTES, + TimestampColumn TIMESTAMP, + PRIMARY KEY (Id) + ) + """); var entity = new NullableFields(); SqlMapper.AddTypeMap(typeof(DateTime), DbType.DateTime2); - await connection.ExecuteAsync($@" -INSERT INTO {tableName} (Id, BoolColumn, LongColumn, ShortColumn, SbyteColumn, FloatColumn, DoubleColumn, DecimalColumn, - ByteColumn, UshortColumn, UintColumn, UlongColumn, TextColumn, BytesColumn, TimestampColumn) -VALUES (@Id, @BoolColumn, @LongColumn, @ShortColumn, @SbyteColumn, - @FloatColumn, @DoubleColumn, @DecimalColumn, - @ByteColumn, @UshortColumn, @UintColumn, - @UlongColumn, @TextColumn, @BytesColumn, @TimestampColumn)", entity); + await connection.ExecuteAsync( + $""" + INSERT INTO {tableName} (Id, BoolColumn, LongColumn, ShortColumn, SbyteColumn, FloatColumn, DoubleColumn, + DecimalColumn, ByteColumn, UshortColumn, UintColumn, UlongColumn, TextColumn, BytesColumn, TimestampColumn) + VALUES (@Id, @BoolColumn, @LongColumn, @ShortColumn, @SbyteColumn, @FloatColumn, @DoubleColumn, + @DecimalColumn, @ByteColumn, @UshortColumn, @UintColumn, @UlongColumn, @TextColumn, @BytesColumn, + @TimestampColumn) + """, entity); Assert.Equal(entity, await connection.QuerySingleAsync($"SELECT * FROM {tableName} WHERE Id IS NULL")); @@ -261,6 +201,18 @@ SELECT COUNT(*) FROM {tableName} await connection.ExecuteAsync($"DROP TABLE {tableName};"); } + [Fact] + public async Task Read_Json_JsonDocument_Yson_Fields() + { + await using var connection = await CreateOpenConnectionAsync(); + var jsonModel = await connection.QueryFirstAsync( + "SELECT 1 as Id, Json('{}') as Json, JsonDocument('{}') as JsonDocument, Yson('{a=1u}') as Yson"); + Assert.Equal(1, jsonModel.Id); + Assert.Equal("{}", jsonModel.Json); + Assert.Equal("{}", jsonModel.JsonDocument); + Assert.Equal("{a=1u}"u8.ToArray(), jsonModel.Yson); + } + private record NullableFields { #pragma warning disable CollectionNeverQueried.Local @@ -290,4 +242,12 @@ private record Episode [Column("title")] public string Title { get; init; } = null!; [Column("air_date")] public DateTime AirDate { get; init; } } + + private class JsonModel + { + public int Id { get; init; } + public string Json { get; init; } = null!; + public string JsonDocument { get; init; } = null!; + public byte[] Yson { get; init; } = null!; + } } 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 21b2f97e..85371cf2 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -1,4 +1,3 @@ -using System.Collections; using System.Data; using Xunit; using Ydb.Sdk.Value; @@ -8,30 +7,27 @@ namespace Ydb.Sdk.Ado.Tests; public class YdbCommandTests : TestBase { [Theory] - [ClassData(typeof(TestDataGenerator))] - public async Task ExecuteScalarAsync_WhenSetYdbParameter_ReturnThisValue(Data data) + [MemberData(nameof(DbTypeTestCases))] + [MemberData(nameof(DbTypeTestNullCases))] + public async Task ExecuteScalarAsync_WhenSetYdbParameter_ReturnThisValue(DbType dbType, object? value, + bool isNullable) { await using var connection = await CreateOpenConnectionAsync(); var dbCommand = connection.CreateCommand(); dbCommand.CommandText = "SELECT @var as var;"; var dbParameter = new YdbParameter - { - ParameterName = "var", - DbType = data.DbType, - Value = data.Expected, - IsNullable = data.IsNullable - }; + { ParameterName = "var", DbType = dbType, Value = value, IsNullable = isNullable }; dbCommand.Parameters.Add(dbParameter); - Assert.Equal(data.Expected == null ? DBNull.Value : data.Expected, await dbCommand.ExecuteScalarAsync()); + Assert.Equal(value ?? DBNull.Value, await dbCommand.ExecuteScalarAsync()); var ydbDataReader = await dbCommand.ExecuteReaderAsync(); Assert.Equal(1, ydbDataReader.FieldCount); Assert.Equal("var", ydbDataReader.GetName(0)); - if (!data.IsNullable) + if (value != null) { - Assert.Equal(typeof(T), ydbDataReader.GetFieldType(0)); + Assert.Equal(value.GetType(), ydbDataReader.GetFieldType(0)); } while (await ydbDataReader.NextResultAsync()) @@ -40,23 +36,20 @@ public async Task ExecuteScalarAsync_WhenSetYdbParameter_ReturnThisValue(Data } [Theory] - [ClassData(typeof(TestDataGenerator))] - public async Task ExecuteScalarAsync_WhenSetYdbParameterThenPrepare_ReturnThisValue(Data data) + [MemberData(nameof(DbTypeTestCases))] + [MemberData(nameof(DbTypeTestNullCases))] + public async Task ExecuteScalarAsync_WhenSetYdbParameterThenPrepare_ReturnThisValue(DbType dbType, object? value, + bool isNullable) { await using var connection = await CreateOpenConnectionAsync(); var dbCommand = connection.CreateCommand(); dbCommand.CommandText = "SELECT @var;"; var dbParameter = new YdbParameter - { - ParameterName = "@var", - DbType = data.DbType, - Value = data.Expected, - IsNullable = data.IsNullable - }; + { ParameterName = "@var", DbType = dbType, Value = value, IsNullable = isNullable }; dbCommand.Parameters.Add(dbParameter); - Assert.Equal(data.Expected == null ? DBNull.Value : data.Expected, await dbCommand.ExecuteScalarAsync()); + Assert.Equal(value ?? DBNull.Value, await dbCommand.ExecuteScalarAsync()); } [Fact] @@ -83,14 +76,9 @@ public async Task ExecuteReaderAsync_WhenOptionalIsNull_ThrowFieldIsNull() } [Theory] - [ClassData(typeof(TestDataGenerator))] - public async Task ExecuteScalarAsync_WhenDbTypeIsObject_ReturnThisValue(Data data) + [MemberData(nameof(DbTypeTestCases))] + public async Task ExecuteScalarAsync_WhenDbTypeIsObject_ReturnThisValue(DbType _, object value, bool isNullable) { - if (data.IsNullable) - { - return; - } - await using var connection = await CreateOpenConnectionAsync(); var dbCommand = connection.CreateCommand(); dbCommand.CommandText = "SELECT @var;"; @@ -98,12 +86,12 @@ public async Task ExecuteScalarAsync_WhenDbTypeIsObject_ReturnThisValue(Data< var dbParameter = new YdbParameter { ParameterName = "@var", - Value = data.Expected, - IsNullable = data.IsNullable + Value = value, + IsNullable = isNullable }; dbCommand.Parameters.Add(dbParameter); - Assert.Equal(data.Expected, await dbCommand.ExecuteScalarAsync()); + Assert.Equal(value, await dbCommand.ExecuteScalarAsync()); } [Fact] @@ -177,7 +165,6 @@ public void ExecuteDbDataReader_WhenPreviousIsNotClosed_ThrowException() Assert.True(ydbDataReader.IsClosed); } - [Fact] public async Task ExecuteScalar_WhenSelectNull_ReturnDbNull() { @@ -204,6 +191,56 @@ public async Task ExecuteScalar_WhenSelectNoRows_ReturnNull() .ExecuteScalarAsync()); } + [Fact] + public async Task ExecuteReaderAsync_WhenParamsHaveDifferentTypes_ThrowArgumentException() + { + await using var ydbConnection = await CreateOpenConnectionAsync(); + var ex = await Assert.ThrowsAsync(() => new YdbCommand(ydbConnection) + { + CommandText = "SELECT * FROM T WHERE Ids in (@id1, @id2);", + Parameters = + { + new YdbParameter("id1", DbType.String, "text"), + new YdbParameter("id2", DbType.Int32, 1) + } + }.ExecuteReaderAsync()); + Assert.Equal("All elements in the list must have the same type. " + + "Expected: { \"typeId\": \"UTF8\" }, actual: { \"typeId\": \"INT32\" }", ex.Message); + } + + [Fact] + public async Task ExecuteReaderAsync_WhenParamsHaveNullOrNotNullTypes_ThrowArgumentException() + { + await using var ydbConnection = await CreateOpenConnectionAsync(); + var ex = await Assert.ThrowsAsync(() => new YdbCommand(ydbConnection) + { + CommandText = "SELECT * FROM T WHERE Ids in (@id1, @id2);", + Parameters = + { + new YdbParameter("id1", DbType.Int32), + new YdbParameter("id2", DbType.Int32, 1) + } + }.ExecuteReaderAsync()); + Assert.Equal("All elements in the list must have the same type. " + + "Expected: { \"optionalType\": { \"item\": { \"typeId\": \"INT32\" } } }, " + + "actual: { \"typeId\": \"INT32\" }", ex.Message); + } + + [Fact] + public async Task ExecuteReaderAsync_WhenEmptyList_ReturnEmptyResultSet() + { + await using var ydbConnection = await CreateOpenConnectionAsync(); + var tempTable = $"temp_table_{Guid.NewGuid()}"; + await new YdbCommand(ydbConnection) { CommandText = $"CREATE TABLE `{tempTable}` (a Int32, PRIMARY KEY (a));" } + .ExecuteNonQueryAsync(); + await new YdbCommand(ydbConnection) { CommandText = $"INSERT INTO `{tempTable}` (a) VALUES (1);" } + .ExecuteNonQueryAsync(); + var reader = await new YdbCommand(ydbConnection) { CommandText = $"SELECT * FROM `{tempTable}` WHERE a IN ()" } + .ExecuteReaderAsync(); + Assert.False(await reader.ReadAsync()); + await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE `{tempTable}`" }.ExecuteNonQueryAsync(); + } + public class Data(DbType dbType, T expected, bool isNullable = false) { public bool IsNullable { get; } = isNullable || expected == null; @@ -211,75 +248,87 @@ public class Data(DbType dbType, T expected, bool isNullable = false) public T Expected { get; } = expected; } - private class TestDataGenerator : IEnumerable + + public static readonly TheoryData DbTypeTestCases = new() { - private readonly List _data = - [ - 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), - true) - }, - 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(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } + { DbType.Boolean, true, false }, + { DbType.Boolean, false, false }, + { DbType.Boolean, true, true }, + { DbType.Boolean, false, true }, + { DbType.SByte, (sbyte)-1, false }, + { DbType.SByte, (sbyte)-2, true }, + { DbType.Byte, (byte)200, false }, + { DbType.Byte, (byte)228, true }, + { DbType.Int16, (short)14000, false }, + { DbType.Int16, (short)14000, true }, + { DbType.UInt16, (ushort)40_000, false }, + { DbType.UInt16, (ushort)40_000, true }, + { DbType.Int32, -40_000, false }, + { DbType.Int32, -40_000, true }, + { DbType.UInt32, 4_000_000_000, true }, + { DbType.UInt32, 4_000_000_000, true }, + { DbType.Int64, -4_000_000_000, false }, + { DbType.Int64, -4_000_000_000, true }, + { DbType.UInt64, 10_000_000_000ul, false }, + { DbType.UInt64, 10_000_000_000ul, true }, + { DbType.Single, -1.7f, false }, + { DbType.Single, -1.7f, true }, + { DbType.Double, 123.45, false }, + { DbType.Double, 123.45, true }, + { DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), false }, + { DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), true }, + { DbType.Date, new DateTime(2021, 08, 21), false }, + { DbType.Date, new DateTime(2021, 08, 21), true }, + { DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), false }, + { DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), true }, + { DbType.DateTime2, DateTime.Parse("2029-08-03T06:59:44.8578730Z"), false }, + { DbType.DateTime2, DateTime.Parse("2029-08-09T17:15:29.6935850Z"), false }, + { DbType.DateTime2, new DateTime(2021, 08, 21, 23, 30, 47, 581, DateTimeKind.Local), true }, + { DbType.Binary, "test str"u8.ToArray(), false }, + { DbType.Binary, "test str"u8.ToArray(), true }, + { DbType.String, "unicode str", false }, + { DbType.String, "unicode str", true }, + { DbType.Decimal, -18446744073.709551616m, false }, + { DbType.Decimal, -18446744073.709551616m, true } + }; + + public static readonly TheoryData DbTypeTestNullCases = new() + { + { DbType.Boolean, null, false }, + { DbType.Boolean, null, true }, + { DbType.SByte, null, false }, + { DbType.SByte, null, true }, + { DbType.Byte, null, false }, + { DbType.Byte, null, true }, + { DbType.Int16, null, false }, + { DbType.Int16, null, true }, + { DbType.UInt16, null, false }, + { DbType.UInt16, null, true }, + { DbType.Int32, null, false }, + { DbType.Int32, null, true }, + { DbType.UInt32, null, false }, + { DbType.UInt32, null, true }, + { DbType.Int64, null, false }, + { DbType.Int64, null, true }, + { DbType.UInt64, null, false }, + { DbType.UInt64, null, true }, + { DbType.Single, null, false }, + { DbType.Single, null, true }, + { DbType.Double, null, false }, + { DbType.Double, null, true }, + { DbType.Guid, null, false }, + { DbType.Guid, null, true }, + { DbType.Date, null, false }, + { DbType.Date, null, true }, + { DbType.DateTime, null, false }, + { DbType.DateTime, null, true }, + { DbType.DateTime2, null, false }, + { DbType.DateTime2, null, true }, + { DbType.Binary, null, false }, + { DbType.Binary, null, true }, + { DbType.String, null, false }, + { DbType.String, null, true }, + { DbType.Decimal, null, false }, + { DbType.Decimal, null, true } + }; } 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 baa139e4..bbc37725 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Data; using System.Globalization; using Xunit; @@ -179,8 +180,26 @@ public async Task Decimal_WhenDecimalIsScaleAndPrecision_ReturnDecimal(string? v }.ExecuteNonQueryAsync(); Assert.Equal(expected == null ? DBNull.Value : decimal.Parse(expected, CultureInfo.InvariantCulture), - await new YdbCommand(ydbConnection) { CommandText = $"SELECT d FROM {tableName};" } - .ExecuteScalarAsync()); + await new YdbCommand(ydbConnection) { CommandText = $"SELECT d FROM {tableName};" }.ExecuteScalarAsync()); + + // IN (NULL) returns empty result set + Assert.Equal(expected == null ? null : decimal.Parse(expected, CultureInfo.InvariantCulture), + await new YdbCommand(ydbConnection) + { + CommandText = $"SELECT d FROM {tableName} WHERE d IN @d;", + Parameters = { new YdbParameter("d", new[] { decimalValue }) { Precision = precision, Scale = scale } } + }.ExecuteScalarAsync()); + + // IN NULL always returns an empty result set + Assert.Null(await new YdbCommand(ydbConnection) + { + CommandText = $"SELECT d FROM {tableName} WHERE d IN @d;", + Parameters = + { + new YdbParameter("d", YdbDbType.List | YdbDbType.Decimal) + { Precision = precision, Scale = scale } + } + }.ExecuteScalarAsync()); await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync(); } @@ -240,10 +259,8 @@ public async Task Decimal_WhenNotRepresentableBySystemDecimal_ThrowsOverflowExce } }.ExecuteNonQueryAsync())).Message); - Assert.Equal(0ul, - (ulong)(await new YdbCommand(ydbConnection) { CommandText = $"SELECT COUNT(*) FROM {tableName};" } - .ExecuteScalarAsync())! - ); + Assert.Equal(0ul, (ulong)(await new YdbCommand(ydbConnection) + { CommandText = $"SELECT COUNT(*) FROM {tableName};" }.ExecuteScalarAsync())!); await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync(); } @@ -277,22 +294,21 @@ public async Task Decimal_WhenYdbReturnsDecimalWithPrecisionGreaterThan28_Throws { await using var ydbConnection = await CreateOpenConnectionAsync(); Assert.Equal("Value does not fit into decimal", (await Assert.ThrowsAsync(() => - new YdbCommand(ydbConnection) - { CommandText = $"SELECT (CAST('{value}' AS Decimal({precision}, {scale})));" } - .ExecuteScalarAsync()) - ).Message); + new YdbCommand(ydbConnection) + { CommandText = $"SELECT (CAST('{value}' AS Decimal({precision}, {scale})));" } + .ExecuteScalarAsync())).Message); } [Fact] public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue() { - foreach (var ydbType in Enum.GetValues()) + foreach (var ydbDbType in Enum.GetValues()) { - if (ydbType == YdbDbType.Unspecified) continue; + if (ydbDbType is YdbDbType.Unspecified or YdbDbType.List) continue; var tableName = $"Null_YdbDbType_{Random.Shared.Next()}"; await using var ydbConnection = await CreateOpenConnectionAsync(); - var ydbTypeStr = ydbType == YdbDbType.Decimal ? "Decimal(22, 9)" : ydbType.ToString(); + var ydbTypeStr = ydbDbType == YdbDbType.Decimal ? "Decimal(22, 9)" : ydbDbType.ToString(); await new YdbCommand(ydbConnection) { CommandText = $"CREATE TABLE {tableName}(Id Int32, Type {ydbTypeStr}, PRIMARY KEY (Id))" } .ExecuteNonQueryAsync(); @@ -300,7 +316,7 @@ public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue() await new YdbCommand(ydbConnection) { CommandText = $"INSERT INTO {tableName}(Id, Type) VALUES (1, @Type);", - Parameters = { new YdbParameter("Type", ydbType) } + Parameters = { new YdbParameter("Type", ydbDbType) } }.ExecuteNonQueryAsync(); Assert.Equal(DBNull.Value, await new YdbCommand(ydbConnection) @@ -309,6 +325,14 @@ public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue() Parameters = { new YdbParameter("Id", DbType.Int32, 1) } }.ExecuteScalarAsync()); + // Error: Can't lookup Optional in collection of JsonDocument: types Optional and T are not comparable + if (ydbDbType is not (YdbDbType.Yson or YdbDbType.Json or YdbDbType.JsonDocument)) + Assert.Null(await new YdbCommand(ydbConnection) + { + CommandText = $"SELECT Type FROM {tableName} WHERE Type IN @Type;", + Parameters = { new YdbParameter("Type", YdbDbType.List | ydbDbType) } + }.ExecuteScalarAsync()); // return empty + await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync(); } } @@ -381,14 +405,14 @@ PRIMARY KEY (Int32Column) new YdbParameter("DoubleColumn", YdbDbType.Double, 1.0), new YdbParameter("DefaultDecimalColumn", YdbDbType.Decimal, 1m), new YdbParameter("CustomDecimalColumn", YdbDbType.Decimal, 1m) { Precision = 35, Scale = 5 }, - new YdbParameter("Uint8Column", YdbDbType.UInt8, (byte)1), - new YdbParameter("Uint16Column", YdbDbType.UInt16, (ushort)1), - new YdbParameter("Uint32Column", YdbDbType.UInt32, (uint)1), - new YdbParameter("Uint64Column", YdbDbType.UInt64, (ulong)1), + new YdbParameter("Uint8Column", YdbDbType.Uint8, (byte)1), + new YdbParameter("Uint16Column", YdbDbType.Uint16, (ushort)1), + new YdbParameter("Uint32Column", YdbDbType.Uint32, (uint)1), + new YdbParameter("Uint64Column", YdbDbType.Uint64, (ulong)1), new YdbParameter("TextColumn", YdbDbType.Text, string.Empty), new YdbParameter("BytesColumn", YdbDbType.Bytes, Array.Empty()), new YdbParameter("DateColumn", YdbDbType.Date, DateTime.UnixEpoch), - new YdbParameter("DatetimeColumn", YdbDbType.DateTime, DateTime.UnixEpoch), + new YdbParameter("DatetimeColumn", YdbDbType.Datetime, DateTime.UnixEpoch), new YdbParameter("TimestampColumn", YdbDbType.Timestamp, DateTime.UnixEpoch), new YdbParameter("IntervalColumn", YdbDbType.Interval, TimeSpan.Zero), new YdbParameter("JsonColumn", YdbDbType.Json, "{}"), @@ -435,9 +459,9 @@ PRIMARY KEY (Int32Column) Assert.Equal(DateTime.UnixEpoch, ydbDataReader.GetDateTime(16)); Assert.Equal(DateTime.UnixEpoch, ydbDataReader.GetDateTime(17)); Assert.Equal(TimeSpan.Zero, ydbDataReader.GetInterval(18)); - Assert.Equal("{}", ydbDataReader.GetJson(19)); - Assert.Equal("{}", ydbDataReader.GetJsonDocument(20)); - Assert.Equal("{a=1u}"u8.ToArray(), ydbDataReader.GetYson(21)); + Assert.Equal("{}", ydbDataReader.GetString(19)); + Assert.Equal("{}", ydbDataReader.GetString(20)); + Assert.Equal("{a=1u}"u8.ToArray(), ydbDataReader.GetBytes(21)); Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(22)); Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(23)); Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(24)); @@ -447,4 +471,315 @@ PRIMARY KEY (Int32Column) await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName}" }.ExecuteNonQueryAsync(); } + + private static readonly DateTime SomeTimestamp = DateTime.Parse("2025-11-02T18:47:14.112353"); + private static readonly DateTime SomeDatetime = DateTime.Parse("2025-11-02T18:47"); + private static readonly DateTime SomeDate = DateTime.Parse("2025-11-02"); + + public static TheoryData ListParams => new() + { + { YdbDbType.Bool, new List { false, true, false } }, + { YdbDbType.Bool, (bool[])[false, true, false] }, + { YdbDbType.Int8, new List { 1, 2, 3 } }, + { YdbDbType.Int8, new sbyte[] { 1, 2, 3 } }, + { YdbDbType.Int16, new List { 1, 2, 3 } }, + { YdbDbType.Int16, new short[] { 1, 2, 3 } }, + { YdbDbType.Int32, new List { 1, 2, 3 } }, + { YdbDbType.Int32, (int[])[1, 2, 3] }, + { YdbDbType.Int64, new List { 1, 2, 3 } }, + { YdbDbType.Int64, new long[] { 1, 2, 3 } }, + { YdbDbType.Uint8, new List { 1, 2, 3 } }, + { YdbDbType.Uint16, new List { 1, 2, 3 } }, + { YdbDbType.Uint16, new ushort[] { 1, 2, 3 } }, + { YdbDbType.Uint32, new List { 1, 2, 3 } }, + { YdbDbType.Uint32, new uint[] { 1, 2, 3 } }, + { YdbDbType.Uint64, new List { 1, 2, 3 } }, + { YdbDbType.Uint64, new ulong[] { 1, 2, 3 } }, + { YdbDbType.Float, new List { 1, 2, 3 } }, + { YdbDbType.Float, new float[] { 1, 2, 3 } }, + { YdbDbType.Double, new List { 1, 2, 3 } }, + { YdbDbType.Double, new double[] { 1, 2, 3 } }, + { YdbDbType.Decimal, new List { 1, 2, 3 } }, + { YdbDbType.Decimal, new decimal[] { 1, 2, 3 } }, + { YdbDbType.Text, new List { "1", "2", "3" } }, + { YdbDbType.Text, (string[])["1", "2", "3"] }, + { YdbDbType.Bytes, new List { new byte[] { 1, 1 }, new byte[] { 2, 2 }, new byte[] { 3, 3 } } }, + { YdbDbType.Bytes, (byte[][])[[1, 1], [2, 2], [3, 3]] }, + { YdbDbType.Date, new List { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14) } }, + { YdbDbType.Date, new DateOnly[] { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14) } }, + { + YdbDbType.Timestamp, + new List { SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3) } + }, + { + YdbDbType.Timestamp, + (DateTime[])[SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3)] + }, + { YdbDbType.Interval, new List { TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3) } }, + { YdbDbType.Interval, (TimeSpan[])[TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3)] }, + { YdbDbType.Bool, new List { false, true, false, null } }, + { YdbDbType.Bool, (bool?[])[false, true, false, null] }, + { YdbDbType.Int8, new List { 1, 2, 3, null } }, + { YdbDbType.Int8, new sbyte?[] { 1, 2, 3, null } }, + { YdbDbType.Int16, new List { 1, 2, 3, null } }, + { YdbDbType.Int16, new short?[] { 1, 2, 3, null } }, + { YdbDbType.Int32, new List { 1, 2, 3, null } }, + { YdbDbType.Int32, (int?[])[1, 2, 3, null] }, + { YdbDbType.Int64, new List { 1, 2, 3, null } }, + { YdbDbType.Int64, new long?[] { 1, 2, 3, null } }, + { YdbDbType.Uint8, new List { 1, 2, 3, null } }, + { YdbDbType.Uint16, new List { 1, 2, 3, null } }, + { YdbDbType.Uint16, new ushort?[] { 1, 2, 3, null } }, + { YdbDbType.Uint32, new List { 1, 2, 3, null } }, + { YdbDbType.Uint32, new uint?[] { 1, 2, 3, null } }, + { YdbDbType.Uint64, new List { 1, 2, 3, null } }, + { YdbDbType.Uint64, new ulong?[] { 1, 2, 3, null } }, + { YdbDbType.Float, new List { 1, 2, 3, null } }, + { YdbDbType.Float, new float?[] { 1, 2, 3, null } }, + { YdbDbType.Double, new List { 1, 2, 3, null } }, + { YdbDbType.Double, new double?[] { 1, 2, 3, null } }, + { YdbDbType.Decimal, new List { 1, 2, 3, null } }, + { YdbDbType.Decimal, new decimal?[] { 1, 2, 3, null } }, + { YdbDbType.Text, new List { "1", "2", "3", null } }, + { YdbDbType.Text, (string?[])["1", "2", "3", null] }, + { YdbDbType.Bytes, new List { new byte[] { 1, 1 }, new byte[] { 2, 2 }, new byte[] { 3, 3 }, null } }, + { YdbDbType.Bytes, (byte[]?[])[[1, 1], [2, 2], [3, 3], null] }, + { + YdbDbType.Date, new List + { new DateOnly(2001, 2, 26), new DateOnly(2002, 2, 24), new DateOnly(2010, 3, 14), null } + }, + { YdbDbType.Date, new DateOnly?[] { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14), null } }, + { + YdbDbType.Timestamp, + new List { SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3), null } + }, + { + YdbDbType.Timestamp, + (DateTime?[])[SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3), null] + }, + { + YdbDbType.Interval, + new List { TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3), null } + }, + { YdbDbType.Interval, (TimeSpan?[])[TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3), null] } + }; + + public static TheoryData ExtraParams = new() + { + { + YdbDbType.Timestamp64, new List + { SomeTimestamp.AddYears(-100), SomeTimestamp.AddYears(200), SomeTimestamp.AddYears(-300) } + }, + { + YdbDbType.Timestamp64, + (DateTime[])[SomeTimestamp.AddYears(-100), SomeTimestamp.AddYears(200), SomeTimestamp.AddYears(-300)] + }, + { + YdbDbType.Timestamp64, new List + { SomeTimestamp.AddYears(-100), SomeTimestamp.AddYears(200), SomeTimestamp.AddYears(-300), null } + }, + { + YdbDbType.Timestamp64, + (DateTime?[])[SomeTimestamp.AddYears(-100), SomeTimestamp.AddYears(200), SomeTimestamp.AddYears(-300), null] + }, + { + YdbDbType.Datetime64, new List + { SomeDatetime.AddYears(-100), SomeDatetime.AddYears(200), SomeDatetime.AddYears(-300) } + }, + { + YdbDbType.Datetime64, + (DateTime[])[SomeDatetime.AddYears(-100), SomeDatetime.AddYears(200), SomeDatetime.AddYears(-300)] + }, + { + YdbDbType.Datetime64, new List + { SomeDatetime.AddYears(-100), SomeDatetime.AddYears(200), SomeDatetime.AddYears(-300), null } + }, + { + YdbDbType.Datetime64, + (DateTime?[])[SomeDatetime.AddYears(-100), SomeDatetime.AddYears(200), SomeDatetime.AddYears(-300), null] + }, + { + YdbDbType.Date32, new List + { SomeDate.AddYears(-100), SomeDate.AddDays(200), SomeDate.AddDays(-300) } + }, + { + YdbDbType.Date32, + (DateTime[])[SomeDate.AddYears(-100), SomeDate.AddDays(200), SomeDate.AddDays(-300)] + }, + { + YdbDbType.Date32, new List + { SomeDate.AddYears(-100), SomeDate.AddDays(200), SomeDate.AddDays(-300), null } + }, + { + YdbDbType.Date32, + (DateTime?[])[SomeDate.AddYears(-100), SomeDate.AddDays(200), SomeDate.AddDays(-300), null] + }, + { + YdbDbType.Datetime, new List + { SomeDatetime.AddYears(1), SomeTimestamp.AddYears(2), SomeTimestamp.AddYears(3) } + }, + { + YdbDbType.Datetime, + (DateTime[])[SomeDatetime.AddYears(1), SomeTimestamp.AddYears(2), SomeTimestamp.AddYears(3)] + }, + { + YdbDbType.Datetime, new List + { SomeDatetime.AddYears(1), SomeTimestamp.AddYears(2), SomeTimestamp.AddYears(3), null } + }, + { + YdbDbType.Datetime, + (DateTime?[])[SomeDatetime.AddYears(1), SomeTimestamp.AddYears(2), SomeTimestamp.AddYears(3), null] + }, + { YdbDbType.Date, new List { SomeDate.AddYears(1), SomeDate.AddYears(2), SomeDate.AddYears(3) } }, + { + YdbDbType.Date, + (DateTime[])[SomeDate.AddYears(1), SomeDate.AddYears(2), SomeDate.AddYears(3)] + }, + { + YdbDbType.Date, + new List { SomeDate.AddYears(1), SomeDate.AddYears(2), SomeDate.AddYears(3), null } + }, + { + YdbDbType.Date, + (DateTime?[])[SomeDate.AddYears(1), SomeDate.AddYears(2), SomeDate.AddYears(3), null] + }, + { + YdbDbType.Interval64, + new List { TimeSpan.FromDays(-1), TimeSpan.FromDays(2), TimeSpan.FromDays(-3), null } + }, + { + YdbDbType.Interval64, + (TimeSpan?[])[TimeSpan.FromDays(-1), TimeSpan.FromDays(2), TimeSpan.FromDays(-3), null] + }, + { + YdbDbType.Json, + new List { "{\"type\": \"json1\"}", "{\"type\": \"json2\"}", "{\"type\": \"json3\"}", null } + }, + { + YdbDbType.Json, + (string?[])["{\"type\": \"json1\"}", "{\"type\": \"json2\"}", "{\"type\": \"json3\"}", null] + }, + { + YdbDbType.JsonDocument, + new List { "{\"type\": \"json1\"}", "{\"type\": \"json2\"}", "{\"type\": \"json3\"}", null } + }, + { + YdbDbType.JsonDocument, + (string?[])["{\"type\": \"json1\"}", "{\"type\": \"json2\"}", "{\"type\": \"json3\"}", null] + }, + { + YdbDbType.Yson, + new List { "{a=1u}"u8.ToArray(), "{a=2u}"u8.ToArray(), null } + }, + { + YdbDbType.Yson, + (byte[]?[])["{a=1u}"u8.ToArray(), "{a=2u}"u8.ToArray(), null] + }, + { YdbDbType.Int64, new List { 1, 2u, (byte)3 } }, + { YdbDbType.Int64, new object[] { 1, 2u, (byte)3 } } // only not null objects + }; + + [Theory] + [MemberData(nameof(ListParams))] + public async Task YdbParameter_SetValue_ArrayOrList_ConvertsToYdbList(YdbDbType ydbDbType, IList list) + { + await using var ydbConnection = await CreateOpenConnectionAsync(); + var testTable = $"auto_cast_ydb_list_{Guid.NewGuid()}"; + var dbTypeStr = ydbDbType == YdbDbType.Decimal ? "Decimal(22, 9)" : ydbDbType.ToString(); + await new YdbCommand(ydbConnection) + { CommandText = $"CREATE TABLE `{testTable}`(id Uuid, type {dbTypeStr}, PRIMARY KEY(id));" } + .ExecuteNonQueryAsync(); + await new YdbCommand(ydbConnection) + { + CommandText = + $"INSERT INTO `{testTable}`(id, type) " + + "SELECT id, type FROM AS_TABLE(ListMap($list, ($x) -> { RETURN <|id: RandomUuid($x), type: $x|> }));", + Parameters = { new YdbParameter("list", list) } + }.ExecuteNonQueryAsync(); + + var count = await new YdbCommand(ydbConnection) + { + CommandText = $"SELECT COUNT(*) FROM `{testTable}` WHERE type IN $list", + Parameters = { new YdbParameter("list", list) } + }.ExecuteScalarAsync(); + + Assert.Equal(3ul, count); + Assert.Equal((ulong)list.Count, await new YdbCommand(ydbConnection) + { CommandText = $"SELECT COUNT(*) FROM `{testTable}`" }.ExecuteScalarAsync()); + + await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE `{testTable}`" }.ExecuteNonQueryAsync(); + } + + [Theory] + [MemberData(nameof(ListParams))] + [MemberData(nameof(ExtraParams))] + public async Task YdbParameter_Value_WithYdbDbTypeList_ProducesListOfSpecifiedType(YdbDbType ydbDbType, IList list) + { + await using var ydbConnection = await CreateOpenConnectionAsync(); + var testTable = $"ydb_list_{Guid.NewGuid()}"; + var dbTypeStr = ydbDbType == YdbDbType.Decimal ? "Decimal(22, 9)" : ydbDbType.ToString(); + await new YdbCommand(ydbConnection) + { CommandText = $"CREATE TABLE `{testTable}`(id Uuid, type {dbTypeStr}, PRIMARY KEY(id));" } + .ExecuteNonQueryAsync(); + await new YdbCommand(ydbConnection) + { + CommandText = + $"INSERT INTO `{testTable}`(id, type) " + + "SELECT id, type FROM AS_TABLE(ListMap($list, ($x) -> { RETURN <|id: RandomUuid($x), type: $x|> }));", + Parameters = { new YdbParameter("list", YdbDbType.List | ydbDbType, list) } + }.ExecuteNonQueryAsync(); + + if (ydbDbType is not (YdbDbType.Json or YdbDbType.JsonDocument or YdbDbType.Yson)) + { + Assert.Equal(3ul, await new YdbCommand(ydbConnection) + { + CommandText = $"SELECT COUNT(*) FROM `{testTable}` WHERE type IN $list", + Parameters = { new YdbParameter("list", YdbDbType.List | ydbDbType, list) } + }.ExecuteScalarAsync()); + } + + Assert.Equal((ulong)list.Count, await new YdbCommand(ydbConnection) + { CommandText = $"SELECT COUNT(*) FROM `{testTable}`" }.ExecuteScalarAsync()); + + await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE `{testTable}`" }.ExecuteNonQueryAsync(); + } + + + [Fact] + public void YdbParameter_SetValue_ListOrArray_InvalidInputs_Throws() + { + Assert.Equal("Writing value of 'System.Object[]' is not supported for parameters having YdbDbType 'List'", + Assert.Throws(() => new YdbParameter("list", + YdbDbType.List | YdbDbType.Bool, new object[] { true, false, "string" }).TypedValue).Message); + + Assert.Equal( + "Writing value of 'System.Object[]' is not supported for parameters having YdbDbType 'List'", + Assert.Throws(() => new YdbParameter("list", + YdbDbType.List | YdbDbType.Decimal, new object[] { 1.0m, false, 2.0m }).TypedValue).Message); + + Assert.Equal("All elements in the list must have the same type. Expected: { \"typeId\": \"INT32\" }, " + + "actual: { \"typeId\": \"UINT32\" }", Assert.Throws(() => + new YdbParameter("list", new List { 1, 2u, (byte)3 }).TypedValue).Message); + + Assert.Equal("All elements in the list must have the same type. Expected: { \"typeId\": \"INT32\" }, " + + "actual: { \"typeId\": \"UINT32\" }", Assert.Throws(() => + new YdbParameter("list", new object[] { 1, 2u, (byte)3 }).TypedValue).Message); + + Assert.Equal("Collection of type 'System.Collections.Generic.List`1[System.Object]' contains null. " + + "Specify YdbDbType (e.g. YdbDbType.List | YdbDbType.) " + + "or use a strongly-typed collection (e.g., List).", Assert.Throws(() => + new YdbParameter("list", new List { 1, null }).TypedValue).Message); + + Assert.Equal("Collection of type 'System.Object[]' contains null. " + + "Specify YdbDbType (e.g. YdbDbType.List | YdbDbType.) " + + "or use a strongly-typed collection (e.g., List).", Assert.Throws(() => + new YdbParameter("list", new object?[] { 1, null }).TypedValue).Message); + } + + [Fact] + public void YdbParameter_SetYdbDbTypeList_Throws() => + Assert.Equal("Cannot set YdbDbType to just List. " + + "Use Binary-Or with the element type (e.g. Array of dates is YdbDbType.List | YdbDbType.Date). " + + "(Parameter 'value')", + Assert.Throws(() => new YdbParameter("list", YdbDbType.List)).Message); }