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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ jobs:
with:
dotnet-version: 10.0.x

- name: .NET 11.0
uses: actions/setup-dotnet@v5
with:
dotnet-version: 11.0.x
dotnet-quality: preview

- name: Build
run: |
try {
Expand Down
38 changes: 37 additions & 1 deletion docs/decfloat.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@

`DECFLOAT(16)` and `DECFLOAT(34)` from Firebird 4 is handled by `FbDecFloat` type. `FbDecFloat` can be used as parameter value for `FbParameter`.

### Values are never rounded silently

`DECFLOAT(16)` holds 16 significant digits and `DECFLOAT(34)` holds 34. A value that cannot be represented exactly in the precision or exponent range of the target (for example a 17 digit value passed to a `DECFLOAT(16)` parameter) throws `OverflowException`. Values that only need trailing zeros removed or the exponent adjusted (e.g. `1E384` in `DECFLOAT(16)`) are accepted.

### Other .NET types

Parameters: besides `FbDecFloat`, a `DECFLOAT` parameter accepts `decimal`, `double`, `float`, the integral types and `BigInteger`:

* `decimal` is converted exactly, including its scale (`1.10m` stays `1.10`).
* `double` and `float` are converted to the shortest decimal that converts back to the same binary value (`0.1` is `0.1`, `0.1 + 0.2` is `0.30000000000000004`), which may have up to 17 significant digits.

Reading a `DECFLOAT` column:

* `GetDecimal` is exact; it throws `OverflowException` when the value cannot be represented exactly as `decimal` (too large, too many digits, NaN or infinity).
* `GetDouble` and `GetFloat` return the nearest value (NaN and infinities are mapped).
* `GetInt16`, `GetInt32`, `GetInt64` and `GetByte` round to the nearest integer, ties to even, and throw `OverflowException` when out of range.

`FbDecFloat` has explicit conversions from and to `decimal`, `double` and `float`, with the same rules.

### .NET 11 `Decimal64` and `Decimal128`

On .NET 11 or later, the IEEE 754 decimal types `System.Numerics.Decimal64` and `System.Numerics.Decimal128` match `DECFLOAT(16)` and `DECFLOAT(34)`. Conversions are exact, including the exponent (`1.10` stays `1.10`), negative zero, infinities and NaNs.

* Parameters: a `Decimal128` value is a `FbDbType.Dec34` parameter, a `Decimal64` or `Decimal32` value a `FbDbType.Dec16` parameter. They can also be passed to other numeric parameters. A `Decimal128` value passed to a `DECFLOAT(16)` parameter works when it fits exactly, otherwise it throws `OverflowException`.
* Reader: `GetFieldValue<Decimal128>()` and `GetFieldValue<Decimal64>()` read `DECFLOAT` and the other numeric types; they throw `OverflowException` when the value does not fit exactly (e.g. a 34 digit `DECFLOAT(34)` value read as `Decimal64`).
* `FbDecFloat` has explicit conversions from `Decimal32`, `Decimal64` and `Decimal128`, and to `Decimal64` and `Decimal128`.

```csharp
command.CommandText = "insert into t (d34) values (@d34)";
command.Parameters.AddWithValue("d34", Decimal128.Parse("1234567890123456789012345678.901234", CultureInfo.InvariantCulture));
...
var value = reader.GetFieldValue<Decimal128>(0);
```

By default `GetValue`, `GetFieldType`, `ExecuteScalar` and output parameters use `FbDecFloat`. With the connection string option `DecFloat Type=Ieee754` (`FbConnectionStringBuilder.DecFloatType = FbDecFloatType.Ieee754`) they use `Decimal64` for `DECFLOAT(16)` and `Decimal128` for `DECFLOAT(34)` instead, so that `DataTable`s, `DataAdapter`s and other code relying on `GetValue` get the .NET types. This option requires .NET 11 or later.

### Examples

Examples can be found in [`FbDecFloat16SupportTests`](../src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs) and [`FbDecFloat34SupportTests`](../src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs).
Examples can be found in [`FbDecFloat16SupportTests`](../src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs), [`FbDecFloat34SupportTests`](../src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs) and [`FbDecimalIeee754SupportTests`](../src/FirebirdSql.Data.FirebirdClient.Tests/FbDecimalIeee754SupportTests.cs).
34 changes: 34 additions & 0 deletions src/FirebirdSql.Data.FirebirdClient.Tests/ConnectionStringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

//$Authors = Jiri Cincura (jiri@cincura.net)

using System;
using System.Globalization;
using System.Threading;
using FirebirdSql.Data.Common;
Expand Down Expand Up @@ -182,6 +183,39 @@ public void WireCryptMixedCase()
Assert.AreEqual(FbWireCrypt.Required, cs.WireCrypt);
}

[Test]
public void DecFloatTypeDefault()
{
var cs = new ConnectionString("database=test.fdb");
Assert.AreEqual(FbDecFloatType.FbDecFloat, cs.DecFloatType);
}

[TestCase("decfloat type=ieee754")]
[TestCase("DecFloatType=Ieee754")]
public void DecFloatTypeParsing(string connectionString)
{
var cs = new ConnectionString(connectionString);
Assert.AreEqual(FbDecFloatType.Ieee754, cs.DecFloatType);
}

[Test]
public void DecFloatTypeInvalidValue()
{
Assert.Throws<NotSupportedException>(() => new ConnectionString("decfloat type=decimal"));
}

[Test]
public void DecFloatTypeIeee754Validation()
{
var cs = new ConnectionString("database=localhost:test.fdb;user=sysdba;password=masterkey;decfloat type=ieee754");
#if NET11_0_OR_GREATER
Assert.DoesNotThrow(cs.Validate);
#else
var ex = Assert.Throws<ArgumentException>(cs.Validate);
StringAssert.Contains("'DecFloat Type'", ex.Message);
#endif
}

[Test]
public void ParsingDatabaseOldStyleHostnameWithoutPortWithoutPath()
{
Expand Down
106 changes: 106 additions & 0 deletions src/FirebirdSql.Data.FirebirdClient.Tests/DecimalCodecTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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.Numerics;
using FirebirdSql.Data.Common;
using FirebirdSql.Data.TestsBase;
using FirebirdSql.Data.Types;
using NUnit.Framework;

namespace FirebirdSql.Data.FirebirdClient.Tests;

[NoServerCategory]
public class DecimalCodecTests
{
static readonly object[] RepresentableDecFloat16Values = new object[]
{
new FbDecFloat(BigInteger.Parse("9999999999999999"), 369),
new FbDecFloat(BigInteger.Parse("-9999999999999999"), 369),
new FbDecFloat(1, -398),
new FbDecFloat(-1, -398),
// representable only after normalization
new FbDecFloat(1, 384),
new FbDecFloat(-1, 384),
new FbDecFloat(BigInteger.Parse("100000000000000000000"), -20),
new FbDecFloat(BigInteger.Parse("12345678901234560"), 0),
new FbDecFloat(100, -400),
new FbDecFloat(0, 1000),
new FbDecFloat(0, -1000),
};
[TestCaseSource(nameof(RepresentableDecFloat16Values))]
public void DecFloat16EncodesRepresentableValueExactly(FbDecFloat value)
{
var result = DecimalCodec.DecFloat16.ParseBytes(DecimalCodec.DecFloat16.EncodeDecimal(value));
Assert.AreEqual(value, result);
}

static readonly object[] RepresentableDecFloat34Values = new object[]
{
new FbDecFloat(BigInteger.Parse("9999999999999999999999999999999999"), 6111),
new FbDecFloat(1, -6176),
// representable only after normalization
new FbDecFloat(1, 6144),
new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012340"), 0),
new FbDecFloat(BigInteger.Parse("-12345678901234567890123456789012340000"), -40),
new FbDecFloat(1000, -6179),
new FbDecFloat(0, int.MaxValue),
new FbDecFloat(0, int.MinValue),
};
[TestCaseSource(nameof(RepresentableDecFloat34Values))]
public void DecFloat34EncodesRepresentableValueExactly(FbDecFloat value)
{
var result = DecimalCodec.DecFloat34.ParseBytes(DecimalCodec.DecFloat34.EncodeDecimal(value));
Assert.AreEqual(value, result);
}

[Test]
public void ZeroExponentIsClamped()
{
var result = DecimalCodec.DecFloat16.ParseBytes(DecimalCodec.DecFloat16.EncodeDecimal(new FbDecFloat(0, 1000)));
Assert.AreEqual(369, result.Exponent);
}

static readonly object[] UnrepresentableDecFloat16Values = new object[]
{
new FbDecFloat(BigInteger.Parse("30000000000000004"), -17),
new FbDecFloat(BigInteger.Parse("-12345678901234567"), 0),
new FbDecFloat(1, 385),
new FbDecFloat(BigInteger.Parse("9999999999999999"), 370),
new FbDecFloat(1, -399),
new FbDecFloat(15, -400),
new FbDecFloat(1, int.MaxValue),
new FbDecFloat(1, int.MinValue),
};
[TestCaseSource(nameof(UnrepresentableDecFloat16Values))]
public void DecFloat16ThrowsInsteadOfRounding(FbDecFloat value)
{
Assert.Throws<OverflowException>(() => DecimalCodec.DecFloat16.EncodeDecimal(value));
}

static readonly object[] UnrepresentableDecFloat34Values = new object[]
{
new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012345"), 0),
new FbDecFloat(1, 6145),
new FbDecFloat(1, -6177),
};
[TestCaseSource(nameof(UnrepresentableDecFloat34Values))]
public void DecFloat34ThrowsInsteadOfRounding(FbDecFloat value)
{
Assert.Throws<OverflowException>(() => DecimalCodec.DecFloat34.EncodeDecimal(value));
}
}
53 changes: 53 additions & 0 deletions src/FirebirdSql.Data.FirebirdClient.Tests/FbBatchCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,59 @@ public override async Task TearDown()
await base.TearDown();
}

#if NET11_0_OR_GREATER
[Test]
public async Task Decimal128AndDecimal64ProperlyInDatabase()
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "recreate table batch_decfloat (id int, d16 decfloat(16), d34 decfloat(34))";
await cmd.ExecuteNonQueryAsync();
}
try
{
var d34 = new[] { Decimal128.Parse("1234567890123456789012345678.901234", System.Globalization.CultureInfo.InvariantCulture), Decimal128.Parse("-1.10", System.Globalization.CultureInfo.InvariantCulture) };
var d16 = new[] { Decimal64.Parse("1.10", System.Globalization.CultureInfo.InvariantCulture), Decimal64.Parse("-9.999999999999999E+384", System.Globalization.CultureInfo.InvariantCulture) };
await using (var cmd = Connection.CreateBatchCommand())
{
cmd.CommandText = "insert into batch_decfloat values (@id, @d16, @d34)";
for (var i = 0; i < d34.Length; i++)
{
var batch = cmd.AddBatchParameters();
batch.Add("id", i);
batch.Add("d16", d16[i]);
batch.Add("d34", d34[i]);
}
var result = await cmd.ExecuteNonQueryAsync();
Assert.IsTrue(result.AllSuccess);
}
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "select d16, d34 from batch_decfloat order by id";
await using (var reader = await cmd.ExecuteReaderAsync())
{
for (var i = 0; i < d34.Length; i++)
{
Assert.IsTrue(await reader.ReadAsync());
// same bits: same value and same exponent
Assert.AreEqual(Decimal64.EncodeDecimal(d16[i]), Decimal64.EncodeDecimal(reader.GetFieldValue<Decimal64>(0)));
Assert.AreEqual(Decimal128.EncodeDecimal(d34[i]), Decimal128.EncodeDecimal(reader.GetFieldValue<Decimal128>(1)));
}
Assert.IsFalse(await reader.ReadAsync());
}
}
}
finally
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "drop table batch_decfloat";
await cmd.ExecuteNonQueryAsync();
}
}
}
#endif

[Test]
public async Task DataProperlyInDatabase()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,19 @@ public void WireCryptGetter()
var b = new FbConnectionStringBuilder("wire crypt=required");
Assert.AreEqual(FbWireCrypt.Required, b.WireCrypt);
}

[Test]
public void DecFloatTypeSetter()
{
var b = new FbConnectionStringBuilder();
b.DecFloatType = FbDecFloatType.Ieee754;
Assert.AreEqual("decfloat type=Ieee754", b.ToString());
}

[Test]
public void DecFloatTypeGetter()
{
Assert.AreEqual(FbDecFloatType.FbDecFloat, new FbConnectionStringBuilder().DecFloatType);
Assert.AreEqual(FbDecFloatType.Ieee754, new FbConnectionStringBuilder("decfloattype=ieee754").DecFloatType);
}
}
Loading