I've created a patch (attached) to implement the changes discussed below.

This change moves the definition of the NumericVar structure to numeric.h.
Function definitions for functions used to work with NumericVar are also
moved to the header as are definitions of functions used to convert
NumericVar to Numeric. (Numeric is used to store numeric and decimal types.)

All of this is so that third-party libraries can access numeric and decimal
values without having to access the opaque Numeric structure.

There is actually no new code. Code is simply moved from numeric.c to
numeric.h.

This is a patch against branch master.

This successfully compiles and is tested with regression tests.

Also attached is a code sample that uses the change.

Please provide feedback. I'm planning to submit this for the March
commitfest.

           -Ed

On Wed, Sep 18, 2024 at 9:50 AM Robert Haas <robertmh...@gmail.com> wrote:

> On Sat, Sep 14, 2024 at 2:10 PM Ed Behn <e...@behn.us> wrote:
> >     Was there a resolution of this? I'm wondering if it is worth it for
> me to submit a PR for the next commitfest.
>
> Well, it seems like what you want is different than what I want, and
> what Tom wants is different from both of us. I'd like there to be a
> way forward here but at the moment I'm not quite sure what it is.
>
> --
> Robert Haas
> EDB: http://www.enterprisedb.com
>
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index 40dcbc7b67..126b7dc452 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -50,60 +50,6 @@
 #define NUMERIC_DEBUG
  */
 
-
-/* ----------
- * Local data types
- *
- * Numeric values are represented in a base-NBASE floating point format.
- * Each "digit" ranges from 0 to NBASE-1.  The type NumericDigit is signed
- * and wide enough to store a digit.  We assume that NBASE*NBASE can fit in
- * an int.  Although the purely calculational routines could handle any even
- * NBASE that's less than sqrt(INT_MAX), in practice we are only interested
- * in NBASE a power of ten, so that I/O conversions and decimal rounding
- * are easy.  Also, it's actually more efficient if NBASE is rather less than
- * sqrt(INT_MAX), so that there is "headroom" for mul_var and div_var to
- * postpone processing carries.
- *
- * Values of NBASE other than 10000 are considered of historical interest only
- * and are no longer supported in any sense; no mechanism exists for the client
- * to discover the base, so every client supporting binary mode expects the
- * base-10000 format.  If you plan to change this, also note the numeric
- * abbreviation code, which assumes NBASE=10000.
- * ----------
- */
-
-#if 0
-#define NBASE		10
-#define HALF_NBASE	5
-#define DEC_DIGITS	1			/* decimal digits per NBASE digit */
-#define MUL_GUARD_DIGITS	4	/* these are measured in NBASE digits */
-#define DIV_GUARD_DIGITS	8
-
-typedef signed char NumericDigit;
-#endif
-
-#if 0
-#define NBASE		100
-#define HALF_NBASE	50
-#define DEC_DIGITS	2			/* decimal digits per NBASE digit */
-#define MUL_GUARD_DIGITS	3	/* these are measured in NBASE digits */
-#define DIV_GUARD_DIGITS	6
-
-typedef signed char NumericDigit;
-#endif
-
-#if 1
-#define NBASE		10000
-#define HALF_NBASE	5000
-#define DEC_DIGITS	4			/* decimal digits per NBASE digit */
-#define MUL_GUARD_DIGITS	2	/* these are measured in NBASE digits */
-#define DIV_GUARD_DIGITS	4
-
-typedef int16 NumericDigit;
-#endif
-
-#define NBASE_SQR	(NBASE * NBASE)
-
 /*
  * The Numeric type as stored on disk.
  *
@@ -252,75 +198,6 @@ struct NumericData
 	 | ((n)->choice.n_short.n_header & NUMERIC_SHORT_WEIGHT_MASK)) \
 	: ((n)->choice.n_long.n_weight))
 
-/*
- * Maximum weight of a stored Numeric value (based on the use of int16 for the
- * weight in NumericLong).  Note that intermediate values held in NumericVar
- * and NumericSumAccum variables may have much larger weights.
- */
-#define NUMERIC_WEIGHT_MAX			PG_INT16_MAX
-
-/* ----------
- * NumericVar is the format we use for arithmetic.  The digit-array part
- * is the same as the NumericData storage format, but the header is more
- * complex.
- *
- * The value represented by a NumericVar is determined by the sign, weight,
- * ndigits, and digits[] array.  If it is a "special" value (NaN or Inf)
- * then only the sign field matters; ndigits should be zero, and the weight
- * and dscale fields are ignored.
- *
- * Note: the first digit of a NumericVar's value is assumed to be multiplied
- * by NBASE ** weight.  Another way to say it is that there are weight+1
- * digits before the decimal point.  It is possible to have weight < 0.
- *
- * buf points at the physical start of the palloc'd digit buffer for the
- * NumericVar.  digits points at the first digit in actual use (the one
- * with the specified weight).  We normally leave an unused digit or two
- * (preset to zeroes) between buf and digits, so that there is room to store
- * a carry out of the top digit without reallocating space.  We just need to
- * decrement digits (and increment weight) to make room for the carry digit.
- * (There is no such extra space in a numeric value stored in the database,
- * only in a NumericVar in memory.)
- *
- * If buf is NULL then the digit buffer isn't actually palloc'd and should
- * not be freed --- see the constants below for an example.
- *
- * dscale, or display scale, is the nominal precision expressed as number
- * of digits after the decimal point (it must always be >= 0 at present).
- * dscale may be more than the number of physically stored fractional digits,
- * implying that we have suppressed storage of significant trailing zeroes.
- * It should never be less than the number of stored digits, since that would
- * imply hiding digits that are present.  NOTE that dscale is always expressed
- * in *decimal* digits, and so it may correspond to a fractional number of
- * base-NBASE digits --- divide by DEC_DIGITS to convert to NBASE digits.
- *
- * rscale, or result scale, is the target precision for a computation.
- * Like dscale it is expressed as number of *decimal* digits after the decimal
- * point, and is always >= 0 at present.
- * Note that rscale is not stored in variables --- it's figured on-the-fly
- * from the dscales of the inputs.
- *
- * While we consistently use "weight" to refer to the base-NBASE weight of
- * a numeric value, it is convenient in some scale-related calculations to
- * make use of the base-10 weight (ie, the approximate log10 of the value).
- * To avoid confusion, such a decimal-units weight is called a "dweight".
- *
- * NB: All the variable-level functions are written in a style that makes it
- * possible to give one and the same variable as argument and destination.
- * This is feasible because the digit buffer is separate from the variable.
- * ----------
- */
-typedef struct NumericVar
-{
-	int			ndigits;		/* # of digits in digits[] - can be 0! */
-	int			weight;			/* weight of first digit */
-	int			sign;			/* NUMERIC_POS, _NEG, _NAN, _PINF, or _NINF */
-	int			dscale;			/* display scale */
-	NumericDigit *buf;			/* start of palloc'd space for digits[] */
-	NumericDigit *digits;		/* base-NBASE digits */
-} NumericVar;
-
-
 /* ----------
  * Data for generate_series
  * ----------
@@ -491,8 +368,6 @@ static void dump_var(const char *str, NumericVar *var);
 			 pfree(buf); \
 	} while (0)
 
-#define init_var(v)		memset(v, 0, sizeof(NumericVar))
-
 #define NUMERIC_DIGITS(num) (NUMERIC_HEADER_IS_SHORT(num) ? \
 	(num)->choice.n_short.n_data : (num)->choice.n_long.n_data)
 #define NUMERIC_NDIGITS(num) \
@@ -502,10 +377,6 @@ static void dump_var(const char *str, NumericVar *var);
 	(weight) <= NUMERIC_SHORT_WEIGHT_MAX && \
 	(weight) >= NUMERIC_SHORT_WEIGHT_MIN)
 
-static void alloc_var(NumericVar *var, int ndigits);
-static void free_var(NumericVar *var);
-static void zero_var(NumericVar *var);
-
 static bool set_var_from_str(const char *str, const char *cp,
 							 NumericVar *dest, const char **endptr,
 							 Node *escontext);
@@ -514,9 +385,6 @@ static bool set_var_from_non_decimal_integer_str(const char *str,
 												 int base, NumericVar *dest,
 												 const char **endptr,
 												 Node *escontext);
-static void set_var_from_num(Numeric num, NumericVar *dest);
-static void init_var_from_num(Numeric num, NumericVar *dest);
-static void set_var_from_var(const NumericVar *value, NumericVar *dest);
 static char *get_str_from_var(const NumericVar *var);
 static char *get_str_from_var_sci(const NumericVar *var, int rscale);
 
@@ -524,8 +392,6 @@ static void numericvar_serialize(StringInfo buf, const NumericVar *var);
 static void numericvar_deserialize(StringInfo buf, NumericVar *var);
 
 static Numeric duplicate_numeric(Numeric num);
-static Numeric make_result(const NumericVar *var);
-static Numeric make_result_opt_error(const NumericVar *var, bool *have_error);
 
 static bool apply_typmod(NumericVar *var, int32 typmod, Node *escontext);
 static bool apply_typmod_special(Numeric num, int32 typmod, Node *escontext);
@@ -7067,7 +6933,7 @@ dump_var(const char *str, NumericVar *var)
  *
  *	Allocate a digit buffer of ndigits digits (plus a spare digit for rounding)
  */
-static void
+void
 alloc_var(NumericVar *var, int ndigits)
 {
 	digitbuf_free(var->buf);
@@ -7083,7 +6949,7 @@ alloc_var(NumericVar *var, int ndigits)
  *
  *	Return the digit buffer of a variable to the free pool
  */
-static void
+void
 free_var(NumericVar *var)
 {
 	digitbuf_free(var->buf);
@@ -7099,7 +6965,7 @@ free_var(NumericVar *var)
  *	Set a variable to ZERO.
  *	Note: its dscale is not touched.
  */
-static void
+void
 zero_var(NumericVar *var)
 {
 	digitbuf_free(var->buf);
@@ -7534,7 +7400,7 @@ invalid_syntax:
  *
  *	Convert the packed db format into a variable
  */
-static void
+void
 set_var_from_num(Numeric num, NumericVar *dest)
 {
 	int			ndigits;
@@ -7565,7 +7431,7 @@ set_var_from_num(Numeric num, NumericVar *dest)
  *	propagate to the original Numeric! It's OK to use it as the destination
  *	argument of one of the calculational functions, though.
  */
-static void
+void
 init_var_from_num(Numeric num, NumericVar *dest)
 {
 	dest->ndigits = NUMERIC_NDIGITS(num);
@@ -7582,7 +7448,7 @@ init_var_from_num(Numeric num, NumericVar *dest)
  *
  *	Copy one variable into another
  */
-static void
+void
 set_var_from_var(const NumericVar *value, NumericVar *dest)
 {
 	NumericDigit *newbuf;
@@ -7896,7 +7762,7 @@ duplicate_numeric(Numeric num)
  *	If "have_error" isn't NULL, on overflow *have_error is set to true and
  *	NULL is returned.  This is helpful when caller needs to handle errors.
  */
-static Numeric
+Numeric
 make_result_opt_error(const NumericVar *var, bool *have_error)
 {
 	Numeric		result;
@@ -8005,7 +7871,7 @@ make_result_opt_error(const NumericVar *var, bool *have_error)
  *
  *	An interface to make_result_opt_error() without "have_error" argument.
  */
-static Numeric
+Numeric
 make_result(const NumericVar *var)
 {
 	return make_result_opt_error(var, NULL);
diff --git a/src/include/utils/numeric.h b/src/include/utils/numeric.h
index 9e79fc376c..32e77d2d13 100644
--- a/src/include/utils/numeric.h
+++ b/src/include/utils/numeric.h
@@ -42,6 +42,59 @@
 
 #define NUMERIC_MAX_RESULT_SCALE	(NUMERIC_MAX_PRECISION * 2)
 
+/* ----------
+ * Local data types
+ *
+ * Numeric values are represented in a base-NBASE floating point format.
+ * Each "digit" ranges from 0 to NBASE-1.  The type NumericDigit is signed
+ * and wide enough to store a digit.  We assume that NBASE*NBASE can fit in
+ * an int.  Although the purely calculational routines could handle any even
+ * NBASE that's less than sqrt(INT_MAX), in practice we are only interested
+ * in NBASE a power of ten, so that I/O conversions and decimal rounding
+ * are easy.  Also, it's actually more efficient if NBASE is rather less than
+ * sqrt(INT_MAX), so that there is "headroom" for mul_var and div_var to
+ * postpone processing carries.
+ *
+ * Values of NBASE other than 10000 are considered of historical interest only
+ * and are no longer supported in any sense; no mechanism exists for the client
+ * to discover the base, so every client supporting binary mode expects the
+ * base-10000 format.  If you plan to change this, also note the numeric
+ * abbreviation code, which assumes NBASE=10000.
+ * ----------
+ */
+
+#if 0
+#define NBASE		10
+#define HALF_NBASE	5
+#define DEC_DIGITS	1			/* decimal digits per NBASE digit */
+#define MUL_GUARD_DIGITS	4	/* these are measured in NBASE digits */
+#define DIV_GUARD_DIGITS	8
+
+typedef signed char NumericDigit;
+#endif
+
+#if 0
+#define NBASE		100
+#define HALF_NBASE	50
+#define DEC_DIGITS	2			/* decimal digits per NBASE digit */
+#define MUL_GUARD_DIGITS	3	/* these are measured in NBASE digits */
+#define DIV_GUARD_DIGITS	6
+
+typedef signed char NumericDigit;
+#endif
+
+#if 1
+#define NBASE		10000
+#define HALF_NBASE	5000
+#define DEC_DIGITS	4			/* decimal digits per NBASE digit */
+#define MUL_GUARD_DIGITS	2	/* these are measured in NBASE digits */
+#define DIV_GUARD_DIGITS	4
+
+typedef int16 NumericDigit;
+#endif
+
+#define NBASE_SQR	(NBASE * NBASE)
+
 /*
  * For inherently inexact calculations such as division and square root,
  * we try to get at least this many significant digits; the idea is to
@@ -49,8 +102,85 @@
  */
 #define NUMERIC_MIN_SIG_DIGITS		16
 
+/*
+ * sign field of NumericVar
+ */
+
+#define NUMERIC_POS      0x0000
+#define NUMERIC_NEG      0x4000
+#define NUMERIC_NAN      0xC000
+#define NUMERIC_PINF     0xD000
+#define NUMERIC_NINF     0xF000
+
+/*
+ * Maximum weight of a stored Numeric value (based on the use of int16 for the
+ * weight in NumericLong).  Note that intermediate values held in NumericVar
+ * and NumericSumAccum variables may have much larger weights.
+ */
+ #define NUMERIC_WEIGHT_MAX			PG_INT16_MAX
+
+/* ----------
+ * NumericVar is the format we use for arithmetic.  The digit-array part
+ * is the same as the NumericData storage format, but the header is more
+ * complex.
+ *
+ * The value represented by a NumericVar is determined by the sign, weight,
+ * ndigits, and digits[] array.  If it is a "special" value (NaN or Inf)
+ * then only the sign field matters; ndigits should be zero, and the weight
+ * and dscale fields are ignored.
+ *
+ * Note: the first digit of a NumericVar's value is assumed to be multiplied
+ * by NBASE ** weight.  Another way to say it is that there are weight+1
+ * digits before the decimal point.  It is possible to have weight < 0.
+ *
+ * buf points at the physical start of the palloc'd digit buffer for the
+ * NumericVar.  digits points at the first digit in actual use (the one
+ * with the specified weight).  We normally leave an unused digit or two
+ * (preset to zeroes) between buf and digits, so that there is room to store
+ * a carry out of the top digit without reallocating space.  We just need to
+ * decrement digits (and increment weight) to make room for the carry digit.
+ * (There is no such extra space in a numeric value stored in the database,
+ * only in a NumericVar in memory.)
+ *
+ * If buf is NULL then the digit buffer isn't actually palloc'd and should
+ * not be freed --- see the constants below for an example.
+ *
+ * dscale, or display scale, is the nominal precision expressed as number
+ * of digits after the decimal point (it must always be >= 0 at present).
+ * dscale may be more than the number of physically stored fractional digits,
+ * implying that we have suppressed storage of significant trailing zeroes.
+ * It should never be less than the number of stored digits, since that would
+ * imply hiding digits that are present.  NOTE that dscale is always expressed
+ * in *decimal* digits, and so it may correspond to a fractional number of
+ * base-NBASE digits --- divide by DEC_DIGITS to convert to NBASE digits.
+ *
+ * rscale, or result scale, is the target precision for a computation.
+ * Like dscale it is expressed as number of *decimal* digits after the decimal
+ * point, and is always >= 0 at present.
+ * Note that rscale is not stored in variables --- it's figured on-the-fly
+ * from the dscales of the inputs.
+ *
+ * While we consistently use "weight" to refer to the base-NBASE weight of
+ * a numeric value, it is convenient in some scale-related calculations to
+ * make use of the base-10 weight (ie, the approximate log10 of the value).
+ * To avoid confusion, such a decimal-units weight is called a "dweight".
+ *
+ * NB: All the variable-level functions are written in a style that makes it
+ * possible to give one and the same variable as argument and destination.
+ * This is feasible because the digit buffer is separate from the variable.
+ * ----------
+ */
+typedef struct NumericVar
+{
+	int			ndigits;		/* # of digits in digits[] - can be 0! */
+	int			weight;			/* weight of first digit */
+	int			sign;			/* NUMERIC_POS, _NEG, _NAN, _PINF, or _NINF */
+	int			dscale;			/* display scale */
+	NumericDigit *buf;			/* start of palloc'd space for digits[] */
+	NumericDigit *digits;		/* base-NBASE digits */
+} NumericVar;
+
 /* The actual contents of Numeric are private to numeric.c */
-struct NumericData;
 typedef struct NumericData *Numeric;
 
 /*
@@ -79,9 +209,23 @@ NumericGetDatum(Numeric X)
 #define PG_GETARG_NUMERIC_COPY(n) DatumGetNumericCopy(PG_GETARG_DATUM(n))
 #define PG_RETURN_NUMERIC(x)	  return NumericGetDatum(x)
 
+#define init_var(v)		memset(v, 0, sizeof(NumericVar))
+
 /*
  * Utility functions in numeric.c
  */
+
+extern void alloc_var(NumericVar *var, int ndigits);
+extern void free_var(NumericVar *var);
+extern void zero_var(NumericVar *var);
+
+extern void set_var_from_num(Numeric num, NumericVar *dest);
+extern void init_var_from_num(Numeric num, NumericVar *dest);
+extern void set_var_from_var(const NumericVar *value, NumericVar *dest);
+
+extern Numeric make_result(const NumericVar *var);
+extern Numeric make_result_opt_error(const NumericVar *var, bool *have_error);
+
 extern bool numeric_is_nan(Numeric num);
 extern bool numeric_is_inf(Numeric num);
 extern int32 numeric_maximum_size(int32 typmod);
#include "postgres.h"
#include "utils/numeric.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(display_numeric);
Datum display_numeric(PG_FUNCTION_ARGS)
{
    Numeric num = PG_GETARG_NUMERIC(0);
    NumericVar arg;

    init_var(&arg);
    set_var_from_num(num, &arg);

    ereport(NOTICE, errmsg("weight=%d", arg.weight));
    ereport(NOTICE, errmsg("dscale=%d", arg.dscale));

    switch(arg.sign)
    {
    case NUMERIC_POS:
        ereport(NOTICE, errmsg("Pos"));
        break;
    case NUMERIC_NEG:
        ereport(NOTICE, errmsg("Neg"));
        break;
    case NUMERIC_NAN:
        ereport(NOTICE, errmsg("NaN"));
        break;
    case NUMERIC_PINF:
        ereport(NOTICE, errmsg("+Inf"));
        break;
    case NUMERIC_NINF:
        ereport(NOTICE, errmsg("-Inf"));
        break;
    default:
        ereport(NOTICE, errmsg("SIGN=0x%x", arg.sign));
        break;
    }

    for (int i = 0; i < arg.ndigits; i++)
        ereport(NOTICE, errmsg(" %04d", arg.digits[i]));

    free_var(&arg);

    PG_RETURN_VOID();
}

PG_FUNCTION_INFO_V1(negate_numeric);
Datum negate_numeric(PG_FUNCTION_ARGS)
{
    Numeric num = PG_GETARG_NUMERIC(0);
    NumericVar arg;

    init_var(&arg);
    set_var_from_num(num, &arg);

    NumericVar result;
    init_var(&result);
    alloc_var(&result, arg.ndigits);

    switch(arg.sign)
    {
    case NUMERIC_POS:
        result.sign = NUMERIC_NEG;
        break;
    case NUMERIC_NEG:
        result.sign = NUMERIC_POS;
        break;
    case NUMERIC_NAN:
        result.sign = NUMERIC_NAN;
        break;
    case NUMERIC_PINF:
        result.sign = NUMERIC_NINF;
        break;
    case NUMERIC_NINF:
        result.sign = NUMERIC_PINF;
        break;
    default:
        ereport(ERROR, errmsg("Unknown sign"));
        break;
    }

    if(result.sign == NUMERIC_POS || result.sign == NUMERIC_NEG)
    {
        result.weight = arg.weight;
        result.dscale = arg.dscale;
    
        for (int i = 0; i < arg.ndigits; i++)
            result.digits[i] = arg.digits[i];
    }

    free_var(&arg);
    PG_RETURN_NUMERIC(make_result(&result));
}

Attachment: numeric_var_test.sql
Description: application/sql

Reply via email to