From 40848bbc9a45e171726a1b465a00a438b60d110e Mon Sep 17 00:00:00 2001
From: Alexandra Wang <alexandra.wang.oss@gmail.com>
Date: Tue, 3 Feb 2026 14:36:52 -0800
Subject: [PATCH v1 2/2] Implement lax mode for jsonb subscripting

Jsonb subscripting predates the SQL/JSON standard and had custom
behavior that diverges from the SQL/JSON simplified accessor, which
has identical syntax. This commit aligns jsonb subscripting with the
SQL/JSON standard by enabling lax mode for integer subscripts on
non-array values.

Per SQL/JSON lax mode, when an integer subscript is applied to a
non-array value (object or scalar), the value is treated as if wrapped
in a single-element array. Index 0 and -1 are in-bounds (first/last
element of the wrapped array), while other indices are out-of-bounds.

Read behavior:
  - In-bounds (0 or -1): returns the value itself
  - Out-of-bounds: returns NULL

Examples:
  SELECT ('123'::jsonb)[0];        -- was NULL, now 123
  SELECT ('{"a":1}'::jsonb)[-1];   -- was NULL, now {"a":1}
  SELECT ('123'::jsonb)[2];        -- still NULL (out-of-bounds)

Write behavior is also changed to maintain put-get consistency (if
reading val[0] returns the value, then writing val[0] should replace
that value):
  - In-bounds (0 or -1): replaces the value
  - Out-of-bounds: wraps value in array, then inserts at the index

Examples:
  -- In-bounds assignment (was ERROR, now replaces)
  UPDATE t SET val[0] = '"x"' WHERE val = '123'; -- result: val becomes "x"
  -- Out-of-bounds assignment (wraps and inserts with NULL gap,
  -- note that filling gaps is exsting behavior for array assignment)
  UPDATE t SET val[2] = '"x"' WHERE val = '123'; -- result: val becomes [123, null, "x"]
---
 doc/src/sgml/json.sgml              |  25 ++-
 src/backend/utils/adt/jsonbsubs.c   |   9 +-
 src/backend/utils/adt/jsonfuncs.c   | 323 ++++++++++++++++++++++++++--
 src/include/utils/jsonb.h           |   6 +-
 src/test/regress/expected/jsonb.out | 229 ++++++++++++++++++--
 src/test/regress/sql/jsonb.sql      |  86 ++++++++
 6 files changed, 626 insertions(+), 52 deletions(-)

diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml
index a584aab6ac1..6b175d8dc9a 100644
--- a/doc/src/sgml/json.sgml
+++ b/doc/src/sgml/json.sgml
@@ -626,6 +626,14 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"';
    The result of a subscripting expression is always of the jsonb data type.
   </para>
 
+  <para>
+   When an integer subscript is applied to a <type>jsonb</type> value that is not
+   an array (i.e., an object or a scalar), the value is treated as if it were a
+   single-element array per SQL/JSON lax mode. Thus, index 0 and -1 access the
+   value itself, while other indices are out-of-bounds and return
+   <literal>NULL</literal>.
+  </para>
+
   <para>
    <command>UPDATE</command> statements may use subscripting in the
    <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript
@@ -635,10 +643,10 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"';
    <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an
    object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal>
    is not defined, it will be created as an empty object and filled as
-   necessary. However, if any <literal>val</literal> itself or one of the
-   intermediary values is defined as a non-object such as a string, number, or
-   <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so
-   an error is raised and the transaction aborted.
+   necessary. However, if a text subscript (key access) is applied to a non-object
+   <literal>val</literal>, traversal cannot proceed so an error is raised. Integer
+   subscripts on any non-array <literal>val</literal> use lax mode as described
+   above, treating the value as a single-element array.
   </para>
 
   <para>
@@ -672,6 +680,15 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"';
 -- Numeric subscripts are truncated toward zero
 SELECT ('[1, 2, 3]'::jsonb)[1.7];  -- returns 2
 
+-- Integer subscript on a non-array value (lax mode)
+SELECT ('{"a": 1}'::jsonb)[0];     -- returns {"a": 1}
+SELECT ('"hello"'::jsonb)[-1];    -- returns "hello"
+SELECT ('{"a": 1}'::jsonb)[1];     -- returns NULL (out-of-bounds)
+
+-- Lax mode also works for assignment
+UPDATE table_name SET jsonb_field['a'][0] = '"x"';
+-- If jsonb_field['a'] is "hello", it becomes "x"
+
 </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 b110d30298d..ef240ae7234 100644
--- a/src/backend/utils/adt/jsonbsubs.c
+++ b/src/backend/utils/adt/jsonbsubs.c
@@ -261,7 +261,8 @@ jsonb_subscript_fetch(ExprState *state,
 									  workspace->index,
 									  sbsrefstate->numupper,
 									  op->resnull,
-									  false);
+									  false,
+									  workspace->indexOid);
 }
 
 /*
@@ -320,7 +321,8 @@ jsonb_subscript_assign(ExprState *state,
 	*op->resvalue = jsonb_set_element(jsonbSource,
 									  workspace->index,
 									  sbsrefstate->numupper,
-									  &replacevalue);
+									  &replacevalue,
+									  workspace->indexOid);
 	/* The result is never NULL, so no need to change *op->resnull */
 }
 
@@ -353,7 +355,8 @@ jsonb_subscript_fetch_old(ExprState *state,
 												   sbsrefstate->upperindex,
 												   sbsrefstate->numupper,
 												   &sbsrefstate->prevnull,
-												   false);
+												   false,
+												   NULL);
 	}
 }
 
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index d5b64d7fca5..ac9f4d57ace 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -480,15 +480,25 @@ static void IteratorConcat(JsonbIterator **it1, JsonbIterator **it2,
 static void setPath(JsonbIterator **it, const Datum *path_elems,
 					const bool *path_nulls, int path_len,
 					JsonbInState *st, int level, JsonbValue *newval,
-					int op_type);
+					int op_type, const Oid *index_oid);
 static void setPathObject(JsonbIterator **it, const Datum *path_elems,
 						  const bool *path_nulls, int path_len, JsonbInState *st,
 						  int level,
-						  JsonbValue *newval, uint32 npairs, int op_type);
+						  JsonbValue *newval, uint32 npairs, int op_type,
+						  const Oid *index_oid);
 static void setPathArray(JsonbIterator **it, const Datum *path_elems,
 						 const bool *path_nulls, int path_len, JsonbInState *st,
 						 int level,
-						 JsonbValue *newval, uint32 nelems, int op_type);
+						 JsonbValue *newval, uint32 nelems, int op_type,
+						 const Oid *index_oid);
+static Jsonb *wrap_container_in_array(JsonbContainer *container);
+static Jsonb *wrap_scalar_in_array(JsonbValue *scalar);
+static bool is_lax_subscript_in_bounds(Datum subscript, long *index_out);
+static void setPath_lax_assignment(Datum subscript, int level, int path_len,
+								   JsonbInState *st, JsonbValue *newval, int op_type,
+								   Jsonb *value, bool is_scalar,
+								   const Datum *path_elems, const bool *path_nulls,
+								   const Oid *index_oid);
 
 /* function supporting iterate_json_values */
 static JsonParseErrorType iterate_values_scalar(void *state, char *token, JsonTokenType tokentype);
@@ -1519,7 +1529,7 @@ get_jsonb_path_all(FunctionCallInfo fcinfo, bool as_text)
 
 	deconstruct_array_builtin(path, TEXTOID, &pathtext, &pathnulls, &npath);
 
-	res = jsonb_get_element(jb, pathtext, npath, &isnull, as_text);
+	res = jsonb_get_element(jb, pathtext, npath, &isnull, as_text, NULL);
 
 	if (isnull)
 		PG_RETURN_NULL();
@@ -1527,8 +1537,142 @@ get_jsonb_path_all(FunctionCallInfo fcinfo, bool as_text)
 		PG_RETURN_DATUM(res);
 }
 
+/*
+ * Check whether the subscript at the given level is a lax array access
+ * (integer or numeric index, as opposed to a text key for field access).
+ */
+static inline bool
+is_lax_array_access(const Oid *index_oid, int level)
+{
+	return index_oid != NULL &&
+		(index_oid[level] == INT4OID || index_oid[level] == NUMERICOID);
+}
+
+/*
+ * Wrap a jsonb object in a single-element array.
+ * Used for out-of-bounds lax-mode assignment (reads return NULL instead).
+ */
+static Jsonb *
+wrap_container_in_array(JsonbContainer *container)
+{
+	JsonbInState wrapState = {0};
+	JsonbIterator *it = JsonbIteratorInit(container);
+	JsonbIteratorToken tok;
+	JsonbValue	v;
+
+	pushJsonbValue(&wrapState, WJB_BEGIN_ARRAY, NULL);
+	while ((tok = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
+		pushJsonbValue(&wrapState, tok, tok < WJB_BEGIN_ARRAY ? &v : NULL);
+	pushJsonbValue(&wrapState, WJB_END_ARRAY, NULL);
+
+	return JsonbValueToJsonb(wrapState.result);
+}
+
+/*
+ * Wrap a scalar JsonbValue in a single-element, non-raw array.
+ * Used for out-of-bounds lax-mode assignment (reads return NULL instead).
+ */
+static Jsonb *
+wrap_scalar_in_array(JsonbValue *scalar)
+{
+	JsonbValue	wrapper;
+
+	wrapper.type = jbvArray;
+	wrapper.val.array.nElems = 1;
+	wrapper.val.array.elems = scalar;
+	wrapper.val.array.rawScalar = false;
+	return JsonbValueToJsonb(&wrapper);
+}
+
+/*
+ * Parse an integer subscript and check if it's in-bounds for a
+ * single-element array (index 0 or -1).
+ */
+static bool
+is_lax_subscript_in_bounds(Datum subscript, long *index_out)
+{
+	char	   *indextext = TextDatumGetCString(subscript);
+	char	   *endptr;
+	long		index;
+
+	index = strtol(indextext, &endptr, 10);
+	if (index_out)
+		*index_out = index;
+	return (index == 0 || index == -1);
+}
+
+/*
+ * Handle lax array access on a non-array value during assignment.
+ * The value must already be materialized as a Jsonb.
+ *
+ * For in-bounds (0/-1): virtual wrap - either push newval (terminal) or
+ * recurse at level+1 (intermediate).
+ * For out-of-bounds: actual wrap and recurse at same level.
+ */
+static void
+setPath_lax_assignment(Datum subscript, int level, int path_len,
+					   JsonbInState *st, JsonbValue *newval, int op_type,
+					   Jsonb *value, bool is_scalar,
+					   const Datum *path_elems, const bool *path_nulls,
+					   const Oid *index_oid)
+{
+	long		index;
+	JsonbIterator *newit;
+
+	if (is_lax_subscript_in_bounds(subscript, &index))
+	{
+		/* In-bounds: virtual wrap */
+		if (level == path_len - 1)
+		{
+			/* Terminal: replace with newval directly */
+			if (newval != NULL && !(op_type & JB_PATH_DELETE))
+				pushJsonbValue(st, WJB_VALUE, newval);
+		}
+		else
+		{
+			/* Intermediate: skip this level, recurse at level+1 */
+			newit = JsonbIteratorInit(&value->root);
+			setPath(&newit, path_elems, path_nulls, path_len,
+					st, level + 1, newval, op_type, index_oid);
+		}
+	}
+	else
+	{
+		/* Out-of-bounds: actual wrap */
+		Jsonb	   *wrapped;
+
+		if (is_scalar)
+		{
+			/*
+			 * Scalar is stored as rawScalar array in Jsonb.  Extract it by
+			 * iterating: WJB_BEGIN_ARRAY, WJB_ELEM (the scalar), ...
+			 */
+			JsonbIterator *extract_it = JsonbIteratorInit(&value->root);
+			JsonbValue	scalar;
+
+			JsonbIteratorNext(&extract_it, &scalar, false); /* WJB_BEGIN_ARRAY */
+			JsonbIteratorNext(&extract_it, &scalar, false); /* WJB_ELEM */
+			wrapped = wrap_scalar_in_array(&scalar);
+		}
+		else
+			wrapped = wrap_container_in_array(&value->root);
+
+		newit = JsonbIteratorInit(&wrapped->root);
+		setPath(&newit, path_elems, path_nulls, path_len,
+				st, level, newval, op_type, index_oid);
+	}
+}
+
+/*
+ * Fetch an element from a jsonb value using a path of subscripts.
+ *
+ * If index_oid is non-NULL, it provides the type OID of each subscript
+ * to enable SQL/JSON lax mode: integer subscripts on non-array values
+ * treat the value as a single-element array.  If NULL, lax mode is disabled.
+ */
 Datum
-jsonb_get_element(Jsonb *jb, const Datum *path, int npath, bool *isnull, bool as_text)
+jsonb_get_element(Jsonb *jb, const Datum *path, int npath, bool *isnull,
+				  bool as_text, const Oid *index_oid)
 {
 	JsonbContainer *container = &jb->root;
 	JsonbValue *jbvp = NULL;
@@ -1546,9 +1690,13 @@ jsonb_get_element(Jsonb *jb, const Datum *path, int npath, bool *isnull, bool as
 	else
 	{
 		Assert(JB_ROOT_IS_ARRAY(jb) && JB_ROOT_IS_SCALAR(jb));
-		/* Extract the scalar value, if it is what we'll return */
-		if (npath <= 0)
-			jbvp = getIthJsonbValueFromContainer(container, 0);
+
+		/*
+		 * Extract the scalar value.  We need this both when returning the
+		 * scalar directly (npath <= 0) and for lax mode in-bounds access
+		 * which also returns the scalar (npath > 0, index 0 or -1).
+		 */
+		jbvp = getIthJsonbValueFromContainer(container, 0);
 	}
 
 	/*
@@ -1576,6 +1724,48 @@ jsonb_get_element(Jsonb *jb, const Datum *path, int npath, bool *isnull, bool as
 
 	for (i = 0; i < npath; i++)
 	{
+		if (is_lax_array_access(index_oid, i) && !have_array)
+		{
+			/*
+			 * SQL/JSON lax mode: integer subscript on a non-array value
+			 * (scalar or object).  Virtual wrap: treat as single-element
+			 * array.  Index 0 or -1 returns the value; other indices return
+			 * NULL.
+			 */
+			if (is_lax_subscript_in_bounds(path[i], NULL))
+			{
+				/*
+				 * In-bounds for virtual single-element array.
+				 */
+				if (i == npath - 1)
+				{
+					/*
+					 * Terminal: return current value.  For nested objects,
+					 * jbvp is already set from previous iteration.  For root
+					 * level object, create jbvp from jb.  For scalars, jbvp
+					 * is already set.
+					 */
+					if (have_object && jbvp == NULL)
+					{
+						/* Root level object */
+						jbvp = palloc(sizeof(JsonbValue));
+						jbvp->type = jbvBinary;
+						jbvp->val.binary.data = container;
+						jbvp->val.binary.len = VARSIZE(jb) - VARHDRSZ;
+					}
+					break;
+				}
+				/* Intermediate: continue to next path element */
+				continue;
+			}
+			else
+			{
+				/* Out-of-bounds: return NULL */
+				*isnull = true;
+				return PointerGetDatum(NULL);
+			}
+		}
+
 		if (have_object)
 		{
 			text	   *subscr = DatumGetTextPP(path[i]);
@@ -1675,9 +1865,16 @@ jsonb_get_element(Jsonb *jb, const Datum *path, int npath, bool *isnull, bool as
 	}
 }
 
+/*
+ * Assign a value to a jsonb element at a path of subscripts.
+ *
+ * If index_oid is non-NULL, it provides the type OID of each subscript
+ * to enable SQL/JSON lax mode: integer subscripts on non-array values
+ * treat the value as a single-element array.  If NULL, lax mode is disabled.
+ */
 Datum
 jsonb_set_element(Jsonb *jb, const Datum *path, int path_len,
-				  JsonbValue *newval)
+				  JsonbValue *newval, const Oid *index_oid)
 {
 	JsonbInState state = {0};
 	JsonbIterator *it;
@@ -1690,7 +1887,7 @@ jsonb_set_element(Jsonb *jb, const Datum *path, int path_len,
 
 	setPath(&it, path, path_nulls, path_len, &state, 0, newval,
 			JB_PATH_CREATE | JB_PATH_FILL_GAPS |
-			JB_PATH_CONSISTENT_POSITION);
+			JB_PATH_CONSISTENT_POSITION, index_oid);
 
 	pfree(path_nulls);
 
@@ -4886,7 +5083,7 @@ jsonb_set(PG_FUNCTION_ARGS)
 	it = JsonbIteratorInit(&in->root);
 
 	setPath(&it, path_elems, path_nulls, path_len, &st,
-			0, &newval, create ? JB_PATH_CREATE : JB_PATH_REPLACE);
+			0, &newval, create ? JB_PATH_CREATE : JB_PATH_REPLACE, NULL);
 
 	PG_RETURN_JSONB_P(JsonbValueToJsonb(st.result));
 }
@@ -4990,7 +5187,7 @@ jsonb_delete_path(PG_FUNCTION_ARGS)
 	it = JsonbIteratorInit(&in->root);
 
 	setPath(&it, path_elems, path_nulls, path_len, &st,
-			0, NULL, JB_PATH_DELETE);
+			0, NULL, JB_PATH_DELETE, NULL);
 
 	PG_RETURN_JSONB_P(JsonbValueToJsonb(st.result));
 }
@@ -5032,7 +5229,7 @@ jsonb_insert(PG_FUNCTION_ARGS)
 	it = JsonbIteratorInit(&in->root);
 
 	setPath(&it, path_elems, path_nulls, path_len, &st, 0, &newval,
-			after ? JB_PATH_INSERT_AFTER : JB_PATH_INSERT_BEFORE);
+			after ? JB_PATH_INSERT_AFTER : JB_PATH_INSERT_BEFORE, NULL);
 
 	PG_RETURN_JSONB_P(JsonbValueToJsonb(st.result));
 }
@@ -5166,13 +5363,18 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2,
  * and a negative index out of the range, so this behavior will be prevented
  * and return an error.
  *
+ * If index_oid is non-NULL, it provides the type OID of each subscript
+ * to enable SQL/JSON lax mode: integer subscripts on non-array values
+ * treat the value as a single-element array.  If NULL, lax mode is disabled.
+ *
  * All path elements before the last must already exist
  * whatever bits in op_type are set, or nothing is done.
  */
 static void
 setPath(JsonbIterator **it, const Datum *path_elems,
 		const bool *path_nulls, int path_len,
-		JsonbInState *st, int level, JsonbValue *newval, int op_type)
+		JsonbInState *st, int level, JsonbValue *newval, int op_type,
+		const Oid *index_oid)
 {
 	JsonbValue	v;
 	JsonbIteratorToken r;
@@ -5191,6 +5393,26 @@ setPath(JsonbIterator **it, const Datum *path_elems,
 	{
 		case WJB_BEGIN_ARRAY:
 
+			/*
+			 * SQL/JSON lax mode: if this subscript is an element access
+			 * (integer) and the value is a raw scalar, handle lax assignment.
+			 */
+			if (is_lax_array_access(index_oid, level) && v.val.array.rawScalar)
+			{
+				JsonbValue	scalar_v;
+				Jsonb	   *scalar_jb;
+
+				/* Consume scalar + end_array from original iterator */
+				JsonbIteratorNext(it, &scalar_v, false);	/* WJB_ELEM */
+				JsonbIteratorNext(it, &v, false);	/* WJB_END_ARRAY */
+
+				scalar_jb = JsonbValueToJsonb(&scalar_v);
+				setPath_lax_assignment(path_elems[level], level, path_len,
+									   st, newval, op_type, scalar_jb, true,
+									   path_elems, path_nulls, index_oid);
+				break;
+			}
+
 			/*
 			 * If instructed complain about attempts to replace within a raw
 			 * scalar value. This happens even when current level is equal to
@@ -5207,15 +5429,57 @@ setPath(JsonbIterator **it, const Datum *path_elems,
 
 			pushJsonbValue(st, r, NULL);
 			setPathArray(it, path_elems, path_nulls, path_len, st, level,
-						 newval, v.val.array.nElems, op_type);
+						 newval, v.val.array.nElems, op_type,
+						 index_oid);
 			r = JsonbIteratorNext(it, &v, false);
 			Assert(r == WJB_END_ARRAY);
 			pushJsonbValue(st, r, NULL);
 			break;
 		case WJB_BEGIN_OBJECT:
+
+			/*
+			 * SQL/JSON lax mode: if this subscript is an element access
+			 * (integer) on an object, handle according to the index value and
+			 * whether this is an intermediate or terminal subscript.
+			 */
+			if (is_lax_array_access(index_oid, level))
+			{
+				JsonbInState objState = {0};
+				JsonbIteratorToken tok;
+				JsonbValue	objv;
+				Jsonb	   *obj;
+				int			depth = 1;
+
+				/*
+				 * Materialize the object by consuming from the iterator
+				 * (tracking depth to handle nested objects/arrays).
+				 */
+				pushJsonbValue(&objState, WJB_BEGIN_OBJECT, NULL);
+				while ((tok = JsonbIteratorNext(it, &objv, false)) != WJB_DONE)
+				{
+					if (tok == WJB_BEGIN_OBJECT || tok == WJB_BEGIN_ARRAY)
+						depth++;
+					else if (tok == WJB_END_OBJECT || tok == WJB_END_ARRAY)
+						depth--;
+
+					pushJsonbValue(&objState, tok,
+								   tok < WJB_BEGIN_ARRAY ? &objv : NULL);
+
+					if (depth == 0)
+						break;
+				}
+				obj = JsonbValueToJsonb(objState.result);
+
+				setPath_lax_assignment(path_elems[level], level, path_len,
+									   st, newval, op_type, obj, false,
+									   path_elems, path_nulls, index_oid);
+				break;
+			}
+
 			pushJsonbValue(st, r, NULL);
 			setPathObject(it, path_elems, path_nulls, path_len, st, level,
-						  newval, v.val.object.nPairs, op_type);
+						  newval, v.val.object.nPairs, op_type,
+						  index_oid);
 			r = JsonbIteratorNext(it, &v, true);
 			Assert(r == WJB_END_OBJECT);
 			pushJsonbValue(st, r, NULL);
@@ -5223,6 +5487,21 @@ setPath(JsonbIterator **it, const Datum *path_elems,
 		case WJB_ELEM:
 		case WJB_VALUE:
 
+			/*
+			 * SQL/JSON lax mode: if this subscript is an element access
+			 * (integer) on a scalar value, handle lax assignment.
+			 */
+			if (is_lax_array_access(index_oid, level) &&
+				(level <= path_len - 1))
+			{
+				Jsonb	   *scalar_jb = JsonbValueToJsonb(&v);
+
+				setPath_lax_assignment(path_elems[level], level, path_len,
+									   st, newval, op_type, scalar_jb, true,
+									   path_elems, path_nulls, index_oid);
+				break;
+			}
+
 			/*
 			 * If instructed complain about attempts to replace within a
 			 * scalar value. This happens even when current level is equal to
@@ -5250,7 +5529,8 @@ setPath(JsonbIterator **it, const Datum *path_elems,
 static void
 setPathObject(JsonbIterator **it, const Datum *path_elems, const bool *path_nulls,
 			  int path_len, JsonbInState *st, int level,
-			  JsonbValue *newval, uint32 npairs, int op_type)
+			  JsonbValue *newval, uint32 npairs, int op_type,
+			  const Oid *index_oid)
 {
 	text	   *pathelem = NULL;
 	int			i;
@@ -5317,7 +5597,8 @@ setPathObject(JsonbIterator **it, const Datum *path_elems, const bool *path_null
 			{
 				pushJsonbValue(st, r, &k);
 				setPath(it, path_elems, path_nulls, path_len,
-						st, level + 1, newval, op_type);
+						st, level + 1, newval, op_type,
+						index_oid);
 			}
 		}
 		else
@@ -5388,7 +5669,8 @@ setPathObject(JsonbIterator **it, const Datum *path_elems, const bool *path_null
 static void
 setPathArray(JsonbIterator **it, const Datum *path_elems, const bool *path_nulls,
 			 int path_len, JsonbInState *st, int level,
-			 JsonbValue *newval, uint32 nelems, int op_type)
+			 JsonbValue *newval, uint32 nelems, int op_type,
+			 const Oid *index_oid)
 {
 	JsonbValue	v;
 	int			idx,
@@ -5489,7 +5771,8 @@ setPathArray(JsonbIterator **it, const Datum *path_elems, const bool *path_nulls
 			}
 			else
 				setPath(it, path_elems, path_nulls, path_len,
-						st, level + 1, newval, op_type);
+						st, level + 1, newval, op_type,
+						index_oid);
 		}
 		else
 		{
diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index ca13efba0fb..48c8a3ceca6 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -454,9 +454,11 @@ extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res);
 extern const char *JsonbTypeName(JsonbValue *val);
 
 extern Datum jsonb_set_element(Jsonb *jb, const Datum *path, int path_len,
-							   JsonbValue *newval);
+							   JsonbValue *newval,
+							   const Oid *index_oid);
 extern Datum jsonb_get_element(Jsonb *jb, const Datum *path, int npath,
-							   bool *isnull, bool as_text);
+							   bool *isnull, bool as_text,
+							   const Oid *index_oid);
 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/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out
index f0ea654434f..71fdee8e759 100644
--- a/src/test/regress/expected/jsonb.out
+++ b/src/test/regress/expected/jsonb.out
@@ -5025,7 +5025,7 @@ select ('123'::jsonb)['a'];
 select ('123'::jsonb)[0];
  jsonb 
 -------
- 
+ 123
 (1 row)
 
 select ('123'::jsonb)[NULL];
@@ -5041,9 +5041,9 @@ select ('{"a": 1}'::jsonb)['a'];
 (1 row)
 
 select ('{"a": 1}'::jsonb)[0];
- jsonb 
--------
- 
+  jsonb   
+----------
+ {"a": 1}
 (1 row)
 
 select ('{"a": 1}'::jsonb)['not_exist'];
@@ -5109,7 +5109,7 @@ select ('[1, "2", null]'::jsonb)[1]['a'];
 select ('[1, "2", null]'::jsonb)[1][0];
  jsonb 
 -------
- 
+ "2"
 (1 row)
 
 select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['b'];
@@ -5279,10 +5279,10 @@ select * from test_jsonb_subscript;
 update test_jsonb_subscript set test_json = NULL where id = 3;
 update test_jsonb_subscript set test_json[0] = '1';
 select * from test_jsonb_subscript;
- id |                           test_json                           
-----+---------------------------------------------------------------
-  1 | {"0": 1, "a": [1, 2, 3], "another_key": null}
-  2 | {"0": 1, "a": [1, 2, 3], "key": "value", "another_key": null}
+ id | test_json 
+----+-----------
+  1 | 1
+  2 | 1
   3 | [1]
 (3 rows)
 
@@ -5355,9 +5355,9 @@ delete from test_jsonb_subscript;
 insert into test_jsonb_subscript values (1, '{}');
 update test_jsonb_subscript set test_json[0]['a'] = '1';
 select * from test_jsonb_subscript;
- id |    test_json    
-----+-----------------
-  1 | {"0": {"a": 1}}
+ id | test_json 
+----+-----------
+  1 | {"a": 1}
 (1 row)
 
 -- the start jsonb is an array
@@ -5397,9 +5397,9 @@ insert into test_jsonb_subscript values (1, '{}');
 update test_jsonb_subscript set test_json['a']['b'][10] = '1';
 update test_jsonb_subscript set test_json['a'][10][10] = '1';
 select * from test_jsonb_subscript;
- id |                                                                      test_json                                                                       
-----+------------------------------------------------------------------------------------------------------------------------------------------------------
-  1 | {"a": {"b": [null, null, null, null, null, null, null, null, null, null, 1], "10": [null, null, null, null, null, null, null, null, null, null, 1]}}
+ id |                                                                                               test_json                                                                                                
+----+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  1 | {"a": [{"b": [null, null, null, null, null, null, null, null, null, null, 1]}, null, null, null, null, null, null, null, null, null, [null, null, null, null, null, null, null, null, null, null, 1]]}
 (1 row)
 
 -- an empty sub element
@@ -5431,23 +5431,15 @@ update test_jsonb_subscript set test_json['a']['b']['c'] = '1';
 ERROR:  cannot replace existing key
 DETAIL:  The path assumes key is a composite object, but it is a scalar value.
 update test_jsonb_subscript set test_json['a'][0] = '1';
-ERROR:  cannot replace existing key
-DETAIL:  The path assumes key is a composite object, but it is a scalar value.
 update test_jsonb_subscript set test_json['a'][0]['c'] = '1';
 ERROR:  cannot replace existing key
 DETAIL:  The path assumes key is a composite object, but it is a scalar value.
 update test_jsonb_subscript set test_json['a'][0][0] = '1';
-ERROR:  cannot replace existing key
-DETAIL:  The path assumes key is a composite object, but it is a scalar value.
 -- trying replace assuming a composite object, but it's a raw scalar
 delete from test_jsonb_subscript;
 insert into test_jsonb_subscript values (1, 'null');
 update test_jsonb_subscript set test_json[0] = '1';
-ERROR:  cannot replace existing key
-DETAIL:  The path assumes key is a composite object, but it is a scalar value.
 update test_jsonb_subscript set test_json[0][0] = '1';
-ERROR:  cannot replace existing key
-DETAIL:  The path assumes key is a composite object, but it is a scalar value.
 -- try some things with short-header and toasted subscript values
 drop table test_jsonb_subscript;
 create temp table test_jsonb_subscript (
@@ -6013,3 +6005,194 @@ select * from test_jsonb_numeric_subscript;
 (1 row)
 
 drop table test_jsonb_numeric_subscript;
+-- integer subscript on non-array values
+select ('123'::jsonb)[0];
+ jsonb 
+-------
+ 123
+(1 row)
+
+select ('123'::jsonb)[-1];
+ jsonb 
+-------
+ 123
+(1 row)
+
+select ('123'::jsonb)[1];
+ jsonb 
+-------
+ 
+(1 row)
+
+select ('{"a": 1}'::jsonb)[0];
+  jsonb   
+----------
+ {"a": 1}
+(1 row)
+
+select ('{"a": 1}'::jsonb)[-1];
+  jsonb   
+----------
+ {"a": 1}
+(1 row)
+
+select ('{"a": 1}'::jsonb)[1];
+ jsonb 
+-------
+ 
+(1 row)
+
+-- chained subscript with integer index
+select ('{"a": 1}'::jsonb)['a'][0];
+ jsonb 
+-------
+ 1
+(1 row)
+
+select ('{"a": 1}'::jsonb)['a'][1]; -- out-of-bounds
+ jsonb 
+-------
+ 
+(1 row)
+
+select ('{"a": {"b": 2}}'::jsonb)['a'][0];
+  jsonb   
+----------
+ {"b": 2}
+(1 row)
+
+select ('{"a": {"b": 2}}'::jsonb)['a'][0]['b'];
+ jsonb 
+-------
+ 2
+(1 row)
+
+-- integer vs text subscript on object with numeric key
+select ('{"0": "value"}'::jsonb)[0];
+     jsonb      
+----------------
+ {"0": "value"}
+(1 row)
+
+select ('{"0": "value"}'::jsonb)['0'];
+  jsonb  
+---------
+ "value"
+(1 row)
+
+select ('{"0": "value"}'::jsonb)[0.0];
+     jsonb      
+----------------
+ {"0": "value"}
+(1 row)
+
+-- Write tests: integer subscript assignment on non-array values
+create temp table test_jsonb_lax(val jsonb);
+-- in-bounds on scalar
+insert into test_jsonb_lax values ('123');
+update test_jsonb_lax set val[0] = '"x"';
+select val from test_jsonb_lax;
+ val 
+-----
+ "x"
+(1 row)
+
+-- in-bounds on object
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[-1] = '"x"';
+select val from test_jsonb_lax;
+ val 
+-----
+ "x"
+(1 row)
+
+-- in-bounds on nested object
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": {"b": 1}}');
+update test_jsonb_lax set val[0] = '"x"';
+select val from test_jsonb_lax;
+ val 
+-----
+ "x"
+(1 row)
+
+-- out-of-bounds wraps in array
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[1] = '"x"';
+select val from test_jsonb_lax;
+       val       
+-----------------
+ [{"a": 1}, "x"]
+(1 row)
+
+-- out-of-bounds negative is error
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[-2] = '"x"';
+ERROR:  path element at position 1 is out of range: -2
+select val from test_jsonb_lax;
+   val    
+----------
+ {"a": 1}
+(1 row)
+
+-- integer subscript vs text subscript on object with numeric key
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"0": "value"}');
+update test_jsonb_lax set val[0] = '"x"';
+select val from test_jsonb_lax;
+ val 
+-----
+ "x"
+(1 row)
+
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"0": "value"}');
+update test_jsonb_lax set val['0'] = '"x"';
+select val from test_jsonb_lax;
+    val     
+------------
+ {"0": "x"}
+(1 row)
+
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"0": "value"}');
+update test_jsonb_lax set val[0.0] = '"x"';
+select val from test_jsonb_lax;
+ val 
+-----
+ "x"
+(1 row)
+
+-- numeric subscript truncates toward zero
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[0.7] = '"x"';
+select val from test_jsonb_lax;
+ val 
+-----
+ "x"
+(1 row)
+
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[-1.7] = '"x"';
+select val from test_jsonb_lax;
+ val 
+-----
+ "x"
+(1 row)
+
+-- chained subscript on intermediate non-array
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": "hello"}');
+update test_jsonb_lax set val['a'][0] = '"x"';
+select val from test_jsonb_lax;
+    val     
+------------
+ {"a": "x"}
+(1 row)
+
+drop table test_jsonb_lax;
diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql
index d7d26205534..84860f6912b 100644
--- a/src/test/regress/sql/jsonb.sql
+++ b/src/test/regress/sql/jsonb.sql
@@ -1633,3 +1633,89 @@ 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;
+
+-- integer subscript on non-array values
+select ('123'::jsonb)[0];
+select ('123'::jsonb)[-1];
+select ('123'::jsonb)[1];
+select ('{"a": 1}'::jsonb)[0];
+select ('{"a": 1}'::jsonb)[-1];
+select ('{"a": 1}'::jsonb)[1];
+
+-- chained subscript with integer index
+select ('{"a": 1}'::jsonb)['a'][0];
+select ('{"a": 1}'::jsonb)['a'][1]; -- out-of-bounds
+select ('{"a": {"b": 2}}'::jsonb)['a'][0];
+select ('{"a": {"b": 2}}'::jsonb)['a'][0]['b'];
+
+-- integer vs text subscript on object with numeric key
+select ('{"0": "value"}'::jsonb)[0];
+select ('{"0": "value"}'::jsonb)['0'];
+select ('{"0": "value"}'::jsonb)[0.0];
+
+-- Write tests: integer subscript assignment on non-array values
+create temp table test_jsonb_lax(val jsonb);
+
+-- in-bounds on scalar
+insert into test_jsonb_lax values ('123');
+update test_jsonb_lax set val[0] = '"x"';
+select val from test_jsonb_lax;
+
+-- in-bounds on object
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[-1] = '"x"';
+select val from test_jsonb_lax;
+
+-- in-bounds on nested object
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": {"b": 1}}');
+update test_jsonb_lax set val[0] = '"x"';
+select val from test_jsonb_lax;
+
+-- out-of-bounds wraps in array
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[1] = '"x"';
+select val from test_jsonb_lax;
+
+-- out-of-bounds negative is error
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[-2] = '"x"';
+select val from test_jsonb_lax;
+
+-- integer subscript vs text subscript on object with numeric key
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"0": "value"}');
+update test_jsonb_lax set val[0] = '"x"';
+select val from test_jsonb_lax;
+
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"0": "value"}');
+update test_jsonb_lax set val['0'] = '"x"';
+select val from test_jsonb_lax;
+
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"0": "value"}');
+update test_jsonb_lax set val[0.0] = '"x"';
+select val from test_jsonb_lax;
+
+-- numeric subscript truncates toward zero
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[0.7] = '"x"';
+select val from test_jsonb_lax;
+
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": 1}');
+update test_jsonb_lax set val[-1.7] = '"x"';
+select val from test_jsonb_lax;
+
+-- chained subscript on intermediate non-array
+truncate test_jsonb_lax;
+insert into test_jsonb_lax values ('{"a": "hello"}');
+update test_jsonb_lax set val['a'][0] = '"x"';
+select val from test_jsonb_lax;
+
+drop table test_jsonb_lax;
-- 
2.50.1 (Apple Git-155)

