From a8b386df15fa4dba9c80ebfaab8d2a732d85ac82 Mon Sep 17 00:00:00 2001
From: "David E. Wheeler" <david@justatheory.com>
Date: Sat, 15 Jun 2024 19:44:21 +0530
Subject: [PATCH v3] Teach jsonpath string() to unwrap in lax mode

All other jsonpath methods, aside from type() and .size(), will unwrap
an array in lax mode, but string(), added in 66ea94e, overlooked this
behavior. A discussion on pgsql-hackers[1] cites the SQL standard:

> General Rule 11 g ii 6) A) says just "if MODE is lax and <JSON method>
> is not type or size, then let BASE be Unwrap(BASE)." No special
> exemption there for string(), nor further below at C) XV) for the
> operation of string().

So teach string() to also unwrap in lax mode, update the test for this
behavior, and add a line to the docs about the behavior of methods in
lax mode.

  [1]: https://www.postgresql.org/message-id/flat/A64AE04F-4410-42B7-A141-7A7349260F4D%40justatheory.com
---
 doc/src/sgml/func.sgml                       |  5 ++++-
 src/backend/utils/adt/jsonpath_exec.c        |  3 +++
 src/test/regress/expected/jsonb_jsonpath.out | 12 +++++++++++-
 src/test/regress/sql/jsonb_jsonpath.sql      |  1 +
 4 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 17c44bc..2609269 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -17792,7 +17792,10 @@ ERROR:  jsonpath member accessor can only be applied to an object
     methods available in <type>jsonpath</type>.  Note that while the unary
     operators and methods can be applied to multiple values resulting from a
     preceding path step, the binary operators (addition etc.) can only be
-    applied to single values.
+    applied to single values.  In lax mode, methods applied to an array will be
+    executed for each value in the array.  The exceptions are
+    <literal>.type()</literal> and <literal>.size()</literal>, which apply to
+    the array itself.
    </para>
 
    <table id="functions-sqljson-op-table">
diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index ceb3003..c30d059 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -1606,6 +1606,9 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp,
 				JsonbValue	jbv;
 				char	   *tmp = NULL;
 
+				if (unwrap && JsonbType(jb) == jbvArray)
+					return executeItemUnwrapTargetArray(cxt, jsp, jb, found, false);
+
 				switch (JsonbType(jb))
 				{
 					case jbvString:
diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out
index c3f8e82..a6112e8 100644
--- a/src/test/regress/expected/jsonb_jsonpath.out
+++ b/src/test/regress/expected/jsonb_jsonpath.out
@@ -2525,7 +2525,10 @@ select jsonb_path_query('null', '$.string()', silent => true);
 (0 rows)
 
 select jsonb_path_query('[]', '$.string()');
-ERROR:  jsonpath item method .string() can only be applied to a bool, string, numeric, or datetime value
+ jsonb_path_query 
+------------------
+(0 rows)
+
 select jsonb_path_query('[]', 'strict $.string()');
 ERROR:  jsonpath item method .string() can only be applied to a bool, string, numeric, or datetime value
 select jsonb_path_query('{}', '$.string()');
@@ -2576,6 +2579,13 @@ select jsonb_path_query('1234', '$.string().type()');
  "string"
 (1 row)
 
+select jsonb_path_query('[2, true]', '$.string()');
+ jsonb_path_query 
+------------------
+ "2"
+ "true"
+(2 rows)
+
 select jsonb_path_query('"2023-08-15 12:34:56 +5:30"', '$.timestamp().string()');
 ERROR:  cannot convert value from timestamptz to timestamp without time zone usage
 HINT:  Use *_tz() function for time zone support.
diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql
index cbd2db5..5e14f77 100644
--- a/src/test/regress/sql/jsonb_jsonpath.sql
+++ b/src/test/regress/sql/jsonb_jsonpath.sql
@@ -586,6 +586,7 @@ select jsonb_path_query('"1.23aaa"', '$.string()');
 select jsonb_path_query('1234', '$.string()');
 select jsonb_path_query('true', '$.string()');
 select jsonb_path_query('1234', '$.string().type()');
+select jsonb_path_query('[2, true]', '$.string()');
 select jsonb_path_query('"2023-08-15 12:34:56 +5:30"', '$.timestamp().string()');
 select jsonb_path_query_tz('"2023-08-15 12:34:56 +5:30"', '$.timestamp().string()'); -- should work
 select jsonb_path_query_array('[1.23, "yes", false]', '$[*].string()');
-- 
1.8.3.1

