diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/BigDecimalTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/BigDecimalTests.cs new file mode 100644 index 000000000..38b348742 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/BigDecimalTests.cs @@ -0,0 +1,114 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = F.D.Castel + +using System; +using System.Globalization; +using System.Numerics; +using FirebirdSql.Data.Common; +using FirebirdSql.Data.TestsBase; +using NUnit.Framework; + +namespace FirebirdSql.Data.FirebirdClient.Tests; + +[NoServerCategory] +public class BigDecimalTests +{ + static readonly object[] FitsDecimalSource = new object[] + { + new object[] { "0", 0, 0m }, + new object[] { "0", 30, 0m }, + new object[] { "1234500000", 6, 1234.5m }, + new object[] { "1234567", 6, 1.234567m }, + new object[] { "-1234567", 6, -1.234567m }, + new object[] { "1000000000000000000000000000000", 30, 1m }, + new object[] { "79228162514264337593543950335", 0, decimal.MaxValue }, + new object[] { "-79228162514264337593543950335", 0, decimal.MinValue }, + new object[] { "792281625142643375935439503350", 1, decimal.MaxValue }, + new object[] { "1", 28, 0.0000000000000000000000000001m }, + new object[] { "79228162514264337593543950335", 28, 7.9228162514264337593543950335m }, + }; + [TestCaseSource(nameof(FitsDecimalSource))] + public void CreatesDecimalWithSmallestScaleWhenExact(string unscaled, int scale, decimal expected) + { + var result = BigDecimal.Create(BigInteger.Parse(unscaled, CultureInfo.InvariantCulture), scale); + Assert.IsInstanceOf(result); + Assert.AreEqual(expected, (decimal)result); + Assert.AreEqual(expected.Scale, ((decimal)result).Scale); + } + + static readonly object[] DoesNotFitDecimalSource = new object[] + { + new object[] { "79228162514264337593543950336", 0, "79228162514264337593543950336" }, + new object[] { "-79228162514264337593543950336", 0, "-79228162514264337593543950336" }, + new object[] { "1", 29, "0.00000000000000000000000000001" }, + new object[] { "-123", 30, "-0.000000000000000000000000000123" }, + new object[] { "12345678901234567890123456789012345678", 6, "12345678901234567890123456789012.345678" }, + new object[] { "123456789012345678901234567890000", 3, "123456789012345678901234567890" }, + }; + [TestCaseSource(nameof(DoesNotFitDecimalSource))] + public void CreatesBigDecimalWhenNotExact(string unscaled, int scale, string expected) + { + var result = BigDecimal.Create(BigInteger.Parse(unscaled, CultureInfo.InvariantCulture), scale); + Assert.IsInstanceOf(result); + Assert.AreEqual(expected, ((BigDecimal)result).ToString(CultureInfo.InvariantCulture)); + Assert.Throws(() => Convert.ToDecimal(result, CultureInfo.InvariantCulture)); + Assert.AreEqual(double.Parse(expected, CultureInfo.InvariantCulture), Convert.ToDouble(result, CultureInfo.InvariantCulture)); + Assert.AreEqual(expected, Convert.ToString(result, CultureInfo.InvariantCulture)); + } + + [Test] + public void FormatsWithCulture() + { + var value = new BigDecimal(BigInteger.Parse("-12345678901234567890123456789012345678"), 6); + Assert.AreEqual("-12345678901234567890123456789012,345678", value.ToString(new CultureInfo("pt-BR"))); + Assert.AreEqual("-12345678901234567890123456789012.345678", value.ToString(new CultureInfo("en-US"))); + } + + [TestCase("25", 1, 2)] + [TestCase("35", 1, 4)] + [TestCase("-25", 1, -2)] + [TestCase("-35", 1, -4)] + [TestCase("26", 1, 3)] + [TestCase("24", 1, 2)] + [TestCase("123", 0, 123)] + public void ConvertsToIntegerRoundingToEven(string unscaled, int scale, long expected) + { + var value = new BigDecimal(BigInteger.Parse(unscaled, CultureInfo.InvariantCulture), scale); + Assert.AreEqual(expected, Convert.ToInt64(value, CultureInfo.InvariantCulture)); + Assert.AreEqual((int)expected, Convert.ToInt32(value, CultureInfo.InvariantCulture)); + } + + [Test] + public void ConvertsToIntegerThrowsOnOverflow() + { + var value = new BigDecimal(BigInteger.Parse("12345678901234567890123456789012345678"), 0); + Assert.Throws(() => Convert.ToInt64(value, CultureInfo.InvariantCulture)); + } + + [TestCase("12345678901234567890123456789012345678", 0, true, "12345678901234567890123456789012345678")] + [TestCase("12345678901234567890123456789012000000", 6, true, "12345678901234567890123456789012")] + [TestCase("12345678901234567890123456789012345678", 6, false, null)] + public void ConvertsToBigIntegerOnlyWhenIntegral(string unscaled, int scale, bool expectedSuccess, string expected) + { + var value = new BigDecimal(BigInteger.Parse(unscaled, CultureInfo.InvariantCulture), scale); + Assert.AreEqual(expectedSuccess, value.TryToBigInteger(out var result)); + if (expectedSuccess) + { + Assert.AreEqual(BigInteger.Parse(expected, CultureInfo.InvariantCulture), result); + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbLongNumericsSupportTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbLongNumericsSupportTests.cs index b3de472c6..3d9ee474d 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbLongNumericsSupportTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbLongNumericsSupportTests.cs @@ -17,6 +17,7 @@ using System; using System.Globalization; +using System.Numerics; using System.Threading.Tasks; using FirebirdSql.Data.TestsBase; using NUnit.Framework; @@ -70,6 +71,108 @@ public async Task PassesValueCorrectly(decimal value, int scale) } } + static readonly object[] FitsDecimalValues = new[] + { + new object[] { "1.000000000000000000000000000000", 30, 1m }, + new object[] { "1234.500000", 6, 1234.5m }, + new object[] { "123", 0, 123m }, + new object[] { "79228162514264337593543950335", 0, decimal.MaxValue }, + new object[] { "-79228162514264337593543950335", 0, decimal.MinValue }, + new object[] { "0.0000000000000000000000000001", 28, 0.0000000000000000000000000001m }, + new object[] { "0.000000000000000000000000000100", 30, 0.0000000000000000000000000001m }, + }; + + [TestCaseSource(nameof(FitsDecimalValues))] + public async Task ReadsValueFittingDecimalExactly(string literal, int scale, decimal expected) + { + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = $"select cast('{literal}' as numeric(38, {scale})) from rdb$database"; + await using (var reader = await cmd.ExecuteReaderAsync()) + { + await reader.ReadAsync(); + Assert.AreEqual(typeof(decimal), reader.GetFieldType(0)); + var result = reader.GetValue(0); + Assert.IsInstanceOf(result); + Assert.AreEqual(expected, (decimal)result); + Assert.AreEqual(expected.ToString(CultureInfo.InvariantCulture), ((decimal)result).ToString(CultureInfo.InvariantCulture)); + } + } + } + + static readonly object[] DoesNotFitDecimalValues = new[] + { + new object[] { "12345678901234567890123456789012345678", 0 }, + new object[] { "-12345678901234567890123456789012345678", 0 }, + new object[] { "79228162514264337593543950336", 0 }, + new object[] { "12345678901234567890123456789012.345678", 6 }, + new object[] { "0.000000000000000000000000000001", 30 }, + new object[] { "-0.000123456789012345678901234567", 30 }, + }; + + [TestCaseSource(nameof(DoesNotFitDecimalValues))] + public async Task ReadsValueNotFittingDecimalWithoutFailingTheRow(string literal, int scale) + { + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = $"select cast('{literal}' as numeric(38, {scale})), 1 from rdb$database"; + await using (var reader = await cmd.ExecuteReaderAsync()) + { + Assert.IsTrue(await reader.ReadAsync()); + Assert.AreEqual(1, reader.GetInt32(1)); + var format = NumberFormatInfo.CurrentInfo; + Assert.AreEqual(literal.Replace("-", format.NegativeSign).Replace(".", format.NumberDecimalSeparator), reader.GetString(0)); + Assert.AreEqual(double.Parse(literal, CultureInfo.InvariantCulture), reader.GetDouble(0)); + Assert.Throws(() => reader.GetValue(0)); + Assert.Throws(() => reader.GetDecimal(0)); + Assert.Throws(() => reader.GetFieldValue(0)); + } + } + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "select cast('1234.5' as numeric(38, 6)) from rdb$database"; + Assert.AreEqual(1234.5m, await cmd.ExecuteScalarAsync(), "connection must remain usable"); + } + } + + static readonly object[] BigIntegerValues = new[] + { + new object[] { "12345678901234567890123456789012345678", 0, "12345678901234567890123456789012345678" }, + new object[] { "-12345678901234567890123456789012345678", 0, "-12345678901234567890123456789012345678" }, + new object[] { "123", 0, "123" }, + new object[] { "12345678901234567890123456789012.000000", 6, "12345678901234567890123456789012" }, + new object[] { "1234.000000", 6, "1234" }, + }; + + [TestCaseSource(nameof(BigIntegerValues))] + public async Task ReadsIntegralValueAsBigInteger(string literal, int scale, string expected) + { + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = $"select cast('{literal}' as numeric(38, {scale})) from rdb$database"; + await using (var reader = await cmd.ExecuteReaderAsync()) + { + await reader.ReadAsync(); + Assert.AreEqual(BigInteger.Parse(expected, CultureInfo.InvariantCulture), reader.GetFieldValue(0)); + } + } + } + + [TestCase("1234.5", 6)] + [TestCase("12345678901234567890123456789012.5", 6)] + public async Task ReadsFractionalValueAsBigIntegerThrows(string literal, int scale) + { + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = $"select cast('{literal}' as numeric(38, {scale})) from rdb$database"; + await using (var reader = await cmd.ExecuteReaderAsync()) + { + await reader.ReadAsync(); + Assert.Throws(() => reader.GetFieldValue(0)); + } + } + } + [Test] public async Task ReadsValueNullCorrectly() { diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs index 54b5698cc..487f34613 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs @@ -1569,6 +1569,10 @@ protected object ReadRawValue(IXdrReader xdr, DbField field) case DbDataType.Decimal: case DbDataType.Numeric: + if ((field.DataType & ~1) == IscCodes.SQL_INT128) + { + return TypeDecoder.DecodeInt128Numeric(xdr.ReadInt128(), field.NumericScale); + } return xdr.ReadDecimal(field.DataType, field.NumericScale); case DbDataType.Float: @@ -1667,6 +1671,10 @@ protected async ValueTask ReadRawValueAsync(IXdrReader xdr, DbField fiel case DbDataType.Decimal: case DbDataType.Numeric: + if ((field.DataType & ~1) == IscCodes.SQL_INT128) + { + return TypeDecoder.DecodeInt128Numeric(await xdr.ReadInt128Async(cancellationToken).ConfigureAwait(false), field.NumericScale); + } return await xdr.ReadDecimalAsync(field.DataType, field.NumericScale, cancellationToken).ConfigureAwait(false); case DbDataType.Float: diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs index 67a86c7cd..1957b4b0f 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs @@ -413,7 +413,7 @@ public override void Execute(int timeout, IDescriptorFiller descriptorFiller) for (var i = 0; i < values.Length; i++) { var d = descriptor[i]; - var value = d.DbValue.GetValue(); + var value = d.DbValue.GetDecodedValue(); values[i] = new DbValue(this, d, value); } @@ -485,7 +485,7 @@ public override async ValueTask ExecuteAsync(int timeout, IDescriptorFiller desc for (var i = 0; i < values.Length; i++) { var d = descriptor[i]; - var value = await d.DbValue.GetValueAsync(cancellationToken).ConfigureAwait(false); + var value = await d.DbValue.GetDecodedValueAsync(cancellationToken).ConfigureAwait(false); values[i] = new DbValue(this, d, value); } @@ -573,7 +573,7 @@ public override DbValue[] Fetch() for (var i = 0; i < row.Length; i++) { var d = _fields[i]; - var value = d.DbValue.GetValue(); + var value = d.DbValue.GetDecodedValue(); row[i] = new DbValue(this, d, value); } return row; @@ -643,7 +643,7 @@ public override async ValueTask FetchAsync(CancellationToken cancella for (var i = 0; i < row.Length; i++) { var d = _fields[i]; - var value = await d.DbValue.GetValueAsync(cancellationToken).ConfigureAwait(false); + var value = await d.DbValue.GetDecodedValueAsync(cancellationToken).ConfigureAwait(false); row[i] = new DbValue(this, d, value); } return row; diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/BigDecimal.cs b/src/FirebirdSql.Data.FirebirdClient/Common/BigDecimal.cs new file mode 100644 index 000000000..fca1a52d4 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Common/BigDecimal.cs @@ -0,0 +1,153 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = F.D.Castel + +using System; +using System.Globalization; +using System.Numerics; + +namespace FirebirdSql.Data.Common; + +// Exact value of an INT128-backed NUMERIC/DECIMAL (value = Unscaled / 10^Scale) that cannot be represented +// exactly as System.Decimal. Created only when that conversion is impossible, so ToDecimal always throws; +// the exact value is available through ToString and approximations through ToDouble/ToSingle. +internal readonly struct BigDecimal : IConvertible +{ + public BigInteger Unscaled { get; } + public int Scale { get; } + + public BigDecimal(BigInteger unscaled, int scale) + { + Unscaled = unscaled; + Scale = scale; + } + + // Returns unscaled / 10^scale as System.Decimal when that is exact, otherwise as a BigDecimal. + // Either way the smallest scale that represents the value is used (trailing zeros removed), + // which is what the previous decimal-only decoding produced for the values it could handle. + public static object Create(BigInteger unscaled, int scale) + { + if (unscaled.IsZero) + { + return 0m; + } + while (scale > 0) + { + var quotient = BigInteger.DivRem(unscaled, 10, out var remainder); + if (!remainder.IsZero) + break; + unscaled = quotient; + scale--; + } + var magnitude = BigInteger.Abs(unscaled); + if (scale > 28 || scale < 0 || magnitude > MaxDecimalMagnitude) + { + return new BigDecimal(unscaled, scale); + } + var bits = (UInt128)magnitude; + return new decimal((int)(uint)bits, (int)(uint)(bits >> 32), (int)(uint)(bits >> 64), unscaled.Sign < 0, (byte)scale); + } + + public bool TryToBigInteger(out BigInteger result) + { + if (Scale <= 0) + { + result = Unscaled * BigInteger.Pow(10, -Scale); + return true; + } + result = BigInteger.DivRem(Unscaled, BigInteger.Pow(10, Scale), out var remainder); + return remainder.IsZero; + } + + static readonly BigInteger MaxDecimalMagnitude = (BigInteger)decimal.MaxValue; + + public override string ToString() => ToString(CultureInfo.CurrentCulture); + + public string ToString(IFormatProvider provider) + { + var format = NumberFormatInfo.GetInstance(provider); + var digits = BigInteger.Abs(Unscaled).ToString(CultureInfo.InvariantCulture); + var sign = Unscaled.Sign < 0 ? format.NegativeSign : string.Empty; + if (Scale <= 0) + { + return sign + digits + new string('0', -Scale); + } + if (digits.Length <= Scale) + { + digits = new string('0', Scale - digits.Length + 1) + digits; + } + return sign + digits.Substring(0, digits.Length - Scale) + format.NumberDecimalSeparator + digits.Substring(digits.Length - Scale); + } + + OverflowException CannotConvert(string type) + { + return new OverflowException($"Value {ToString(CultureInfo.InvariantCulture)} cannot be represented exactly as {type}."); + } + + // Rounds to the nearest integer, ties to even (same as Convert.ToInt64(decimal)). + BigInteger ToInteger() + { + if (Scale <= 0) + return Unscaled * BigInteger.Pow(10, -Scale); + var divisor = BigInteger.Pow(10, Scale); + var quotient = BigInteger.DivRem(Unscaled, divisor, out var remainder); + var twice = BigInteger.Abs(remainder) * 2; + if (twice > divisor || (twice == divisor && !quotient.IsEven)) + { + quotient += Unscaled.Sign; + } + return quotient; + } + + public TypeCode GetTypeCode() => TypeCode.Object; + public bool ToBoolean(IFormatProvider provider) => !Unscaled.IsZero; + public char ToChar(IFormatProvider provider) => throw new InvalidCastException($"Invalid cast from {nameof(BigDecimal)} to {nameof(Char)}."); + public DateTime ToDateTime(IFormatProvider provider) => throw new InvalidCastException($"Invalid cast from {nameof(BigDecimal)} to {nameof(DateTime)}."); + public decimal ToDecimal(IFormatProvider provider) => throw CannotConvert(nameof(Decimal)); + public double ToDouble(IFormatProvider provider) => double.Parse(ToString(CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture); + public float ToSingle(IFormatProvider provider) => float.Parse(ToString(CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture); + public sbyte ToSByte(IFormatProvider provider) => (sbyte)ToInteger(); + public byte ToByte(IFormatProvider provider) => (byte)ToInteger(); + public short ToInt16(IFormatProvider provider) => (short)ToInteger(); + public ushort ToUInt16(IFormatProvider provider) => (ushort)ToInteger(); + public int ToInt32(IFormatProvider provider) => (int)ToInteger(); + public uint ToUInt32(IFormatProvider provider) => (uint)ToInteger(); + public long ToInt64(IFormatProvider provider) => (long)ToInteger(); + public ulong ToUInt64(IFormatProvider provider) => (ulong)ToInteger(); + public object ToType(Type conversionType, IFormatProvider provider) + { + if (conversionType == typeof(BigDecimal) || conversionType == typeof(object)) + return this; + if (conversionType == typeof(string)) + return ToString(provider); + return Type.GetTypeCode(conversionType) switch + { + TypeCode.Boolean => ToBoolean(provider), + TypeCode.SByte => ToSByte(provider), + TypeCode.Byte => ToByte(provider), + TypeCode.Int16 => ToInt16(provider), + TypeCode.UInt16 => ToUInt16(provider), + TypeCode.Int32 => ToInt32(provider), + TypeCode.UInt32 => ToUInt32(provider), + TypeCode.Int64 => ToInt64(provider), + TypeCode.UInt64 => ToUInt64(provider), + TypeCode.Single => ToSingle(provider), + TypeCode.Double => ToDouble(provider), + TypeCode.Decimal => ToDecimal(provider), + _ => throw new InvalidCastException($"Invalid cast from {nameof(BigDecimal)} to {conversionType}."), + }; + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs index 3be89be74..f287f583a 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs @@ -450,9 +450,9 @@ public void SetValue(byte[] buffer) break; case IscCodes.SQL_INT128: - if (_numericScale < 0) + if (DbDataType is DbDataType.Numeric or DbDataType.Decimal) { - DbValue.SetValue(TypeDecoder.DecodeDecimal(Int128Helper.GetInt128(buffer), _numericScale, _dataType)); + DbValue.SetValue(TypeDecoder.DecodeInt128Numeric(Int128Helper.GetInt128(buffer), _numericScale)); } else { diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs index 44b79a0f1..9a399aaed 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs @@ -95,6 +95,11 @@ public object GetValue() } default: + if (_value is BigDecimal bigDecimal) + { + // the field's CLR type is System.Decimal, which cannot hold this value exactly: throws + return bigDecimal.ToDecimal(CultureInfo.InvariantCulture); + } return _value; } } @@ -138,10 +143,25 @@ public async ValueTask GetValueAsync(CancellationToken cancellationToken } default: + if (_value is BigDecimal bigDecimal) + { + // the field's CLR type is System.Decimal, which cannot hold this value exactly: throws + return bigDecimal.ToDecimal(CultureInfo.InvariantCulture); + } return _value; } } + // Like GetValue, but keeps a BigDecimal as is (GetValue throws for it); used to copy decoded values into rows. + public object GetDecodedValue() + { + return _value is BigDecimal ? _value : GetValue(); + } + public ValueTask GetDecodedValueAsync(CancellationToken cancellationToken = default) + { + return _value is BigDecimal ? new ValueTask(_value) : GetValueAsync(cancellationToken); + } + public void SetValue(object value) { _value = value; @@ -273,6 +293,9 @@ public BigInteger GetInt128() short s => s, int i => i, long l => l, + decimal d when decimal.Truncate(d) == d => new BigInteger(d), + BigDecimal bd when bd.TryToBigInteger(out var bi) => bi, + decimal or BigDecimal => throw new OverflowException($"Value {Convert.ToString(_value, CultureInfo.InvariantCulture)} cannot be represented exactly as {nameof(BigInteger)}."), _ => (BigInteger)_value, }; } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/TypeDecoder.cs b/src/FirebirdSql.Data.FirebirdClient/Common/TypeDecoder.cs index f7d058520..2552a09b2 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/TypeDecoder.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/TypeDecoder.cs @@ -24,6 +24,13 @@ namespace FirebirdSql.Data.Common; internal static class TypeDecoder { + // INT128-backed NUMERIC/DECIMAL: System.Decimal when the value fits exactly (the common case), + // otherwise a BigDecimal, so that reading the row never fails and never rounds silently. + public static object DecodeInt128Numeric(BigInteger value, int scale) + { + return BigDecimal.Create(value, scale < 0 ? -scale : scale); + } + public static decimal DecodeDecimal(object value, int scale, int type) { var shift = scale < 0 ? -scale : scale;