From 5c5948f61a156f3024bd76ee0ec6c2300b64e3f7 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Tue, 22 Jul 2025 11:11:07 -0400
Subject: [PATCH v1 3/3] Micro-optimize datatype conversions in
 datum_to_jsonb_internal.

The general case for converting to a JSONB numeric value is to run the
source datatype's output function and then numeric_in, but we can do
substantially better than that for integer and numeric source values.
This patch improves the speed of jsonb_agg by 30% for integer input,
and nearly 2X for numeric input.

Sadly, the obvious idea of using float4_numeric and float8_numeric
to speed up those cases doesn't work: they are actually slower than
the generic coerce-via-I/O method, and not by a small amount.
They might round off differently than this code has historically done,
too.  Leave that alone pending possible changes in those functions.

We can also do better than the existing code for text/varchar/bpchar
source data; this optimization is similar to one that already exists
in the json_agg() code.  That saves 20% or so for such inputs.

Also make a couple of other minor improvements, such as not giving
JSONTYPE_CAST its own special case outside the switch when it could
perfectly well be handled inside, and not using dubious string hacking
to detect infinity and NaN results.
---
 src/backend/utils/adt/jsonb.c | 115 ++++++++++++++++++++++++----------
 1 file changed, 81 insertions(+), 34 deletions(-)

diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index abe0d620566..0c964052958 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -19,6 +19,7 @@
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
 #include "utils/json.h"
 #include "utils/jsonb.h"
 #include "utils/jsonfuncs.h"
@@ -631,7 +632,8 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
 						bool key_scalar)
 {
 	char	   *outputstr;
-	bool		numeric_error;
+	Numeric		numeric_val;
+	bool		numeric_to_string;
 	JsonbValue	jb;
 	bool		scalar_jsonb = false;
 
@@ -656,9 +658,6 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
 	}
 	else
 	{
-		if (tcategory == JSONTYPE_CAST)
-			val = OidFunctionCall1(outfuncoid, val);
-
 		switch (tcategory)
 		{
 			case JSONTYPE_ARRAY:
@@ -682,41 +681,73 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
 				}
 				break;
 			case JSONTYPE_NUMERIC:
-				outputstr = OidOutputFunctionCall(outfuncoid, val);
 				if (key_scalar)
 				{
-					/* always quote keys */
+					/* always stringify keys */
+					numeric_to_string = true;
+					numeric_val = NULL; /* pacify stupider compilers */
+				}
+				else
+				{
+					Datum		numd;
+
+					switch (outfuncoid)
+					{
+						case F_NUMERIC_OUT:
+							numeric_val = DatumGetNumeric(val);
+							break;
+						case F_INT2OUT:
+							numeric_val = int64_to_numeric(DatumGetInt16(val));
+							break;
+						case F_INT4OUT:
+							numeric_val = int64_to_numeric(DatumGetInt32(val));
+							break;
+						case F_INT8OUT:
+							numeric_val = int64_to_numeric(DatumGetInt64(val));
+							break;
+#ifdef NOT_USED
+
+							/*
+							 * Ideally we'd short-circuit these two cases
+							 * using float[48]_numeric.  However, those
+							 * functions are currently slower than the generic
+							 * coerce-via-I/O approach.  And they may round
+							 * off differently.  Until/unless that gets fixed,
+							 * continue to use coerce-via-I/O for floats.
+							 */
+						case F_FLOAT4OUT:
+							numd = DirectFunctionCall1(float4_numeric, val);
+							numeric_val = DatumGetNumeric(numd);
+							break;
+						case F_FLOAT8OUT:
+							numd = DirectFunctionCall1(float8_numeric, val);
+							numeric_val = DatumGetNumeric(numd);
+							break;
+#endif
+						default:
+							outputstr = OidOutputFunctionCall(outfuncoid, val);
+							numd = DirectFunctionCall3(numeric_in,
+													   CStringGetDatum(outputstr),
+													   ObjectIdGetDatum(InvalidOid),
+													   Int32GetDatum(-1));
+							numeric_val = DatumGetNumeric(numd);
+							break;
+					}
+					/* Must convert to string if it's Inf or NaN */
+					numeric_to_string = (numeric_is_inf(numeric_val) ||
+										 numeric_is_nan(numeric_val));
+				}
+				if (numeric_to_string)
+				{
+					outputstr = OidOutputFunctionCall(outfuncoid, val);
 					jb.type = jbvString;
 					jb.val.string.len = strlen(outputstr);
 					jb.val.string.val = outputstr;
 				}
 				else
 				{
-					/*
-					 * Make it numeric if it's a valid JSON number, otherwise
-					 * a string. Invalid numeric output will always have an
-					 * 'N' or 'n' in it (I think).
-					 */
-					numeric_error = (strchr(outputstr, 'N') != NULL ||
-									 strchr(outputstr, 'n') != NULL);
-					if (!numeric_error)
-					{
-						Datum		numd;
-
-						jb.type = jbvNumeric;
-						numd = DirectFunctionCall3(numeric_in,
-												   CStringGetDatum(outputstr),
-												   ObjectIdGetDatum(InvalidOid),
-												   Int32GetDatum(-1));
-						jb.val.numeric = DatumGetNumeric(numd);
-						pfree(outputstr);
-					}
-					else
-					{
-						jb.type = jbvString;
-						jb.val.string.len = strlen(outputstr);
-						jb.val.string.val = outputstr;
-					}
+					jb.type = jbvNumeric;
+					jb.val.numeric = numeric_val;
 				}
 				break;
 			case JSONTYPE_DATE:
@@ -738,6 +769,9 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
 				jb.val.string.len = strlen(jb.val.string.val);
 				break;
 			case JSONTYPE_CAST:
+				/* cast to JSON, and then process as JSON */
+				val = OidFunctionCall1(outfuncoid, val);
+				/* FALL THROUGH */
 			case JSONTYPE_JSON:
 				{
 					/* parse the json right into the existing result object */
@@ -793,11 +827,24 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
 				}
 				break;
 			default:
-				outputstr = OidOutputFunctionCall(outfuncoid, val);
+				/* special-case text types to save useless palloc/memcpy ops */
+				if (outfuncoid == F_TEXTOUT ||
+					outfuncoid == F_VARCHAROUT ||
+					outfuncoid == F_BPCHAROUT)
+				{
+					text	   *txt = DatumGetTextPP(val);
+
+					jb.val.string.len = VARSIZE_ANY_EXHDR(txt);
+					jb.val.string.val = VARDATA_ANY(txt);
+				}
+				else
+				{
+					outputstr = OidOutputFunctionCall(outfuncoid, val);
+					jb.val.string.len = strlen(outputstr);
+					jb.val.string.val = outputstr;
+				}
 				jb.type = jbvString;
-				jb.val.string.len = strlen(outputstr);
 				(void) checkStringLen(jb.val.string.len, NULL);
-				jb.val.string.val = outputstr;
 				break;
 		}
 	}
-- 
2.43.5

