summaryrefslogtreecommitdiff
path: root/src/backend/utils
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2019-07-26 11:59:00 -0400
committerTom Lane <tgl@sss.pgh.pa.us>2019-07-26 11:59:00 -0400
commit81b29c87114cdcc2ff1a9247124c59d94d20a237 (patch)
treef796ac564e70c788ea9a9d419e0c024c270dbbb1 /src/backend/utils
parent7ea91ae1980d52655837e7e3e563eb75eecbe29d (diff)
Fix loss of fractional digits for large values in cash_numeric().
Money values exceeding about 18 digits (depending on lc_monetary) could be inaccurately converted to numeric, due to select_div_scale() deciding it didn't need to compute any fractional digits. Force its hand by setting the dscale of one division input to equal the number of fractional digits we need. In passing, rearrange the logic to not do useless work in locales where money values are considered integral. Per bug #15925 from Slawomir Chodnicki. Back-patch to all supported branches. Discussion: https://postgr.es/m/15925-da9953e2674bb5c8@postgresql.org
Diffstat (limited to 'src/backend/utils')
-rw-r--r--src/backend/utils/adt/cash.c58
1 files changed, 38 insertions, 20 deletions
diff --git a/src/backend/utils/adt/cash.c b/src/backend/utils/adt/cash.c
index f15e766bc73..3c532d94892 100644
--- a/src/backend/utils/adt/cash.c
+++ b/src/backend/utils/adt/cash.c
@@ -987,13 +987,8 @@ Datum
cash_numeric(PG_FUNCTION_ARGS)
{
Cash money = PG_GETARG_CASH(0);
- Numeric result;
+ Datum result;
int fpoint;
- int64 scale;
- int i;
- Datum amount;
- Datum numeric_scale;
- Datum quotient;
struct lconv *lconvert = PGLC_localeconv();
/* see comments about frac_digits in cash_in() */
@@ -1001,22 +996,45 @@ cash_numeric(PG_FUNCTION_ARGS)
if (fpoint < 0 || fpoint > 10)
fpoint = 2;
- /* compute required scale factor */
- scale = 1;
- for (i = 0; i < fpoint; i++)
- scale *= 10;
-
- /* form the result as money / scale */
- amount = DirectFunctionCall1(int8_numeric, Int64GetDatum(money));
- numeric_scale = DirectFunctionCall1(int8_numeric, Int64GetDatum(scale));
- quotient = DirectFunctionCall2(numeric_div, amount, numeric_scale);
+ /* convert the integral money value to numeric */
+ result = DirectFunctionCall1(int8_numeric, Int64GetDatum(money));
- /* forcibly round to exactly the intended number of digits */
- result = DatumGetNumeric(DirectFunctionCall2(numeric_round,
- quotient,
- Int32GetDatum(fpoint)));
+ /* scale appropriately, if needed */
+ if (fpoint > 0)
+ {
+ int64 scale;
+ int i;
+ Datum numeric_scale;
+ Datum quotient;
+
+ /* compute required scale factor */
+ scale = 1;
+ for (i = 0; i < fpoint; i++)
+ scale *= 10;
+ numeric_scale = DirectFunctionCall1(int8_numeric,
+ Int64GetDatum(scale));
+
+ /*
+ * Given integral inputs approaching INT64_MAX, select_div_scale()
+ * might choose a result scale of zero, causing loss of fractional
+ * digits in the quotient. We can ensure an exact result by setting
+ * the dscale of either input to be at least as large as the desired
+ * result scale. numeric_round() will do that for us.
+ */
+ numeric_scale = DirectFunctionCall2(numeric_round,
+ numeric_scale,
+ Int32GetDatum(fpoint));
+
+ /* Now we can safely divide ... */
+ quotient = DirectFunctionCall2(numeric_div, result, numeric_scale);
+
+ /* ... and forcibly round to exactly the intended number of digits */
+ result = DirectFunctionCall2(numeric_round,
+ quotient,
+ Int32GetDatum(fpoint));
+ }
- PG_RETURN_NUMERIC(result);
+ PG_RETURN_DATUM(result);
}
/* numeric_cash()