Skip to content

Accept and return ordinary .NET numbers for DECFLOAT - #1307

Open
fdcastel wants to merge 5 commits into
FirebirdSQL:masterfrom
fdcastel:fix-decfloat-clr-values
Open

fdcastel wants to merge 5 commits into
FirebirdSQL:masterfrom
fdcastel:fix-decfloat-clr-values

Conversation

@fdcastel

@fdcastel fdcastel commented Nov 7, 2026

Copy link
Copy Markdown
Member

Fixes #1300. Based on #1306.

Summary

DECFLOAT columns and parameters now work with the usual .NET numeric types. Values are never rounded silently: a value that does not fit exactly throws OverflowException.

Changes

Parameters (DbValue.GetDecFloat): decimal, double, float, the integral types and BigInteger are converted to FbDecFloat, using the rules of #1306. They used to throw InvalidCastException, also with AddWithValue.

Reader (DbValue getters, so GetFieldValue<T> too):

Accessor DECFLOAT value
GetDecimal exact, keeping the scale (1.10 becomes 1.10m); OverflowException when the value is not exact (too large, too many digits, NaN or infinity)
GetDouble, GetFloat nearest value, as on NUMERIC columns; NaN and infinities are mapped; overflow becomes infinity
GetInt16, GetInt32, GetInt64, GetByte nearest integer, ties to even (as Convert does for decimal columns); OverflowException when out of range, NaN or infinity

FbDecFloat gets the matching public explicit conversions to decimal, double and float. docs/decfloat.md documents the rules.

Writing is stricter than the server's own CAST. A double whose shortest exact form has 17 digits (e.g. 0.1 + 0.2) throws for a DECFLOAT(16) parameter, whereas CAST(0.1e0 + 0.2e0 AS DECFLOAT(16)) rounds to 0.3000000000000000. This is deliberate (see #1306): the provider does not round a value silently. Values that fit exactly are stored unchanged, as the round-trip tests below show.

Tests

  • FbDecFloatTypeTests (NoServer): the new conversions, including limits, NaN, infinities, negative zero and rounding to even.
  • FbDecFloat16SupportTests and FbDecFloat34SupportTests:
    • .NET values passed to DECFLOAT parameters; values that do not fit throw,
    • GetDecimal, GetDouble, GetFloat and GetInt*: exact, nearest, rounding and throwing cases.
  • Without the DbValue changes, 92 of the new tests fail. With them, the embedded + NoServer suite passes: 834 passed, 0 failed.

…rmat

DecimalCodec.EncodeDecimal wrote coefficients with more digits than the
format allows, and exponents outside its range, without any check (only a
Debug.Assert). In Release builds the excess bits overflowed into the
combination/exponent fields and the server stored a different value, e.g.:

  12345678901234567890123456789012345 -> DECFLOAT(34) stored 8345678901234567890123456789012345
  30000000000000004E-17               -> DECFLOAT(16) stored 0.08000000000000004
  1E400                               -> DECFLOAT(16) stored 8.000000000000001E-353

The encoder now brings the value into the format's ranges without changing
its numeric value (dropping trailing zeros of the coefficient, folding the
exponent into the coefficient, clamping the exponent of zero), and throws
OverflowException when the value cannot be represented exactly (too many
significant digits, exponent overflow or underflow). Values that previously
got corrupted although representable (e.g. 1E384 in DECFLOAT(16)) now work.
The conversions formatted the value with a fixed custom format and then
removed the decimal point, assuming one was present. This threw
ArgumentOutOfRangeException for every value without a fractional part,
e.g. (FbDecFloat)1m, (FbDecFloat)100m, (FbDecFloat)2.0, and for values
smaller than the format's decimal places, e.g. (FbDecFloat)1e-20. It also
dropped the scale of decimals (1.10m became 1.1) and digits beyond 15
decimal places of doubles (7 for floats).

- decimal: converted exactly from its 96-bit integer and scale, so the
  value and its scale are preserved (1.10m -> 110E-2), including -0.
- double/float: converted to the shortest decimal that round-trips to the
  same binary value ("R" format), e.g. 0.1 -> 1E-1, 0.1 + 0.2 ->
  30000000000000004E-17, 1e-20 -> 1E-20; -0.0 -> negative zero.

A converted double may have up to 17 significant digits; sending it to a
DECFLOAT(16) throws OverflowException instead of being silently rounded
(this relies on the encoder range check of the previous commit).
Random.NextInt64() never returns a negative value, so the random doubles
were all positive. Use the full long range and assert both signs are covered.
Parameters: a DECFLOAT parameter only accepted FbDecFloat; any other value
(decimal, double, int, ...) threw InvalidCastException, even when the
parameter was created by AddWithValue with a decimal. Values of type
decimal, double, float, the integral types and BigInteger are now converted
to FbDecFloat (decimal exactly, double/float to the shortest round-trip
value). A value that does not fit the DECFLOAT precision exactly throws
OverflowException instead of being rounded.

Reader: GetDecimal, GetDouble, GetFloat, GetInt16/32/64 and GetByte (and
the matching GetFieldValue<T>) on a DECFLOAT column threw
InvalidCastException because FbDecFloat is not IConvertible. Now:

- GetDecimal: exact, keeping the scale (1.10 -> 1.10m); OverflowException
  when the value cannot be represented exactly (too large, too many digits,
  NaN, infinity).
- GetDouble/GetFloat: nearest value, like on NUMERIC columns; NaN and
  infinities are mapped, out-of-range values become infinity or zero.
- Integer getters: nearest integer, ties to even, as Convert does for
  decimal columns; OverflowException when out of range, NaN or infinity.

FbDecFloat gets the matching public explicit conversions to decimal, double
and float.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DECFLOAT columns and parameters do not work with decimal, double or integers

1 participant