public inbox for [email protected]
help / color / mirror / Atom feedjson/jsonb cleanup + FmgrInfo caching
4+ messages / 3 participants
[nested] [flat]
* json/jsonb cleanup + FmgrInfo caching
@ 2026-07-02 16:25 Robert Haas <[email protected]>
0 siblings, 1 reply; 4+ messages in thread
From: Robert Haas @ 2026-07-02 16:25 UTC (permalink / raw)
To: pgsql-hackers
Hi,
While working on my "sandboxing untrusted code" project, I found
myself investigating how the JSON and JSONB code calls type output and
cast functions. I feel like this needs some cleanup in order to avoid
blocking that project, and it turns out that there are also
significant opportunities to improve performance, so here are some
patches. One caveat: one of these patches causes a small backward
compatibility break, because the current behavior is wrong. See below
for full details.
First, a quick performance demonstration:
CREATE TABLE hstores AS SELECT hstore('k', g::text) x FROM
generate_series(1,4000000) g;
SELECT any_value(json_build_object('a', x)) FROM hstores;
Unpatched, median-of-three runs was 398.571 ms. Patched, 193.714 ms.
That's a 2.05x speedup, which is the highest I observed in my test
queries. What I found is that this technique shows the largest gains
with user-defined types like hstore, smaller gains with built-in types
like int, and the smallest gains of all with container types such as a
user-defined record type. Generally, jsonb benefited more than json.
The aggregates - json_agg and jsonb_agg - showed very little benefit
or even small regressions, but I'm pretty confident that the
regressions are simply noise that goes away with a sufficiently large
number of sufficiently-careful test runs. Everything else gets faster,
often by 50%+. Leaving aside the aggregates which are expected to show
little or no benefit, here's one of the less-sympathetic cases:
CREATE TABLE ints AS SELECT x FROM generate_series(1,4000000) x;
SELECT any_value(to_json(x)) FROM ints;
The speedup is smaller here because it's json rather than jsonb and
because it's int rather than hstore, but it's still 117.461 ms
unpatched vs. 89.895 patched, a 30% speedup.
OK, now let's go through the patches:
0001 refactors the json_categorize_type() function to initialize an
FmgrInfo instead of returning a base function OID. All of the built-in
JSON aggregates are updated to cache this FmgrInfo across calls, but
it really doesn't save much. In the process of working on this
refactoring it came to light that the current behavior of
json_check_mutability() is incorrect: it erroneously treats record,
anyarray, and anycompatiblearray as immutable when in fact they should
be treated as stable. This refactoring preserves that incorrect
behavior.
0002 fixes the bug discovered during the development of 0001 by
removing the special case. AFAICT, the anyarray and anycompatiblearray
cases are unreachable, but the record case is reachable, and the
included test case shows how this could hypothetically matter. It
seems unlikely we'll inconvenience any significant number of users by
changing this, but in theory somebody's upgrade could fail.
0003 moves some code around to avoid problems with circular header
dependencies, creating new files jsontypes.c/h.
0004 refactors the SQL-level JSON constructors -- JSON_OBJECT,
JSON_ARRAY, and JSON_SCALAR -- to make use of the new type caching
infrastructure.
0005 refactors the SQL-callable functions similarly. This means
to_json(b), json(b)_build_object, and json(b)_build_array.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
[application/octet-stream] v1-0003-Create-jsontypes.c-h-and-use-that-to-tidy-up-a-fe.patch (18.7K, ../../CA+TgmoaiothgQrw9OtgsMzBUCnqJ2jdaGTbS6o3fkjCd+LfzWw@mail.gmail.com/2-v1-0003-Create-jsontypes.c-h-and-use-that-to-tidy-up-a-fe.patch)
download | inline diff:
From ee7db0d84bed0d9608555fc847d485ed51d98006 Mon Sep 17 00:00:00 2001
From: Robert Haas <[email protected]>
Date: Sat, 4 Apr 2026 18:22:33 -0400
Subject: [PATCH v1 3/5] Create jsontypes.c/h and use that to tidy up a few
things
To avoid a circular dependency with jsonb.h, datum_to_jsonb has
up until now been declared in jsonfuncs.h, because it depends on
JsonTypeCategory, which is also declared in jsonfuncs.h. This is
not great, because it's defined in jsonb.c. The natural way to
get this declaration into jsonb.h would be to make that file
include jsonfuncs.h, but that doesn't work because jsonfuncs.h
already includes jsonb.h.
The best solution seems to be to create a new header file, since over
time jsonfuncs.h has become cluttered with various kinds of unrelated
declarations. So, create jsontypes.h and move the declaration of
JsonTypeCategory there, along with the public functions that depend on
it, and move the corresponding C code to a new file jsontypes.c. This
new header file is then included by both json.h and jsonb.h, allowing
datum_to_json and datum_to_jsonb to be declared in json.h and jsonb.h,
matching where they are defined.
This also paves the way for broader use of JsonTypeCategory, which
would otherwise require increasingly ugly hacks.
---
src/backend/utils/adt/Makefile | 1 +
src/backend/utils/adt/jsonfuncs.c | 221 ---------------------------
src/backend/utils/adt/jsontypes.c | 244 ++++++++++++++++++++++++++++++
src/backend/utils/adt/meson.build | 1 +
src/include/utils/json.h | 3 +
src/include/utils/jsonb.h | 3 +
src/include/utils/jsonfuncs.h | 25 ---
src/include/utils/jsontypes.h | 41 +++++
8 files changed, 293 insertions(+), 246 deletions(-)
create mode 100644 src/backend/utils/adt/jsontypes.c
create mode 100644 src/include/utils/jsontypes.h
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 0c7621957c1..5d604850466 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -56,6 +56,7 @@ OBJS = \
jsonb_util.o \
jsonfuncs.o \
jsonbsubs.o \
+ jsontypes.o \
jsonpath.o \
jsonpath_exec.o \
jsonpath_gram.o \
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index c289dbae715..f931f388465 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -506,8 +506,6 @@ static JsonParseErrorType transform_string_values_object_field_start(void *state
static JsonParseErrorType transform_string_values_array_element_start(void *state, bool isnull);
static JsonParseErrorType transform_string_values_scalar(void *state, char *token, JsonTokenType tokentype);
-/* function supporting json_categorize_type/json_check_mutability */
-static Oid get_json_cast_for_type(Oid typoid);
/*
* pg_parse_json_or_errsave
@@ -5957,222 +5955,3 @@ json_get_first_token(text *json, bool throw_error)
return JSON_TOKEN_INVALID; /* invalid json */
}
-
-/*
- * Determine how we want to print values of a given type in datum_to_json(b).
- *
- * Given the datatype OID, return its JsonTypeCategory, as well as an FmgrInfo
- * for the type's output function or cast function. For categories that do not
- * require calling a function, outflinfo is not touched.
- */
-void
-json_categorize_type(Oid typoid, bool is_jsonb,
- JsonTypeCategory *tcategory, FmgrInfo *outflinfo)
-{
- bool use_type_output_function = false;
-
- /* Look through any domain */
- typoid = getBaseType(typoid);
-
- switch (typoid)
- {
- case BOOLOID:
- *tcategory = JSONTYPE_BOOL;
- break;
-
- case INT2OID:
- case INT4OID:
- case INT8OID:
- case FLOAT4OID:
- case FLOAT8OID:
- case NUMERICOID:
- use_type_output_function = true;
- *tcategory = JSONTYPE_NUMERIC;
- break;
-
- case DATEOID:
- *tcategory = JSONTYPE_DATE;
- break;
-
- case TIMESTAMPOID:
- *tcategory = JSONTYPE_TIMESTAMP;
- break;
-
- case TIMESTAMPTZOID:
- *tcategory = JSONTYPE_TIMESTAMPTZ;
- break;
-
- case JSONOID:
- use_type_output_function = !is_jsonb;
- *tcategory = JSONTYPE_JSON;
- break;
-
- case JSONBOID:
- use_type_output_function = !is_jsonb;
- *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON;
- break;
-
- default:
- /* Check for arrays and composites */
- if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID
- || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID)
- *tcategory = JSONTYPE_ARRAY;
- else if (type_is_rowtype(typoid)) /* includes RECORDOID */
- *tcategory = JSONTYPE_COMPOSITE;
- else
- {
- Oid castfunc = get_json_cast_for_type(typoid);
-
- if (OidIsValid(castfunc))
- {
- fmgr_info(castfunc, outflinfo);
- *tcategory = JSONTYPE_CAST;
- }
- else
- {
- use_type_output_function = true;
- *tcategory = JSONTYPE_OTHER;
- }
- }
- break;
- }
-
- if (use_type_output_function)
- {
- Oid typoutput;
- bool typisvarlena;
-
- getTypeOutputInfo(typoid, &typoutput, &typisvarlena);
- fmgr_info(typoutput, outflinfo);
- }
-}
-
-/*
- * Check whether a type conversion to JSON or JSONB involves any mutable
- * functions. This recurses into container types (arrays, composites,
- * ranges, multiranges, domains) to check their element/sub types.
- *
- * The caller must initialize *has_mutable to false before calling.
- * If any mutable function is found, *has_mutable is set to true.
- */
-void
-json_check_mutability(Oid typoid, bool *has_mutable)
-{
- char att_typtype = get_typtype(typoid);
-
- /* since this function recurses, it could be driven to stack overflow */
- check_stack_depth();
-
- Assert(has_mutable != NULL);
-
- if (*has_mutable)
- return;
-
- if (att_typtype == TYPTYPE_DOMAIN)
- {
- json_check_mutability(getBaseType(typoid), has_mutable);
- return;
- }
- else if (att_typtype == TYPTYPE_COMPOSITE)
- {
- /*
- * For a composite type, recurse into its attributes. Use the
- * typcache to avoid opening the relation directly.
- */
- TupleDesc tupdesc = lookup_rowtype_tupdesc(typoid, -1);
-
- for (int i = 0; i < tupdesc->natts; i++)
- {
- Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
-
- if (attr->attisdropped)
- continue;
-
- json_check_mutability(attr->atttypid, has_mutable);
- if (*has_mutable)
- break;
- }
- ReleaseTupleDesc(tupdesc);
- return;
- }
- else if (att_typtype == TYPTYPE_RANGE)
- {
- json_check_mutability(get_range_subtype(typoid), has_mutable);
- return;
- }
- else if (att_typtype == TYPTYPE_MULTIRANGE)
- {
- json_check_mutability(get_multirange_range(typoid), has_mutable);
- return;
- }
- else
- {
- Oid att_typelem = get_element_type(typoid);
-
- if (OidIsValid(att_typelem))
- {
- /* recurse into array element type */
- json_check_mutability(att_typelem, has_mutable);
- return;
- }
- }
-
- switch (typoid)
- {
- case BOOLOID:
- case INT2OID:
- case INT4OID:
- case INT8OID:
- case FLOAT4OID:
- case FLOAT8OID:
- case NUMERICOID:
- case JSONOID:
- case JSONBOID:
- /* known immutable */
- break;
- case DATEOID:
- case TIMESTAMPOID:
- case TIMESTAMPTZOID:
- *has_mutable = true;
- break;
- default:
- {
- Oid castfunc = get_json_cast_for_type(typoid);
- Oid funcoid;
-
- if (OidIsValid(castfunc))
- funcoid = castfunc;
- else
- {
- bool typisvarlena;
-
- getTypeOutputInfo(typoid, &funcoid, &typisvarlena);
- }
- if (func_volatile(funcoid) != PROVOLATILE_IMMUTABLE)
- *has_mutable = true;
- }
- break;
- }
-}
-
-/*
- * Return the OID of a cast function from typoid to JSON, or InvalidOid if
- * there is no such cast. As a matter of policy, we only consider explicit,
- * user-defined casts.
- */
-static Oid
-get_json_cast_for_type(Oid typoid)
-{
- if (typoid >= FirstNormalObjectId)
- {
- Oid castfunc;
- CoercionPathType ctype;
-
- ctype = find_coercion_pathway(JSONOID, typoid,
- COERCION_EXPLICIT,
- &castfunc);
- if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc))
- return castfunc;
- }
- return InvalidOid;
-}
diff --git a/src/backend/utils/adt/jsontypes.c b/src/backend/utils/adt/jsontypes.c
new file mode 100644
index 00000000000..adbaceebc8a
--- /dev/null
+++ b/src/backend/utils/adt/jsontypes.c
@@ -0,0 +1,244 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsontypes.c
+ * Functions for JSON type categorization.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/jsontypes.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/transam.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "parser/parse_coerce.h"
+#include "utils/jsontypes.h"
+#include "utils/lsyscache.h"
+#include "utils/typcache.h"
+
+static Oid get_json_cast_for_type(Oid typoid);
+
+/*
+ * Determine how we want to print values of a given type in datum_to_json(b).
+ *
+ * Given the datatype OID, return its JsonTypeCategory, as well as an FmgrInfo
+ * for the type's output function or cast function. For categories that do not
+ * require calling a function, outflinfo is not touched.
+ */
+void
+json_categorize_type(Oid typoid, bool is_jsonb,
+ JsonTypeCategory *tcategory, FmgrInfo *outflinfo)
+{
+ bool use_type_output_function = false;
+
+ /* Look through any domain */
+ typoid = getBaseType(typoid);
+
+ switch (typoid)
+ {
+ case BOOLOID:
+ *tcategory = JSONTYPE_BOOL;
+ break;
+
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ use_type_output_function = true;
+ *tcategory = JSONTYPE_NUMERIC;
+ break;
+
+ case DATEOID:
+ *tcategory = JSONTYPE_DATE;
+ break;
+
+ case TIMESTAMPOID:
+ *tcategory = JSONTYPE_TIMESTAMP;
+ break;
+
+ case TIMESTAMPTZOID:
+ *tcategory = JSONTYPE_TIMESTAMPTZ;
+ break;
+
+ case JSONOID:
+ use_type_output_function = !is_jsonb;
+ *tcategory = JSONTYPE_JSON;
+ break;
+
+ case JSONBOID:
+ use_type_output_function = !is_jsonb;
+ *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON;
+ break;
+
+ default:
+ /* Check for arrays and composites */
+ if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID
+ || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID)
+ *tcategory = JSONTYPE_ARRAY;
+ else if (type_is_rowtype(typoid)) /* includes RECORDOID */
+ *tcategory = JSONTYPE_COMPOSITE;
+ else
+ {
+ Oid castfunc = get_json_cast_for_type(typoid);
+
+ if (OidIsValid(castfunc))
+ {
+ fmgr_info(castfunc, outflinfo);
+ *tcategory = JSONTYPE_CAST;
+ }
+ else
+ {
+ use_type_output_function = true;
+ *tcategory = JSONTYPE_OTHER;
+ }
+ }
+ break;
+ }
+
+ if (use_type_output_function)
+ {
+ Oid typoutput;
+ bool typisvarlena;
+
+ getTypeOutputInfo(typoid, &typoutput, &typisvarlena);
+ fmgr_info(typoutput, outflinfo);
+ }
+}
+
+/*
+ * Check whether a type conversion to JSON or JSONB involves any mutable
+ * functions. This recurses into container types (arrays, composites,
+ * ranges, multiranges, domains) to check their element/sub types.
+ *
+ * The caller must initialize *has_mutable to false before calling.
+ * If any mutable function is found, *has_mutable is set to true.
+ */
+void
+json_check_mutability(Oid typoid, bool *has_mutable)
+{
+ char att_typtype = get_typtype(typoid);
+
+ /* since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ Assert(has_mutable != NULL);
+
+ if (*has_mutable)
+ return;
+
+ if (att_typtype == TYPTYPE_DOMAIN)
+ {
+ json_check_mutability(getBaseType(typoid), has_mutable);
+ return;
+ }
+ else if (att_typtype == TYPTYPE_COMPOSITE)
+ {
+ /*
+ * For a composite type, recurse into its attributes. Use the
+ * typcache to avoid opening the relation directly.
+ */
+ TupleDesc tupdesc = lookup_rowtype_tupdesc(typoid, -1);
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+ if (attr->attisdropped)
+ continue;
+
+ json_check_mutability(attr->atttypid, has_mutable);
+ if (*has_mutable)
+ break;
+ }
+ ReleaseTupleDesc(tupdesc);
+ return;
+ }
+ else if (att_typtype == TYPTYPE_RANGE)
+ {
+ json_check_mutability(get_range_subtype(typoid), has_mutable);
+ return;
+ }
+ else if (att_typtype == TYPTYPE_MULTIRANGE)
+ {
+ json_check_mutability(get_multirange_range(typoid), has_mutable);
+ return;
+ }
+ else
+ {
+ Oid att_typelem = get_element_type(typoid);
+
+ if (OidIsValid(att_typelem))
+ {
+ /* recurse into array element type */
+ json_check_mutability(att_typelem, has_mutable);
+ return;
+ }
+ }
+
+ switch (typoid)
+ {
+ case BOOLOID:
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ case JSONOID:
+ case JSONBOID:
+ /* known immutable */
+ break;
+ case DATEOID:
+ case TIMESTAMPOID:
+ case TIMESTAMPTZOID:
+ *has_mutable = true;
+ break;
+ default:
+ {
+ Oid castfunc = get_json_cast_for_type(typoid);
+ Oid funcoid;
+
+ if (OidIsValid(castfunc))
+ funcoid = castfunc;
+ else
+ {
+ bool typisvarlena;
+
+ getTypeOutputInfo(typoid, &funcoid, &typisvarlena);
+ }
+ if (func_volatile(funcoid) != PROVOLATILE_IMMUTABLE)
+ *has_mutable = true;
+ }
+ break;
+ }
+}
+
+/*
+ * Return the OID of a cast function from typoid to JSON, or InvalidOid if
+ * there is no such cast. As a matter of policy, we only consider explicit,
+ * user-defined casts.
+ */
+static Oid
+get_json_cast_for_type(Oid typoid)
+{
+ if (typoid >= FirstNormalObjectId)
+ {
+ Oid castfunc;
+ CoercionPathType ctype;
+
+ ctype = find_coercion_pathway(JSONOID, typoid,
+ COERCION_EXPLICIT,
+ &castfunc);
+ if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc))
+ return castfunc;
+ }
+ return InvalidOid;
+}
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index d793f8145f6..be8411c66f2 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -55,6 +55,7 @@ backend_sources += files(
'jsonb_util.c',
'jsonbsubs.c',
'jsonfuncs.c',
+ 'jsontypes.c',
'jsonpath.c',
'jsonpath_exec.c',
'like.c',
diff --git a/src/include/utils/json.h b/src/include/utils/json.h
index 2f4be40518d..31f88fbbff6 100644
--- a/src/include/utils/json.h
+++ b/src/include/utils/json.h
@@ -15,8 +15,11 @@
#define JSON_H
#include "lib/stringinfo.h"
+#include "utils/jsontypes.h"
/* functions in json.c */
+extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory,
+ FmgrInfo *outflinfo);
extern void composite_to_json(Datum composite, StringInfo result,
bool use_line_feeds);
extern void escape_json(StringInfo buf, const char *str);
diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index ca13efba0fb..67e4675a908 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -14,6 +14,7 @@
#include "lib/stringinfo.h"
#include "utils/array.h"
+#include "utils/jsontypes.h"
#include "utils/numeric.h"
/* Tokens used when sequentially processing a jsonb value */
@@ -457,6 +458,8 @@ extern Datum jsonb_set_element(Jsonb *jb, const Datum *path, int path_len,
JsonbValue *newval);
extern Datum jsonb_get_element(Jsonb *jb, const Datum *path, int npath,
bool *isnull, bool as_text);
+extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
+ FmgrInfo *outflinfo);
extern bool to_jsonb_is_immutable(Oid typoid);
extern Datum jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls,
const Oid *types, bool absent_on_null,
diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h
index 96409557f29..8db155d6d23 100644
--- a/src/include/utils/jsonfuncs.h
+++ b/src/include/utils/jsonfuncs.h
@@ -64,31 +64,6 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state,
extern text *transform_json_string_values(text *json, void *action_state,
JsonTransformStringValuesAction transform_action);
-/* Type categories returned by json_categorize_type */
-typedef enum
-{
- JSONTYPE_NULL, /* null, so we didn't bother to identify */
- JSONTYPE_BOOL, /* boolean (built-in types only) */
- JSONTYPE_NUMERIC, /* numeric (ditto) */
- JSONTYPE_DATE, /* we use special formatting for datetimes */
- JSONTYPE_TIMESTAMP,
- JSONTYPE_TIMESTAMPTZ,
- JSONTYPE_JSON, /* JSON (and JSONB, if not is_jsonb) */
- JSONTYPE_JSONB, /* JSONB (if is_jsonb) */
- JSONTYPE_ARRAY, /* array */
- JSONTYPE_COMPOSITE, /* composite */
- JSONTYPE_CAST, /* something with an explicit cast to JSON */
- JSONTYPE_OTHER, /* all else */
-} JsonTypeCategory;
-
-extern void json_categorize_type(Oid typoid, bool is_jsonb,
- JsonTypeCategory *tcategory,
- FmgrInfo *outflinfo);
-extern void json_check_mutability(Oid typoid, bool *has_mutable);
-extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory,
- FmgrInfo *outflinfo);
-extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
- FmgrInfo *outflinfo);
extern Datum jsonb_from_text(text *js, bool unique_keys);
extern Datum json_populate_type(Datum json_val, Oid json_type,
diff --git a/src/include/utils/jsontypes.h b/src/include/utils/jsontypes.h
new file mode 100644
index 00000000000..4fe3911f69c
--- /dev/null
+++ b/src/include/utils/jsontypes.h
@@ -0,0 +1,41 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsontypes.h
+ * Declarations for JSON type categorization.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/jsontypes.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef JSONTYPES_H
+#define JSONTYPES_H
+
+#include "fmgr.h"
+
+/* Type categories returned by json_categorize_type */
+typedef enum
+{
+ JSONTYPE_NULL, /* null, so we didn't bother to identify */
+ JSONTYPE_BOOL, /* boolean (built-in types only) */
+ JSONTYPE_NUMERIC, /* numeric (ditto) */
+ JSONTYPE_DATE, /* we use special formatting for datetimes */
+ JSONTYPE_TIMESTAMP,
+ JSONTYPE_TIMESTAMPTZ,
+ JSONTYPE_JSON, /* JSON (and JSONB, if not is_jsonb) */
+ JSONTYPE_JSONB, /* JSONB (if is_jsonb) */
+ JSONTYPE_ARRAY, /* array */
+ JSONTYPE_COMPOSITE, /* composite */
+ JSONTYPE_CAST, /* something with an explicit cast to JSON */
+ JSONTYPE_OTHER, /* all else */
+} JsonTypeCategory;
+
+extern void json_categorize_type(Oid typoid, bool is_jsonb,
+ JsonTypeCategory *tcategory,
+ FmgrInfo *outflinfo);
+extern void json_check_mutability(Oid typoid, bool *has_mutable);
+
+#endif /* JSONTYPES_H */
--
2.50.1 (Apple Git-155)
[application/octet-stream] v1-0005-Cache-JSON-type-information-in-various-SQL-callab.patch (12.6K, ../../CA+TgmoaiothgQrw9OtgsMzBUCnqJ2jdaGTbS6o3fkjCd+LfzWw@mail.gmail.com/3-v1-0005-Cache-JSON-type-information-in-various-SQL-callab.patch)
download | inline diff:
From a79ab58fc4249de1af6c6805622bc10e8413a71f Mon Sep 17 00:00:00 2001
From: Robert Haas <[email protected]>
Date: Thu, 2 Jul 2026 10:36:13 -0400
Subject: [PATCH v1 5/5] Cache JSON type information in various SQL-callable
functions.
to_json, json_build_array, json_build_object, to_jsonb,
jsonb_build_array, and jsonb_build_object now cache any needed
JsonTypeCategory and associated FmgrInfo in fn_extra, rather than
recomputing it for every call.
This may significantly improve performance in queries which
execute these functions repeatedly.
---
src/backend/utils/adt/json.c | 46 ++++-----
src/backend/utils/adt/jsonb.c | 44 ++++-----
src/backend/utils/adt/jsontypes.c | 153 ++++++++++++++++++++++++++++++
src/include/utils/jsontypes.h | 15 +++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 207 insertions(+), 52 deletions(-)
diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c
index 24b41f0a376..e1ff6d9f94b 100644
--- a/src/backend/utils/adt/json.c
+++ b/src/backend/utils/adt/json.c
@@ -676,18 +676,26 @@ to_json(PG_FUNCTION_ARGS)
{
Datum val = PG_GETARG_DATUM(0);
Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
- JsonTypeCategory tcategory;
- FmgrInfo outflinfo;
+ JsonTypeCache *jcache;
if (val_type == InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("could not determine input data type")));
- json_categorize_type(val_type, false,
- &tcategory, &outflinfo);
+ if (fcinfo->flinfo->fn_extra == NULL)
+ {
+ MemoryContext oldcontext;
- PG_RETURN_DATUM(datum_to_json(val, tcategory, &outflinfo));
+ oldcontext = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt);
+ jcache = json_build_type_cache(false, 1, &val_type);
+ fcinfo->flinfo->fn_extra = jcache;
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ jcache = fcinfo->flinfo->fn_extra;
+
+ PG_RETURN_DATUM(datum_to_json(val, jcache->categories[0], &jcache->flinfos[0]));
}
/*
@@ -1272,23 +1280,16 @@ json_build_object(PG_FUNCTION_ARGS)
{
Datum *args;
bool *nulls;
- Oid *types;
int nargs;
JsonTypeCategory *categories;
FmgrInfo *outflinfos;
- /* build argument values to build the object */
- nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
-
+ nargs = json_extract_variadic_args(false, fcinfo, &args, &nulls,
+ &categories, &outflinfos);
if (nargs < 0)
PG_RETURN_NULL();
-
- categories = palloc_array(JsonTypeCategory, nargs);
- outflinfos = palloc0_array(FmgrInfo, nargs);
- for (int i = 0; i < nargs; i++)
- json_categorize_type(types[i], false,
- &categories[i], &outflinfos[i]);
+ if (nargs == 0)
+ PG_RETURN_TEXT_P(cstring_to_text_with_len("{}", 2));
PG_RETURN_DATUM(json_build_object_worker(nargs, args, nulls,
categories, outflinfos,
@@ -1341,23 +1342,18 @@ json_build_array(PG_FUNCTION_ARGS)
{
Datum *args;
bool *nulls;
- Oid *types;
int nargs;
JsonTypeCategory *categories;
FmgrInfo *outflinfos;
/* build argument values to build the array */
- nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
+ nargs = json_extract_variadic_args(false, fcinfo, &args, &nulls,
+ &categories, &outflinfos);
if (nargs < 0)
PG_RETURN_NULL();
-
- categories = palloc_array(JsonTypeCategory, nargs);
- outflinfos = palloc0_array(FmgrInfo, nargs);
- for (int i = 0; i < nargs; i++)
- json_categorize_type(types[i], false,
- &categories[i], &outflinfos[i]);
+ if (nargs == 0)
+ PG_RETURN_TEXT_P(cstring_to_text_with_len("[]", 2));
PG_RETURN_DATUM(json_build_array_worker(nargs, args, nulls,
categories, outflinfos,
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 56e94acd2a8..6876ef22680 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -1059,18 +1059,26 @@ to_jsonb(PG_FUNCTION_ARGS)
{
Datum val = PG_GETARG_DATUM(0);
Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
- JsonTypeCategory tcategory;
- FmgrInfo outflinfo;
+ JsonTypeCache *jcache;
if (val_type == InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("could not determine input data type")));
- json_categorize_type(val_type, true,
- &tcategory, &outflinfo);
+ if (fcinfo->flinfo->fn_extra == NULL)
+ {
+ MemoryContext oldcontext;
- PG_RETURN_DATUM(datum_to_jsonb(val, tcategory, &outflinfo));
+ oldcontext = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt);
+ jcache = json_build_type_cache(true, 1, &val_type);
+ fcinfo->flinfo->fn_extra = jcache;
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ jcache = fcinfo->flinfo->fn_extra;
+
+ PG_RETURN_DATUM(datum_to_jsonb(val, jcache->categories[0], &jcache->flinfos[0]));
}
/*
@@ -1157,24 +1165,15 @@ jsonb_build_object(PG_FUNCTION_ARGS)
{
Datum *args;
bool *nulls;
- Oid *types;
int nargs;
JsonTypeCategory *categories;
FmgrInfo *outflinfos;
- /* build argument values to build the object */
- nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
-
+ nargs = json_extract_variadic_args(true, fcinfo, &args, &nulls,
+ &categories, &outflinfos);
if (nargs < 0)
PG_RETURN_NULL();
- categories = palloc_array(JsonTypeCategory, nargs);
- outflinfos = palloc0_array(FmgrInfo, nargs);
- for (int i = 0; i < nargs; i++)
- json_categorize_type(types[i], true,
- &categories[i], &outflinfos[i]);
-
PG_RETURN_DATUM(jsonb_build_object_worker(nargs, args, nulls,
categories, outflinfos,
false, false));
@@ -1230,24 +1229,15 @@ jsonb_build_array(PG_FUNCTION_ARGS)
{
Datum *args;
bool *nulls;
- Oid *types;
int nargs;
JsonTypeCategory *categories;
FmgrInfo *outflinfos;
- /* build argument values to build the array */
- nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
-
+ nargs = json_extract_variadic_args(true, fcinfo, &args, &nulls,
+ &categories, &outflinfos);
if (nargs < 0)
PG_RETURN_NULL();
- categories = palloc_array(JsonTypeCategory, nargs);
- outflinfos = palloc0_array(FmgrInfo, nargs);
- for (int i = 0; i < nargs; i++)
- json_categorize_type(types[i], true,
- &categories[i], &outflinfos[i]);
-
PG_RETURN_DATUM(jsonb_build_array_worker(nargs, args, nulls,
categories, outflinfos,
false));
diff --git a/src/backend/utils/adt/jsontypes.c b/src/backend/utils/adt/jsontypes.c
index adbaceebc8a..8af1f43d09d 100644
--- a/src/backend/utils/adt/jsontypes.c
+++ b/src/backend/utils/adt/jsontypes.c
@@ -16,8 +16,10 @@
#include "access/transam.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
+#include "fmgr.h"
#include "miscadmin.h"
#include "parser/parse_coerce.h"
+#include "utils/array.h"
#include "utils/jsontypes.h"
#include "utils/lsyscache.h"
#include "utils/typcache.h"
@@ -242,3 +244,154 @@ get_json_cast_for_type(Oid typoid)
}
return InvalidOid;
}
+
+/*
+ * Run json_categorize_type() over an array of type OIDs and build a cache
+ * from the results.
+ */
+JsonTypeCache *
+json_build_type_cache(bool is_jsonb, int nargs, Oid *types)
+{
+ JsonTypeCache *jcache = palloc_object(JsonTypeCache);
+
+ jcache->nargs = nargs;
+ jcache->categories = palloc_array(JsonTypeCategory, nargs);
+ jcache->flinfos = palloc0_array(FmgrInfo, nargs);
+
+ for (int i = 0; i < nargs; ++i)
+ json_categorize_type(types[i], is_jsonb, &jcache->categories[i],
+ &jcache->flinfos[i]);
+
+ return jcache;
+}
+
+/*
+ * Extract all the arguments in a possibly-variadic FunctionCallInfo, producing
+ * arrays of datums and null flags, and also categorize those arguments using
+ * json_categorize_type, caching state in fcinfo->fn_extra to improve performance.
+ *
+ * The return value is the number of arguments that the caller should process, or
+ * -1 if the function was called using the VARIADIC syntax with a NULL array. If
+ * the return value is >0, *args and *nulls are set to palloc'd arrays of the
+ * extracted arguments, and *categories and *outflinfos are set to arrays which
+ * hold the results of json_categorize_type for the corresponding argument position.
+ */
+int
+json_extract_variadic_args(bool is_jsonb,
+ FunctionCallInfo fcinfo,
+ Datum **args, bool **nulls,
+ JsonTypeCategory **categories,
+ FmgrInfo **outflinfos)
+{
+ int nargs;
+
+ if (!get_fn_expr_variadic(fcinfo->flinfo))
+ {
+ JsonTypeCache *jcache;
+
+ nargs = PG_NARGS();
+ if (nargs == 0)
+ return 0;
+
+ /* Build a cache on first call, to speed up future calls. */
+ if (fcinfo->flinfo->fn_extra == NULL)
+ {
+ MemoryContext oldcontext;
+ Oid *types;
+
+ types = palloc_array(Oid, nargs);
+ for (int i = 0; i < nargs; ++i)
+ types[i] = get_fn_expr_argtype(fcinfo->flinfo, i);
+
+ oldcontext = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt);
+ jcache = json_build_type_cache(is_jsonb, nargs, types);
+ fcinfo->flinfo->fn_extra = jcache;
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ jcache = fcinfo->flinfo->fn_extra;
+
+ /* Get category and flinfo arrays from cache. */
+ *categories = jcache->categories;
+ *outflinfos = jcache->flinfos;
+
+ /* Extract arguments and isnull flags. */
+ *args = palloc_array(Datum, nargs);
+ *nulls = palloc_array(bool, nargs);
+ for (int i = 0; i < nargs; ++i)
+ {
+ if (PG_ARGISNULL(i))
+ {
+ (*nulls)[i] = true;
+ (*args)[i] = (Datum) 0;
+ }
+ else
+ {
+ (*nulls)[i] = false;
+ (*args)[i] = PG_GETARG_DATUM(i);
+ }
+ }
+ }
+ else if (PG_ARGISNULL(0))
+ {
+ /* Special case: VARIADIC NULL::sometype[] */
+ nargs = -1;
+ }
+ else
+ {
+ ArrayType *variadic_array = PG_GETARG_ARRAYTYPE_P(0);
+ Oid variadic_element_type = ARR_ELEMTYPE(variadic_array);
+ bool typbyval;
+ char typalign;
+ int16 typlen;
+ JsonTypeCache *jcache;
+
+ /* Deconstruct the array. */
+ Assert(PG_NARGS() == 1);
+ get_typlenbyvalalign(variadic_element_type, &typlen, &typbyval, &typalign);
+ deconstruct_array(variadic_array, variadic_element_type, typlen, typbyval,
+ typalign, args, nulls, &nargs);
+ if (nargs == 0)
+ return 0;
+
+ /* Build cache for the array element type on first pass. */
+ if (fcinfo->flinfo->fn_extra == NULL)
+ {
+ MemoryContext oldcontext;
+
+ oldcontext = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt);
+ jcache = json_build_type_cache(is_jsonb, 1, &variadic_element_type);
+ fcinfo->flinfo->fn_extra = jcache;
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ jcache = fcinfo->flinfo->fn_extra;
+
+ /*
+ * When a VARIADIC array is passed, there's only one argument data type,
+ * but the caller expects category and flinfo arrays of the same length as
+ * the number of arguments, and the number of arguments can vary on every
+ * call. To gain as much performance as we can without complicating code
+ * elsewhere, save the single data type in the cache and then build out
+ * arrays of the requisite length by copying.
+ */
+ *categories = palloc_array(JsonTypeCategory, nargs);
+ *outflinfos = palloc0_array(FmgrInfo, nargs);
+ if (OidIsValid(jcache->flinfos[0].fn_oid))
+ {
+ for (int i = 0; i < nargs; ++i)
+ {
+ (*categories)[i] = jcache->categories[0];
+ fmgr_info_copy(&(*outflinfos)[i], &jcache->flinfos[0],
+ CurrentMemoryContext);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < nargs; ++i)
+ (*categories)[i] = jcache->categories[0];
+ }
+ }
+
+ return nargs;
+}
diff --git a/src/include/utils/jsontypes.h b/src/include/utils/jsontypes.h
index 4fe3911f69c..0372bf96418 100644
--- a/src/include/utils/jsontypes.h
+++ b/src/include/utils/jsontypes.h
@@ -33,9 +33,24 @@ typedef enum
JSONTYPE_OTHER, /* all else */
} JsonTypeCategory;
+typedef struct
+{
+ int nargs;
+ JsonTypeCategory *categories;
+ FmgrInfo *flinfos;
+} JsonTypeCache;
+
extern void json_categorize_type(Oid typoid, bool is_jsonb,
JsonTypeCategory *tcategory,
FmgrInfo *outflinfo);
extern void json_check_mutability(Oid typoid, bool *has_mutable);
+extern JsonTypeCache *json_build_type_cache(bool is_jsonb,
+ int nargs,
+ Oid *types);
+extern int json_extract_variadic_args(bool is_jsonb,
+ FunctionCallInfo fcinfo,
+ Datum **args, bool **nulls,
+ JsonTypeCategory **categories,
+ FmgrInfo **outflinfos);
#endif /* JSONTYPES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3a2720fb5f9..ca9726c4931 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1509,6 +1509,7 @@ JsonTablePlanState
JsonTableSiblingJoin
JsonTokenType
JsonTransformStringValuesAction
+JsonTypeCache
JsonTypeCategory
JsonUniqueBuilderState
JsonUniqueCheckState
--
2.50.1 (Apple Git-155)
[application/octet-stream] v1-0002-Don-t-treat-record-json-b-conversions-as-immutabl.patch (3.0K, ../../CA+TgmoaiothgQrw9OtgsMzBUCnqJ2jdaGTbS6o3fkjCd+LfzWw@mail.gmail.com/4-v1-0002-Don-t-treat-record-json-b-conversions-as-immutabl.patch)
download | inline diff:
From 85557388d40d6d3fe3e4ac91ca7b97fe27e29739 Mon Sep 17 00:00:00 2001
From: Robert Haas <[email protected]>
Date: Wed, 1 Jul 2026 10:29:32 -0400
Subject: [PATCH v1 2/5] Don't treat record -> json(b) conversions as
immutable.
The existing behavior is accidental and incorrect. Output functions
are required to be stable, but only some of them are immutable; hence,
output functions for container types should be treated as stable.
The JSON code has not gotten this right. A recent refactoring surfaced
the defect but preserved it via a special-case exception; this commit
removes that exception and updates the test accordingly.
This also removes the corresponding exceptions for anyarray and
anycompatiblearray, which are wrong for the same reasons, but removing
those exceptions is not expected to have any significant user-visible
consequences. Values of type record can be constructed using
ROW(), but there's no equivalent for anyarray or anycompatiblearray.
Note that the change for type record is a (minor) backward compatibility
break that could block upgrades; see the regression test for an example
of a case that would break.
---
src/backend/utils/adt/jsonfuncs.c | 6 ------
src/test/regress/expected/sqljson.out | 2 +-
src/test/regress/sql/sqljson.sql | 1 -
3 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index f769e90ba5e..c289dbae715 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -6135,12 +6135,6 @@ json_check_mutability(Oid typoid, bool *has_mutable)
case TIMESTAMPTZOID:
*has_mutable = true;
break;
- case RECORDOID:
- case ANYARRAYOID:
- case ANYCOMPATIBLEARRAYOID:
- /* XXX incorrectly treated as known immutable */
- break;
-
default:
{
Oid castfunc = get_json_cast_for_type(typoid);
diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out
index fedbeca7a21..f5504c11f66 100644
--- a/src/test/regress/expected/sqljson.out
+++ b/src/test/regress/expected/sqljson.out
@@ -1759,5 +1759,5 @@ SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': 1 RETURNING text) FORMAT JSON);
DROP FUNCTION volatile_one, stable_one;
-- JSON_ARRAY(ROW(...)) is not immutable, so it should be impossible to
-- create a generated column.
--- XXX: Currently, this is erroneously allowed.
CREATE TABLE json_array_of_row (a int, j json GENERATED ALWAYS AS (JSON_ARRAY(ROW(a))) STORED);
+ERROR: generation expression is not immutable
diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql
index 77910137573..0762af26188 100644
--- a/src/test/regress/sql/sqljson.sql
+++ b/src/test/regress/sql/sqljson.sql
@@ -709,5 +709,4 @@ DROP FUNCTION volatile_one, stable_one;
-- JSON_ARRAY(ROW(...)) is not immutable, so it should be impossible to
-- create a generated column.
--- XXX: Currently, this is erroneously allowed.
CREATE TABLE json_array_of_row (a int, j json GENERATED ALWAYS AS (JSON_ARRAY(ROW(a))) STORED);
--
2.50.1 (Apple Git-155)
[application/octet-stream] v1-0004-Allow-JSON_OBJECT-JSON_ARRAY-JSON_SCALAR-to-cache.patch (18.1K, ../../CA+TgmoaiothgQrw9OtgsMzBUCnqJ2jdaGTbS6o3fkjCd+LfzWw@mail.gmail.com/5-v1-0004-Allow-JSON_OBJECT-JSON_ARRAY-JSON_SCALAR-to-cache.patch)
download | inline diff:
From b7b25c5f168a9e79aad5897286f98b722b61cdbe Mon Sep 17 00:00:00 2001
From: Robert Haas <[email protected]>
Date: Sat, 4 Apr 2026 19:16:26 -0400
Subject: [PATCH v1 4/5] Allow JSON_OBJECT, JSON_ARRAY, JSON_SCALAR to cache
JSON type information.
This may significantly improve performance in queries which
execute these functions repeatedly.
---
src/backend/executor/execExpr.c | 28 ++++-----
src/backend/executor/execExprInterp.c | 15 ++---
src/backend/utils/adt/json.c | 86 +++++++++++++-------------
src/backend/utils/adt/jsonb.c | 88 +++++++++++++--------------
src/include/executor/execExpr.h | 9 +--
src/include/utils/json.h | 15 +++--
src/include/utils/jsonb.h | 15 +++--
7 files changed, 129 insertions(+), 127 deletions(-)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 871fd17e1ca..766cd9448f6 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -2399,15 +2399,12 @@ ExecInitExprRec(Expr *node, ExprState *state,
jcstate->constructor = ctor;
jcstate->arg_values = palloc_array(Datum, nargs);
jcstate->arg_nulls = palloc_array(bool, nargs);
- jcstate->arg_types = palloc_array(Oid, nargs);
jcstate->nargs = nargs;
foreach(lc, args)
{
Expr *arg = (Expr *) lfirst(lc);
- jcstate->arg_types[argno] = exprType((Node *) arg);
-
if (IsA(arg, Const))
{
/* Don't evaluate const arguments every round */
@@ -2425,25 +2422,28 @@ ExecInitExprRec(Expr *node, ExprState *state,
argno++;
}
- /* prepare type cache for datum_to_json[b]() */
- if (ctor->type == JSCTOR_JSON_SCALAR)
+ /*
+ * Prepare type cache for json_build_*_worker and
+ * datum_to_json[b], which are used for SCALAR, OBJECT,
+ * and ARRAY constructors.
+ */
+ if (ctor->type != JSCTOR_JSON_PARSE)
{
bool is_jsonb =
ctor->returning->format->format_type == JS_FORMAT_JSONB;
- jcstate->arg_type_cache =
- palloc(sizeof(*jcstate->arg_type_cache) * nargs);
+ jcstate->arg_categories =
+ palloc_array(JsonTypeCategory, nargs);
+ jcstate->arg_outflinfos =
+ palloc0_array(FmgrInfo, nargs);
for (int i = 0; i < nargs; i++)
{
- JsonTypeCategory category;
- Oid typid = jcstate->arg_types[i];
-
- json_categorize_type(typid, is_jsonb,
- &category,
- &jcstate->arg_type_cache[i].outflinfo);
+ Node *arg = (Node *) list_nth(args, i);
- jcstate->arg_type_cache[i].category = (int) category;
+ json_categorize_type(exprType(arg), is_jsonb,
+ &jcstate->arg_categories[i],
+ &jcstate->arg_outflinfos[i]);
}
}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 1ebfb190e67..a57be73be47 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4745,7 +4745,8 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op,
json_build_array_worker) (jcstate->nargs,
jcstate->arg_values,
jcstate->arg_nulls,
- jcstate->arg_types,
+ jcstate->arg_categories,
+ jcstate->arg_outflinfos,
jcstate->constructor->absent_on_null);
else if (ctor->type == JSCTOR_JSON_OBJECT)
res = (is_jsonb ?
@@ -4753,7 +4754,8 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op,
json_build_object_worker) (jcstate->nargs,
jcstate->arg_values,
jcstate->arg_nulls,
- jcstate->arg_types,
+ jcstate->arg_categories,
+ jcstate->arg_outflinfos,
jcstate->constructor->absent_on_null,
jcstate->constructor->unique);
else if (ctor->type == JSCTOR_JSON_SCALAR)
@@ -4766,14 +4768,13 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op,
else
{
Datum value = jcstate->arg_values[0];
- FmgrInfo *outflinfo = &jcstate->arg_type_cache[0].outflinfo;
- JsonTypeCategory category = (JsonTypeCategory)
- jcstate->arg_type_cache[0].category;
if (is_jsonb)
- res = datum_to_jsonb(value, category, outflinfo);
+ res = datum_to_jsonb(value, jcstate->arg_categories[0],
+ &jcstate->arg_outflinfos[0]);
else
- res = datum_to_json(value, category, outflinfo);
+ res = datum_to_json(value, jcstate->arg_categories[0],
+ &jcstate->arg_outflinfos[0]);
}
}
else if (ctor->type == JSCTOR_JSON_PARSE)
diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c
index 788237f6b48..24b41f0a376 100644
--- a/src/backend/utils/adt/json.c
+++ b/src/backend/utils/adt/json.c
@@ -95,8 +95,6 @@ static void array_to_json_internal(Datum array, StringInfo result,
static void datum_to_json_internal(Datum val, bool is_null, StringInfo result,
JsonTypeCategory tcategory,
FmgrInfo *outflinfo, bool key_scalar);
-static void add_json(Datum val, bool is_null, StringInfo result,
- Oid val_type, bool key_scalar);
static text *catenate_stringinfo_string(StringInfo buffer, const char *addon);
/*
@@ -592,35 +590,6 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds)
ReleaseTupleDesc(tupdesc);
}
-/*
- * Append JSON text for "val" to "result".
- *
- * This is just a thin wrapper around datum_to_json. If the same type will be
- * printed many times, avoid using this; better to do the json_categorize_type
- * lookups only once.
- */
-static void
-add_json(Datum val, bool is_null, StringInfo result,
- Oid val_type, bool key_scalar)
-{
- JsonTypeCategory tcategory;
- FmgrInfo outflinfo;
-
- if (val_type == InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("could not determine input data type")));
-
- if (is_null)
- tcategory = JSONTYPE_NULL;
- else
- json_categorize_type(val_type, false,
- &tcategory, &outflinfo);
-
- datum_to_json_internal(val, is_null, result, tcategory, &outflinfo,
- key_scalar);
-}
-
/*
* SQL function array_to_json(row)
*/
@@ -1200,7 +1169,9 @@ catenate_stringinfo_string(StringInfo buffer, const char *addon)
}
Datum
-json_build_object_worker(int nargs, const Datum *args, const bool *nulls, const Oid *types,
+json_build_object_worker(int nargs, const Datum *args, const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos,
bool absent_on_null, bool unique_keys)
{
int i;
@@ -1256,7 +1227,8 @@ json_build_object_worker(int nargs, const Datum *args, const bool *nulls, const
/* save key offset before appending it */
key_offset = out->len;
- add_json(args[i], false, out, types[i], true);
+ datum_to_json_internal(args[i], false, out,
+ categories[i], &outflinfos[i], true);
if (unique_keys)
{
@@ -1282,7 +1254,9 @@ json_build_object_worker(int nargs, const Datum *args, const bool *nulls, const
appendStringInfoString(result, " : ");
/* process value */
- add_json(args[i + 1], nulls[i + 1], result, types[i + 1], false);
+ datum_to_json_internal(args[i + 1], nulls[i + 1], result,
+ categories[i + 1], &outflinfos[i + 1],
+ false);
}
appendStringInfoChar(result, '}');
@@ -1299,15 +1273,26 @@ json_build_object(PG_FUNCTION_ARGS)
Datum *args;
bool *nulls;
Oid *types;
+ int nargs;
+ JsonTypeCategory *categories;
+ FmgrInfo *outflinfos;
/* build argument values to build the object */
- int nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
+ nargs = extract_variadic_args(fcinfo, 0, true,
+ &args, &types, &nulls);
if (nargs < 0)
PG_RETURN_NULL();
- PG_RETURN_DATUM(json_build_object_worker(nargs, args, nulls, types, false, false));
+ categories = palloc_array(JsonTypeCategory, nargs);
+ outflinfos = palloc0_array(FmgrInfo, nargs);
+ for (int i = 0; i < nargs; i++)
+ json_categorize_type(types[i], false,
+ &categories[i], &outflinfos[i]);
+
+ PG_RETURN_DATUM(json_build_object_worker(nargs, args, nulls,
+ categories, outflinfos,
+ false, false));
}
/*
@@ -1320,8 +1305,9 @@ json_build_object_noargs(PG_FUNCTION_ARGS)
}
Datum
-json_build_array_worker(int nargs, const Datum *args, const bool *nulls, const Oid *types,
- bool absent_on_null)
+json_build_array_worker(int nargs, const Datum *args, const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos, bool absent_on_null)
{
int i;
const char *sep = "";
@@ -1338,7 +1324,8 @@ json_build_array_worker(int nargs, const Datum *args, const bool *nulls, const O
appendStringInfoString(&result, sep);
sep = ", ";
- add_json(args[i], nulls[i], &result, types[i], false);
+ datum_to_json_internal(args[i], nulls[i], &result,
+ categories[i], &outflinfos[i], false);
}
appendStringInfoChar(&result, ']');
@@ -1355,15 +1342,26 @@ json_build_array(PG_FUNCTION_ARGS)
Datum *args;
bool *nulls;
Oid *types;
+ int nargs;
+ JsonTypeCategory *categories;
+ FmgrInfo *outflinfos;
- /* build argument values to build the object */
- int nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
+ /* build argument values to build the array */
+ nargs = extract_variadic_args(fcinfo, 0, true,
+ &args, &types, &nulls);
if (nargs < 0)
PG_RETURN_NULL();
- PG_RETURN_DATUM(json_build_array_worker(nargs, args, nulls, types, false));
+ categories = palloc_array(JsonTypeCategory, nargs);
+ outflinfos = palloc0_array(FmgrInfo, nargs);
+ for (int i = 0; i < nargs; i++)
+ json_categorize_type(types[i], false,
+ &categories[i], &outflinfos[i]);
+
+ PG_RETURN_DATUM(json_build_array_worker(nargs, args, nulls,
+ categories, outflinfos,
+ false));
}
/*
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 94fc3d4ecfb..56e94acd2a8 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -52,8 +52,6 @@ static void array_to_jsonb_internal(Datum array, JsonbInState *result);
static void datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
JsonTypeCategory tcategory,
FmgrInfo *outflinfo, bool key_scalar);
-static void add_jsonb(Datum val, bool is_null, JsonbInState *result,
- Oid val_type, bool key_scalar);
static char *JsonbToCStringWorker(StringInfo out, JsonbContainer *in, int estimated_len, bool indent);
static void add_indent(StringInfo out, bool indent, int level);
@@ -1041,37 +1039,6 @@ composite_to_jsonb(Datum composite, JsonbInState *result)
ReleaseTupleDesc(tupdesc);
}
-/*
- * Append JSON text for "val" to "result".
- *
- * This is just a thin wrapper around datum_to_jsonb. If the same type will be
- * printed many times, avoid using this; better to do the json_categorize_type
- * lookups only once.
- */
-
-static void
-add_jsonb(Datum val, bool is_null, JsonbInState *result,
- Oid val_type, bool key_scalar)
-{
- JsonTypeCategory tcategory;
- FmgrInfo outflinfo;
-
- if (val_type == InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("could not determine input data type")));
-
- if (is_null)
- tcategory = JSONTYPE_NULL;
- else
- json_categorize_type(val_type, true,
- &tcategory, &outflinfo);
-
- datum_to_jsonb_internal(val, is_null, result, tcategory, &outflinfo,
- key_scalar);
-}
-
-
/*
* Is the given type immutable when coming out of a JSONB context?
*/
@@ -1129,7 +1096,9 @@ datum_to_jsonb(Datum val, JsonTypeCategory tcategory, FmgrInfo *outflinfo)
}
Datum
-jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls, const Oid *types,
+jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos,
bool absent_on_null, bool unique_keys)
{
int i;
@@ -1166,10 +1135,13 @@ jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls, const
if (skip && !unique_keys)
continue;
- add_jsonb(args[i], false, &result, types[i], true);
+ datum_to_jsonb_internal(args[i], false, &result,
+ categories[i], &outflinfos[i], true);
/* process value */
- add_jsonb(args[i + 1], nulls[i + 1], &result, types[i + 1], false);
+ datum_to_jsonb_internal(args[i + 1], nulls[i + 1], &result,
+ categories[i + 1], &outflinfos[i + 1],
+ false);
}
pushJsonbValue(&result, WJB_END_OBJECT, NULL);
@@ -1186,15 +1158,26 @@ jsonb_build_object(PG_FUNCTION_ARGS)
Datum *args;
bool *nulls;
Oid *types;
+ int nargs;
+ JsonTypeCategory *categories;
+ FmgrInfo *outflinfos;
/* build argument values to build the object */
- int nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
+ nargs = extract_variadic_args(fcinfo, 0, true,
+ &args, &types, &nulls);
if (nargs < 0)
PG_RETURN_NULL();
- PG_RETURN_DATUM(jsonb_build_object_worker(nargs, args, nulls, types, false, false));
+ categories = palloc_array(JsonTypeCategory, nargs);
+ outflinfos = palloc0_array(FmgrInfo, nargs);
+ for (int i = 0; i < nargs; i++)
+ json_categorize_type(types[i], true,
+ &categories[i], &outflinfos[i]);
+
+ PG_RETURN_DATUM(jsonb_build_object_worker(nargs, args, nulls,
+ categories, outflinfos,
+ false, false));
}
/*
@@ -1214,8 +1197,9 @@ jsonb_build_object_noargs(PG_FUNCTION_ARGS)
}
Datum
-jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls, const Oid *types,
- bool absent_on_null)
+jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos, bool absent_on_null)
{
int i;
JsonbInState result;
@@ -1229,7 +1213,8 @@ jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls, const
if (absent_on_null && nulls[i])
continue;
- add_jsonb(args[i], nulls[i], &result, types[i], false);
+ datum_to_jsonb_internal(args[i], nulls[i], &result,
+ categories[i], &outflinfos[i], false);
}
pushJsonbValue(&result, WJB_END_ARRAY, NULL);
@@ -1246,15 +1231,26 @@ jsonb_build_array(PG_FUNCTION_ARGS)
Datum *args;
bool *nulls;
Oid *types;
+ int nargs;
+ JsonTypeCategory *categories;
+ FmgrInfo *outflinfos;
- /* build argument values to build the object */
- int nargs = extract_variadic_args(fcinfo, 0, true,
- &args, &types, &nulls);
+ /* build argument values to build the array */
+ nargs = extract_variadic_args(fcinfo, 0, true,
+ &args, &types, &nulls);
if (nargs < 0)
PG_RETURN_NULL();
- PG_RETURN_DATUM(jsonb_build_array_worker(nargs, args, nulls, types, false));
+ categories = palloc_array(JsonTypeCategory, nargs);
+ outflinfos = palloc0_array(FmgrInfo, nargs);
+ for (int i = 0; i < nargs; i++)
+ json_categorize_type(types[i], true,
+ &categories[i], &outflinfos[i]);
+
+ PG_RETURN_DATUM(jsonb_build_array_worker(nargs, args, nulls,
+ categories, outflinfos,
+ false));
}
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 8650ff57952..168a90876fd 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -17,6 +17,7 @@
#include "executor/nodeAgg.h"
#include "nodes/execnodes.h"
#include "nodes/miscnodes.h"
+#include "utils/jsontypes.h"
/* forward references to avoid circularity */
struct ExprEvalStep;
@@ -827,12 +828,8 @@ typedef struct JsonConstructorExprState
JsonConstructorExpr *constructor;
Datum *arg_values;
bool *arg_nulls;
- Oid *arg_types;
- struct
- {
- int category;
- FmgrInfo outflinfo;
- } *arg_type_cache; /* cache for datum_to_json[b]() */
+ JsonTypeCategory *arg_categories;
+ FmgrInfo *arg_outflinfos;
int nargs;
} JsonConstructorExprState;
diff --git a/src/include/utils/json.h b/src/include/utils/json.h
index 31f88fbbff6..3f9c5bd7ea3 100644
--- a/src/include/utils/json.h
+++ b/src/include/utils/json.h
@@ -28,11 +28,16 @@ extern void escape_json_text(StringInfo buf, const text *txt);
extern char *JsonEncodeDateTime(char *buf, Datum value, Oid typid,
const int *tzp);
extern bool to_json_is_immutable(Oid typoid);
-extern Datum json_build_object_worker(int nargs, const Datum *args, const bool *nulls,
- const Oid *types, bool absent_on_null,
- bool unique_keys);
-extern Datum json_build_array_worker(int nargs, const Datum *args, const bool *nulls,
- const Oid *types, bool absent_on_null);
+extern Datum json_build_object_worker(int nargs, const Datum *args,
+ const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos,
+ bool absent_on_null, bool unique_keys);
+extern Datum json_build_array_worker(int nargs, const Datum *args,
+ const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos,
+ bool absent_on_null);
extern bool json_validate(text *json, bool check_unique_keys, bool throw_error);
#endif /* JSON_H */
diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index 67e4675a908..33f0e716420 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -461,10 +461,15 @@ extern Datum jsonb_get_element(Jsonb *jb, const Datum *path, int npath,
extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
FmgrInfo *outflinfo);
extern bool to_jsonb_is_immutable(Oid typoid);
-extern Datum jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls,
- const Oid *types, bool absent_on_null,
- bool unique_keys);
-extern Datum jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls,
- const Oid *types, bool absent_on_null);
+extern Datum jsonb_build_object_worker(int nargs, const Datum *args,
+ const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos,
+ bool absent_on_null, bool unique_keys);
+extern Datum jsonb_build_array_worker(int nargs, const Datum *args,
+ const bool *nulls,
+ const JsonTypeCategory *categories,
+ FmgrInfo *outflinfos,
+ bool absent_on_null);
#endif /* __JSONB_H__ */
--
2.50.1 (Apple Git-155)
[application/octet-stream] v1-0001-Refactor-json_categorize_type-to-populate-an-Fmgr.patch (39.8K, ../../CA+TgmoaiothgQrw9OtgsMzBUCnqJ2jdaGTbS6o3fkjCd+LfzWw@mail.gmail.com/6-v1-0001-Refactor-json_categorize_type-to-populate-an-Fmgr.patch)
download | inline diff:
From 44efba4a1d4d48ed533c5a26b454c2dd7b6aa433 Mon Sep 17 00:00:00 2001
From: Robert Haas <[email protected]>
Date: Tue, 30 Jun 2026 13:46:43 -0400
Subject: [PATCH v1 1/5] Refactor json_categorize_type() to populate an
FmgrInfo *
By doing this, any code that caches the results of json_categorize_type
now caches the FmgrInfo * rather than just the function OID, which saves
a modest number of CPU cycles. This affects all of the built-in JSON
aggregates (json_agg, json_agg_strict, json_object_agg, jsonb_agg,
etc.), a few plain functions like array_to_json(), and the JSON_SCALAR
constructor. In my testing, the performance gains are small enough that
they are difficult to measure at the SQL level, but doing this much
allows for future refactorings with more significant benefits.
As far as this refactoring goes, the most problematic caller of
json_categorize_type() was json_check_mutability(), which doesn't
actually need an FmgrInfo *. Therefore, I refactored
json_check_mutability() to no longer call json_categorize_type().
Instead, it does the same work directly, except that a little bit of
common logic has been factored out into a new function
get_json_cast_for_type(). This refactoring also made it evident that
json_check_mutability's is_jsonb flag has never had any effect on the
behavior, so it is removed.
Since this is intended as a refactoring commit only, it attempts to
avoid changing the behavior of the code even when that seems to have
previously been incorrect. In the previous coding, json_check_mutability
failed to notice that the output functions for anyarray,
anycompatiblearray, and record are not immutable. Other container types
were determined to be non-immutable by recursing to their element or
base types, but these are implemented as pseudotypes, so the recursive
code does not realize that they can have arbitrary types as
substructure. For now, a special case is added to preserve the
historical behavior, but in a separate commit we should remove the
special case to obtain more correct behavior.
---
src/backend/executor/execExpr.c | 5 +-
src/backend/executor/execExprInterp.c | 6 +-
src/backend/utils/adt/json.c | 125 +++++++++++---------
src/backend/utils/adt/jsonb.c | 109 +++++++++--------
src/backend/utils/adt/jsonfuncs.c | 162 ++++++++++++++------------
src/include/executor/execExpr.h | 2 +-
src/include/utils/jsonfuncs.h | 10 +-
src/test/regress/expected/sqljson.out | 4 +
src/test/regress/sql/sqljson.sql | 5 +
9 files changed, 235 insertions(+), 193 deletions(-)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index cfea7e160c2..871fd17e1ca 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -2437,13 +2437,12 @@ ExecInitExprRec(Expr *node, ExprState *state,
for (int i = 0; i < nargs; i++)
{
JsonTypeCategory category;
- Oid outfuncid;
Oid typid = jcstate->arg_types[i];
json_categorize_type(typid, is_jsonb,
- &category, &outfuncid);
+ &category,
+ &jcstate->arg_type_cache[i].outflinfo);
- jcstate->arg_type_cache[i].outfuncid = outfuncid;
jcstate->arg_type_cache[i].category = (int) category;
}
}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 0634af964a9..1ebfb190e67 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4766,14 +4766,14 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op,
else
{
Datum value = jcstate->arg_values[0];
- Oid outfuncid = jcstate->arg_type_cache[0].outfuncid;
+ FmgrInfo *outflinfo = &jcstate->arg_type_cache[0].outflinfo;
JsonTypeCategory category = (JsonTypeCategory)
jcstate->arg_type_cache[0].category;
if (is_jsonb)
- res = datum_to_jsonb(value, category, outfuncid);
+ res = datum_to_jsonb(value, category, outflinfo);
else
- res = datum_to_json(value, category, outfuncid);
+ res = datum_to_json(value, category, outflinfo);
}
}
else if (ctor->type == JSCTOR_JSON_PARSE)
diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c
index 0fee1b40d63..788237f6b48 100644
--- a/src/backend/utils/adt/json.c
+++ b/src/backend/utils/adt/json.c
@@ -80,21 +80,21 @@ typedef struct JsonAggState
{
StringInfo str;
JsonTypeCategory key_category;
- Oid key_output_func;
+ FmgrInfo key_flinfo;
JsonTypeCategory val_category;
- Oid val_output_func;
+ FmgrInfo val_flinfo;
JsonUniqueBuilderState unique_check;
} JsonAggState;
static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims,
const Datum *vals, const bool *nulls, int *valcount,
- JsonTypeCategory tcategory, Oid outfuncoid,
+ JsonTypeCategory tcategory, FmgrInfo *flinfo,
bool use_line_feeds);
static void array_to_json_internal(Datum array, StringInfo result,
bool use_line_feeds);
static void datum_to_json_internal(Datum val, bool is_null, StringInfo result,
- JsonTypeCategory tcategory, Oid outfuncoid,
- bool key_scalar);
+ JsonTypeCategory tcategory,
+ FmgrInfo *outflinfo, bool key_scalar);
static void add_json(Datum val, bool is_null, StringInfo result,
Oid val_type, bool key_scalar);
static text *catenate_stringinfo_string(StringInfo buffer, const char *addon);
@@ -168,15 +168,19 @@ json_recv(PG_FUNCTION_ARGS)
/*
* Turn a Datum into JSON text, appending the string to "result".
*
- * tcategory and outfuncoid are from a previous call to json_categorize_type,
- * except that if is_null is true then they can be invalid.
+ * tcategory is from a previous call to json_categorize_type, except that if
+ * is_null is true then it can be invalid.
+ *
+ * outflinfo is a pointer to an FmgrInfo populated by the same call to
+ * json_categorize_type, but it is only needed for categories where
+ * json_categorize_type actually populates it.
*
* If key_scalar is true, the value is being printed as a key, so insist
* it's of an acceptable type, and force it to be quoted.
*/
static void
datum_to_json_internal(Datum val, bool is_null, StringInfo result,
- JsonTypeCategory tcategory, Oid outfuncoid,
+ JsonTypeCategory tcategory, FmgrInfo *outflinfo,
bool key_scalar)
{
char *outputstr;
@@ -221,7 +225,7 @@ datum_to_json_internal(Datum val, bool is_null, StringInfo result,
appendStringInfoChar(result, '"');
break;
case JSONTYPE_NUMERIC:
- outputstr = OidOutputFunctionCall(outfuncoid, val);
+ outputstr = OutputFunctionCall(outflinfo, val);
/*
* Don't quote a non-key if it's a valid JSON number (i.e., not
@@ -274,25 +278,26 @@ datum_to_json_internal(Datum val, bool is_null, StringInfo result,
break;
case JSONTYPE_JSON:
/* JSON and JSONB output will already be escaped */
- outputstr = OidOutputFunctionCall(outfuncoid, val);
+ outputstr = OutputFunctionCall(outflinfo, val);
appendStringInfoString(result, outputstr);
pfree(outputstr);
break;
case JSONTYPE_CAST:
- /* outfuncoid refers to a cast function, not an output function */
- jsontext = DatumGetTextPP(OidFunctionCall1(outfuncoid, val));
+ /* outflinfo refers to a cast function, not an output function */
+ jsontext = DatumGetTextPP(FunctionCall1(outflinfo, val));
appendBinaryStringInfo(result, VARDATA_ANY(jsontext),
VARSIZE_ANY_EXHDR(jsontext));
pfree(jsontext);
break;
default:
/* special-case text types to save useless palloc/memcpy cycles */
- if (outfuncoid == F_TEXTOUT || outfuncoid == F_VARCHAROUT ||
- outfuncoid == F_BPCHAROUT)
+ if (outflinfo->fn_oid == F_TEXTOUT ||
+ outflinfo->fn_oid == F_VARCHAROUT ||
+ outflinfo->fn_oid == F_BPCHAROUT)
escape_json_text(result, (text *) DatumGetPointer(val));
else
{
- outputstr = OidOutputFunctionCall(outfuncoid, val);
+ outputstr = OutputFunctionCall(outflinfo, val);
escape_json(result, outputstr);
pfree(outputstr);
}
@@ -429,7 +434,7 @@ JsonEncodeDateTime(char *buf, Datum value, Oid typid, const int *tzp)
static void
array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, const Datum *vals,
const bool *nulls, int *valcount, JsonTypeCategory tcategory,
- Oid outfuncoid, bool use_line_feeds)
+ FmgrInfo *outflinfo, bool use_line_feeds)
{
int i;
const char *sep;
@@ -448,8 +453,7 @@ array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, const Datum
if (dim + 1 == ndims)
{
datum_to_json_internal(vals[*valcount], nulls[*valcount],
- result, tcategory,
- outfuncoid, false);
+ result, tcategory, outflinfo, false);
(*valcount)++;
}
else
@@ -459,7 +463,7 @@ array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, const Datum
* we'll say no.
*/
array_dim_to_json(result, dim + 1, ndims, dims, vals, nulls,
- valcount, tcategory, outfuncoid, false);
+ valcount, tcategory, outflinfo, false);
}
}
@@ -484,7 +488,7 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds)
bool typbyval;
char typalign;
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
ndim = ARR_NDIM(v);
dim = ARR_DIMS(v);
@@ -500,14 +504,14 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds)
&typlen, &typbyval, &typalign);
json_categorize_type(element_type, false,
- &tcategory, &outfuncoid);
+ &tcategory, &outflinfo);
deconstruct_array(v, element_type, typlen, typbyval,
typalign, &elements, &nulls,
&nitems);
array_dim_to_json(result, 0, ndim, dim, elements, nulls, &count, tcategory,
- outfuncoid, use_line_feeds);
+ &outflinfo, use_line_feeds);
pfree(elements);
pfree(nulls);
@@ -558,7 +562,7 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds)
bool isnull;
char *attname;
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
Form_pg_attribute att = TupleDescAttr(tupdesc, i);
if (att->attisdropped)
@@ -575,16 +579,13 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds)
val = heap_getattr(tuple, i + 1, tupdesc, &isnull);
if (isnull)
- {
tcategory = JSONTYPE_NULL;
- outfuncoid = InvalidOid;
- }
else
json_categorize_type(att->atttypid, false, &tcategory,
- &outfuncoid);
+ &outflinfo);
- datum_to_json_internal(val, isnull, result, tcategory, outfuncoid,
- false);
+ datum_to_json_internal(val, isnull, result,
+ tcategory, &outflinfo, false);
}
appendStringInfoChar(result, '}');
@@ -603,7 +604,7 @@ add_json(Datum val, bool is_null, StringInfo result,
Oid val_type, bool key_scalar)
{
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
if (val_type == InvalidOid)
ereport(ERROR,
@@ -611,15 +612,12 @@ add_json(Datum val, bool is_null, StringInfo result,
errmsg("could not determine input data type")));
if (is_null)
- {
tcategory = JSONTYPE_NULL;
- outfuncoid = InvalidOid;
- }
else
json_categorize_type(val_type, false,
- &tcategory, &outfuncoid);
+ &tcategory, &outflinfo);
- datum_to_json_internal(val, is_null, result, tcategory, outfuncoid,
+ datum_to_json_internal(val, is_null, result, tcategory, &outflinfo,
key_scalar);
}
@@ -697,7 +695,7 @@ to_json_is_immutable(Oid typoid)
{
bool has_mutable = false;
- json_check_mutability(typoid, false, &has_mutable);
+ json_check_mutability(typoid, &has_mutable);
return !has_mutable;
}
@@ -710,7 +708,7 @@ to_json(PG_FUNCTION_ARGS)
Datum val = PG_GETARG_DATUM(0);
Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
if (val_type == InvalidOid)
ereport(ERROR,
@@ -718,24 +716,27 @@ to_json(PG_FUNCTION_ARGS)
errmsg("could not determine input data type")));
json_categorize_type(val_type, false,
- &tcategory, &outfuncoid);
+ &tcategory, &outflinfo);
- PG_RETURN_DATUM(datum_to_json(val, tcategory, outfuncoid));
+ PG_RETURN_DATUM(datum_to_json(val, tcategory, &outflinfo));
}
/*
* Turn a Datum into JSON text.
*
- * tcategory and outfuncoid are from a previous call to json_categorize_type.
+ * tcategory is from a previous call to json_categorize_type.
+ *
+ * outflinfo is a pointer to an FmgrInfo populated by the same call to
+ * json_categorize_type, but it is only needed for categories where
+ * json_categorize_type actually populates it.
*/
Datum
-datum_to_json(Datum val, JsonTypeCategory tcategory, Oid outfuncoid)
+datum_to_json(Datum val, JsonTypeCategory tcategory, FmgrInfo *outflinfo)
{
StringInfoData result;
initStringInfo(&result);
- datum_to_json_internal(val, false, &result, tcategory, outfuncoid,
- false);
+ datum_to_json_internal(val, false, &result, tcategory, outflinfo, false);
return PointerGetDatum(cstring_to_text_with_len(result.data, result.len));
}
@@ -772,16 +773,19 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null)
* Make this state object in a context where it will persist for the
* duration of the aggregate call. MemoryContextSwitchTo is only
* needed the first time, as the StringInfo routines make sure they
- * use the right context to enlarge the object if necessary.
+ * use the right context to enlarge the object if necessary, and
+ * val_flinfo will also keep track of the context used to initialize
+ * it.
*/
oldcontext = MemoryContextSwitchTo(aggcontext);
state = palloc_object(JsonAggState);
state->str = makeStringInfo();
+
+ json_categorize_type(arg_type, false, &state->val_category,
+ &state->val_flinfo);
MemoryContextSwitchTo(oldcontext);
appendStringInfoChar(state->str, '[');
- json_categorize_type(arg_type, false, &state->val_category,
- &state->val_output_func);
}
else
{
@@ -798,7 +802,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null)
if (PG_ARGISNULL(1))
{
datum_to_json_internal((Datum) 0, true, state->str, JSONTYPE_NULL,
- InvalidOid, false);
+ NULL, false);
PG_RETURN_POINTER(state);
}
@@ -813,7 +817,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null)
}
datum_to_json_internal(val, false, state->str, state->val_category,
- state->val_output_func, false);
+ &state->val_flinfo, false);
/*
* The transition type for json_agg() is declared to be "internal", which
@@ -991,10 +995,15 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo,
Oid arg_type;
/*
- * Make the StringInfo in a context where it will persist for the
- * duration of the aggregate call. Switching context is only needed
- * for this initial step, as the StringInfo and dynahash routines make
- * sure they use the right context to enlarge the object if necessary.
+ * For this initial call, we need to switch to aggcontext. That's
+ * important for the StringInfo, for the dynahash created by the call
+ * to json_unique_builder_init(), and for the calls to
+ * json_categorize_type, which set up state->key_flinfo and
+ * state->val_flinfo.
+ *
+ * (It won't be necessary to switch contexts on subsequent calls to
+ * this function, since all of these objects are careful to make sure
+ * that subsequent allocations use the correct context.)
*/
oldcontext = MemoryContextSwitchTo(aggcontext);
state = palloc_object(JsonAggState);
@@ -1003,7 +1012,6 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo,
json_unique_builder_init(&state->unique_check);
else
memset(&state->unique_check, 0, sizeof(state->unique_check));
- MemoryContextSwitchTo(oldcontext);
arg_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
@@ -1013,7 +1021,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo,
errmsg("could not determine data type for argument %d", 1)));
json_categorize_type(arg_type, false, &state->key_category,
- &state->key_output_func);
+ &state->key_flinfo);
arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2);
@@ -1023,7 +1031,8 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo,
errmsg("could not determine data type for argument %d", 2)));
json_categorize_type(arg_type, false, &state->val_category,
- &state->val_output_func);
+ &state->val_flinfo);
+ MemoryContextSwitchTo(oldcontext);
appendStringInfoString(state->str, "{ ");
}
@@ -1077,7 +1086,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo,
key_offset = out->len;
datum_to_json_internal(arg, false, out, state->key_category,
- state->key_output_func, true);
+ &state->key_flinfo, true);
if (unique_keys)
{
@@ -1107,8 +1116,8 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo,
arg = PG_GETARG_DATUM(2);
datum_to_json_internal(arg, PG_ARGISNULL(2), state->str,
- state->val_category,
- state->val_output_func, false);
+ state->val_category, &state->val_flinfo,
+ false);
PG_RETURN_POINTER(state);
}
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 864c5ac1c85..94fc3d4ecfb 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -29,9 +29,9 @@ typedef struct JsonbAggState
{
JsonbInState pstate;
JsonTypeCategory key_category;
- Oid key_output_func;
+ FmgrInfo key_flinfo;
JsonTypeCategory val_category;
- Oid val_output_func;
+ FmgrInfo val_flinfo;
} JsonbAggState;
static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys,
@@ -47,11 +47,11 @@ static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenTy
static void composite_to_jsonb(Datum composite, JsonbInState *result);
static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims,
const Datum *vals, const bool *nulls, int *valcount,
- JsonTypeCategory tcategory, Oid outfuncoid);
+ JsonTypeCategory tcategory, FmgrInfo *outflinfo);
static void array_to_jsonb_internal(Datum array, JsonbInState *result);
static void datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
- JsonTypeCategory tcategory, Oid outfuncoid,
- bool key_scalar);
+ JsonTypeCategory tcategory,
+ FmgrInfo *outflinfo, bool key_scalar);
static void add_jsonb(Datum val, bool is_null, JsonbInState *result,
Oid val_type, bool key_scalar);
static char *JsonbToCStringWorker(StringInfo out, JsonbContainer *in, int estimated_len, bool indent);
@@ -617,8 +617,12 @@ add_indent(StringInfo out, bool indent, int level)
/*
* Turn a Datum into jsonb, adding it to the result JsonbInState.
*
- * tcategory and outfuncoid are from a previous call to json_categorize_type,
- * except that if is_null is true then they can be invalid.
+ * tcategory is from a previous call to json_categorize_type, except that if
+ * is_null is true then it can be invalid.
+ *
+ * outflinfo is a pointer to an FmgrInfo populated by the same call to
+ * json_categorize_type, but it is only needed for categories where
+ * json_categorize_type actually populates it.
*
* If key_scalar is true, the value is stored as a key, so insist
* it's of an acceptable type, and force it to be a jbvString.
@@ -628,7 +632,7 @@ add_indent(StringInfo out, bool indent, int level)
*/
static void
datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
- JsonTypeCategory tcategory, Oid outfuncoid,
+ JsonTypeCategory tcategory, FmgrInfo *outflinfo,
bool key_scalar)
{
char *outputstr;
@@ -691,7 +695,7 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
{
Datum numd;
- switch (outfuncoid)
+ switch (outflinfo->fn_oid)
{
case F_NUMERIC_OUT:
numeric_val = DatumGetNumeric(val);
@@ -725,7 +729,7 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
break;
#endif
default:
- outputstr = OidOutputFunctionCall(outfuncoid, val);
+ outputstr = OutputFunctionCall(outflinfo, val);
numd = DirectFunctionCall3(numeric_in,
CStringGetDatum(outputstr),
ObjectIdGetDatum(InvalidOid),
@@ -739,7 +743,7 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
}
if (numeric_to_string)
{
- outputstr = OidOutputFunctionCall(outfuncoid, val);
+ outputstr = OutputFunctionCall(outflinfo, val);
jb.type = jbvString;
jb.val.string.len = strlen(outputstr);
jb.val.string.val = outputstr;
@@ -770,7 +774,7 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
break;
case JSONTYPE_CAST:
/* cast to JSON, and then process as JSON */
- val = OidFunctionCall1(outfuncoid, val);
+ val = FunctionCall1(outflinfo, val);
pg_fallthrough;
case JSONTYPE_JSON:
{
@@ -828,9 +832,9 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
break;
default:
/* special-case text types to save useless palloc/memcpy ops */
- if (outfuncoid == F_TEXTOUT ||
- outfuncoid == F_VARCHAROUT ||
- outfuncoid == F_BPCHAROUT)
+ if (outflinfo->fn_oid == F_TEXTOUT ||
+ outflinfo->fn_oid == F_VARCHAROUT ||
+ outflinfo->fn_oid == F_BPCHAROUT)
{
text *txt = DatumGetTextPP(val);
@@ -839,7 +843,7 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
}
else
{
- outputstr = OidOutputFunctionCall(outfuncoid, val);
+ outputstr = OutputFunctionCall(outflinfo, val);
jb.val.string.len = strlen(outputstr);
jb.val.string.val = outputstr;
}
@@ -897,7 +901,7 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result,
static void
array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, const Datum *vals,
const bool *nulls, int *valcount, JsonTypeCategory tcategory,
- Oid outfuncoid)
+ FmgrInfo *outflinfo)
{
int i;
@@ -909,14 +913,14 @@ array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, const Da
{
if (dim + 1 == ndims)
{
- datum_to_jsonb_internal(vals[*valcount], nulls[*valcount], result, tcategory,
- outfuncoid, false);
+ datum_to_jsonb_internal(vals[*valcount], nulls[*valcount],
+ result, tcategory, outflinfo, false);
(*valcount)++;
}
else
{
array_dim_to_jsonb(result, dim + 1, ndims, dims, vals, nulls,
- valcount, tcategory, outfuncoid);
+ valcount, tcategory, outflinfo);
}
}
@@ -941,7 +945,7 @@ array_to_jsonb_internal(Datum array, JsonbInState *result)
bool typbyval;
char typalign;
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
ndim = ARR_NDIM(v);
dim = ARR_DIMS(v);
@@ -958,14 +962,14 @@ array_to_jsonb_internal(Datum array, JsonbInState *result)
&typlen, &typbyval, &typalign);
json_categorize_type(element_type, true,
- &tcategory, &outfuncoid);
+ &tcategory, &outflinfo);
deconstruct_array(v, element_type, typlen, typbyval,
typalign, &elements, &nulls,
&nitems);
- array_dim_to_jsonb(result, 0, ndim, dim, elements, nulls, &count, tcategory,
- outfuncoid);
+ array_dim_to_jsonb(result, 0, ndim, dim, elements, nulls, &count,
+ tcategory, &outflinfo);
pfree(elements);
pfree(nulls);
@@ -1005,7 +1009,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result)
bool isnull;
char *attname;
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
JsonbValue v;
Form_pg_attribute att = TupleDescAttr(tupdesc, i);
@@ -1024,15 +1028,12 @@ composite_to_jsonb(Datum composite, JsonbInState *result)
val = heap_getattr(tuple, i + 1, tupdesc, &isnull);
if (isnull)
- {
tcategory = JSONTYPE_NULL;
- outfuncoid = InvalidOid;
- }
else
json_categorize_type(att->atttypid, true, &tcategory,
- &outfuncoid);
+ &outflinfo);
- datum_to_jsonb_internal(val, isnull, result, tcategory, outfuncoid,
+ datum_to_jsonb_internal(val, isnull, result, tcategory, &outflinfo,
false);
}
@@ -1053,7 +1054,7 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result,
Oid val_type, bool key_scalar)
{
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
if (val_type == InvalidOid)
ereport(ERROR,
@@ -1061,15 +1062,12 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result,
errmsg("could not determine input data type")));
if (is_null)
- {
tcategory = JSONTYPE_NULL;
- outfuncoid = InvalidOid;
- }
else
json_categorize_type(val_type, true,
- &tcategory, &outfuncoid);
+ &tcategory, &outflinfo);
- datum_to_jsonb_internal(val, is_null, result, tcategory, outfuncoid,
+ datum_to_jsonb_internal(val, is_null, result, tcategory, &outflinfo,
key_scalar);
}
@@ -1082,7 +1080,7 @@ to_jsonb_is_immutable(Oid typoid)
{
bool has_mutable = false;
- json_check_mutability(typoid, true, &has_mutable);
+ json_check_mutability(typoid, &has_mutable);
return !has_mutable;
}
@@ -1095,7 +1093,7 @@ to_jsonb(PG_FUNCTION_ARGS)
Datum val = PG_GETARG_DATUM(0);
Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
JsonTypeCategory tcategory;
- Oid outfuncoid;
+ FmgrInfo outflinfo;
if (val_type == InvalidOid)
ereport(ERROR,
@@ -1103,24 +1101,28 @@ to_jsonb(PG_FUNCTION_ARGS)
errmsg("could not determine input data type")));
json_categorize_type(val_type, true,
- &tcategory, &outfuncoid);
+ &tcategory, &outflinfo);
- PG_RETURN_DATUM(datum_to_jsonb(val, tcategory, outfuncoid));
+ PG_RETURN_DATUM(datum_to_jsonb(val, tcategory, &outflinfo));
}
/*
* Turn a Datum into jsonb.
*
- * tcategory and outfuncoid are from a previous call to json_categorize_type.
+ * tcategory is from a previous call to json_categorize_type.
+ *
+ * outflinfo is a pointer to an FmgrInfo populated by the same call to
+ * json_categorize_type, but it is only needed for categories where
+ * json_categorize_type actually populates it.
*/
Datum
-datum_to_jsonb(Datum val, JsonTypeCategory tcategory, Oid outfuncoid)
+datum_to_jsonb(Datum val, JsonTypeCategory tcategory, FmgrInfo *outflinfo)
{
JsonbInState result;
memset(&result, 0, sizeof(JsonbInState));
- datum_to_jsonb_internal(val, false, &result, tcategory, outfuncoid,
+ datum_to_jsonb_internal(val, false, &result, tcategory, outflinfo,
false);
return JsonbPGetDatum(JsonbValueToJsonb(result.result));
@@ -1490,6 +1492,7 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null)
if (PG_ARGISNULL(0))
{
Oid arg_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
+ MemoryContext oldcontext;
if (arg_type == InvalidOid)
ereport(ERROR,
@@ -1501,8 +1504,11 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null)
result->outcontext = aggcontext;
pushJsonbValue(result, WJB_BEGIN_ARRAY, NULL);
+ /* make sure to set up val_flinfo in the correct context */
+ oldcontext = MemoryContextSwitchTo(aggcontext);
json_categorize_type(arg_type, true, &state->val_category,
- &state->val_output_func);
+ &state->val_flinfo);
+ MemoryContextSwitchTo(oldcontext);
}
else
{
@@ -1522,7 +1528,7 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null)
val = PG_ARGISNULL(1) ? (Datum) 0 : PG_GETARG_DATUM(1);
datum_to_jsonb_internal(val, PG_ARGISNULL(1), result, state->val_category,
- state->val_output_func, false);
+ &state->val_flinfo, false);
PG_RETURN_POINTER(state);
}
@@ -1599,6 +1605,7 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo,
if (PG_ARGISNULL(0))
{
Oid arg_type;
+ MemoryContext oldcontext;
state = MemoryContextAllocZero(aggcontext, sizeof(JsonbAggState));
result = &state->pstate;
@@ -1607,6 +1614,9 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo,
result->parseState->unique_keys = unique_keys;
result->parseState->skip_nulls = absent_on_null;
+ /* make sure to set up key_flinfo/val_flinfo in the correct context */
+ oldcontext = MemoryContextSwitchTo(aggcontext);
+
arg_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
if (arg_type == InvalidOid)
@@ -1615,7 +1625,7 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo,
errmsg("could not determine input data type")));
json_categorize_type(arg_type, true, &state->key_category,
- &state->key_output_func);
+ &state->key_flinfo);
arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2);
@@ -1625,7 +1635,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo,
errmsg("could not determine input data type")));
json_categorize_type(arg_type, true, &state->val_category,
- &state->val_output_func);
+ &state->val_flinfo);
+ MemoryContextSwitchTo(oldcontext);
}
else
{
@@ -1656,12 +1667,12 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo,
val = PG_GETARG_DATUM(1);
datum_to_jsonb_internal(val, false, result, state->key_category,
- state->key_output_func, true);
+ &state->key_flinfo, true);
val = PG_ARGISNULL(2) ? (Datum) 0 : PG_GETARG_DATUM(2);
datum_to_jsonb_internal(val, PG_ARGISNULL(2), result, state->val_category,
- state->val_output_func, false);
+ &state->val_flinfo, false);
PG_RETURN_POINTER(state);
}
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index 97cc3d60340..f769e90ba5e 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -506,6 +506,8 @@ static JsonParseErrorType transform_string_values_object_field_start(void *state
static JsonParseErrorType transform_string_values_array_element_start(void *state, bool isnull);
static JsonParseErrorType transform_string_values_scalar(void *state, char *token, JsonTokenType tokentype);
+/* function supporting json_categorize_type/json_check_mutability */
+static Oid get_json_cast_for_type(Oid typoid);
/*
* pg_parse_json_or_errsave
@@ -5959,25 +5961,22 @@ json_get_first_token(text *json, bool throw_error)
/*
* Determine how we want to print values of a given type in datum_to_json(b).
*
- * Given the datatype OID, return its JsonTypeCategory, as well as the type's
- * output function OID. If the returned category is JSONTYPE_CAST, we return
- * the OID of the type->JSON cast function instead.
+ * Given the datatype OID, return its JsonTypeCategory, as well as an FmgrInfo
+ * for the type's output function or cast function. For categories that do not
+ * require calling a function, outflinfo is not touched.
*/
void
json_categorize_type(Oid typoid, bool is_jsonb,
- JsonTypeCategory *tcategory, Oid *outfuncoid)
+ JsonTypeCategory *tcategory, FmgrInfo *outflinfo)
{
- bool typisvarlena;
+ bool use_type_output_function = false;
/* Look through any domain */
typoid = getBaseType(typoid);
- *outfuncoid = InvalidOid;
-
switch (typoid)
{
case BOOLOID:
- *outfuncoid = F_BOOLOUT;
*tcategory = JSONTYPE_BOOL;
break;
@@ -5987,32 +5986,29 @@ json_categorize_type(Oid typoid, bool is_jsonb,
case FLOAT4OID:
case FLOAT8OID:
case NUMERICOID:
- getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
+ use_type_output_function = true;
*tcategory = JSONTYPE_NUMERIC;
break;
case DATEOID:
- *outfuncoid = F_DATE_OUT;
*tcategory = JSONTYPE_DATE;
break;
case TIMESTAMPOID:
- *outfuncoid = F_TIMESTAMP_OUT;
*tcategory = JSONTYPE_TIMESTAMP;
break;
case TIMESTAMPTZOID:
- *outfuncoid = F_TIMESTAMPTZ_OUT;
*tcategory = JSONTYPE_TIMESTAMPTZ;
break;
case JSONOID:
- getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
+ use_type_output_function = !is_jsonb;
*tcategory = JSONTYPE_JSON;
break;
case JSONBOID:
- getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
+ use_type_output_function = !is_jsonb;
*tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON;
break;
@@ -6020,50 +6016,35 @@ json_categorize_type(Oid typoid, bool is_jsonb,
/* Check for arrays and composites */
if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID
|| typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID)
- {
- *outfuncoid = F_ARRAY_OUT;
*tcategory = JSONTYPE_ARRAY;
- }
else if (type_is_rowtype(typoid)) /* includes RECORDOID */
- {
- *outfuncoid = F_RECORD_OUT;
*tcategory = JSONTYPE_COMPOSITE;
- }
else
{
- /*
- * It's probably the general case. But let's look for a cast
- * to json (note: not to jsonb even if is_jsonb is true), if
- * it's not built-in.
- */
- *tcategory = JSONTYPE_OTHER;
- if (typoid >= FirstNormalObjectId)
+ Oid castfunc = get_json_cast_for_type(typoid);
+
+ if (OidIsValid(castfunc))
{
- Oid castfunc;
- CoercionPathType ctype;
-
- ctype = find_coercion_pathway(JSONOID, typoid,
- COERCION_EXPLICIT,
- &castfunc);
- if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc))
- {
- *outfuncoid = castfunc;
- *tcategory = JSONTYPE_CAST;
- }
- else
- {
- /* non builtin type with no cast */
- getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
- }
+ fmgr_info(castfunc, outflinfo);
+ *tcategory = JSONTYPE_CAST;
}
else
{
- /* any other builtin type */
- getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
+ use_type_output_function = true;
+ *tcategory = JSONTYPE_OTHER;
}
}
break;
}
+
+ if (use_type_output_function)
+ {
+ Oid typoutput;
+ bool typisvarlena;
+
+ getTypeOutputInfo(typoid, &typoutput, &typisvarlena);
+ fmgr_info(typoutput, outflinfo);
+ }
}
/*
@@ -6075,11 +6056,9 @@ json_categorize_type(Oid typoid, bool is_jsonb,
* If any mutable function is found, *has_mutable is set to true.
*/
void
-json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable)
+json_check_mutability(Oid typoid, bool *has_mutable)
{
char att_typtype = get_typtype(typoid);
- JsonTypeCategory tcategory;
- Oid outfuncoid;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -6091,7 +6070,7 @@ json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable)
if (att_typtype == TYPTYPE_DOMAIN)
{
- json_check_mutability(getBaseType(typoid), is_jsonb, has_mutable);
+ json_check_mutability(getBaseType(typoid), has_mutable);
return;
}
else if (att_typtype == TYPTYPE_COMPOSITE)
@@ -6109,7 +6088,7 @@ json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable)
if (attr->attisdropped)
continue;
- json_check_mutability(attr->atttypid, is_jsonb, has_mutable);
+ json_check_mutability(attr->atttypid, has_mutable);
if (*has_mutable)
break;
}
@@ -6118,14 +6097,12 @@ json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable)
}
else if (att_typtype == TYPTYPE_RANGE)
{
- json_check_mutability(get_range_subtype(typoid), is_jsonb,
- has_mutable);
+ json_check_mutability(get_range_subtype(typoid), has_mutable);
return;
}
else if (att_typtype == TYPTYPE_MULTIRANGE)
{
- json_check_mutability(get_multirange_range(typoid), is_jsonb,
- has_mutable);
+ json_check_mutability(get_multirange_range(typoid), has_mutable);
return;
}
else
@@ -6135,36 +6112,73 @@ json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable)
if (OidIsValid(att_typelem))
{
/* recurse into array element type */
- json_check_mutability(att_typelem, is_jsonb, has_mutable);
+ json_check_mutability(att_typelem, has_mutable);
return;
}
}
- json_categorize_type(typoid, is_jsonb, &tcategory, &outfuncoid);
-
- switch (tcategory)
+ switch (typoid)
{
- case JSONTYPE_NULL:
- case JSONTYPE_BOOL:
- case JSONTYPE_NUMERIC:
+ case BOOLOID:
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ case JSONOID:
+ case JSONBOID:
+ /* known immutable */
break;
-
- case JSONTYPE_DATE:
- case JSONTYPE_TIMESTAMP:
- case JSONTYPE_TIMESTAMPTZ:
+ case DATEOID:
+ case TIMESTAMPOID:
+ case TIMESTAMPTZOID:
*has_mutable = true;
break;
-
- case JSONTYPE_JSON:
- case JSONTYPE_JSONB:
- case JSONTYPE_ARRAY:
- case JSONTYPE_COMPOSITE:
+ case RECORDOID:
+ case ANYARRAYOID:
+ case ANYCOMPATIBLEARRAYOID:
+ /* XXX incorrectly treated as known immutable */
break;
- case JSONTYPE_CAST:
- case JSONTYPE_OTHER:
- if (func_volatile(outfuncoid) != PROVOLATILE_IMMUTABLE)
- *has_mutable = true;
+ default:
+ {
+ Oid castfunc = get_json_cast_for_type(typoid);
+ Oid funcoid;
+
+ if (OidIsValid(castfunc))
+ funcoid = castfunc;
+ else
+ {
+ bool typisvarlena;
+
+ getTypeOutputInfo(typoid, &funcoid, &typisvarlena);
+ }
+ if (func_volatile(funcoid) != PROVOLATILE_IMMUTABLE)
+ *has_mutable = true;
+ }
break;
}
}
+
+/*
+ * Return the OID of a cast function from typoid to JSON, or InvalidOid if
+ * there is no such cast. As a matter of policy, we only consider explicit,
+ * user-defined casts.
+ */
+static Oid
+get_json_cast_for_type(Oid typoid)
+{
+ if (typoid >= FirstNormalObjectId)
+ {
+ Oid castfunc;
+ CoercionPathType ctype;
+
+ ctype = find_coercion_pathway(JSONOID, typoid,
+ COERCION_EXPLICIT,
+ &castfunc);
+ if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc))
+ return castfunc;
+ }
+ return InvalidOid;
+}
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index c61b3d624d5..8650ff57952 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -831,7 +831,7 @@ typedef struct JsonConstructorExprState
struct
{
int category;
- Oid outfuncid;
+ FmgrInfo outflinfo;
} *arg_type_cache; /* cache for datum_to_json[b]() */
int nargs;
} JsonConstructorExprState;
diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h
index 27713be3aeb..96409557f29 100644
--- a/src/include/utils/jsonfuncs.h
+++ b/src/include/utils/jsonfuncs.h
@@ -82,13 +82,13 @@ typedef enum
} JsonTypeCategory;
extern void json_categorize_type(Oid typoid, bool is_jsonb,
- JsonTypeCategory *tcategory, Oid *outfuncoid);
-extern void json_check_mutability(Oid typoid, bool is_jsonb,
- bool *has_mutable);
+ JsonTypeCategory *tcategory,
+ FmgrInfo *outflinfo);
+extern void json_check_mutability(Oid typoid, bool *has_mutable);
extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory,
- Oid outfuncoid);
+ FmgrInfo *outflinfo);
extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
- Oid outfuncoid);
+ FmgrInfo *outflinfo);
extern Datum jsonb_from_text(text *js, bool unique_keys);
extern Datum json_populate_type(Datum json_val, Oid json_type,
diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out
index 0f337bda325..fedbeca7a21 100644
--- a/src/test/regress/expected/sqljson.out
+++ b/src/test/regress/expected/sqljson.out
@@ -1757,3 +1757,7 @@ SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': 1 RETURNING text) FORMAT JSON);
(1 row)
DROP FUNCTION volatile_one, stable_one;
+-- JSON_ARRAY(ROW(...)) is not immutable, so it should be impossible to
+-- create a generated column.
+-- XXX: Currently, this is erroneously allowed.
+CREATE TABLE json_array_of_row (a int, j json GENERATED ALWAYS AS (JSON_ARRAY(ROW(a))) STORED);
diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql
index a68747733a1..77910137573 100644
--- a/src/test/regress/sql/sqljson.sql
+++ b/src/test/regress/sql/sqljson.sql
@@ -706,3 +706,8 @@ SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': stable_one() RETURNING text) FORMAT
EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': 1 RETURNING text) FORMAT JSON);
SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': 1 RETURNING text) FORMAT JSON);
DROP FUNCTION volatile_one, stable_one;
+
+-- JSON_ARRAY(ROW(...)) is not immutable, so it should be impossible to
+-- create a generated column.
+-- XXX: Currently, this is erroneously allowed.
+CREATE TABLE json_array_of_row (a int, j json GENERATED ALWAYS AS (JSON_ARRAY(ROW(a))) STORED);
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 4+ messages in thread
* Re: json/jsonb cleanup + FmgrInfo caching
@ 2026-07-08 15:16 Andrew Dunstan <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 1 reply; 4+ messages in thread
From: Andrew Dunstan @ 2026-07-08 15:16 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: pgsql-hackers
On Thu, Jul 2, 2026 at 12:25 PM Robert Haas <[email protected]> wrote:
> Hi,
>
> While working on my "sandboxing untrusted code" project, I found
> myself investigating how the JSON and JSONB code calls type output and
> cast functions. I feel like this needs some cleanup in order to avoid
> blocking that project, and it turns out that there are also
> significant opportunities to improve performance, so here are some
> patches. One caveat: one of these patches causes a small backward
> compatibility break, because the current behavior is wrong. See below
> for full details.
>
> First, a quick performance demonstration:
>
> CREATE TABLE hstores AS SELECT hstore('k', g::text) x FROM
> generate_series(1,4000000) g;
> SELECT any_value(json_build_object('a', x)) FROM hstores;
>
> Unpatched, median-of-three runs was 398.571 ms. Patched, 193.714 ms.
> That's a 2.05x speedup, which is the highest I observed in my test
> queries. What I found is that this technique shows the largest gains
> with user-defined types like hstore, smaller gains with built-in types
> like int, and the smallest gains of all with container types such as a
> user-defined record type. Generally, jsonb benefited more than json.
> The aggregates - json_agg and jsonb_agg - showed very little benefit
> or even small regressions, but I'm pretty confident that the
> regressions are simply noise that goes away with a sufficiently large
> number of sufficiently-careful test runs. Everything else gets faster,
> often by 50%+. Leaving aside the aggregates which are expected to show
> little or no benefit, here's one of the less-sympathetic cases:
>
> CREATE TABLE ints AS SELECT x FROM generate_series(1,4000000) x;
> SELECT any_value(to_json(x)) FROM ints;
>
> The speedup is smaller here because it's json rather than jsonb and
> because it's int rather than hstore, but it's still 117.461 ms
> unpatched vs. 89.895 patched, a 30% speedup.
>
> OK, now let's go through the patches:
>
> 0001 refactors the json_categorize_type() function to initialize an
> FmgrInfo instead of returning a base function OID. All of the built-in
> JSON aggregates are updated to cache this FmgrInfo across calls, but
> it really doesn't save much. In the process of working on this
> refactoring it came to light that the current behavior of
> json_check_mutability() is incorrect: it erroneously treats record,
> anyarray, and anycompatiblearray as immutable when in fact they should
> be treated as stable. This refactoring preserves that incorrect
> behavior.
>
> 0002 fixes the bug discovered during the development of 0001 by
> removing the special case. AFAICT, the anyarray and anycompatiblearray
> cases are unreachable, but the record case is reachable, and the
> included test case shows how this could hypothetically matter. It
> seems unlikely we'll inconvenience any significant number of users by
> changing this, but in theory somebody's upgrade could fail.
>
> 0003 moves some code around to avoid problems with circular header
> dependencies, creating new files jsontypes.c/h.
>
> 0004 refactors the SQL-level JSON constructors -- JSON_OBJECT,
> JSON_ARRAY, and JSON_SCALAR -- to make use of the new type caching
> infrastructure.
>
> 0005 refactors the SQL-callable functions similarly. This means
> to_json(b), json(b)_build_object, and json(b)_build_array.
>
>
>
Looks pretty good.
The old `add_json()`/`add_jsonb()` helpers (removed in 0004/0005) checked
`val_type == InvalidOid` and raised a clean
`ERRCODE_INVALID_PARAMETER_VALUE` / "could not determine input data type"
error. Now I think we'd get something like "cache lookup failed for type
0", which is rather more opaque. Not sure if that matters.
For VARIADIC arrays,
for (int i = 0; i < nargs; ++i)
fmgr_info_copy(&(*outflinfos)[i], &jcache->flinfos[0],
CurrentMemoryContext);
is going to copy the same flinfo for every array element for every row, The
comment says that it's done that way to avoid complicating the code, and
that seems reasonable. I don't know how often these functions are used with
explicit VARIADIC array arguments, so it might be a niche case.
cheers
andrew
^ permalink raw reply [nested|flat] 4+ messages in thread
* Re: json/jsonb cleanup + FmgrInfo caching
@ 2026-07-09 15:15 Robert Haas <[email protected]>
parent: Andrew Dunstan <[email protected]>
0 siblings, 1 reply; 4+ messages in thread
From: Robert Haas @ 2026-07-09 15:15 UTC (permalink / raw)
To: Andrew Dunstan <[email protected]>; +Cc: pgsql-hackers
On Wed, Jul 8, 2026 at 11:16 AM Andrew Dunstan <[email protected]> wrote:
> Looks pretty good.
Thanks for looking.
> The old `add_json()`/`add_jsonb()` helpers (removed in 0004/0005) checked `val_type == InvalidOid` and raised a clean `ERRCODE_INVALID_PARAMETER_VALUE` / "could not determine input data type" error. Now I think we'd get something like "cache lookup failed for type 0", which is rather more opaque. Not sure if that matters.
I definitely don't want to show "cache lookup failed" errors, but I
don't think that's reachable. If I'm wrong and it is, we should add a
guard. Do you have a test case, by any chance?
> For VARIADIC arrays,
>
> for (int i = 0; i < nargs; ++i)
> fmgr_info_copy(&(*outflinfos)[i], &jcache->flinfos[0], CurrentMemoryContext);
>
> is going to copy the same flinfo for every array element for every row, The comment says that it's done that way to avoid complicating the code, and that seems reasonable. I don't know how often these functions are used with explicit VARIADIC array arguments, so it might be a niche case.
Right. I think it is a niche case, but I also don't think this is
really costing us anything. As far as I can tell from my benchmarking
to date, fmgr_info() is kind of expensive, but fmgr_info_copy() is
pretty cheap. So, we could arrange to share a single flinfo for every
argument of the VARIADIC list, but the savings are pretty limited
because we wouldn't save fmgr_info() calls, only fmgr_info_copy()
calls. And on the downside json{,b}_build_{array,object}_worker would
all need to support two modes, one where there is an fcinfo per Datum
and a second where there is one fcinfo for all Datums. There's a
code-complexity argument against that, but it would also have some
cost in CPU cycles, because every call would have to branch into one
path or the other. We'd be slowing down both variadic and non-variadic
calls slightly by adding those branches, in the hopes that saving
fmgr_info_copy() calls in the non-variadic case would be valuable
enough to make it worthwhile. Somebody can certainly try that, but my
guess is that there won't be any performance benefit worth caring
about and the code will be uglier.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 4+ messages in thread
* Re: json/jsonb cleanup + FmgrInfo caching
@ 2026-07-09 15:22 Tom Lane <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 0 replies; 4+ messages in thread
From: Tom Lane @ 2026-07-09 15:22 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; pgsql-hackers
Robert Haas <[email protected]> writes:
> Right. I think it is a niche case, but I also don't think this is
> really costing us anything. As far as I can tell from my benchmarking
> to date, fmgr_info() is kind of expensive, but fmgr_info_copy() is
> pretty cheap. So, we could arrange to share a single flinfo for every
> argument of the VARIADIC list, but the savings are pretty limited
> because we wouldn't save fmgr_info() calls, only fmgr_info_copy()
> calls.
The hole in that argument is that fmgr_info_copy() does
dstinfo->fn_extra = NULL;
so that if the function-to-be-called caches anything in fn_extra,
it will have to populate that cache for each argument. That could be
quite expensive. It might still not justify complicating the setup
code, but I don't think it's as clear-cut as you suggest. Perhaps
some benchmarking is in order.
regards, tom lane
^ permalink raw reply [nested|flat] 4+ messages in thread
end of thread, other threads:[~2026-07-09 15:22 UTC | newest]
Thread overview: 4+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-07-02 16:25 json/jsonb cleanup + FmgrInfo caching Robert Haas <[email protected]>
2026-07-08 15:16 ` Andrew Dunstan <[email protected]>
2026-07-09 15:15 ` Robert Haas <[email protected]>
2026-07-09 15:22 ` Tom Lane <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox