From cb607e569e78487f93ca20f7648ac962bffcf40f Mon Sep 17 00:00:00 2001
From: Ewan Young <kdbase.hack@gmail.com>
Date: Thu, 2 Jul 2026 04:12:51 +0800
Subject: [PATCH] Fix jsonpath .decimal() to honor silent mode

The jsonpath .decimal(precision[, scale]) method built its numeric typmod
by calling numerictypmodin() through DirectFunctionCall1(), which throws a
hard error for a precision outside 1..NUMERIC_MAX_PRECISION or a scale
outside NUMERIC_MIN_SCALE..NUMERIC_MAX_SCALE.  That error escaped silent
mode (silent => true, and the @? / @@ operators), unlike the neighbouring
error paths in .decimal() which already report softly, so e.g.

    select jsonb_path_query('1.5', '$.decimal(0)', '{}', true);

threw "NUMERIC precision 0 must be between 1 and 1000" instead of
returning no rows.

Factor the precision/scale range checks and typmod packing out of
numerictypmodin() into a new make_numeric_typmod_safe() that reports
errors through an optional ErrorSaveContext, and call it from both
numerictypmodin() (with a NULL escontext, preserving the existing hard
errors and messages) and the .decimal() code, which now passes its
ErrorSaveContext when not throwing errors.  This also lets .decimal()
drop the CString-array round-trip it used to reach numerictypmodin().

Oversight in 66ea94e8e606; same class as 954e57708ea6 (.split_part()).
---
 src/backend/utils/adt/jsonpath_exec.c        | 28 +++++------
 src/backend/utils/adt/numeric.c              | 52 +++++++++++---------
 src/include/utils/numeric.h                  |  2 +
 src/test/regress/expected/jsonb_jsonpath.out | 32 ++++++++++++
 src/test/regress/sql/jsonb_jsonpath.sql      |  7 +++
 5 files changed, 82 insertions(+), 39 deletions(-)

diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index 6cc2acb4254..2ab5defa175 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -1489,14 +1489,10 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp,
 				if (jsp->type == jpiDecimal && jsp->content.args.left)
 				{
 					Datum		numdatum;
-					Datum		dtypmod;
+					int32		dtypmod;
 					int32		precision;
 					int32		scale = 0;
 					bool		noerr;
-					ArrayType  *arrtypmod;
-					Datum		datums[2];
-					char		pstr[12];	/* sign, 10 digits and '\0' */
-					char		sstr[12];	/* sign, 10 digits and '\0' */
 					ErrorSaveContext escontext = {T_ErrorSaveContext};
 
 					jspGetLeftArg(jsp, &elem);
@@ -1527,22 +1523,21 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp,
 					}
 
 					/*
-					 * numerictypmodin() takes the precision and scale in the
-					 * form of CString arrays.
+					 * Pack the precision and scale into a numeric typmod.
+					 * An out-of-range precision or scale is reported softly
+					 * (via escontext) when not throwing errors, so that silent
+					 * mode is honored; this reuses numerictypmodin()'s error
+					 * messages.
 					 */
-					pg_ltoa(precision, pstr);
-					datums[0] = CStringGetDatum(pstr);
-					pg_ltoa(scale, sstr);
-					datums[1] = CStringGetDatum(sstr);
-					arrtypmod = construct_array_builtin(datums, 2, CSTRINGOID);
-
-					dtypmod = DirectFunctionCall1(numerictypmodin,
-												  PointerGetDatum(arrtypmod));
+					dtypmod = make_numeric_typmod_safe(precision, scale,
+													   jspThrowErrors(cxt) ? NULL : (Node *) &escontext);
+					if (escontext.error_occurred)
+						return jperError;
 
 					/* Convert numstr to Numeric with typmod */
 					Assert(numstr != NULL);
 					noerr = DirectInputFunctionCallSafe(numeric_in, numstr,
-														InvalidOid, DatumGetInt32(dtypmod),
+														InvalidOid, dtypmod,
 														(Node *) &escontext,
 														&numdatum);
 
@@ -1553,7 +1548,6 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp,
 													 numstr, jspOperationName(jsp->type), "numeric"))));
 
 					num = DatumGetNumeric(numdatum);
-					pfree(arrtypmod);
 				}
 
 				jbv.type = jbvNumeric;
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index cb23dfe9b95..db787a92105 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -1305,6 +1305,34 @@ numeric		(PG_FUNCTION_ARGS)
 	PG_RETURN_NUMERIC(new);
 }
 
+/*
+ * make_numeric_typmod_safe() -
+ *
+ *	Validate a numeric precision and scale and pack them into a typmod value.
+ *
+ *	If escontext points to an ErrorSaveContext, an out-of-range precision or
+ *	scale is reported softly and the function returns -1; otherwise it throws.
+ *	This lets callers that already hold the precision/scale as integers (e.g.
+ *	numerictypmodin() and jsonpath's .decimal() method) share the range checks
+ *	and error messages while supporting soft error reporting.
+ */
+int32
+make_numeric_typmod_safe(int32 precision, int32 scale, Node *escontext)
+{
+	if (precision < 1 || precision > NUMERIC_MAX_PRECISION)
+		ereturn(escontext, -1,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("NUMERIC precision %d must be between 1 and %d",
+						precision, NUMERIC_MAX_PRECISION)));
+	if (scale < NUMERIC_MIN_SCALE || scale > NUMERIC_MAX_SCALE)
+		ereturn(escontext, -1,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("NUMERIC scale %d must be between %d and %d",
+						scale, NUMERIC_MIN_SCALE, NUMERIC_MAX_SCALE)));
+
+	return make_numeric_typmod(precision, scale);
+}
+
 Datum
 numerictypmodin(PG_FUNCTION_ARGS)
 {
@@ -1316,29 +1344,9 @@ numerictypmodin(PG_FUNCTION_ARGS)
 	tl = ArrayGetIntegerTypmods(ta, &n);
 
 	if (n == 2)
-	{
-		if (tl[0] < 1 || tl[0] > NUMERIC_MAX_PRECISION)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("NUMERIC precision %d must be between 1 and %d",
-							tl[0], NUMERIC_MAX_PRECISION)));
-		if (tl[1] < NUMERIC_MIN_SCALE || tl[1] > NUMERIC_MAX_SCALE)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("NUMERIC scale %d must be between %d and %d",
-							tl[1], NUMERIC_MIN_SCALE, NUMERIC_MAX_SCALE)));
-		typmod = make_numeric_typmod(tl[0], tl[1]);
-	}
+		typmod = make_numeric_typmod_safe(tl[0], tl[1], NULL);
 	else if (n == 1)
-	{
-		if (tl[0] < 1 || tl[0] > NUMERIC_MAX_PRECISION)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("NUMERIC precision %d must be between 1 and %d",
-							tl[0], NUMERIC_MAX_PRECISION)));
-		/* scale defaults to zero */
-		typmod = make_numeric_typmod(tl[0], 0);
-	}
+		typmod = make_numeric_typmod_safe(tl[0], 0, NULL);	/* scale defaults to zero */
 	else
 	{
 		ereport(ERROR,
diff --git a/src/include/utils/numeric.h b/src/include/utils/numeric.h
index b1cf40ed9fd..ea289dabfeb 100644
--- a/src/include/utils/numeric.h
+++ b/src/include/utils/numeric.h
@@ -101,6 +101,8 @@ extern Numeric numeric_div_safe(Numeric num1, Numeric num2, Node *escontext);
 extern Numeric numeric_mod_safe(Numeric num1, Numeric num2, Node *escontext);
 extern int32 numeric_int4_safe(Numeric num, Node *escontext);
 extern int64 numeric_int8_safe(Numeric num, Node *escontext);
+extern int32 make_numeric_typmod_safe(int32 precision, int32 scale,
+									  Node *escontext);
 
 extern Numeric random_numeric(pg_prng_state *state,
 							  Numeric rmin, Numeric rmax);
diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out
index 81efebc3d0f..1bd2dd2e63f 100644
--- a/src/test/regress/expected/jsonb_jsonpath.out
+++ b/src/test/regress/expected/jsonb_jsonpath.out
@@ -2336,6 +2336,38 @@ select jsonb_path_query('12.3', '$.decimal(12345678901,1)');
 ERROR:  precision of jsonpath item method .decimal() is out of range for type integer
 select jsonb_path_query('12.3', '$.decimal(1,12345678901)');
 ERROR:  scale of jsonpath item method .decimal() is out of range for type integer
+-- an out-of-range precision or scale must be trappable in silent mode
+select jsonb_path_query('12345.678', '$.decimal(0, 6)', silent => true);
+ jsonb_path_query 
+------------------
+(0 rows)
+
+select jsonb_path_query('12345.678', '$.decimal(1001, 6)', silent => true);
+ jsonb_path_query 
+------------------
+(0 rows)
+
+select jsonb_path_query('1234.5678', '$.decimal(-6, +2)', silent => true);
+ jsonb_path_query 
+------------------
+(0 rows)
+
+select jsonb_path_query('1234.5678', '$.decimal(6, -1001)', silent => true);
+ jsonb_path_query 
+------------------
+(0 rows)
+
+select jsonb_path_query('1234.5678', '$.decimal(6, 1001)', silent => true);
+ jsonb_path_query 
+------------------
+(0 rows)
+
+select '1234.5678'::jsonb @? '$.decimal(0)';
+ ?column? 
+----------
+ 
+(1 row)
+
 -- Test .integer()
 select jsonb_path_query('null', '$.integer()');
 ERROR:  jsonpath item method .integer() can only be applied to a string or numeric value
diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql
index c1f4ab5422e..a338271bd75 100644
--- a/src/test/regress/sql/jsonb_jsonpath.sql
+++ b/src/test/regress/sql/jsonb_jsonpath.sql
@@ -523,6 +523,13 @@ select jsonb_path_query('0.0012345', '$.decimal(2,4)');
 select jsonb_path_query('-0.00123456', '$.decimal(2,-4)');
 select jsonb_path_query('12.3', '$.decimal(12345678901,1)');
 select jsonb_path_query('12.3', '$.decimal(1,12345678901)');
+-- an out-of-range precision or scale must be trappable in silent mode
+select jsonb_path_query('12345.678', '$.decimal(0, 6)', silent => true);
+select jsonb_path_query('12345.678', '$.decimal(1001, 6)', silent => true);
+select jsonb_path_query('1234.5678', '$.decimal(-6, +2)', silent => true);
+select jsonb_path_query('1234.5678', '$.decimal(6, -1001)', silent => true);
+select jsonb_path_query('1234.5678', '$.decimal(6, 1001)', silent => true);
+select '1234.5678'::jsonb @? '$.decimal(0)';
 
 -- Test .integer()
 select jsonb_path_query('null', '$.integer()');
-- 
2.47.3

