v5-0003-jsonb-optimize-extract-path-casts-to-scalar-types.patch
application/octet-stream
Filename: v5-0003-jsonb-optimize-extract-path-casts-to-scalar-types.patch
Type: application/octet-stream
Part: 1
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v5-0003
Subject: jsonb: optimize extract-path casts to scalar types
| File | + | − |
|---|---|---|
| src/backend/utils/adt/jsonb.c | 42 | 1 |
| src/backend/utils/adt/jsonfuncs.c | 186 | 2 |
| src/include/catalog/pg_proc.dat | 30 | 1 |
| src/test/regress/expected/jsonb.out | 227 | 0 |
| src/test/regress/sql/jsonb.sql | 61 | 0 |
From 04e738b46adda4f9edb948cf5729091f85ac9830 Mon Sep 17 00:00:00 2001
From: Haibo Yan <haibo.yan@apple.com>
Date: Fri, 10 Apr 2026 12:08:25 -0700
Subject: [PATCH v5 3/5] jsonb: optimize extract-path casts to scalar types
Extend the existing support-function rewrite to jsonb extract-path
operations, including both #> and direct jsonb_extract_path(...)
calls.
Supported casts are rewritten directly to explicit typed extractor
functions for numeric, bool, int2, int4, int8, float4, and float8.
---
src/backend/utils/adt/jsonb.c | 43 +++++-
src/backend/utils/adt/jsonfuncs.c | 188 ++++++++++++++++++++++-
src/include/catalog/pg_proc.dat | 31 +++-
src/test/regress/expected/jsonb.out | 227 ++++++++++++++++++++++++++++
src/test/regress/sql/jsonb.sql | 61 ++++++++
5 files changed, 546 insertions(+), 4 deletions(-)
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 5abb51d47da..7eba437b8a4 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -1834,6 +1834,7 @@ cannotCastJsonbValue(enum jbvType type, const char *sqltype, Node *escontext)
* Supported extraction families:
* - jsonb_object_field(j, 'key') / j -> 'key' / j['key']
* - jsonb_array_element(j, idx) / j -> idx / j[idx]
+ * - jsonb_extract_path(j, ...) / j #> '{a,b}'
*/
Datum
jsonb_cast_support(PG_FUNCTION_ARGS)
@@ -2010,6 +2011,46 @@ jsonb_cast_support(PG_FUNCTION_ARGS)
else
PG_RETURN_POINTER(NULL);
}
+ else if (inner_funcid == F_JSONB_EXTRACT_PATH)
+ {
+ if (fexpr->funcid == F_NUMERIC_JSONB)
+ {
+ replacement_funcid = F_JSONB_EXTRACT_PATH_NUMERIC;
+ replacement_rettype = NUMERICOID;
+ }
+ else if (fexpr->funcid == F_BOOL_JSONB)
+ {
+ replacement_funcid = F_JSONB_EXTRACT_PATH_BOOL;
+ replacement_rettype = BOOLOID;
+ }
+ else if (fexpr->funcid == F_INT4_JSONB)
+ {
+ replacement_funcid = F_JSONB_EXTRACT_PATH_INT4;
+ replacement_rettype = INT4OID;
+ }
+ else if (fexpr->funcid == F_INT8_JSONB)
+ {
+ replacement_funcid = F_JSONB_EXTRACT_PATH_INT8;
+ replacement_rettype = INT8OID;
+ }
+ else if (fexpr->funcid == F_FLOAT8_JSONB)
+ {
+ replacement_funcid = F_JSONB_EXTRACT_PATH_FLOAT8;
+ replacement_rettype = FLOAT8OID;
+ }
+ else if (fexpr->funcid == F_INT2_JSONB)
+ {
+ replacement_funcid = F_JSONB_EXTRACT_PATH_INT2;
+ replacement_rettype = INT2OID;
+ }
+ else if (fexpr->funcid == F_FLOAT4_JSONB)
+ {
+ replacement_funcid = F_JSONB_EXTRACT_PATH_FLOAT4;
+ replacement_rettype = FLOAT4OID;
+ }
+ else
+ PG_RETURN_POINTER(NULL);
+ }
else
PG_RETURN_POINTER(NULL);
@@ -2241,4 +2282,4 @@ JsonbUnquote(Jsonb *jb)
}
else
return JsonbToCString(NULL, &jb->root, VARSIZE(jb));
-}
\ No newline at end of file
+}
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index cc9c51c5cbf..b3fd1dfb631 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -1060,7 +1060,6 @@ jsonb_value_to_float8_datum(JsonbValue *v)
return DirectFunctionCall1(numeric_float8, NumericGetDatum(v->val.numeric));
}
-
static Datum
jsonb_value_to_int2_datum(JsonbValue *v)
{
@@ -1181,6 +1180,191 @@ DEFINE_JSONB_ARRAY_ELEMENT_TYPED(jsonb_array_element_float8, jsonb_value_to_floa
DEFINE_JSONB_ARRAY_ELEMENT_TYPED(jsonb_array_element_int2, jsonb_value_to_int2_datum)
DEFINE_JSONB_ARRAY_ELEMENT_TYPED(jsonb_array_element_float4, jsonb_value_to_float4_datum)
+/*
+ * Walk a text[] path through a jsonb value and return the leaf JsonbValue,
+ * or NULL. Returns NULL when a path element is missing, the leaf is JSON
+ * null, or the path array contains null elements.
+ *
+ * For an empty path, returns the root value itself (as a jbvBinary for
+ * containers, or the unwrapped scalar for scalar jsonb). The caller's
+ * conversion helper then decides whether to accept or reject the type.
+ *
+ * For arrays, path elements are parsed as integers (matching the existing
+ * jsonb_extract_path / jsonb_get_element traversal semantics).
+ */
+static JsonbValue *
+jsonb_extract_path_lookup(Jsonb *jb, ArrayType *path, JsonbValue *vbuf)
+{
+ Datum *pathtext;
+ int npath;
+ JsonbContainer *container = &jb->root;
+ JsonbValue *jbvp = NULL;
+ bool have_object,
+ have_array;
+ int i;
+
+ /* If the path array contains any nulls, return NULL */
+ if (array_contains_nulls(path))
+ return NULL;
+
+ deconstruct_array_builtin(path, TEXTOID, &pathtext, NULL, &npath);
+
+ /* Identify top-level container type */
+ if (JB_ROOT_IS_OBJECT(jb))
+ {
+ have_object = true;
+ have_array = false;
+ }
+ else if (JB_ROOT_IS_ARRAY(jb) && !JB_ROOT_IS_SCALAR(jb))
+ {
+ have_object = false;
+ have_array = true;
+ }
+ else
+ {
+ Assert(JB_ROOT_IS_ARRAY(jb) && JB_ROOT_IS_SCALAR(jb));
+
+ /* For a scalar root with an empty path, return the scalar itself */
+ if (npath <= 0)
+ {
+ jbvp = getIthJsonbValueFromContainer(container, 0);
+ if (jbvp == NULL || jbvp->type == jbvNull)
+ return NULL;
+ return jbvp;
+ }
+
+ /* Non-empty path into a scalar yields NULL */
+ return NULL;
+ }
+
+ /*
+ * For an empty path on a container root, return the container as a
+ * jbvBinary value. The conversion helper will reject it with the
+ * usual "cannot cast jsonb array or object" error.
+ */
+ if (npath <= 0)
+ {
+ vbuf->type = jbvBinary;
+ vbuf->val.binary.data = container;
+ vbuf->val.binary.len = VARSIZE(jb) - VARHDRSZ;
+ return vbuf;
+ }
+
+ for (i = 0; i < npath; i++)
+ {
+ if (have_object)
+ {
+ text *subscr = DatumGetTextPP(pathtext[i]);
+
+ jbvp = getKeyJsonValueFromContainer(container,
+ VARDATA_ANY(subscr),
+ VARSIZE_ANY_EXHDR(subscr),
+ NULL);
+ }
+ else if (have_array)
+ {
+ int lindex;
+ uint32 index;
+ char *indextext = TextDatumGetCString(pathtext[i]);
+ char *endptr;
+
+ errno = 0;
+ lindex = strtoint(indextext, &endptr, 10);
+ if (endptr == indextext || *endptr != '\0' || errno != 0)
+ return NULL;
+
+ if (lindex >= 0)
+ {
+ index = (uint32) lindex;
+ }
+ else
+ {
+ uint32 nelements;
+
+ if (!JsonContainerIsArray(container))
+ elog(ERROR, "not a jsonb array");
+
+ nelements = JsonContainerSize(container);
+
+ if (lindex == INT_MIN || -lindex > nelements)
+ return NULL;
+ else
+ index = nelements + lindex;
+ }
+
+ jbvp = getIthJsonbValueFromContainer(container, index);
+ }
+ else
+ {
+ /* scalar: cannot traverse further */
+ return NULL;
+ }
+
+ if (jbvp == NULL)
+ return NULL;
+
+ /* If this is the last path element, we have our leaf */
+ if (i == npath - 1)
+ break;
+
+ /* Descend into the next container level */
+ if (jbvp->type == jbvBinary)
+ {
+ container = jbvp->val.binary.data;
+ have_object = JsonContainerIsObject(container);
+ have_array = JsonContainerIsArray(container);
+ }
+ else
+ {
+ /* scalar at intermediate step: no further traversal */
+ have_object = false;
+ have_array = false;
+ }
+ }
+
+ /* Missing value or JSON null both map to SQL NULL */
+ if (jbvp == NULL || jbvp->type == jbvNull)
+ return NULL;
+
+ return jbvp;
+}
+
+/*
+ * Thin-wrapper macro for the jsonb_extract_path_<type> extractor family.
+ * Same pattern as the object-field and array-element wrapper macros.
+ */
+#define DEFINE_JSONB_EXTRACT_PATH_TYPED(fname, convfn) \
+Datum \
+fname(PG_FUNCTION_ARGS) \
+{ \
+ Jsonb *jb = PG_GETARG_JSONB_P(0); \
+ ArrayType *path = PG_GETARG_ARRAYTYPE_P(1); \
+ JsonbValue vbuf; \
+ JsonbValue *v; \
+ Datum result; \
+\
+ v = jsonb_extract_path_lookup(jb, path, &vbuf); \
+ if (v == NULL) \
+ { \
+ PG_FREE_IF_COPY(path, 1); \
+ PG_FREE_IF_COPY(jb, 0); \
+ PG_RETURN_NULL(); \
+ } \
+\
+ result = convfn(v); \
+ PG_FREE_IF_COPY(path, 1); \
+ PG_FREE_IF_COPY(jb, 0); \
+ return result; \
+}
+
+DEFINE_JSONB_EXTRACT_PATH_TYPED(jsonb_extract_path_numeric, jsonb_value_to_numeric_datum)
+DEFINE_JSONB_EXTRACT_PATH_TYPED(jsonb_extract_path_bool, jsonb_value_to_bool_datum)
+DEFINE_JSONB_EXTRACT_PATH_TYPED(jsonb_extract_path_int4, jsonb_value_to_int4_datum)
+DEFINE_JSONB_EXTRACT_PATH_TYPED(jsonb_extract_path_int8, jsonb_value_to_int8_datum)
+DEFINE_JSONB_EXTRACT_PATH_TYPED(jsonb_extract_path_float8, jsonb_value_to_float8_datum)
+DEFINE_JSONB_EXTRACT_PATH_TYPED(jsonb_extract_path_int2, jsonb_value_to_int2_datum)
+DEFINE_JSONB_EXTRACT_PATH_TYPED(jsonb_extract_path_float4, jsonb_value_to_float4_datum)
+
Datum
json_array_element(PG_FUNCTION_ARGS)
{
@@ -6427,4 +6611,4 @@ json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable)
*has_mutable = true;
break;
}
-}
\ No newline at end of file
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad752a0a9e0..d8ac9cbf053 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12827,6 +12827,26 @@
proname => 'jsonb_array_element_float8', prorettype => 'float8',
proargtypes => 'jsonb int4', proargnames => '{from_json,element_index}',
prosrc => 'jsonb_array_element_float8' },
+{ oid => '9965', descr => 'extract numeric from jsonb by path',
+ proname => 'jsonb_extract_path_numeric', prorettype => 'numeric',
+ proargtypes => 'jsonb _text', proargnames => '{from_json,path_elems}',
+ prosrc => 'jsonb_extract_path_numeric' },
+{ oid => '9966', descr => 'extract boolean from jsonb by path',
+ proname => 'jsonb_extract_path_bool', prorettype => 'bool',
+ proargtypes => 'jsonb _text', proargnames => '{from_json,path_elems}',
+ prosrc => 'jsonb_extract_path_bool' },
+{ oid => '9967', descr => 'extract int4 from jsonb by path',
+ proname => 'jsonb_extract_path_int4', prorettype => 'int4',
+ proargtypes => 'jsonb _text', proargnames => '{from_json,path_elems}',
+ prosrc => 'jsonb_extract_path_int4' },
+{ oid => '9968', descr => 'extract int8 from jsonb by path',
+ proname => 'jsonb_extract_path_int8', prorettype => 'int8',
+ proargtypes => 'jsonb _text', proargnames => '{from_json,path_elems}',
+ prosrc => 'jsonb_extract_path_int8' },
+{ oid => '9969', descr => 'extract float8 from jsonb by path',
+ proname => 'jsonb_extract_path_float8', prorettype => 'float8',
+ proargtypes => 'jsonb _text', proargnames => '{from_json,path_elems}',
+ prosrc => 'jsonb_extract_path_float8' },
{ oid => '9982', descr => 'extract int2 from jsonb array by element index',
proname => 'jsonb_array_element_int2', prorettype => 'int2',
proargtypes => 'jsonb int4', proargnames => '{from_json,element_index}',
@@ -12836,4 +12856,13 @@
proargtypes => 'jsonb int4', proargnames => '{from_json,element_index}',
prosrc => 'jsonb_array_element_float4' },
-]
\ No newline at end of file
+{ oid => '9984', descr => 'extract int2 from jsonb by path',
+ proname => 'jsonb_extract_path_int2', prorettype => 'int2',
+ proargtypes => 'jsonb _text', proargnames => '{from_json,path_elems}',
+ prosrc => 'jsonb_extract_path_int2' },
+{ oid => '9985', descr => 'extract float4 from jsonb by path',
+ proname => 'jsonb_extract_path_float4', prorettype => 'float4',
+ proargtypes => 'jsonb _text', proargnames => '{from_json,path_elems}',
+ prosrc => 'jsonb_extract_path_float4' },
+
+]
diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out
index 36bc82095be..b96c095525c 100644
--- a/src/test/regress/expected/jsonb.out
+++ b/src/test/regress/expected/jsonb.out
@@ -1247,6 +1247,233 @@ SELECT jsonb_array_element_float4('[3.14, 2.5]'::jsonb, 1);
2.5
(1 row)
+-- Optimized typed extraction: extract-path family
+-- The planner rewrites (j #> path)::type and (jsonb_extract_path(j, ...))::type
+-- into direct typed extractor calls for the same target types.
+-- Section P1: planner rewrite verification (operator form #>)
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::numeric FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+---------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_numeric(test_json, '{field4}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::int4 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_int4(test_json, '{field4}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::float8 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_float8(test_json, '{field4}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field6,f1}')::int8 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+---------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_int8(test_json, '{field6,f1}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field7}')::bool FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_bool(test_json, '{field7}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::int2 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_int2(test_json, '{field4}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::float4 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_float4(test_json, '{field4}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+-- Section P1b: planner rewrite verification (direct function-call form)
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (jsonb_extract_path(test_json, 'field4'))::float8 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_float8(test_json, '{field4}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (jsonb_extract_path(test_json, 'field6', 'f1'))::int4 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+---------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_int4(test_json, '{field6,f1}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+-- Section P1c: planner rewrite verification (VARIADIC ARRAY form)
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (jsonb_extract_path(test_json, VARIADIC ARRAY['field6','f1']))::int4 FROM test_jsonb WHERE json_type = 'object';
+ QUERY PLAN
+---------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+ Output: jsonb_extract_path_int4(test_json, '{field6,f1}'::text[])
+ Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+-- Section P2: correct execution through rewritten path
+SELECT (test_json #> '{field4}')::int4 FROM test_jsonb WHERE json_type = 'object';
+ int4
+------
+ 4
+(1 row)
+
+SELECT (test_json #> '{field4}')::numeric FROM test_jsonb WHERE json_type = 'object';
+ numeric
+---------
+ 4
+(1 row)
+
+SELECT (test_json #> '{field4}')::float8 FROM test_jsonb WHERE json_type = 'object';
+ float8
+--------
+ 4
+(1 row)
+
+SELECT (test_json #> '{field6,f1}')::int4 FROM test_jsonb WHERE json_type = 'object';
+ int4
+------
+ 9
+(1 row)
+
+SELECT (test_json #> '{field6,f1}')::int8 FROM test_jsonb WHERE json_type = 'object';
+ int8
+------
+ 9
+(1 row)
+
+SELECT (test_json #> '{field7}')::bool FROM test_jsonb WHERE json_type = 'object';
+ bool
+------
+ t
+(1 row)
+
+-- array index in path
+SELECT ('{"a":[10,20,30]}'::jsonb #> '{a,1}')::int4;
+ int4
+------
+ 20
+(1 row)
+
+-- Section P3: NULL semantics
+SELECT (test_json #> '{nonexistent}')::int4 FROM test_jsonb WHERE json_type = 'object'; -- missing key
+ int4
+------
+
+(1 row)
+
+SELECT (test_json #> '{field3}')::numeric FROM test_jsonb WHERE json_type = 'object'; -- JSON null leaf
+ numeric
+---------
+
+(1 row)
+
+SELECT (test_json #> '{field6,missing}')::int4 FROM test_jsonb WHERE json_type = 'object'; -- missing nested key
+ int4
+------
+
+(1 row)
+
+-- null in path array
+SELECT jsonb_extract_path_int4('{"a":1}'::jsonb, ARRAY[NULL]::text[]);
+ jsonb_extract_path_int4
+-------------------------
+
+(1 row)
+
+-- empty path on container root produces cast error
+SELECT jsonb_extract_path_int4('{"a":1}'::jsonb, '{}'::text[]);
+ERROR: cannot cast jsonb array or object to type integer
+-- Section P4: type-mismatch errors
+SELECT (test_json #> '{field1}')::int4 FROM test_jsonb WHERE json_type = 'object'; -- string to int4
+ERROR: cannot cast jsonb string to type integer
+SELECT (test_json #> '{field5}')::float8 FROM test_jsonb WHERE json_type = 'object'; -- array to float8
+ERROR: cannot cast jsonb array or object to type double precision
+SELECT (test_json #> '{field6}')::int8 FROM test_jsonb WHERE json_type = 'object'; -- object to int8
+ERROR: cannot cast jsonb array or object to type bigint
+-- Section P5: direct calls to extract-path typed extractor builtins
+SELECT jsonb_extract_path_int4('{"a":{"b":42}}'::jsonb, ARRAY['a','b']);
+ jsonb_extract_path_int4
+-------------------------
+ 42
+(1 row)
+
+SELECT jsonb_extract_path_int8('{"x":99}'::jsonb, ARRAY['x']);
+ jsonb_extract_path_int8
+-------------------------
+ 99
+(1 row)
+
+SELECT jsonb_extract_path_float8('{"a":3.14}'::jsonb, ARRAY['a']);
+ jsonb_extract_path_float8
+---------------------------
+ 3.14
+(1 row)
+
+SELECT jsonb_extract_path_numeric('{"a":1.23}'::jsonb, ARRAY['a']);
+ jsonb_extract_path_numeric
+----------------------------
+ 1.23
+(1 row)
+
+SELECT jsonb_extract_path_bool('{"a":true}'::jsonb, ARRAY['a']);
+ jsonb_extract_path_bool
+-------------------------
+ t
+(1 row)
+
+-- direct calls: missing path
+SELECT jsonb_extract_path_int4('{"a":1}'::jsonb, ARRAY['b']);
+ jsonb_extract_path_int4
+-------------------------
+
+(1 row)
+
+-- direct calls: type-mismatch
+SELECT jsonb_extract_path_int4('{"a":"text"}'::jsonb, ARRAY['a']);
+ERROR: cannot cast jsonb string to type integer
+-- direct calls: array index in path
+SELECT jsonb_extract_path_int4('{"a":[10,20]}'::jsonb, ARRAY['a','1']);
+ jsonb_extract_path_int4
+-------------------------
+ 20
+(1 row)
+
+-- direct calls: int2 and float4
+SELECT jsonb_extract_path_int2('{"a":{"b":7}}'::jsonb, ARRAY['a','b']);
+ jsonb_extract_path_int2
+-------------------------
+ 7
+(1 row)
+
+SELECT jsonb_extract_path_float4('{"a":3.14}'::jsonb, ARRAY['a']);
+ jsonb_extract_path_float4
+---------------------------
+ 3.14
+(1 row)
+
SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'scalar';
?column?
----------
diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql
index 69c21776f84..11191fd0f82 100644
--- a/src/test/regress/sql/jsonb.sql
+++ b/src/test/regress/sql/jsonb.sql
@@ -358,6 +358,67 @@ SELECT jsonb_array_element_int2('[10, 20]'::jsonb, 0);
SELECT jsonb_array_element_float4('[3.14, 2.5]'::jsonb, 1);
+-- Optimized typed extraction: extract-path family
+-- The planner rewrites (j #> path)::type and (jsonb_extract_path(j, ...))::type
+-- into direct typed extractor calls for the same target types.
+
+-- Section P1: planner rewrite verification (operator form #>)
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::numeric FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::int4 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::float8 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field6,f1}')::int8 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field7}')::bool FROM test_jsonb WHERE json_type = 'object';
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::int2 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json #> '{field4}')::float4 FROM test_jsonb WHERE json_type = 'object';
+-- Section P1b: planner rewrite verification (direct function-call form)
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (jsonb_extract_path(test_json, 'field4'))::float8 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (jsonb_extract_path(test_json, 'field6', 'f1'))::int4 FROM test_jsonb WHERE json_type = 'object';
+
+-- Section P1c: planner rewrite verification (VARIADIC ARRAY form)
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (jsonb_extract_path(test_json, VARIADIC ARRAY['field6','f1']))::int4 FROM test_jsonb WHERE json_type = 'object';
+
+-- Section P2: correct execution through rewritten path
+SELECT (test_json #> '{field4}')::int4 FROM test_jsonb WHERE json_type = 'object';
+SELECT (test_json #> '{field4}')::numeric FROM test_jsonb WHERE json_type = 'object';
+SELECT (test_json #> '{field4}')::float8 FROM test_jsonb WHERE json_type = 'object';
+SELECT (test_json #> '{field6,f1}')::int4 FROM test_jsonb WHERE json_type = 'object';
+SELECT (test_json #> '{field6,f1}')::int8 FROM test_jsonb WHERE json_type = 'object';
+SELECT (test_json #> '{field7}')::bool FROM test_jsonb WHERE json_type = 'object';
+-- array index in path
+SELECT ('{"a":[10,20,30]}'::jsonb #> '{a,1}')::int4;
+
+-- Section P3: NULL semantics
+SELECT (test_json #> '{nonexistent}')::int4 FROM test_jsonb WHERE json_type = 'object'; -- missing key
+SELECT (test_json #> '{field3}')::numeric FROM test_jsonb WHERE json_type = 'object'; -- JSON null leaf
+SELECT (test_json #> '{field6,missing}')::int4 FROM test_jsonb WHERE json_type = 'object'; -- missing nested key
+-- null in path array
+SELECT jsonb_extract_path_int4('{"a":1}'::jsonb, ARRAY[NULL]::text[]);
+-- empty path on container root produces cast error
+SELECT jsonb_extract_path_int4('{"a":1}'::jsonb, '{}'::text[]);
+
+-- Section P4: type-mismatch errors
+SELECT (test_json #> '{field1}')::int4 FROM test_jsonb WHERE json_type = 'object'; -- string to int4
+SELECT (test_json #> '{field5}')::float8 FROM test_jsonb WHERE json_type = 'object'; -- array to float8
+SELECT (test_json #> '{field6}')::int8 FROM test_jsonb WHERE json_type = 'object'; -- object to int8
+
+-- Section P5: direct calls to extract-path typed extractor builtins
+SELECT jsonb_extract_path_int4('{"a":{"b":42}}'::jsonb, ARRAY['a','b']);
+SELECT jsonb_extract_path_int8('{"x":99}'::jsonb, ARRAY['x']);
+SELECT jsonb_extract_path_float8('{"a":3.14}'::jsonb, ARRAY['a']);
+SELECT jsonb_extract_path_numeric('{"a":1.23}'::jsonb, ARRAY['a']);
+SELECT jsonb_extract_path_bool('{"a":true}'::jsonb, ARRAY['a']);
+-- direct calls: missing path
+SELECT jsonb_extract_path_int4('{"a":1}'::jsonb, ARRAY['b']);
+-- direct calls: type-mismatch
+SELECT jsonb_extract_path_int4('{"a":"text"}'::jsonb, ARRAY['a']);
+-- direct calls: array index in path
+SELECT jsonb_extract_path_int4('{"a":[10,20]}'::jsonb, ARRAY['a','1']);
+-- direct calls: int2 and float4
+SELECT jsonb_extract_path_int2('{"a":{"b":7}}'::jsonb, ARRAY['a','b']);
+SELECT jsonb_extract_path_float4('{"a":3.14}'::jsonb, ARRAY['a']);
+
+
SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'scalar';
SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'array';
SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'object';
--
2.52.0