From 16d52d78c599847c93c685d6694157c5ebefffe5 Mon Sep 17 00:00:00 2001 From: Alexandra Wang Date: Tue, 3 Feb 2026 12:58:07 -0800 Subject: [PATCH v1 1/2] Support numeric type for jsonb subscripting Previously, using a numeric value as a jsonb subscript would error. Now numeric subscripts are accepted and truncated toward zero to produce an integer index. This matches the semantics of array access in json_query() per the SQL/JSON standard. Examples: SELECT ('["a","b","c"]'::jsonb)[1.7]; -- returns "b" (truncates to 1) SELECT ('["a","b","c"]'::jsonb)[-1.7]; -- returns "c" (truncates to -1) --- doc/src/sgml/json.sgml | 7 ++- src/backend/utils/adt/jsonbsubs.c | 73 +++++++++++++++++------------ src/test/regress/expected/jsonb.out | 69 +++++++++++++++++++++++++-- src/test/regress/sql/jsonb.sql | 18 +++++++ 4 files changed, 132 insertions(+), 35 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 206eadb8f7b..a584aab6ac1 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -621,7 +621,8 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"'; subscripting expressions, following the same rules as the path argument in the jsonb_set function. If a jsonb value is an array, numeric subscripts start at zero, and negative integers count - backwards from the last element of the array. Slice expressions are not supported. + backwards from the last element of the array. Non-integer numeric subscripts are + truncated toward zero. Slice expressions are not supported. The result of a subscripting expression is always of the jsonb data type. @@ -667,6 +668,10 @@ UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; + +-- Numeric subscripts are truncated toward zero +SELECT ('[1, 2, 3]'::jsonb)[1.7]; -- returns 2 + jsonb assignment via subscripting handles a few edge cases diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c index 6bf55f19d6a..b110d30298d 100644 --- a/src/backend/utils/adt/jsonbsubs.c +++ b/src/backend/utils/adt/jsonbsubs.c @@ -21,6 +21,7 @@ #include "parser/parse_expr.h" #include "utils/builtins.h" #include "utils/jsonb.h" +#include "utils/numeric.h" /* SubscriptingRefState.workspace for jsonb subscripting execution */ @@ -78,41 +79,34 @@ jsonb_subscript_transform(SubscriptingRef *sbsref, if (subExprType != UNKNOWNOID) { - Oid targets[2] = {INT4OID, TEXTOID}; + Oid int4Oid = INT4OID; + Oid numericOid = NUMERICOID; + Oid textOid = TEXTOID; + bool can_int; + bool can_numeric; + bool can_text; /* - * Jsonb can handle multiple subscript types, but cases when a - * subscript could be coerced to multiple target types must be - * avoided, similar to overloaded functions. It could be - * possibly extend with jsonpath in the future. + * Check which types the subscript can be coerced to. We + * prefer INT4 for integer types (fast path), NUMERIC for + * numeric types (allows truncation toward zero per SQL/JSON + * spec), and TEXT for text types (field access). */ - for (int i = 0; i < 2; i++) - { - if (can_coerce_type(1, &subExprType, &targets[i], COERCION_IMPLICIT)) - { - /* - * One type has already succeeded, it means there are - * two coercion targets possible, failure. - */ - if (targetType != UNKNOWNOID) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("subscript type %s is not supported", format_type_be(subExprType)), - errhint("jsonb subscript must be coercible to only one type, integer or text."), - parser_errposition(pstate, exprLocation(subExpr)))); - - targetType = targets[i]; - } - } - - /* - * No suitable types were found, failure. - */ - if (targetType == UNKNOWNOID) + can_int = can_coerce_type(1, &subExprType, &int4Oid, COERCION_IMPLICIT); + can_numeric = can_coerce_type(1, &subExprType, &numericOid, COERCION_IMPLICIT); + can_text = can_coerce_type(1, &subExprType, &textOid, COERCION_IMPLICIT); + + if (can_int && !can_text) + targetType = INT4OID; + else if (can_text && !can_int) + targetType = TEXTOID; + else if (can_numeric) + targetType = NUMERICOID; + else ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("subscript type %s is not supported", format_type_be(subExprType)), - errhint("jsonb subscript must be coercible to either integer or text."), + errhint("jsonb subscript must be coercible to either numeric or text."), parser_errposition(pstate, exprLocation(subExpr)))); } else @@ -187,7 +181,9 @@ jsonb_subscript_check_subscripts(ExprState *state, * an empty source. */ if (sbsrefstate->numupper > 0 && sbsrefstate->upperprovided[0] && - !sbsrefstate->upperindexnull[0] && workspace->indexOid[0] == INT4OID) + !sbsrefstate->upperindexnull[0] && + (workspace->indexOid[0] == INT4OID || + workspace->indexOid[0] == NUMERICOID)) workspace->expectArray = true; /* Process upper subscripts */ @@ -217,6 +213,23 @@ jsonb_subscript_check_subscripts(ExprState *state, workspace->index[i] = CStringGetTextDatum(cs); } + else if (workspace->indexOid[i] == NUMERICOID) + { + /* + * Truncate numeric toward zero per SQL/JSON spec, then + * convert to int4 text for element access. + */ + Datum truncated; + Datum int_val; + char *cs; + + truncated = DirectFunctionCall2(numeric_trunc, + sbsrefstate->upperindex[i], + Int32GetDatum(0)); + int_val = DirectFunctionCall1(numeric_int4, truncated); + cs = DatumGetCString(DirectFunctionCall1(int4out, int_val)); + workspace->index[i] = CStringGetTextDatum(cs); + } else workspace->index[i] = sbsrefstate->upperindex[i]; } diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 4e2467852db..f0ea654434f 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5077,10 +5077,11 @@ select ('[1, "2", null]'::jsonb)['1']; (1 row) select ('[1, "2", null]'::jsonb)[1.0]; -ERROR: subscript type numeric is not supported -LINE 1: select ('[1, "2", null]'::jsonb)[1.0]; - ^ -HINT: jsonb subscript must be coercible to either integer or text. + jsonb +------- + "2" +(1 row) + select ('[1, "2", null]'::jsonb)[2]; jsonb ------- @@ -5952,3 +5953,63 @@ select ('{"text": "hello"}'::jsonb).text; {"text": "hello"} (1 row) +-- non-integer numeric subscripts +select ('[1, "2", null]'::jsonb)[0.0]; + jsonb +------- + 1 +(1 row) + +select ('[1, "2", null]'::jsonb)[0.9]; + jsonb +------- + 1 +(1 row) + +select ('[1, "2", null]'::jsonb)[1.9]; + jsonb +------- + "2" +(1 row) + +select ('[1, "2", null]'::jsonb)[-1.0]; + jsonb +------- + null +(1 row) + +select ('[1, "2", null]'::jsonb)[-1.9]; + jsonb +------- + null +(1 row) + +select ('[1, "2", null]'::jsonb)[1::numeric]; + jsonb +------- + "2" +(1 row) + +select ('[1, "2", null]'::jsonb)[1.5::float8]; -- errors +ERROR: subscript type double precision is not supported +LINE 1: select ('[1, "2", null]'::jsonb)[1.5::float8]; + ^ +HINT: jsonb subscript must be coercible to either numeric or text. +-- non-integer numeric subscript assignment +create temp table test_jsonb_numeric_subscript(id int, val jsonb); +insert into test_jsonb_numeric_subscript values (1, '[1, 2, 3]'); +update test_jsonb_numeric_subscript set val[1.0] = '"x"'; +select * from test_jsonb_numeric_subscript; + id | val +----+------------- + 1 | [1, "x", 3] +(1 row) + +update test_jsonb_numeric_subscript set val[1.9] = '"y"'; +select * from test_jsonb_numeric_subscript; + id | val +----+------------- + 1 | [1, "y", 3] +(1 row) + +drop table test_jsonb_numeric_subscript; diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index d28ed1c1e85..d7d26205534 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1615,3 +1615,21 @@ select ('true'::jsonb)::bool; select ('true'::jsonb).bool; select ('{"text": "hello"}'::jsonb)::text; select ('{"text": "hello"}'::jsonb).text; + +-- non-integer numeric subscripts +select ('[1, "2", null]'::jsonb)[0.0]; +select ('[1, "2", null]'::jsonb)[0.9]; +select ('[1, "2", null]'::jsonb)[1.9]; +select ('[1, "2", null]'::jsonb)[-1.0]; +select ('[1, "2", null]'::jsonb)[-1.9]; +select ('[1, "2", null]'::jsonb)[1::numeric]; +select ('[1, "2", null]'::jsonb)[1.5::float8]; -- errors + +-- non-integer numeric subscript assignment +create temp table test_jsonb_numeric_subscript(id int, val jsonb); +insert into test_jsonb_numeric_subscript values (1, '[1, 2, 3]'); +update test_jsonb_numeric_subscript set val[1.0] = '"x"'; +select * from test_jsonb_numeric_subscript; +update test_jsonb_numeric_subscript set val[1.9] = '"y"'; +select * from test_jsonb_numeric_subscript; +drop table test_jsonb_numeric_subscript; -- 2.50.1 (Apple Git-155)