From 16d52d78c599847c93c685d6694157c5ebefffe5 Mon Sep 17 00:00:00 2001
From: Alexandra Wang <alexandra.wang.oss@gmail.com>
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 <literal>path</literal>
    argument in the <literal>jsonb_set</literal> function. If a <type>jsonb</type>
    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.
   </para>
 
@@ -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
+
 </programlisting>
 
    <type>jsonb</type> 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)

