Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
ports: [ "2135:2135", "2136:2136", "8765:8765" ]
env:
YDB_LOCAL_SURVIVE_RESTART: true
YDB_FEATURE_FLAGS: enable_parameterized_decimal
options: '--name ydb-local -h localhost'

steps:
Expand Down
3 changes: 2 additions & 1 deletion src/Ydb.Sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- Fixed bug: interval value parsing in microseconds and double instead of ticks.
- Feat ADO.NET: decimal type with arbitrary precision/scale ([#498](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/498)).
- Fixed bug: interval value parsing in microseconds and double instead of ticks ([#497](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/497)).
- ADO.NET: Changed `IBulkUpsertImporter.AddRowAsync` signature: `object?[] row``params object[]`.

## v0.21.0
Expand Down
2 changes: 1 addition & 1 deletion src/Ydb.Sdk/src/Ado/Schema/SchemaUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static class SchemaUtils
Type.Types.PrimitiveTypeId.String => "Bytes",
_ => type.TypeId.ToString()
},
Type.TypeOneofCase.DecimalType => "Decimal(22, 9)",
Type.TypeOneofCase.DecimalType => $"Decimal({type.DecimalType.Precision}, {type.DecimalType.Scale})",
_ => "Unknown"
};

Expand Down
20 changes: 17 additions & 3 deletions src/Ydb.Sdk/src/Ado/YdbParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using Google.Protobuf.WellKnownTypes;
using Ydb.Sdk.Value;

namespace Ydb.Sdk.Ado;
Expand Down Expand Up @@ -30,8 +31,6 @@ public sealed class YdbParameter : DbParameter
{ DbType.Byte, YdbValue.MakeOptionalUint8() },
{ DbType.DateTime2, YdbValue.MakeOptionalTimestamp() },
{ DbType.DateTimeOffset, YdbValue.MakeOptionalTimestamp() },
{ DbType.Decimal, YdbValue.MakeOptionalDecimal() },
{ DbType.Currency, YdbValue.MakeOptionalDecimal() },
{ DbType.Guid, YdbValue.MakeOptionalUuid() }
};

Expand Down Expand Up @@ -93,10 +92,23 @@ public override string SourceColumn
public override bool SourceColumnNullMapping { get; set; }
public override int Size { get; set; }

public override byte Precision { get; set; }
public override byte Scale { get; set; }

internal YdbValue YdbValue => Value switch
{
YdbValue ydbValue => ydbValue,
null or DBNull when YdbNullByDbType.TryGetValue(DbType, out var value) => value,
null or DBNull when DbType is DbType.Decimal or DbType.Currency => Precision == 0 && Scale == 0
? YdbValue.MakeOptionalDecimal()
: new YdbValue(
new Type
{
OptionalType = new OptionalType
{ Item = new Type { DecimalType = new DecimalType { Precision = Precision, Scale = Scale } } }
},
new Ydb.Value { NullFlagValue = new NullValue() }
),
string valueString when DbType is DbType.String or DbType.AnsiString or DbType.AnsiStringFixedLength
or DbType.StringFixedLength or DbType.Object => YdbValue.MakeUtf8(valueString),
bool boolValue when DbType is DbType.Boolean or DbType.Object => YdbValue.MakeBool(boolValue),
Expand Down Expand Up @@ -126,7 +138,9 @@ string valueString when DbType is DbType.String or DbType.AnsiString or DbType.A
},
long longValue when DbType is DbType.Int64 or DbType.Object => YdbValue.MakeInt64(longValue),
decimal decimalValue when DbType is DbType.Decimal or DbType.Currency or DbType.Object =>
YdbValue.MakeDecimal(decimalValue),
Precision == 0 && Scale == 0
? YdbValue.MakeDecimal(decimalValue)
: YdbValue.MakeDecimalWithPrecision(decimalValue, precision: Precision, scale: Scale),
ulong ulongValue when DbType is DbType.UInt64 or DbType.Object => YdbValue.MakeUint64(ulongValue),
uint uintValue => DbType switch
{
Expand Down
2 changes: 1 addition & 1 deletion src/Ydb.Sdk/src/Value/YdbValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private static string ToYql(Type type) =>
type.TypeCase switch
{
Type.TypeOneofCase.TypeId => type.TypeId.ToString(),
Type.TypeOneofCase.DecimalType => "Decimal(22, 9)",
Type.TypeOneofCase.DecimalType => $"Decimal({type.DecimalType.Precision}, {type.DecimalType.Scale})",
Type.TypeOneofCase.OptionalType => $"{ToYql(type.OptionalType.Item)}?",
Type.TypeOneofCase.ListType => $"List<{ToYql(type.ListType.Item)}>",
Type.TypeOneofCase.VoidType => "Void",
Expand Down
80 changes: 14 additions & 66 deletions src/Ydb.Sdk/src/Value/YdbValueBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,86 +86,33 @@ public static YdbValue MakeUuid(Guid guid)
new Ydb.Value { Low128 = low, High128 = high });
}

private static byte GetDecimalScale(decimal value)
public static YdbValue MakeDecimalWithPrecision(decimal value, uint precision, uint scale)
{
var bits = decimal.GetBits(value);
var flags = bits[3];
var scale = (byte)((flags >> 16) & 0x7F);
return scale;
}

private static uint GetDecimalPrecision(decimal value)
{
var bits = decimal.GetBits(value);
value = new decimal(lo: bits[0], mid: bits[1], hi: bits[2], isNegative: false, scale: 0);

var precision = 0u;
while (value != decimal.Zero)
{
value = Math.Round(value / 10);
precision++;
}

return precision;
}
value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal
value = Math.Round(value, (int)scale);

private static Ydb.Value MakeDecimalValue(decimal value)
{
var type = new Type { DecimalType = new DecimalType { Scale = scale, Precision = precision } };
var bits = decimal.GetBits(value);

var low64 = ((ulong)(uint)bits[1] << 32) + (uint)bits[0];
var high64 = (ulong)(uint)bits[2];
var lo = ((ulong)bits[1] << 32) + (uint)bits[0];
var hi = (ulong)bits[2];

unchecked
{
// make value negative
if (value < 0)
{
low64 = ~low64;
high64 = ~high64;
lo = ~lo;
hi = ~hi;

if (low64 == (ulong)-1L)
if (lo == (ulong)-1L)
{
high64 += 1;
hi += 1;
}

low64 += 1;
lo += 1;
}
}

return new Ydb.Value
{
Low128 = low64,
High128 = high64
};
}

public static YdbValue MakeDecimalWithPrecision(decimal value, uint? precision = null, uint? scale = null)
{
var valueScale = GetDecimalScale(value);
var valuePrecision = GetDecimalPrecision(value);
scale ??= GetDecimalScale(value);
precision ??= valuePrecision;

if ((int)valuePrecision - valueScale > (int)precision - scale)
{
throw new InvalidCastException(
$"Decimal with precision ({valuePrecision}, {valueScale}) can't fit into ({precision}, {scale})");
}

// multiply for fill value with trailing zeros
// ex: 123.45 -> 123.4500...00
value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal
value = Math.Round(value, (int)scale);

var type = new Type
{
DecimalType = new DecimalType { Scale = (uint)scale, Precision = (uint)precision }
};

var ydbValue = MakeDecimalValue(value);

return new YdbValue(type, ydbValue);
return new YdbValue(type, new Ydb.Value { Low128 = lo, High128 = hi });
}

public static YdbValue MakeDecimal(decimal value) => MakeDecimalWithPrecision(value, 22, 9);
Expand All @@ -175,7 +122,8 @@ private static YdbValue MakeOptional(YdbValue value) =>
new Type { OptionalType = new OptionalType { Item = value._protoType } },
value.TypeId != YdbTypeId.OptionalType
? value._protoValue
: new Ydb.Value { NestedValue = value._protoValue });
: new Ydb.Value { NestedValue = value._protoValue }
);

// TODO: MakeEmptyList with complex types
public static YdbValue MakeEmptyList(YdbTypeId typeId) =>
Expand Down
29 changes: 13 additions & 16 deletions src/Ydb.Sdk/src/Value/YdbValueParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,31 +150,28 @@ public Guid GetUuid()
public decimal GetDecimal()
{
EnsureType(Type.TypeOneofCase.DecimalType);
var low64 = _protoValue.Low128;
var high64 = _protoValue.High128;

var lo = _protoValue.Low128;
var hi = _protoValue.High128;
var scale = _protoType.DecimalType.Scale;

var isNegative = false;
var isNegative = (hi & 0x8000_0000_0000_0000UL) != 0;

unchecked
{
if (high64 >> 63 == 1) // if negative
if (isNegative)
{
isNegative = true;
if (low64 == 0)
{
high64 -= 1;
}
if (lo == 0)
hi--;

low64 -= 1;

low64 = ~low64;
high64 = ~high64;
lo--;
lo = ~lo;
hi = ~hi;
}
}

return new decimal((int)low64, (int)(low64 >> 32), (int)high64, isNegative, (byte)scale);
if (hi >> 32 != 0)
throw new OverflowException("Value does not fit into decimal");

return new decimal((int)lo, (int)(lo >> 32), (int)hi, isNegative, (byte)scale);
}

public bool? GetOptionalBool() => GetOptional()?.GetBool();
Expand Down
15 changes: 2 additions & 13 deletions src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Value/YdbValueUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,34 +274,23 @@ public void DecimalType()

Assert.Null(YdbValue.MakeOptionalDecimal().GetOptionalDecimal());
Assert.Null((decimal?)(YdbValue)(decimal?)null);

Assert.Equal("Decimal with precision (30, 0) can't fit into (22, 9)",
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimal(decimal.MaxValue)).Message);
}

[Fact]
public void DecimalTypeWithPrecision()
{
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m).GetDecimal());
Assert.Equal(12345m, YdbValue.MakeDecimal(12345m).GetDecimal());
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m, precision: 5, scale: 0).GetDecimal());
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m, precision: 7, scale: 2).GetDecimal());
Assert.Equal(123.46m, YdbValue.MakeDecimalWithPrecision(123.456m, precision: 5, scale: 2).GetDecimal());
Assert.Equal(-18446744073.709551616m,
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m).GetDecimal());
YdbValue.MakeDecimal(-18446744073.709551616m).GetDecimal());
Assert.Equal(-18446744073.709551616m,
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m, precision: 21, scale: 9).GetDecimal());
Assert.Equal(-18446744074m,
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m, precision: 12, scale: 0).GetDecimal());
Assert.Equal(-184467440730709551616m,
YdbValue.MakeDecimalWithPrecision(-184467440730709551616m, precision: 21, scale: 0).GetDecimal());


Assert.Equal("Decimal with precision (5, 0) can't fit into (4, 0)",
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimalWithPrecision(12345m, precision: 4))
.Message);
Assert.Equal("Decimal with precision (5, 0) can't fit into (5, 2)",
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimalWithPrecision(12345m, precision: 5, 2))
.Message);
}

[Fact]
Expand Down
41 changes: 41 additions & 0 deletions src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Data;
using System.Globalization;
using System.Text;
using Xunit;
using Ydb.Sdk.Value;
Expand Down Expand Up @@ -470,4 +471,44 @@ public async Task Date_WhenSetDateOnly_ReturnDateTime()
ydbCommand.Parameters.AddWithValue("dateOnly", DbType.Date, new DateOnly(2102, 2, 24));
Assert.Equal(new DateTime(2102, 2, 24), await ydbCommand.ExecuteScalarAsync());
}

[Theory]
[InlineData("12345", "12345.0000000000", 22, 9)]
[InlineData("54321", "54321", 5, 0)]
[InlineData("493235.4", "493235.40", 7, 2)]
[InlineData("123.46", "123.46", 5, 2)]
[InlineData("-184467434073.70911616", "-184467434073.7091161600", 35, 10)]
[InlineData("-18446744074", "-18446744074", 12, 0)]
[InlineData("-184467440730709551616", "-184467440730709551616", 21, 0)]
[InlineData("-218446744073.709551616", "-218446744073.7095516160", 22, 10)]
[InlineData(null, null, 22, 9)]
[InlineData(null, null, 35, 9)]
[InlineData(null, null, 35, 0)]
public async Task Decimal_WhenDecimalIsScaleAndPrecision_ReturnDecimal(string? value, string? expected,
byte precision, byte scale)
{
await using var ydbConnection = await CreateOpenConnectionAsync();
var decimalTableName = $"DecimalTable_{Random.Shared.Next()}";
var decimalValue = value == null ? (decimal?)null : decimal.Parse(value, CultureInfo.InvariantCulture);
var ydbCommand = new YdbCommand(ydbConnection)
{
CommandText = $"""
CREATE TABLE {decimalTableName} (
DecimalField Decimal({precision}, {scale}),
PRIMARY KEY (DecimalField)
)
"""
};
await ydbCommand.ExecuteNonQueryAsync();
ydbCommand.CommandText = $"INSERT INTO {decimalTableName}(DecimalField) VALUES (@DecimalField);";
ydbCommand.Parameters.Add(
new YdbParameter("DecimalField", DbType.Decimal, decimalValue)
{ Precision = precision, Scale = scale });
await ydbCommand.ExecuteNonQueryAsync();

ydbCommand.CommandText = $"SELECT DecimalField FROM {decimalTableName}";
Assert.Equal(expected == null ? DBNull.Value : decimal.Parse(expected, CultureInfo.InvariantCulture),
await ydbCommand.ExecuteScalarAsync());
ydbCommand.CommandText = "DROP TABLE {decimalTableName};";
}
}
Loading