From bb99e8cdc1523c96c76f941a3027f3fdac3fdfdc Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Sun, 7 Apr 2024 19:49:41 +0900
Subject: [PATCH v51 1/2] Fix JsonExpr deparsing to emit QUOTES and WRAPPER
 correctly

Currently, get_json_expr_options() does not emit the default values
for QUOTES (KEEP QUOTES) and WRAPPER (WITHOUT WRAPPER).  That causes
the deparsed JSON_TABLE() columns, such as those contained in a a
view's query, to behave differently when executed than the original
definition.  That's because the rules encoded in
transformJsonTableColumns() will choose either JSON_VALUE() or
JSON_QUERY() as implementation depending on the QUOTE and WRAPPER
specification to execute a given path's expression, which have
slightly different semantics.

Moreover, make sure to only emit them in combinations that are
currently supported in transformJsonFuncExpr().  For example, QUOTES
clause cannot be specified (KEEP or OMIT) if the WRAPPER option is
not WITHOUT WRAPPER.

Reported-by: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/CACJufxEqhqsfrg_p7EMyo5zak3d767iFDL8vz_4%3DZBHpOtrghw%40mail.gmail.com
---
 src/backend/utils/adt/ruleutils.c             | 12 ++++++
 .../regress/expected/sqljson_jsontable.out    | 42 +++++++++----------
 .../regress/expected/sqljson_queryfuncs.out   |  8 ++--
 3 files changed, 37 insertions(+), 25 deletions(-)

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 411841047d..02c308553c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8848,9 +8848,21 @@ get_json_expr_options(JsonExpr *jsexpr, deparse_context *context,
 			appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER");
 		else if (jsexpr->wrapper == JSW_UNCONDITIONAL)
 			appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER");
+		else if (jsexpr->wrapper == JSW_NONE || jsexpr->wrapper == JSW_UNSPEC)
+			/* The default */
+			appendStringInfo(context->buf, " WITHOUT WRAPPER");
 
 		if (jsexpr->omit_quotes)
 			appendStringInfo(context->buf, " OMIT QUOTES");
+
+		/*
+		 * Don't emit the default QUOTES behavior if the WRAPPER behavior is
+		 * incompatible.  transformJsonFuncExpr() only allows specifying
+		 * QUOTES behavior if WRAPPER behavior is either unspecified or is
+		 * WITHOUT WRAPPER.
+		 */
+		else if (jsexpr->wrapper == JSW_NONE || jsexpr->wrapper == JSW_UNSPEC)
+			appendStringInfo(context->buf, " KEEP QUOTES");
 	}
 
 	if (jsexpr->on_empty && jsexpr->on_empty->btype != default_behavior)
diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out
index c58a98ac4f..aeb2079f04 100644
--- a/src/test/regress/expected/sqljson_jsontable.out
+++ b/src/test/regress/expected/sqljson_jsontable.out
@@ -302,11 +302,11 @@ CREATE OR REPLACE VIEW public.jsonb_table_view3 AS
                 1 + 2 AS a,
                 '"foo"'::json AS "b c"
             COLUMNS (
-                js json PATH '$',
-                jb jsonb PATH '$',
-                jst text FORMAT JSON PATH '$',
-                jsc character(4) FORMAT JSON PATH '$',
-                jsv character varying(4) FORMAT JSON PATH '$'
+                js json PATH '$' WITHOUT WRAPPER KEEP QUOTES,
+                jb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES,
+                jst text FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES,
+                jsc character(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES,
+                jsv character varying(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES
             )
         )
 \sv jsonb_table_view4
@@ -321,8 +321,8 @@ CREATE OR REPLACE VIEW public.jsonb_table_view4 AS
                 1 + 2 AS a,
                 '"foo"'::json AS "b c"
             COLUMNS (
-                jsb jsonb PATH '$',
-                jsbq jsonb PATH '$' OMIT QUOTES,
+                jsb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES,
+                jsbq jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES,
                 aaa integer PATH '$."aaa"',
                 aaa1 integer PATH '$."aaa"'
             )
@@ -357,12 +357,12 @@ CREATE OR REPLACE VIEW public.jsonb_table_view6 AS
                 1 + 2 AS a,
                 '"foo"'::json AS "b c"
             COLUMNS (
-                js2 json PATH '$',
+                js2 json PATH '$' WITHOUT WRAPPER KEEP QUOTES,
                 jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER,
-                jsb2q jsonb PATH '$' OMIT QUOTES,
-                ia integer[] PATH '$',
-                ta text[] PATH '$',
-                jba jsonb[] PATH '$'
+                jsb2q jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES,
+                ia integer[] PATH '$' WITHOUT WRAPPER KEEP QUOTES,
+                ta text[] PATH '$' WITHOUT WRAPPER KEEP QUOTES,
+                jba jsonb[] PATH '$' WITHOUT WRAPPER KEEP QUOTES
             )
         )
 EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view2;
@@ -374,19 +374,19 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view2;
 (3 rows)
 
 EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view3;
-                                                                                                                                        QUERY PLAN                                                                                                                                        
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                              
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Table Function Scan on "json_table"
    Output: "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv
-   Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_0 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$'))
+   Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_0 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (js json PATH '$' WITHOUT WRAPPER KEEP QUOTES, jb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES, jst text FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsc character(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsv character varying(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES))
 (3 rows)
 
 EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view4;
-                                                                                                                  QUERY PLAN                                                                                                                  
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                        QUERY PLAN                                                                                                                                        
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Table Function Scan on "json_table"
    Output: "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1
-   Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_0 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"'))
+   Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_0 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (jsb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsbq jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"'))
 (3 rows)
 
 EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view5;
@@ -398,11 +398,11 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view5;
 (3 rows)
 
 EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view6;
-                                                                                                                                              QUERY PLAN                                                                                                                                               
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                               
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Table Function Scan on "json_table"
    Output: "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba
-   Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_0 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$'))
+   Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_0 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (js2 json PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES, ia integer[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, ta text[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, jba jsonb[] PATH '$' WITHOUT WRAPPER KEEP QUOTES))
 (3 rows)
 
 -- JSON_TABLE() with alias
diff --git a/src/test/regress/expected/sqljson_queryfuncs.out b/src/test/regress/expected/sqljson_queryfuncs.out
index 873cbac960..0561c71e29 100644
--- a/src/test/regress/expected/sqljson_queryfuncs.out
+++ b/src/test/regress/expected/sqljson_queryfuncs.out
@@ -1071,15 +1071,15 @@ Check constraints:
     "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr))
     "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT 12 ON EMPTY ERROR ON ERROR) > i)
     "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)
-    "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))
+    "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) WITHOUT WRAPPER OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))
 
 SELECT check_clause
 FROM information_schema.check_constraints
 WHERE constraint_name LIKE 'test_jsonb_constraint%'
 ORDER BY 1;
-                                                      check_clause                                                      
-------------------------------------------------------------------------------------------------------------------------
- (JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))
+                                                              check_clause                                                              
+----------------------------------------------------------------------------------------------------------------------------------------
+ (JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) WITHOUT WRAPPER OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))
  (JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)
  (JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT 12 ON EMPTY ERROR ON ERROR) > i)
  (js IS JSON)
-- 
2.43.0

