From 75471b513e83e62c2804881a5d820fcb82f5eaf9 Mon Sep 17 00:00:00 2001
From: Alexandra Wang <alexandra.wang.oss@gmail.com>
Date: Tue, 8 Jul 2025 22:18:07 -0700
Subject: [PATCH v13 6/7] Implement Jsonb subscripting with slicing

Previously, slicing was not supported for jsonb subscripting. This commit
implements subscripting with slicing as part of the JSON simplified accessor
syntax specified in SQL:2023.

Co-authored-by: Nikita Glukhov <glukhov.n.a@gmail.com>
Co-authored-by: Alexandra Wang <alexandra.wang.oss@gmail.com>
Reviewed-by: Andrew Dunstan <andrew@dunslane.net>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Reviewed-by: Mark Dilger <mark.dilger@enterprisedb.com>
Reviewed-by: Jian He <jian.universality@gmail.com>
Reviewed-by: Vik Fearing <vik@postgresfriends.org>
Reviewed-by: Nikita Malakhov <hukutoc@gmail.com>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Tested-by: Mark Dilger <mark.dilger@enterprisedb.com>
Tested-by: Jian He <jian.universality@gmail.com>
---
 src/backend/utils/adt/jsonbsubs.c             |  33 +++-
 .../ecpg/test/expected/sql-sqljson.c          |  16 +-
 .../ecpg/test/expected/sql-sqljson.stderr     |  26 ++--
 .../ecpg/test/expected/sql-sqljson.stdout     |   3 +
 src/interfaces/ecpg/test/sql/sqljson.pgc      |   7 +-
 src/test/regress/expected/jsonb.out           | 145 ++++++++++++++++--
 src/test/regress/sql/jsonb.sql                |  53 ++++++-
 7 files changed, 243 insertions(+), 40 deletions(-)

diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c
index 5808383db26..458ca69d645 100644
--- a/src/backend/utils/adt/jsonbsubs.c
+++ b/src/backend/utils/adt/jsonbsubs.c
@@ -192,9 +192,10 @@ make_jsonpath_item_int(int32 val, List **exprs)
  * - pstate: parse state context
  * - expr: input expression node
  * - exprs: list of expression nodes (updated in place)
+ * - no_error: returns NULL when the data type doesn't match. Otherwise, emits an ERROR.
  */
 static JsonPathParseItem *
-make_jsonpath_item_expr(ParseState *pstate, Node *expr, List **exprs)
+make_jsonpath_item_expr(ParseState *pstate, Node *expr, List **exprs, bool no_error)
 {
 	Const	   *cnst;
 
@@ -207,7 +208,14 @@ make_jsonpath_item_expr(ParseState *pstate, Node *expr, List **exprs)
 			return make_jsonpath_item_int(DatumGetInt32(cnst->constvalue), exprs);
 	}
 
-	return NULL;
+	if (no_error)
+		return NULL;
+	else
+		ereport(ERROR,
+				errcode(ERRCODE_DATATYPE_MISMATCH),
+				errmsg("only non-null integer constants are supported for jsonb simplified accessor subscripting"),
+				errhint("use int data type for subscripting with slicing."),
+				parser_errposition(pstate, exprLocation(expr)));
 }
 
 /*
@@ -277,19 +285,28 @@ jsonb_subscript_make_jsonpath(ParseState *pstate, List **indirection, Subscripti
 
 			if (ai->is_slice)
 			{
-				Node	   *expr = ai->uidx ? ai->uidx : ai->lidx;
+				while (list_length(sbsref->reflowerindexpr) < list_length(sbsref->refupperindexpr))
+					sbsref->reflowerindexpr = lappend(sbsref->reflowerindexpr, NULL);
+
+				if (ai->lidx)
+					jpi->value.array.elems[0].from = make_jsonpath_item_expr(pstate, ai->lidx, &sbsref->reflowerindexpr, false);
+				else
+					jpi->value.array.elems[0].from = make_jsonpath_item_int(0, &sbsref->reflowerindexpr);
 
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("jsonb subscript does not support slices"),
-						 parser_errposition(pstate, exprLocation(expr))));
+				if (ai->uidx)
+					jpi->value.array.elems[0].to = make_jsonpath_item_expr(pstate, ai->uidx, &sbsref->refupperindexpr, false);
+				else
+				{
+					jpi->value.array.elems[0].to = make_jsonpath_item(jpiLast);
+					sbsref->refupperindexpr = lappend(sbsref->refupperindexpr, NULL);
+				}
 			}
 			else
 			{
 				JsonPathParseItem *jpi_from = NULL;
 
 				Assert(ai->uidx && !ai->lidx);
-				jpi_from = make_jsonpath_item_expr(pstate, ai->uidx, &sbsref->refupperindexpr);
+				jpi_from = make_jsonpath_item_expr(pstate, ai->uidx, &sbsref->refupperindexpr, true);
 				if (jpi_from == NULL)
 				{
 					/*
diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.c b/src/interfaces/ecpg/test/expected/sql-sqljson.c
index e6a7ece6dab..935b47a3b9a 100644
--- a/src/interfaces/ecpg/test/expected/sql-sqljson.c
+++ b/src/interfaces/ecpg/test/expected/sql-sqljson.c
@@ -505,7 +505,7 @@ if (sqlca.sqlcode < 0) sqlprint();}
 if (sqlca.sqlcode < 0) sqlprint();}
 #line 142 "sqljson.pgc"
 
-	// error
+	printf("Found json=%s\n", json);
 
 	{ ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) . * )", ECPGt_EOIT, 
 	ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), 
@@ -525,14 +525,24 @@ if (sqlca.sqlcode < 0) sqlprint();}
 if (sqlca.sqlcode < 0) sqlprint();}
 #line 148 "sqljson.pgc"
 
-	// error
+	printf("Found json=%s\n", json);
 
-  { ECPGdisconnect(__LINE__, "CURRENT");
+	{ ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) . b [ : ] )", ECPGt_EOIT, 
+	ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), 
+	ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT);
 #line 151 "sqljson.pgc"
 
 if (sqlca.sqlcode < 0) sqlprint();}
 #line 151 "sqljson.pgc"
 
+	printf("Found json=%s\n", json);
+
+  { ECPGdisconnect(__LINE__, "CURRENT");
+#line 154 "sqljson.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 154 "sqljson.pgc"
+
 
   return 0;
 }
diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.stderr b/src/interfaces/ecpg/test/expected/sql-sqljson.stderr
index 19f8c58af06..f3f899c6d87 100644
--- a/src/interfaces/ecpg/test/expected/sql-sqljson.stderr
+++ b/src/interfaces/ecpg/test/expected/sql-sqljson.stderr
@@ -339,13 +339,10 @@ SQL error: schema "jsonb" does not exist on line 121
 [NO_PID]: sqlca: code: 0, state: 00000
 [NO_PID]: ecpg_execute on line 142: using PQexec
 [NO_PID]: sqlca: code: 0, state: 00000
-[NO_PID]: ecpg_check_PQresult on line 142: bad response - ERROR:  jsonb subscript does not support slices
-LINE 1: ..., {"x": [12, {"y":1}]}]}' :: jsonb ) . b [ 1 ] . x [ 0 : ] )
-                                                                ^
+[NO_PID]: ecpg_process_output on line 142: correctly got 1 tuples with 1 fields
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_get_data on line 142: RESULT: [12, {"y": 1}] offset: -1; array: no
 [NO_PID]: sqlca: code: 0, state: 00000
-[NO_PID]: raising sqlstate 42804 (sqlcode -400): jsonb subscript does not support slices on line 142
-[NO_PID]: sqlca: code: -400, state: 42804
-SQL error: jsonb subscript does not support slices on line 142
 [NO_PID]: ecpg_execute on line 145: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) . * ); with 0 parameter(s) on connection ecpg1_regression
 [NO_PID]: sqlca: code: 0, state: 00000
 [NO_PID]: ecpg_execute on line 145: using PQexec
@@ -361,12 +358,17 @@ SQL error: row expansion via "*" is not supported here on line 145
 [NO_PID]: sqlca: code: 0, state: 00000
 [NO_PID]: ecpg_execute on line 148: using PQexec
 [NO_PID]: sqlca: code: 0, state: 00000
-[NO_PID]: ecpg_check_PQresult on line 148: bad response - ERROR:  jsonb subscript does not support slices
-LINE 1: ...x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) [ 'b' ] [ 0 : ] )
-                                                                ^
+[NO_PID]: ecpg_process_output on line 148: correctly got 1 tuples with 1 fields
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_get_data on line 148: RESULT: [{"x": 1}, {"x": [12, {"y": 1}]}] offset: -1; array: no
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 151: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) . b [ : ] ); with 0 parameter(s) on connection ecpg1_regression
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 151: using PQexec
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_process_output on line 151: correctly got 1 tuples with 1 fields
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_get_data on line 151: RESULT: [{"x": 1}, {"x": [12, {"y": 1}]}] offset: -1; array: no
 [NO_PID]: sqlca: code: 0, state: 00000
-[NO_PID]: raising sqlstate 42804 (sqlcode -400): jsonb subscript does not support slices on line 148
-[NO_PID]: sqlca: code: -400, state: 42804
-SQL error: jsonb subscript does not support slices on line 148
 [NO_PID]: ecpg_finish: connection ecpg1_regression closed
 [NO_PID]: sqlca: code: 0, state: 00000
diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.stdout b/src/interfaces/ecpg/test/expected/sql-sqljson.stdout
index 442d36931f1..d01a8457f01 100644
--- a/src/interfaces/ecpg/test/expected/sql-sqljson.stdout
+++ b/src/interfaces/ecpg/test/expected/sql-sqljson.stdout
@@ -35,3 +35,6 @@ Found json=null
 Found json={"x": 1}
 Found json=[1, [12, {"y": 1}]]
 Found json={"x": 1}
+Found json=[12, {"y": 1}]
+Found json=[{"x": 1}, {"x": [12, {"y": 1}]}]
+Found json=[{"x": 1}, {"x": [12, {"y": 1}]}]
diff --git a/src/interfaces/ecpg/test/sql/sqljson.pgc b/src/interfaces/ecpg/test/sql/sqljson.pgc
index 57a9bff424d..9423d25fd0b 100644
--- a/src/interfaces/ecpg/test/sql/sqljson.pgc
+++ b/src/interfaces/ecpg/test/sql/sqljson.pgc
@@ -140,13 +140,16 @@ EXEC SQL END DECLARE SECTION;
 	printf("Found json=%s\n", json);
 
 	EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb).b[1].x[0:]) INTO :json;
-	// error
+	printf("Found json=%s\n", json);
 
 	EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb).*) INTO :json;
 	// error
 
 	EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb)['b'][0:]) INTO :json;
-	// error
+	printf("Found json=%s\n", json);
+
+	EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb).b[:]) INTO :json;
+	printf("Found json=%s\n", json);
 
   EXEC SQL DISCONNECT;
 
diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out
index d781f1518eb..f4a20694102 100644
--- a/src/test/regress/expected/jsonb.out
+++ b/src/test/regress/expected/jsonb.out
@@ -5247,23 +5247,34 @@ select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb).a[1].b
 (1 row)
 
 select ('{"a": 1}'::jsonb)['a':'b']; -- fails
-ERROR:  jsonb subscript does not support slices
+ERROR:  only non-null integer constants are supported for jsonb simplified accessor subscripting
 LINE 1: select ('{"a": 1}'::jsonb)['a':'b'];
-                                       ^
+                                   ^
+HINT:  use int data type for subscripting with slicing.
 select ('[1, "2", null]'::jsonb)[1:2];
-ERROR:  jsonb subscript does not support slices
-LINE 1: select ('[1, "2", null]'::jsonb)[1:2];
-                                           ^
+    jsonb    
+-------------
+ ["2", null]
+(1 row)
+
 select ('[1, "2", null]'::jsonb)[:2];
-ERROR:  jsonb subscript does not support slices
-LINE 1: select ('[1, "2", null]'::jsonb)[:2];
-                                          ^
+     jsonb      
+----------------
+ [1, "2", null]
+(1 row)
+
 select ('[1, "2", null]'::jsonb)[1:];
-ERROR:  jsonb subscript does not support slices
-LINE 1: select ('[1, "2", null]'::jsonb)[1:];
-                                         ^
+    jsonb    
+-------------
+ ["2", null]
+(1 row)
+
 select ('[1, "2", null]'::jsonb)[:];
-ERROR:  jsonb subscript does not support slices
+     jsonb      
+----------------
+ [1, "2", null]
+(1 row)
+
 create TEMP TABLE test_jsonb_subscript (
        id int,
        test_json jsonb
@@ -6005,8 +6016,42 @@ SELECT (jb).a.b.c FROM test_jsonb_dot_notation;
  
 (1 row)
 
-SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t; -- fails
-ERROR:  jsonb subscript does not support slices
+SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t;
+            x             
+--------------------------
+ {"y": "YYY", "z": "ZZZ"}
+(1 row)
+
+SELECT (jb).a[2:3].b FROM test_jsonb_dot_notation;
+     b      
+------------
+ ["c", "d"]
+(1 row)
+
+SELECT (jb).a[2:].b FROM test_jsonb_dot_notation;
+     b      
+------------
+ ["c", "d"]
+(1 row)
+
+SELECT (jb).a[:2].b FROM test_jsonb_dot_notation;
+  b  
+-----
+ "c"
+(1 row)
+
+SELECT (jb).a[:].b FROM test_jsonb_dot_notation;
+     b      
+------------
+ ["c", "d"]
+(1 row)
+
+SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t;
+            x             
+--------------------------
+ {"y": "YYY", "z": "ZZZ"}
+(1 row)
+
 SELECT (jb).a.* FROM test_jsonb_dot_notation; -- fails
 ERROR:  type jsonb is not composite
 -- explains should work
@@ -6042,6 +6087,28 @@ CREATE VIEW test_jsonb_dot_notation_v1 AS SELECT (jb).a[3].x.y FROM test_jsonb_d
 CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS
  SELECT (jb).a[3].x.y AS y
    FROM test_jsonb_dot_notation
+CREATE VIEW v2 AS SELECT (jb).a[3:].x.y[:-1] FROM test_jsonb_dot_notation;
+\sv v2
+CREATE OR REPLACE VIEW public.v2 AS
+ SELECT (jb).a[3:].x.y[0:'-1'::integer] AS y
+   FROM test_jsonb_dot_notation
+SELECT * from v2;
+ y 
+---
+ 
+(1 row)
+
+CREATE VIEW v3 AS SELECT (jb).a[:3].x.y[-1:] FROM test_jsonb_dot_notation;
+\sv v3
+CREATE OR REPLACE VIEW public.v3 AS
+ SELECT (jb).a[0:3].x.y['-1'::integer:] AS y
+   FROM test_jsonb_dot_notation
+SELECT * from v3;
+   y   
+-------
+ "yyy"
+(1 row)
+
 -- mixed syntax
 DROP VIEW test_jsonb_dot_notation_v1;
 EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb)['a'].b FROM test_jsonb_dot_notation;
@@ -6226,5 +6293,55 @@ SELECT * from test_jsonb_dot_notation_v1;
 (1 row)
 
 DROP VIEW public.test_jsonb_dot_notation_v1;
+-- jsonb array access in plpgsql
+DO $$
+    DECLARE
+        a jsonb := '[1,2,3,4,5,6,7]'::jsonb;
+    BEGIN
+        WHILE a IS NOT NULL
+            LOOP
+                RAISE NOTICE '%', a;
+                a := a[2:];
+            END LOOP;
+    END
+$$ LANGUAGE plpgsql;
+NOTICE:  [1, 2, 3, 4, 5, 6, 7]
+NOTICE:  [3, 4, 5, 6, 7]
+NOTICE:  [5, 6, 7]
+NOTICE:  7
+-- jsonb dot access in plpgsql
+DO $$
+    DECLARE
+        a jsonb := '{"": 6, "NU": [{"": [[3]]}, [6], [2], "bCi"], "aaf": [-6, -8]}'::jsonb;
+    BEGIN
+        WHILE a IS NOT NULL
+            LOOP
+                RAISE NOTICE '%', a;
+                a := COALESCE(a."NU", a[2]); -- fails
+            END LOOP;
+    END
+$$ LANGUAGE plpgsql;
+NOTICE:  {"": 6, "NU": [{"": [[3]]}, [6], [2], "bCi"], "aaf": [-6, -8]}
+ERROR:  missing FROM-clause entry for table "a"
+LINE 1: a := COALESCE(a."NU", a[2])
+                      ^
+QUERY:  a := COALESCE(a."NU", a[2])
+CONTEXT:  PL/pgSQL function inline_code_block line 8 at assignment
+DO $$
+    DECLARE
+        a jsonb := '{"": 6, "NU": [{"": [[3]]}, [6], [2], "bCi"], "aaf": [-6, -8]}'::jsonb;
+    BEGIN
+        WHILE a IS NOT NULL
+            LOOP
+                RAISE NOTICE '%', a;
+                a := COALESCE((a)."NU", a[2]); -- succeeds
+            END LOOP;
+    END
+$$ LANGUAGE plpgsql;
+NOTICE:  {"": 6, "NU": [{"": [[3]]}, [6], [2], "bCi"], "aaf": [-6, -8]}
+NOTICE:  [{"": [[3]]}, [6], [2], "bCi"]
+NOTICE:  [2]
 -- clean up
+DROP VIEW v2;
+DROP VIEW v3;
 DROP TABLE test_jsonb_dot_notation;
diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql
index a0fad90a9d5..b8ecefda04d 100644
--- a/src/test/regress/sql/jsonb.sql
+++ b/src/test/regress/sql/jsonb.sql
@@ -1623,7 +1623,12 @@ SELECT (jb).a[2].b FROM test_jsonb_dot_notation;
 SELECT (jb).a.x.y FROM test_jsonb_dot_notation;
 SELECT (jb).b.x.z FROM test_jsonb_dot_notation;
 SELECT (jb).a.b.c FROM test_jsonb_dot_notation;
-SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t; -- fails
+SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t;
+SELECT (jb).a[2:3].b FROM test_jsonb_dot_notation;
+SELECT (jb).a[2:].b FROM test_jsonb_dot_notation;
+SELECT (jb).a[:2].b FROM test_jsonb_dot_notation;
+SELECT (jb).a[:].b FROM test_jsonb_dot_notation;
+SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t;
 SELECT (jb).a.* FROM test_jsonb_dot_notation; -- fails
 
 -- explains should work
@@ -1635,6 +1640,12 @@ SELECT (jb).a[1] FROM test_jsonb_dot_notation;
 -- views should work
 CREATE VIEW test_jsonb_dot_notation_v1 AS SELECT (jb).a[3].x.y FROM test_jsonb_dot_notation;
 \sv test_jsonb_dot_notation_v1
+CREATE VIEW v2 AS SELECT (jb).a[3:].x.y[:-1] FROM test_jsonb_dot_notation;
+\sv v2
+SELECT * from v2;
+CREATE VIEW v3 AS SELECT (jb).a[:3].x.y[-1:] FROM test_jsonb_dot_notation;
+\sv v3
+SELECT * from v3;
 
 -- mixed syntax
 DROP VIEW test_jsonb_dot_notation_v1;
@@ -1695,5 +1706,45 @@ SELECT (jb)['b']['x'].z FROM test_jsonb_dot_notation;
 SELECT * from test_jsonb_dot_notation_v1;
 DROP VIEW public.test_jsonb_dot_notation_v1;
 
+-- jsonb array access in plpgsql
+DO $$
+    DECLARE
+        a jsonb := '[1,2,3,4,5,6,7]'::jsonb;
+    BEGIN
+        WHILE a IS NOT NULL
+            LOOP
+                RAISE NOTICE '%', a;
+                a := a[2:];
+            END LOOP;
+    END
+$$ LANGUAGE plpgsql;
+
+-- jsonb dot access in plpgsql
+DO $$
+    DECLARE
+        a jsonb := '{"": 6, "NU": [{"": [[3]]}, [6], [2], "bCi"], "aaf": [-6, -8]}'::jsonb;
+    BEGIN
+        WHILE a IS NOT NULL
+            LOOP
+                RAISE NOTICE '%', a;
+                a := COALESCE(a."NU", a[2]); -- fails
+            END LOOP;
+    END
+$$ LANGUAGE plpgsql;
+
+DO $$
+    DECLARE
+        a jsonb := '{"": 6, "NU": [{"": [[3]]}, [6], [2], "bCi"], "aaf": [-6, -8]}'::jsonb;
+    BEGIN
+        WHILE a IS NOT NULL
+            LOOP
+                RAISE NOTICE '%', a;
+                a := COALESCE((a)."NU", a[2]); -- succeeds
+            END LOOP;
+    END
+$$ LANGUAGE plpgsql;
+
 -- clean up
+DROP VIEW v2;
+DROP VIEW v3;
 DROP TABLE test_jsonb_dot_notation;
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

