Skip to content

Commit f9c430d

Browse files
Feat ADO.NET: decimal type with arbitrary precision/scale
1 parent 60dfb2c commit f9c430d

File tree

9 files changed

+90
-98
lines changed

9 files changed

+90
-98
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
ports: [ "2135:2135", "2136:2136", "8765:8765" ]
2323
env:
2424
YDB_LOCAL_SURVIVE_RESTART: true
25+
YDB_FEATURE_FLAGS: enable_parameterized_decimal
2526
options: '--name ydb-local -h localhost'
2627

2728
steps:

src/Ydb.Sdk/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
- Fixed bug: interval value parsing in microseconds and double instead of ticks.
1+
- Feat ADO.NET: decimal type with arbitrary precision/scale ([#498](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/498)).
2+
- Fixed bug: interval value parsing in microseconds and double instead of ticks ([#497](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/497)).
23
- ADO.NET: Changed `IBulkUpsertImporter.AddRowAsync` signature: `object?[] row``params object[]`.
34

45
## v0.21.0

src/Ydb.Sdk/src/Ado/Schema/SchemaUtils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal static class SchemaUtils
1212
Type.Types.PrimitiveTypeId.String => "Bytes",
1313
_ => type.TypeId.ToString()
1414
},
15-
Type.TypeOneofCase.DecimalType => "Decimal(22, 9)",
15+
Type.TypeOneofCase.DecimalType => $"Decimal({type.DecimalType.Precision}, {type.DecimalType.Scale})",
1616
_ => "Unknown"
1717
};
1818

src/Ydb.Sdk/src/Ado/YdbParameter.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Data;
33
using System.Data.Common;
44
using System.Diagnostics.CodeAnalysis;
5+
using Google.Protobuf.WellKnownTypes;
56
using Ydb.Sdk.Value;
67

78
namespace Ydb.Sdk.Ado;
@@ -30,8 +31,6 @@ public sealed class YdbParameter : DbParameter
3031
{ DbType.Byte, YdbValue.MakeOptionalUint8() },
3132
{ DbType.DateTime2, YdbValue.MakeOptionalTimestamp() },
3233
{ DbType.DateTimeOffset, YdbValue.MakeOptionalTimestamp() },
33-
{ DbType.Decimal, YdbValue.MakeOptionalDecimal() },
34-
{ DbType.Currency, YdbValue.MakeOptionalDecimal() },
3534
{ DbType.Guid, YdbValue.MakeOptionalUuid() }
3635
};
3736

@@ -93,10 +92,23 @@ public override string SourceColumn
9392
public override bool SourceColumnNullMapping { get; set; }
9493
public override int Size { get; set; }
9594

95+
public override byte Precision { get; set; }
96+
public override byte Scale { get; set; }
97+
9698
internal YdbValue YdbValue => Value switch
9799
{
98100
YdbValue ydbValue => ydbValue,
99101
null or DBNull when YdbNullByDbType.TryGetValue(DbType, out var value) => value,
102+
null or DBNull when DbType is DbType.Decimal or DbType.Currency => Precision == 0 && Scale == 0
103+
? YdbValue.MakeOptionalDecimal()
104+
: new YdbValue(
105+
new Type
106+
{
107+
OptionalType = new OptionalType
108+
{ Item = new Type { DecimalType = new DecimalType { Precision = Precision, Scale = Scale } } }
109+
},
110+
new Ydb.Value { NullFlagValue = new NullValue() }
111+
),
100112
string valueString when DbType is DbType.String or DbType.AnsiString or DbType.AnsiStringFixedLength
101113
or DbType.StringFixedLength or DbType.Object => YdbValue.MakeUtf8(valueString),
102114
bool boolValue when DbType is DbType.Boolean or DbType.Object => YdbValue.MakeBool(boolValue),
@@ -126,7 +138,9 @@ string valueString when DbType is DbType.String or DbType.AnsiString or DbType.A
126138
},
127139
long longValue when DbType is DbType.Int64 or DbType.Object => YdbValue.MakeInt64(longValue),
128140
decimal decimalValue when DbType is DbType.Decimal or DbType.Currency or DbType.Object =>
129-
YdbValue.MakeDecimal(decimalValue),
141+
Precision == 0 && Scale == 0
142+
? YdbValue.MakeDecimal(decimalValue)
143+
: YdbValue.MakeDecimalWithPrecision(decimalValue, precision: Precision, scale: Scale),
130144
ulong ulongValue when DbType is DbType.UInt64 or DbType.Object => YdbValue.MakeUint64(ulongValue),
131145
uint uintValue => DbType switch
132146
{

src/Ydb.Sdk/src/Value/YdbValue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ private static string ToYql(Type type) =>
7575
type.TypeCase switch
7676
{
7777
Type.TypeOneofCase.TypeId => type.TypeId.ToString(),
78-
Type.TypeOneofCase.DecimalType => "Decimal(22, 9)",
78+
Type.TypeOneofCase.DecimalType => $"Decimal({type.DecimalType.Precision}, {type.DecimalType.Scale})",
7979
Type.TypeOneofCase.OptionalType => $"{ToYql(type.OptionalType.Item)}?",
8080
Type.TypeOneofCase.ListType => $"List<{ToYql(type.ListType.Item)}>",
8181
Type.TypeOneofCase.VoidType => "Void",

src/Ydb.Sdk/src/Value/YdbValueBuilder.cs

Lines changed: 14 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -86,86 +86,33 @@ public static YdbValue MakeUuid(Guid guid)
8686
new Ydb.Value { Low128 = low, High128 = high });
8787
}
8888

89-
private static byte GetDecimalScale(decimal value)
89+
public static YdbValue MakeDecimalWithPrecision(decimal value, uint precision, uint scale)
9090
{
91-
var bits = decimal.GetBits(value);
92-
var flags = bits[3];
93-
var scale = (byte)((flags >> 16) & 0x7F);
94-
return scale;
95-
}
96-
97-
private static uint GetDecimalPrecision(decimal value)
98-
{
99-
var bits = decimal.GetBits(value);
100-
value = new decimal(lo: bits[0], mid: bits[1], hi: bits[2], isNegative: false, scale: 0);
101-
102-
var precision = 0u;
103-
while (value != decimal.Zero)
104-
{
105-
value = Math.Round(value / 10);
106-
precision++;
107-
}
108-
109-
return precision;
110-
}
91+
value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal
92+
value = Math.Round(value, (int)scale);
11193

112-
private static Ydb.Value MakeDecimalValue(decimal value)
113-
{
94+
var type = new Type { DecimalType = new DecimalType { Scale = scale, Precision = precision } };
11495
var bits = decimal.GetBits(value);
115-
116-
var low64 = ((ulong)(uint)bits[1] << 32) + (uint)bits[0];
117-
var high64 = (ulong)(uint)bits[2];
96+
var lo = ((ulong)bits[1] << 32) + (uint)bits[0];
97+
var hi = (ulong)bits[2];
11898

11999
unchecked
120100
{
121-
// make value negative
122101
if (value < 0)
123102
{
124-
low64 = ~low64;
125-
high64 = ~high64;
103+
lo = ~lo;
104+
hi = ~hi;
126105

127-
if (low64 == (ulong)-1L)
106+
if (lo == (ulong)-1L)
128107
{
129-
high64 += 1;
108+
hi += 1;
130109
}
131110

132-
low64 += 1;
111+
lo += 1;
133112
}
134113
}
135114

136-
return new Ydb.Value
137-
{
138-
Low128 = low64,
139-
High128 = high64
140-
};
141-
}
142-
143-
public static YdbValue MakeDecimalWithPrecision(decimal value, uint? precision = null, uint? scale = null)
144-
{
145-
var valueScale = GetDecimalScale(value);
146-
var valuePrecision = GetDecimalPrecision(value);
147-
scale ??= GetDecimalScale(value);
148-
precision ??= valuePrecision;
149-
150-
if ((int)valuePrecision - valueScale > (int)precision - scale)
151-
{
152-
throw new InvalidCastException(
153-
$"Decimal with precision ({valuePrecision}, {valueScale}) can't fit into ({precision}, {scale})");
154-
}
155-
156-
// multiply for fill value with trailing zeros
157-
// ex: 123.45 -> 123.4500...00
158-
value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal
159-
value = Math.Round(value, (int)scale);
160-
161-
var type = new Type
162-
{
163-
DecimalType = new DecimalType { Scale = (uint)scale, Precision = (uint)precision }
164-
};
165-
166-
var ydbValue = MakeDecimalValue(value);
167-
168-
return new YdbValue(type, ydbValue);
115+
return new YdbValue(type, new Ydb.Value { Low128 = lo, High128 = hi });
169116
}
170117

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

180128
// TODO: MakeEmptyList with complex types
181129
public static YdbValue MakeEmptyList(YdbTypeId typeId) =>

src/Ydb.Sdk/src/Value/YdbValueParser.cs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,31 +150,28 @@ public Guid GetUuid()
150150
public decimal GetDecimal()
151151
{
152152
EnsureType(Type.TypeOneofCase.DecimalType);
153-
var low64 = _protoValue.Low128;
154-
var high64 = _protoValue.High128;
155-
153+
var lo = _protoValue.Low128;
154+
var hi = _protoValue.High128;
156155
var scale = _protoType.DecimalType.Scale;
157-
158-
var isNegative = false;
156+
var isNegative = (hi & 0x8000_0000_0000_0000UL) != 0;
159157

160158
unchecked
161159
{
162-
if (high64 >> 63 == 1) // if negative
160+
if (isNegative)
163161
{
164-
isNegative = true;
165-
if (low64 == 0)
166-
{
167-
high64 -= 1;
168-
}
162+
if (lo == 0)
163+
hi--;
169164

170-
low64 -= 1;
171-
172-
low64 = ~low64;
173-
high64 = ~high64;
165+
lo--;
166+
lo = ~lo;
167+
hi = ~hi;
174168
}
175169
}
176170

177-
return new decimal((int)low64, (int)(low64 >> 32), (int)high64, isNegative, (byte)scale);
171+
if (hi >> 32 != 0)
172+
throw new OverflowException("Value does not fit into decimal");
173+
174+
return new decimal((int)lo, (int)(lo >> 32), (int)hi, isNegative, (byte)scale);
178175
}
179176

180177
public bool? GetOptionalBool() => GetOptional()?.GetBool();

src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Value/YdbValueUnitTests.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,26 +282,18 @@ public void DecimalType()
282282
[Fact]
283283
public void DecimalTypeWithPrecision()
284284
{
285-
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m).GetDecimal());
285+
Assert.Equal(12345m, YdbValue.MakeDecimal(12345m).GetDecimal());
286286
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m, precision: 5, scale: 0).GetDecimal());
287287
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m, precision: 7, scale: 2).GetDecimal());
288288
Assert.Equal(123.46m, YdbValue.MakeDecimalWithPrecision(123.456m, precision: 5, scale: 2).GetDecimal());
289289
Assert.Equal(-18446744073.709551616m,
290-
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m).GetDecimal());
290+
YdbValue.MakeDecimal(-18446744073.709551616m).GetDecimal());
291291
Assert.Equal(-18446744073.709551616m,
292292
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m, precision: 21, scale: 9).GetDecimal());
293293
Assert.Equal(-18446744074m,
294294
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m, precision: 12, scale: 0).GetDecimal());
295295
Assert.Equal(-184467440730709551616m,
296296
YdbValue.MakeDecimalWithPrecision(-184467440730709551616m, precision: 21, scale: 0).GetDecimal());
297-
298-
299-
Assert.Equal("Decimal with precision (5, 0) can't fit into (4, 0)",
300-
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimalWithPrecision(12345m, precision: 4))
301-
.Message);
302-
Assert.Equal("Decimal with precision (5, 0) can't fit into (5, 2)",
303-
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimalWithPrecision(12345m, precision: 5, 2))
304-
.Message);
305297
}
306298

307299
[Fact]

src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,4 +470,43 @@ public async Task Date_WhenSetDateOnly_ReturnDateTime()
470470
ydbCommand.Parameters.AddWithValue("dateOnly", DbType.Date, new DateOnly(2102, 2, 24));
471471
Assert.Equal(new DateTime(2102, 2, 24), await ydbCommand.ExecuteScalarAsync());
472472
}
473+
474+
[Theory]
475+
[InlineData("12345", "12345,0000000000", 22, 9)]
476+
[InlineData("54321", "54321", 5, 0)]
477+
[InlineData("493235.4", "493235,40", 7, 2)]
478+
[InlineData("123,46", "123,46", 5, 2)]
479+
[InlineData("-184467434073,70911616", "-184467434073,7091161600", 35, 10)]
480+
[InlineData("-18446744074", "-18446744074", 12, 0)]
481+
[InlineData("-184467440730709551616", "-184467440730709551616", 21, 0)]
482+
[InlineData("-218446744073,709551616", "-218446744073,7095516160", 22, 10)]
483+
[InlineData(null, null, 22, 9)]
484+
[InlineData(null, null, 35, 9)]
485+
[InlineData(null, null, 35, 0)]
486+
public async Task Decimal_WhenDecimalIsScaleAndPrecision_ReturnDecimal(string? value, string? expected,
487+
byte precision, byte scale)
488+
{
489+
await using var ydbConnection = await CreateOpenConnectionAsync();
490+
var decimalTableName = $"DecimalTable_{Random.Shared.Next()}";
491+
var decimalValue = value == null ? (decimal?)null : decimal.Parse(value);
492+
var ydbCommand = new YdbCommand(ydbConnection)
493+
{
494+
CommandText = $"""
495+
CREATE TABLE {decimalTableName} (
496+
DecimalField Decimal({precision}, {scale}),
497+
PRIMARY KEY (DecimalField)
498+
)
499+
"""
500+
};
501+
await ydbCommand.ExecuteNonQueryAsync();
502+
ydbCommand.CommandText = $"INSERT INTO {decimalTableName}(DecimalField) VALUES (@DecimalField);";
503+
ydbCommand.Parameters.Add(
504+
new YdbParameter("DecimalField", DbType.Decimal, decimalValue)
505+
{ Precision = precision, Scale = scale });
506+
await ydbCommand.ExecuteNonQueryAsync();
507+
508+
ydbCommand.CommandText = $"SELECT DecimalField FROM {decimalTableName}";
509+
Assert.Equal(expected == null ? DBNull.Value : decimal.Parse(expected), await ydbCommand.ExecuteScalarAsync());
510+
ydbCommand.CommandText = "DROP TABLE {decimalTableName};";
511+
}
473512
}

0 commit comments

Comments
 (0)