Make int64_div_fast_to_numeric() more robust.
authorDean Rasheed <dean.a.rasheed@gmail.com>
Fri, 3 Feb 2023 11:13:34 +0000 (11:13 +0000)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Fri, 3 Feb 2023 11:13:34 +0000 (11:13 +0000)
The prior coding of int64_div_fast_to_numeric() had a number of bugs
that would cause it to fail under different circumstances, such as
with log10val2 <= 0, or log10val2 a multiple of 4, or in the "slow"
numeric path with log10val2 >= 10.

None of those could be triggered by any of our current code, which
only uses log10val2 = 3 or 6. However, they made it a hazard for any
future code that might use it. Also, since this is exported by
numeric.c, users writing their own C code might choose to use it.

Therefore fix, and back-patch to v14, where it was introduced.

Dean Rasheed, reviewed by Tom Lane.

Discussion: https://postgr.es/m/CAEZATCW8gXgW0tgPxPgHDPhVX71%2BSWFRkhnXy%2BTfGDsKLepu2g%40mail.gmail.com

src/backend/utils/adt/numeric.c

index 08c841675d883459680576fbf02a502087b93180..6bf6db6e27bbb0ca8fe904f41b37540c34aa6bf6 100644 (file)
@@ -4235,7 +4235,7 @@ int64_to_numeric(int64 val)
 }
 
 /*
- * Convert val1/(10**val2) to numeric.  This is much faster than normal
+ * Convert val1/(10**log10val2) to numeric.  This is much faster than normal
  * numeric division.
  */
 Numeric
@@ -4243,59 +4243,78 @@ int64_div_fast_to_numeric(int64 val1, int log10val2)
 {
        Numeric         res;
        NumericVar      result;
-       int64           saved_val1 = val1;
+       int                     rscale;
        int                     w;
        int                     m;
 
+       init_var(&result);
+
+       /* result scale */
+       rscale = log10val2 < 0 ? 0 : log10val2;
+
        /* how much to decrease the weight by */
        w = log10val2 / DEC_DIGITS;
-       /* how much is left */
+       /* how much is left to divide by */
        m = log10val2 % DEC_DIGITS;
+       if (m < 0)
+       {
+               m += DEC_DIGITS;
+               w--;
+       }
 
        /*
-        * If there is anything left, multiply the dividend by what's left, then
-        * shift the weight by one more.
+        * If there is anything left to divide by (10^m with 0 < m < DEC_DIGITS),
+        * multiply the dividend by 10^(DEC_DIGITS - m), and shift the weight by
+        * one more.
         */
        if (m > 0)
        {
 #if DEC_DIGITS == 4
-               static int      pow10[] = {1, 10, 100, 1000};
+               static const int pow10[] = {1, 10, 100, 1000};
 #elif DEC_DIGITS == 2
-               static int      pow10[] = {1, 10};
+               static const int pow10[] = {1, 10};
 #elif DEC_DIGITS == 1
-               static int      pow10[] = {1};
+               static const int pow10[] = {1};
 #else
 #error unsupported NBASE
 #endif
+               int64           factor = pow10[DEC_DIGITS - m];
+               int64           new_val1;
 
                StaticAssertDecl(lengthof(pow10) == DEC_DIGITS, "mismatch with DEC_DIGITS");
 
-               if (unlikely(pg_mul_s64_overflow(val1, pow10[DEC_DIGITS - m], &val1)))
+               if (unlikely(pg_mul_s64_overflow(val1, factor, &new_val1)))
                {
-                       /*
-                        * If it doesn't fit, do the whole computation in numeric the slow
-                        * way.  Note that va1l may have been overwritten, so use
-                        * saved_val1 instead.
-                        */
-                       int                     val2 = 1;
+#ifdef HAVE_INT128
+                       /* do the multiplication using 128-bit integers */
+                       int128          tmp;
 
-                       for (int i = 0; i < log10val2; i++)
-                               val2 *= 10;
-                       res = numeric_div_opt_error(int64_to_numeric(saved_val1), int64_to_numeric(val2), NULL);
-                       res = DatumGetNumeric(DirectFunctionCall2(numeric_round,
-                                                                                                         NumericGetDatum(res),
-                                                                                                         Int32GetDatum(log10val2)));
-                       return res;
+                       tmp = (int128) val1 * (int128) factor;
+
+                       int128_to_numericvar(tmp, &result);
+#else
+                       /* do the multiplication using numerics */
+                       NumericVar      tmp;
+
+                       init_var(&tmp);
+
+                       int64_to_numericvar(val1, &result);
+                       int64_to_numericvar(factor, &tmp);
+                       mul_var(&result, &tmp, &result, 0);
+
+                       free_var(&tmp);
+#endif
                }
+               else
+                       int64_to_numericvar(new_val1, &result);
+
                w++;
        }
-
-       init_var(&result);
-
-       int64_to_numericvar(val1, &result);
+       else
+               int64_to_numericvar(val1, &result);
 
        result.weight -= w;
-       result.dscale += w * DEC_DIGITS - (DEC_DIGITS - m);
+       result.dscale = rscale;
 
        res = make_result(&result);