From f02b96e923767c342ca03668a73840223fb853f6 Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Thu, 2 Apr 2026 21:22:54 -0700 Subject: [PATCH v3] jsonb: optimize object-field casts to numeric and bool Add a support function for jsonb-to-numeric/bool casts that recognizes casts over jsonb object-field extraction, including jsonb_object_field(), ->, and subscripting by key, and rewrites them to explicit typed extractor calls. This keeps ordinary SQL syntax unchanged while avoiding extra jsonb scalar wrapping/unwrapping on the optimized path. Stage 1 only: covers object-field extraction to numeric/bool. Co-authored-by: Andy Fan --- src/backend/utils/adt/jsonb.c | 126 +++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 136 ++++++++++++++++++ src/include/catalog/pg_proc.dat | 18 ++- src/test/regress/expected/jsonb.out | 205 +++++++++++++++++++++++++++- src/test/regress/sql/jsonb.sql | 63 ++++++++- 5 files changed, 543 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 864c5ac1c85..1ee15679733 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -17,6 +17,9 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "nodes/supportnodes.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/json.h" @@ -1816,6 +1819,129 @@ cannotCastJsonbValue(enum jbvType type, const char *sqltype, Node *escontext) return (Datum) 0; } +/* + * jsonb_cast_support() + * + * Planner support function for jsonb-to-scalar cast functions, attached via + * prosupport on the jsonb_numeric and jsonb_bool catalog entries. + * + * When the sole argument to the cast is a jsonb_object_field() call (the -> + * operator), we replace the two-step cast(extract(...)) expression with a + * single typed extractor that reads the scalar directly from the in-memory + * JsonbValue, avoiding a round-trip through JsonbValueToJsonb. + * + * For example, (j -> 'a')::numeric is parsed as: + * jsonb_numeric(jsonb_object_field(j, 'a')) + * and is rewritten to: + * jsonb_object_field_numeric(j, 'a') + */ +Datum +jsonb_cast_support(PG_FUNCTION_ARGS) +{ + Node *rawreq = (Node *) PG_GETARG_POINTER(0); + Node *ret = NULL; + + if (IsA(rawreq, SupportRequestSimplify)) + { + SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq; + FuncExpr *fexpr = req->fcall; + Node *arg; + Oid inner_funcid; + List *inner_args; + int location; + Oid replacement_funcid; + Oid replacement_rettype; + FuncExpr *newfexpr; + + /* The cast function must have exactly one argument */ + if (list_length(fexpr->args) != 1) + PG_RETURN_POINTER(NULL); + + arg = (Node *) linitial(fexpr->args); + + /* + * Identify the inner extraction expression. It may appear as a + * FuncExpr, an OpExpr, or a SubscriptingRef, depending on how the + * expression is represented at this point. Accept the supported + * forms. + */ + if (IsA(arg, FuncExpr)) + { + FuncExpr *inner = (FuncExpr *) arg; + + inner_funcid = inner->funcid; + inner_args = inner->args; + location = inner->location; + } + else if (IsA(arg, OpExpr)) + { + OpExpr *inner = (OpExpr *) arg; + + inner_funcid = inner->opfuncid; + inner_args = inner->args; + location = inner->location; + } + else if (IsA(arg, SubscriptingRef)) + { + SubscriptingRef *sbsref = (SubscriptingRef *) arg; + Node *subscript; + + /* + * Only handle the narrow case equivalent to object-field + * extraction: a single text-typed subscript on a jsonb + * container, with no slice and no assignment. + */ + if (sbsref->refcontainertype != JSONBOID) + PG_RETURN_POINTER(NULL); + if (list_length(sbsref->refupperindexpr) != 1) + PG_RETURN_POINTER(NULL); + if (sbsref->reflowerindexpr != NIL) + PG_RETURN_POINTER(NULL); + if (sbsref->refassgnexpr != NULL) + PG_RETURN_POINTER(NULL); + + subscript = (Node *) linitial(sbsref->refupperindexpr); + if (exprType(subscript) != TEXTOID) + PG_RETURN_POINTER(NULL); + + inner_funcid = F_JSONB_OBJECT_FIELD; + inner_args = list_make2(sbsref->refexpr, subscript); + location = exprLocation(arg); + } + else + PG_RETURN_POINTER(NULL); + + /* Only rewrite jsonb_object_field(jsonb, text); verify arity too */ + if (inner_funcid != F_JSONB_OBJECT_FIELD) + PG_RETURN_POINTER(NULL); + if (list_length(inner_args) != 2) + PG_RETURN_POINTER(NULL); + + /* Map the outer cast to the corresponding typed extractor */ + if (fexpr->funcid == F_NUMERIC_JSONB) + { + replacement_funcid = F_JSONB_OBJECT_FIELD_NUMERIC; + replacement_rettype = NUMERICOID; + } + else if (fexpr->funcid == F_BOOL_JSONB) + { + replacement_funcid = F_JSONB_OBJECT_FIELD_BOOL; + replacement_rettype = BOOLOID; + } + else + PG_RETURN_POINTER(NULL); + + /* Build the replacement function call */ + newfexpr = makeFuncExpr(replacement_funcid, replacement_rettype, + inner_args, InvalidOid, InvalidOid, + COERCE_EXPLICIT_CALL); + newfexpr->location = location; + ret = (Node *) newfexpr; + } + + PG_RETURN_POINTER(ret); +} + Datum jsonb_bool(PG_FUNCTION_ARGS) { diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 97cc3d60340..fe11f5edd55 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -921,6 +921,142 @@ jsonb_object_field_text(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } +/* + * Typed scalar extraction from jsonb object fields. + * + * These functions extract a typed scalar directly from the in-memory + * JsonbValue found by key lookup, skipping the intermediate Jsonb + * serialization that occurs with the unoptimized cast-over-extraction path. + * + * They live here alongside jsonb_object_field() because they share the + * same key-lookup logic (getKeyJsonValueFromContainer). + * + * Registered in pg_proc as ordinary SQL-callable builtins; also serve as + * planner rewrite targets for jsonb_cast_support() in jsonb.c. + * + * NULL semantics match the existing cast path: a missing key or a JSON + * null value both produce SQL NULL; a type mismatch raises ERROR. + */ + +/* + * Look up a key in a jsonb object and return the JsonbValue, or NULL. + * Returns NULL (without error) when the input is not an object, the key + * is absent, or the value is JSON null. + */ +static JsonbValue * +jsonb_object_field_lookup(Jsonb *jb, text *key, JsonbValue *vbuf) +{ + JsonbValue *v; + + if (!JB_ROOT_IS_OBJECT(jb)) + return NULL; + + v = getKeyJsonValueFromContainer(&jb->root, + VARDATA_ANY(key), + VARSIZE_ANY_EXHDR(key), + vbuf); + + /* Missing key or JSON null both map to SQL NULL */ + if (v == NULL || v->type == jbvNull) + return NULL; + + return v; +} + +/* + * Raise a type-mismatch error for typed field extraction. + * + * The message wording matches cannotCastJsonbValue() in jsonb.c so that + * the optimized and unoptimized paths produce identical errors. + */ +static void +jsonb_field_cast_error(JsonbValue *v, const char *sqltype) +{ + const char *jsontype; + + switch (v->type) + { + case jbvNull: + jsontype = "null"; + break; + case jbvString: + jsontype = "string"; + break; + case jbvNumeric: + jsontype = "numeric"; + break; + case jbvBool: + jsontype = "boolean"; + break; + case jbvArray: + jsontype = "array"; + break; + case jbvObject: + jsontype = "object"; + break; + case jbvBinary: + jsontype = "array or object"; + break; + default: + elog(ERROR, "unknown jsonb type: %d", (int) v->type); + jsontype = NULL; /* keep compiler quiet */ + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot cast jsonb %s to type %s", + jsontype, sqltype))); +} + +/* + * jsonb_object_field_numeric - extract a numeric value from a jsonb object + * by field name. Returns NULL for missing keys and JSON nulls. + */ +Datum +jsonb_object_field_numeric(PG_FUNCTION_ARGS) +{ + Jsonb *jb = PG_GETARG_JSONB_P(0); + text *key = PG_GETARG_TEXT_PP(1); + JsonbValue vbuf; + JsonbValue *v; + Numeric retValue; + + v = jsonb_object_field_lookup(jb, key, &vbuf); + if (v == NULL) + PG_RETURN_NULL(); + + if (v->type != jbvNumeric) + jsonb_field_cast_error(v, "numeric"); + + retValue = DatumGetNumericCopy(NumericGetDatum(v->val.numeric)); + + PG_FREE_IF_COPY(jb, 0); + PG_RETURN_NUMERIC(retValue); +} + +/* + * jsonb_object_field_bool - extract a boolean value from a jsonb object + * by field name. Returns NULL for missing keys and JSON nulls. + */ +Datum +jsonb_object_field_bool(PG_FUNCTION_ARGS) +{ + Jsonb *jb = PG_GETARG_JSONB_P(0); + text *key = PG_GETARG_TEXT_PP(1); + JsonbValue vbuf; + JsonbValue *v; + + v = jsonb_object_field_lookup(jb, key, &vbuf); + if (v == NULL) + PG_RETURN_NULL(); + + if (v->type != jbvBool) + jsonb_field_cast_error(v, "boolean"); + + PG_FREE_IF_COPY(jb, 0); + PG_RETURN_BOOL(v->val.boolean); +} + Datum json_array_element(PG_FUNCTION_ARGS) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3ea17fc5629..a608f7a2357 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4798,10 +4798,10 @@ prosrc => 'numeric_pg_lsn' }, { oid => '3556', descr => 'convert jsonb to boolean', - proname => 'bool', prorettype => 'bool', proargtypes => 'jsonb', + proname => 'bool', prosupport => 'jsonb_cast_support', prorettype => 'bool', proargtypes => 'jsonb', prosrc => 'jsonb_bool' }, { oid => '3449', descr => 'convert jsonb to numeric', - proname => 'numeric', prorettype => 'numeric', proargtypes => 'jsonb', + proname => 'numeric', prosupport => 'jsonb_cast_support', prorettype => 'numeric', proargtypes => 'jsonb', prosrc => 'jsonb_numeric' }, { oid => '3450', descr => 'convert jsonb to int2', proname => 'int2', prorettype => 'int2', proargtypes => 'jsonb', @@ -12912,4 +12912,18 @@ proname => 'hashoid8extended', prorettype => 'int8', proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' }, + +# jsonb cast optimization support functions +{ oid => '9950', descr => 'planner support for jsonb casts', + proname => 'jsonb_cast_support', prorettype => 'internal', + proargtypes => 'internal', prosrc => 'jsonb_cast_support' }, +{ oid => '9953', descr => 'extract numeric from jsonb object by field name', + proname => 'jsonb_object_field_numeric', prorettype => 'numeric', + proargtypes => 'jsonb text', proargnames => '{from_json,field_name}', + prosrc => 'jsonb_object_field_numeric' }, +{ oid => '9954', descr => 'extract boolean from jsonb object by field name', + proname => 'jsonb_object_field_bool', prorettype => 'bool', + proargtypes => 'jsonb text', proargnames => '{from_json,field_name}', + prosrc => 'jsonb_object_field_bool' }, + ] diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 4e2467852db..11f9cccb649 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -458,7 +458,207 @@ CREATE TEMP TABLE test_jsonb ( INSERT INTO test_jsonb VALUES ('scalar','"a scalar"'), ('array','["zero", "one","two",null,"four","five", [1,2,3],{"f1":9}]'), -('object','{"field1":"val1","field2":"val2","field3":null, "field4": 4, "field5": [1,2,3], "field6": {"f1":9}}'); +('object','{"field1":"val1","field2":"val2","field3":null, "field4": 4, "field5": [1,2,3], "field6": {"f1":9}, "field7": true}'); +-- Optimized typed extraction: the planner rewrites (j->'key')::type into a +-- direct typed extractor call, currently for numeric and bool only. +-- Section 1: planner rewrite verification (rewritten targets) +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_object_field_numeric(test_json, 'field4'::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_object_field_bool(test_json, 'field7'::text) + Filter: (test_jsonb.json_type = 'object'::text) +(3 rows) + +-- Section 1b: planner rewrite verification for subscripting syntax +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_object_field_numeric(test_json, 'field4'::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_object_field_bool(test_json, 'field7'::text) + Filter: (test_jsonb.json_type = 'object'::text) +(3 rows) + +-- Verify that unsupported cast targets are NOT rewritten by this patch. +-- Integer and float casts remain on the original jsonb_object_field + cast path. +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: ((test_json -> 'field4'::text))::integer + Filter: (test_jsonb.json_type = 'object'::text) +(3 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json -> 'field4')::int8 FROM test_jsonb WHERE json_type = 'object'; + QUERY PLAN +--------------------------------------------------- + Seq Scan on pg_temp.test_jsonb + Output: ((test_json -> 'field4'::text))::bigint + 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: ((test_json -> 'field4'::text))::double precision + Filter: (test_jsonb.json_type = 'object'::text) +(3 rows) + +-- Section 2: correct execution through the rewritten path +SELECT (test_json -> 'field4')::numeric FROM test_jsonb WHERE json_type = 'object'; + numeric +--------- + 4 +(1 row) + +SELECT (test_json -> 'field7')::bool FROM test_jsonb WHERE json_type = 'object'; + bool +------ + t +(1 row) + +-- Section 2b: correct execution through subscripting syntax +SELECT (test_json['field4'])::numeric FROM test_jsonb WHERE json_type = 'object'; + test_json +----------- + 4 +(1 row) + +SELECT (test_json['field7'])::bool FROM test_jsonb WHERE json_type = 'object'; + test_json +----------- + t +(1 row) + +-- Unsupported targets still work correctly through the original path +SELECT (test_json -> 'field4')::int4 FROM test_jsonb WHERE json_type = 'object'; + int4 +------ + 4 +(1 row) + +SELECT (test_json -> 'field4')::float8 FROM test_jsonb WHERE json_type = 'object'; + float8 +-------- + 4 +(1 row) + +-- Section 3: NULL semantics (missing key, JSON null, non-object input) +SELECT (test_json -> 'field3')::numeric FROM test_jsonb WHERE json_type = 'object'; -- JSON null + numeric +--------- + +(1 row) + +SELECT (test_json -> 'nonexistent')::numeric FROM test_jsonb WHERE json_type = 'object'; -- missing key + numeric +--------- + +(1 row) + +SELECT (test_json -> 'x')::numeric FROM test_jsonb WHERE json_type = 'array'; -- non-object + numeric +--------- + +(1 row) + +SELECT (test_json -> 'field3')::bool FROM test_jsonb WHERE json_type = 'object'; -- JSON null, bool path + bool +------ + +(1 row) + +-- Section 3b: NULL semantics through subscripting syntax +SELECT (test_json['field3'])::numeric FROM test_jsonb WHERE json_type = 'object'; -- JSON null + test_json +----------- + +(1 row) + +SELECT (test_json['nonexistent'])::numeric FROM test_jsonb WHERE json_type = 'object'; -- missing key + test_json +----------- + +(1 row) + +-- Section 4: type-mismatch errors (scalar and container types) +SELECT (test_json -> 'field1')::numeric FROM test_jsonb WHERE json_type = 'object'; -- string to numeric +ERROR: cannot cast jsonb string to type numeric +SELECT (test_json -> 'field1')::bool FROM test_jsonb WHERE json_type = 'object'; -- string to bool +ERROR: cannot cast jsonb string to type boolean +SELECT (test_json -> 'field5')::numeric FROM test_jsonb WHERE json_type = 'object'; -- array to numeric +ERROR: cannot cast jsonb array or object to type numeric +-- Section 4b: type-mismatch error through subscripting syntax +SELECT (test_json['field1'])::numeric FROM test_jsonb WHERE json_type = 'object'; -- string to numeric +ERROR: cannot cast jsonb string to type numeric +-- Section 5: direct calls to typed extractor builtins +SELECT jsonb_object_field_numeric('{"a": 1}'::jsonb, 'a'); + jsonb_object_field_numeric +---------------------------- + 1 +(1 row) + +SELECT jsonb_object_field_numeric('{"a": 3.14}'::jsonb, 'a'); + jsonb_object_field_numeric +---------------------------- + 3.14 +(1 row) + +SELECT jsonb_object_field_bool('{"a": true}'::jsonb, 'a'); + jsonb_object_field_bool +------------------------- + t +(1 row) + +SELECT jsonb_object_field_bool('{"a": false}'::jsonb, 'a'); + jsonb_object_field_bool +------------------------- + f +(1 row) + +-- direct calls: NULL semantics +SELECT jsonb_object_field_numeric('{"a": 1}'::jsonb, 'missing'); + jsonb_object_field_numeric +---------------------------- + +(1 row) + +SELECT jsonb_object_field_numeric('{"a": null}'::jsonb, 'a'); + jsonb_object_field_numeric +---------------------------- + +(1 row) + +SELECT jsonb_object_field_bool('{"a": true}'::jsonb, 'missing'); + jsonb_object_field_bool +------------------------- + +(1 row) + +-- direct calls: type-mismatch errors +SELECT jsonb_object_field_numeric('{"a": "text"}'::jsonb, 'a'); +ERROR: cannot cast jsonb string to type numeric +SELECT jsonb_object_field_bool('{"a": 1}'::jsonb, 'a'); +ERROR: cannot cast jsonb numeric to type boolean +SELECT jsonb_object_field_numeric('{"a": {"x":1}}'::jsonb, 'a'); -- container to scalar +ERROR: cannot cast jsonb array or object to type numeric SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'scalar'; ?column? ---------- @@ -586,7 +786,8 @@ SELECT jsonb_object_keys(test_json) FROM test_jsonb WHERE json_type = 'object'; field4 field5 field6 -(6 rows) + field7 +(7 rows) -- nulls SELECT (test_json->'field3') IS NULL AS expect_false FROM test_jsonb WHERE json_type = 'object'; diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index d28ed1c1e85..09836bca0a9 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -155,7 +155,68 @@ CREATE TEMP TABLE test_jsonb ( INSERT INTO test_jsonb VALUES ('scalar','"a scalar"'), ('array','["zero", "one","two",null,"four","five", [1,2,3],{"f1":9}]'), -('object','{"field1":"val1","field2":"val2","field3":null, "field4": 4, "field5": [1,2,3], "field6": {"f1":9}}'); +('object','{"field1":"val1","field2":"val2","field3":null, "field4": 4, "field5": [1,2,3], "field6": {"f1":9}, "field7": true}'); + +-- Optimized typed extraction: the planner rewrites (j->'key')::type into a +-- direct typed extractor call, currently for numeric and bool only. + +-- Section 1: planner rewrite verification (rewritten targets) +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json -> 'field4')::numeric FROM test_jsonb WHERE json_type = 'object'; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json -> 'field7')::bool FROM test_jsonb WHERE json_type = 'object'; + +-- Section 1b: planner rewrite verification for subscripting syntax +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field4'])::numeric FROM test_jsonb WHERE json_type = 'object'; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field7'])::bool FROM test_jsonb WHERE json_type = 'object'; + +-- Verify that unsupported cast targets are NOT rewritten by this patch. +-- Integer and float casts remain on the original jsonb_object_field + cast path. +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json -> 'field4')::int4 FROM test_jsonb WHERE json_type = 'object'; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json -> 'field4')::int8 FROM test_jsonb WHERE json_type = 'object'; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json -> 'field4')::float8 FROM test_jsonb WHERE json_type = 'object'; + +-- Section 2: correct execution through the rewritten path +SELECT (test_json -> 'field4')::numeric FROM test_jsonb WHERE json_type = 'object'; +SELECT (test_json -> 'field7')::bool FROM test_jsonb WHERE json_type = 'object'; + +-- Section 2b: correct execution through subscripting syntax +SELECT (test_json['field4'])::numeric FROM test_jsonb WHERE json_type = 'object'; +SELECT (test_json['field7'])::bool FROM test_jsonb WHERE json_type = 'object'; + +-- Unsupported targets still work correctly through the original path +SELECT (test_json -> 'field4')::int4 FROM test_jsonb WHERE json_type = 'object'; +SELECT (test_json -> 'field4')::float8 FROM test_jsonb WHERE json_type = 'object'; + +-- Section 3: NULL semantics (missing key, JSON null, non-object input) +SELECT (test_json -> 'field3')::numeric FROM test_jsonb WHERE json_type = 'object'; -- JSON null +SELECT (test_json -> 'nonexistent')::numeric FROM test_jsonb WHERE json_type = 'object'; -- missing key +SELECT (test_json -> 'x')::numeric FROM test_jsonb WHERE json_type = 'array'; -- non-object +SELECT (test_json -> 'field3')::bool FROM test_jsonb WHERE json_type = 'object'; -- JSON null, bool path + +-- Section 3b: NULL semantics through subscripting syntax +SELECT (test_json['field3'])::numeric FROM test_jsonb WHERE json_type = 'object'; -- JSON null +SELECT (test_json['nonexistent'])::numeric FROM test_jsonb WHERE json_type = 'object'; -- missing key + +-- Section 4: type-mismatch errors (scalar and container types) +SELECT (test_json -> 'field1')::numeric FROM test_jsonb WHERE json_type = 'object'; -- string to numeric +SELECT (test_json -> 'field1')::bool FROM test_jsonb WHERE json_type = 'object'; -- string to bool +SELECT (test_json -> 'field5')::numeric FROM test_jsonb WHERE json_type = 'object'; -- array to numeric + +-- Section 4b: type-mismatch error through subscripting syntax +SELECT (test_json['field1'])::numeric FROM test_jsonb WHERE json_type = 'object'; -- string to numeric + +-- Section 5: direct calls to typed extractor builtins +SELECT jsonb_object_field_numeric('{"a": 1}'::jsonb, 'a'); +SELECT jsonb_object_field_numeric('{"a": 3.14}'::jsonb, 'a'); +SELECT jsonb_object_field_bool('{"a": true}'::jsonb, 'a'); +SELECT jsonb_object_field_bool('{"a": false}'::jsonb, 'a'); +-- direct calls: NULL semantics +SELECT jsonb_object_field_numeric('{"a": 1}'::jsonb, 'missing'); +SELECT jsonb_object_field_numeric('{"a": null}'::jsonb, 'a'); +SELECT jsonb_object_field_bool('{"a": true}'::jsonb, 'missing'); +-- direct calls: type-mismatch errors +SELECT jsonb_object_field_numeric('{"a": "text"}'::jsonb, 'a'); +SELECT jsonb_object_field_bool('{"a": 1}'::jsonb, 'a'); +SELECT jsonb_object_field_numeric('{"a": {"x":1}}'::jsonb, 'a'); -- container to scalar SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'scalar'; SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'array'; -- 2.52.0