Skip to content
Open
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
114 changes: 114 additions & 0 deletions src/FirebirdSql.Data.FirebirdClient.Tests/BigDecimalTests.cs
Original file line number Diff line number Diff line change
@@ -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<decimal>(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<BigDecimal>(result);
Assert.AreEqual(expected, ((BigDecimal)result).ToString(CultureInfo.InvariantCulture));
Assert.Throws<OverflowException>(() => 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<OverflowException>(() => 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

using System;
using System.Globalization;
using System.Numerics;
using System.Threading.Tasks;
using FirebirdSql.Data.TestsBase;
using NUnit.Framework;
Expand Down Expand Up @@ -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<decimal>(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<OverflowException>(() => reader.GetValue(0));
Assert.Throws<OverflowException>(() => reader.GetDecimal(0));
Assert.Throws<OverflowException>(() => reader.GetFieldValue<decimal>(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<BigInteger>(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<OverflowException>(() => reader.GetFieldValue<BigInteger>(0));
}
}
}

[Test]
public async Task ReadsValueNullCorrectly()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -1667,6 +1671,10 @@ protected async ValueTask<object> 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -643,7 +643,7 @@ public override async ValueTask<DbValue[]> 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;
Expand Down
Loading