]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix JSON_ARRAY(query) empty set handling and view deparsing
authorRichard Guo <rguo@postgresql.org>
Fri, 1 May 2026 00:42:00 +0000 (09:42 +0900)
committerRichard Guo <rguo@postgresql.org>
Fri, 1 May 2026 00:42:00 +0000 (09:42 +0900)
According to the SQL/JSON standard, JSON_ARRAY(query) must return an
empty JSON array ('[]') when the subquery returns zero rows.

Previously, the parser rewrote JSON_ARRAY(query) into a JSON_ARRAYAGG
aggregate function.  Because this aggregate evaluates to NULL over an
empty set without a GROUP BY clause, the constructor erroneously
returned NULL.  Additionally, this premature rewrite baked physical
implementation details into the catalog, preventing ruleutils.c from
deparsing the original syntax for views.

This patch resolves both issues by introducing a new
JSCTOR_JSON_ARRAY_QUERY constructor type.  The parser builds the
executable form --- a COALESCE-wrapped JSON_ARRAYAGG subquery --- from
raw parse nodes via transformExprRecurse, and stores it in the func
field.  The original transformed Query is kept in a new orig_query
field so that ruleutils.c can deparse the original syntax for views.
During planning, eval_const_expressions replaces the node with the
pre-built func expression.

The deparsing issue was reported by Tom Lane.

Bump catalog version.

Bug: #19418
Reported-by: Lukas Eder <lukas.eder@gmail.com>
Author: Richard Guo <guofenglinux@gmail.com>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Discussion: https://postgr.es/m/19418-591ba1f29862ef5b@postgresql.org

doc/src/sgml/func/func-json.sgml
src/backend/nodes/nodeFuncs.c
src/backend/optimizer/util/clauses.c
src/backend/parser/parse_expr.c
src/backend/utils/adt/ruleutils.c
src/include/catalog/catversion.h
src/include/nodes/primnodes.h
src/test/regress/expected/sqljson.out
src/test/regress/sql/sqljson.sql

index 4cd338fe6e32d2b905a93feac381f3ee0a420437..3d97e2b53755e43af2ec71329a6e399f53447b09 100644 (file)
          which must be a SELECT query returning a single column. If
          <literal>ABSENT ON NULL</literal> is specified, NULL values are ignored.
          This is always the case if a
-         <replaceable>query_expression</replaceable> is used.
+         <replaceable>query_expression</replaceable> is used. If the query returns
+         no rows, an empty JSON array is returned.
         </para>
         <para>
          <literal>json_array(1,true,json '{"a":null}')</literal>
index 7edbd5b72259d91309543828b3c60582c6c60c9c..f968ac6831416b95ac059754b26a394baabe7819 100644 (file)
@@ -1002,8 +1002,16 @@ exprCollation(const Node *expr)
                        {
                                const JsonConstructorExpr *ctor = (const JsonConstructorExpr *) expr;
 
+                               /*
+                                * Collation comes from coercion if present, otherwise from
+                                * func.  The func fallback is needed in cases where func
+                                * already produces the final output type and no coercion is
+                                * needed (cf. the JSCTOR_JSON_ARRAY_QUERY case).
+                                */
                                if (ctor->coercion)
                                        coll = exprCollation((Node *) ctor->coercion);
+                               else if (ctor->func)
+                                       coll = exprCollation((Node *) ctor->func);
                                else
                                        coll = InvalidOid;
                        }
@@ -1264,8 +1272,11 @@ exprSetCollation(Node *expr, Oid collation)
                        {
                                JsonConstructorExpr *ctor = (JsonConstructorExpr *) expr;
 
+                               /* See comment in exprCollation() */
                                if (ctor->coercion)
                                        exprSetCollation((Node *) ctor->coercion, collation);
+                               else if (ctor->func)
+                                       exprSetCollation((Node *) ctor->func, collation);
                                else
                                        Assert(!OidIsValid(collation)); /* result is always a
                                                                                                         * json[b] type */
index fcf6d7fff2ae4e658f75dab8af8456ea64b4bf29..cd86311bb0b6cca1547e69770612fa4790336cc5 100644 (file)
@@ -3244,7 +3244,6 @@ eval_const_expressions_mutator(Node *node,
                                }
                                break;
                        }
-
                case T_JsonValueExpr:
                        {
                                JsonValueExpr *jve = (JsonValueExpr *) node;
@@ -3268,7 +3267,21 @@ eval_const_expressions_mutator(Node *node,
                                                                                                  (Expr *) formatted_expr,
                                                                                                  copyObject(jve->format));
                        }
+               case T_JsonConstructorExpr:
+                       {
+                               JsonConstructorExpr *jce = (JsonConstructorExpr *) node;
 
+                               /*
+                                * JSCTOR_JSON_ARRAY_QUERY carries a pre-built executable form
+                                * in its func field (a COALESCE-wrapped JSON_ARRAYAGG
+                                * subquery, constructed during parse analysis).  Replace the
+                                * node with that expression and continue simplifying.
+                                */
+                               if (jce->type == JSCTOR_JSON_ARRAY_QUERY)
+                                       return eval_const_expressions_mutator((Node *) jce->func,
+                                                                                                                 context);
+                       }
+                       break;
                case T_SubPlan:
                case T_AlternativeSubPlan:
 
index f535f3b9351dd68e49153224077656c7ef55f983..c3c7aa297204e8ee31c4743320a86c2cd9a9e9b2 100644 (file)
@@ -3792,24 +3792,53 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor)
 }
 
 /*
- * Transform JSON_ARRAY(query [FORMAT] [RETURNING] [ON NULL]) into
- *  (SELECT  JSON_ARRAYAGG(a  [FORMAT] [RETURNING] [ON NULL]) FROM (query) q(a))
+ * Transform JSON_ARRAY(subquery) constructor.
+ *
+ * JSON_ARRAY(subquery) is transformed into a JsonConstructorExpr node of type
+ * JSCTOR_JSON_ARRAY_QUERY.  The node carries:
+ *
+ *  - func: the executable form, which is a COALESCE expression wrapping a
+ *    JSON_ARRAYAGG subquery:
+ *
+ *        COALESCE((SELECT JSON_ARRAYAGG(a) FROM (subquery) q(a)), '[]')
+ *
+ *    The COALESCE ensures that an empty result set produces '[]' rather than
+ *    NULL, per the SQL/JSON standard.
+ *
+ *  - orig_query: the transformed Query of the user's original subquery, so
+ *    that ruleutils.c can deparse the original JSON_ARRAY(SELECT ...) syntax
+ *    for view definitions.
  */
 static Node *
 transformJsonArrayQueryConstructor(ParseState *pstate,
                                                                   JsonArrayQueryConstructor *ctor)
 {
-       SubLink    *sublink = makeNode(SubLink);
-       SelectStmt *select = makeNode(SelectStmt);
-       RangeSubselect *range = makeNode(RangeSubselect);
-       Alias      *alias = makeNode(Alias);
-       ResTarget  *target = makeNode(ResTarget);
-       JsonArrayAgg *agg = makeNode(JsonArrayAgg);
-       ColumnRef  *colref = makeNode(ColumnRef);
        Query      *query;
        ParseState *qpstate;
+       SubLink    *sublink;
+       SelectStmt *select;
+       RangeSubselect *range;
+       Alias      *alias;
+       ResTarget  *target;
+       JsonArrayAgg *agg;
+       ColumnRef  *colref;
+       Node       *exec_expr;
+       CoalesceExpr *coalesce;
+       Const      *empty_const;
+       Oid                     result_type;
+       Oid                     typinput;
+       Oid                     typioparam;
+       int16           typlen;
+       bool            typbyval;
+       JsonReturning *returning;
+       List       *args;
+       Node       *result;
 
-       /* Transform query only for counting target list entries. */
+       /*
+        * Transform a copy of the subquery to validate the single-column
+        * constraint and to obtain the transformed Query for deparsing.  This
+        * uses a private ParseState so it doesn't affect the main parse context.
+        */
        qpstate = make_parsestate(pstate);
 
        query = transformStmt(qpstate, copyObject(ctor->query));
@@ -3822,14 +3851,20 @@ transformJsonArrayQueryConstructor(ParseState *pstate,
 
        free_parsestate(qpstate);
 
+       /*
+        * Build the executable form by constructing query:
+        *
+        * (SELECT JSON_ARRAYAGG(a [FORMAT] [RETURNING]) FROM (subquery) q(a))
+        *
+        * ... using raw parse tree nodes, then transforming via
+        * transformExprRecurse.
+        */
+       colref = makeNode(ColumnRef);
        colref->fields = list_make2(makeString(pstrdup("q")),
                                                                makeString(pstrdup("a")));
        colref->location = ctor->location;
 
-       /*
-        * No formatting necessary, so set formatted_expr to be the same as
-        * raw_expr.
-        */
+       agg = makeNode(JsonArrayAgg);
        agg->arg = makeJsonValueExpr((Expr *) colref, (Expr *) colref,
                                                                 ctor->format);
        agg->absent_on_null = ctor->absent_on_null;
@@ -3838,21 +3873,26 @@ transformJsonArrayQueryConstructor(ParseState *pstate,
        agg->constructor->output = ctor->output;
        agg->constructor->location = ctor->location;
 
+       target = makeNode(ResTarget);
        target->name = NULL;
        target->indirection = NIL;
        target->val = (Node *) agg;
        target->location = ctor->location;
 
+       alias = makeNode(Alias);
        alias->aliasname = pstrdup("q");
        alias->colnames = list_make1(makeString(pstrdup("a")));
 
+       range = makeNode(RangeSubselect);
        range->lateral = false;
        range->subquery = ctor->query;
        range->alias = alias;
 
+       select = makeNode(SelectStmt);
        select->targetList = list_make1(target);
        select->fromClause = list_make1(range);
 
+       sublink = makeNode(SubLink);
        sublink->subLinkType = EXPR_SUBLINK;
        sublink->subLinkId = 0;
        sublink->testexpr = NULL;
@@ -3860,7 +3900,48 @@ transformJsonArrayQueryConstructor(ParseState *pstate,
        sublink->subselect = (Node *) select;
        sublink->location = ctor->location;
 
-       return transformExprRecurse(pstate, (Node *) sublink);
+       exec_expr = transformExprRecurse(pstate, (Node *) sublink);
+
+       /*
+        * Wrap in COALESCE so that an empty result set produces '[]' rather than
+        * NULL.  The empty-array constant is created in the output type so that
+        * the COALESCE arguments have consistent types.
+        */
+       result_type = exprType(exec_expr);
+       getTypeInputInfo(result_type, &typinput, &typioparam);
+       get_typlenbyval(result_type, &typlen, &typbyval);
+
+       empty_const = makeConst(result_type,
+                                                       -1,
+                                                       exprCollation(exec_expr),
+                                                       (int) typlen,
+                                                       OidInputFunctionCall(typinput, "[]",
+                                                                                                typioparam, -1),
+                                                       false,
+                                                       typbyval);
+
+       coalesce = makeNode(CoalesceExpr);
+       coalesce->coalescetype = result_type;
+       coalesce->coalescecollid = exprCollation(exec_expr);
+       coalesce->args = list_make2(exec_expr, empty_const);
+       coalesce->location = ctor->location;
+
+       /*
+        * Build the JSCTOR_JSON_ARRAY_QUERY node.  The COALESCE goes in func as
+        * the executable form; during planning, eval_const_expressions replaces
+        * the entire node with func.  The transformed Query is stored in
+        * orig_query so that ruleutils.c can deparse the original syntax.
+        */
+       args = list_make1(linitial_node(TargetEntry, query->targetList)->expr);
+       returning = transformJsonConstructorOutput(pstate, ctor->output, args);
+
+       result = makeJsonConstructorExpr(pstate, JSCTOR_JSON_ARRAY_QUERY,
+                                                                        NIL, (Expr *) coalesce, returning,
+                                                                        false, ctor->absent_on_null,
+                                                                        ctor->location);
+       ((JsonConstructorExpr *) result)->orig_query = (Node *) query;
+
+       return result;
 }
 
 /*
index c781cdc84d3d8482fd834a2d99efa0d5f83e40e3..75b77bb39f1ab79b0e7865cf7879796356c8396c 100644 (file)
@@ -12281,6 +12281,21 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context,
                get_json_agg_constructor(ctor, context, "JSON_ARRAYAGG", false);
                return;
        }
+       else if (ctor->type == JSCTOR_JSON_ARRAY_QUERY)
+       {
+               Query      *query = castNode(Query, ctor->orig_query);
+
+               appendStringInfo(buf, "JSON_ARRAY(");
+
+               get_query_def(query, buf, context->namespaces, NULL, false,
+                                         context->prettyFlags, context->wrapColumn,
+                                         context->indentLevel);
+
+               get_json_constructor_options(ctor, buf);
+               appendStringInfoChar(buf, ')');
+
+               return;
+       }
 
        switch (ctor->type)
        {
index 1602962dbe135828c25344a3123951ae3562307c..8d5924a32f2048e4da899f22ed050502cb0db7b0 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     202604061
+#define CATALOG_VERSION_NO     202605011
 
 #endif
index 6dfc946c20bd07bd830867a404cc7eb94e116473..7977ee2478320580a90305bb03b49e356d233569 100644 (file)
@@ -1715,26 +1715,38 @@ typedef struct JsonValueExpr
 typedef enum JsonConstructorType
 {
        JSCTOR_JSON_OBJECT = 1,
-       JSCTOR_JSON_ARRAY = 2,
-       JSCTOR_JSON_OBJECTAGG = 3,
-       JSCTOR_JSON_ARRAYAGG = 4,
-       JSCTOR_JSON_PARSE = 5,
-       JSCTOR_JSON_SCALAR = 6,
-       JSCTOR_JSON_SERIALIZE = 7,
+       JSCTOR_JSON_ARRAY,
+       JSCTOR_JSON_ARRAY_QUERY,
+       JSCTOR_JSON_OBJECTAGG,
+       JSCTOR_JSON_ARRAYAGG,
+       JSCTOR_JSON_PARSE,
+       JSCTOR_JSON_SCALAR,
+       JSCTOR_JSON_SERIALIZE,
 } JsonConstructorType;
 
 /*
  * JsonConstructorExpr -
  *             wrapper over FuncExpr/Aggref/WindowFunc for SQL/JSON constructors
+ *
+ * func is the executable expression:
+ * - Aggref/WindowFunc for JSON_OBJECTAGG/JSON_ARRAYAGG,
+ * - CoalesceExpr for JSON_ARRAY_QUERY,
+ * - NULL for other types (the executor calls the underlying json[b]_xxx()
+ *   functions directly).
+ *
+ * orig_query holds the user's original subquery for JSON_ARRAY(query), used
+ * only by ruleutils.c for deparsing; it is not walked because func is
+ * authoritative for all other purposes.
  */
 typedef struct JsonConstructorExpr
 {
        Expr            xpr;
        JsonConstructorType type;       /* constructor type */
        List       *args;
-       Expr       *func;                       /* underlying json[b]_xxx() function call */
+       Expr       *func;                       /* executable expression or NULL */
        Expr       *coercion;           /* coercion to RETURNING type */
        JsonReturning *returning;       /* RETURNING clause */
+       Node       *orig_query;         /* original subquery for deparsing */
        bool            absent_on_null; /* ABSENT ON NULL? */
        bool            unique;                 /* WITH UNIQUE KEYS? (JSON_OBJECT[AGG] only) */
        ParseLoc        location;
index f3be69838bfc9d18b2ee3dd9cb0c5dcd3075d988..a14936a4e6efcbbe3ef44851bb694fc55842dc34 100644 (file)
@@ -724,6 +724,7 @@ SELECT JSON_ARRAY(JSON_ARRAY('{ "a" : 123 }' FORMAT JSON RETURNING text) FORMAT
  [[{ "a" : 123 }]]
 (1 row)
 
+-- JSON_ARRAY(subquery)
 SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i));
  json_array 
 ------------
@@ -757,6 +758,31 @@ SELECT JSON_ARRAY(WITH x AS (SELECT 1) VALUES (TRUE));
  [true]
 (1 row)
 
+-- JSON_ARRAY(subquery) with empty result set
+SELECT JSON_ARRAY(SELECT 1 WHERE FALSE);
+ json_array 
+------------
+ []
+(1 row)
+
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) WHERE i > 4);
+ json_array 
+------------
+ []
+(1 row)
+
+-- JSON_ARRAY(subquery) with a correlated subquery in the WHERE clause
+SELECT * FROM (VALUES (1), (2), (NULL), (4)) t1(a)
+WHERE JSON_ARRAY(
+    SELECT b FROM (VALUES (1), (2), (3)) t2(b) WHERE b = t1.a
+    RETURNING jsonb
+) = '[]'::jsonb;
+ a 
+---
+  
+ 4
+(2 rows)
+
 -- Should fail
 SELECT JSON_ARRAY(SELECT FROM (VALUES (1)) foo(i));
 ERROR:  subquery must return only one column
@@ -1093,7 +1119,7 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
-   Output: (InitPlan expr_1).col1
+   Output: COALESCE((InitPlan expr_1).col1, '[]'::jsonb)
    InitPlan expr_1
      ->  Aggregate
            Output: JSON_ARRAYAGG("*VALUES*".column1 RETURNING jsonb)
@@ -1105,9 +1131,88 @@ CREATE VIEW json_array_subquery_view AS
 SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING jsonb);
 \sv json_array_subquery_view
 CREATE OR REPLACE VIEW public.json_array_subquery_view AS
- SELECT ( SELECT JSON_ARRAYAGG(q.a RETURNING jsonb) AS "json_arrayagg"
-           FROM ( SELECT foo.i
-                   FROM ( VALUES (1), (2), (NULL::integer), (4)) foo(i)) q(a)) AS "json_array"
+ SELECT JSON_ARRAY( SELECT foo.i
+           FROM ( VALUES (1), (2), (NULL::integer), (4)) foo(i) RETURNING jsonb) AS "json_array"
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) ORDER BY i LIMIT 3 RETURNING jsonb);
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Result
+   Output: COALESCE((InitPlan expr_1).col1, '[]'::jsonb)
+   InitPlan expr_1
+     ->  Aggregate
+           Output: JSON_ARRAYAGG("*VALUES*".column1 RETURNING jsonb)
+           ->  Limit
+                 Output: "*VALUES*".column1
+                 ->  Sort
+                       Output: "*VALUES*".column1
+                       Sort Key: "*VALUES*".column1
+                       ->  Values Scan on "*VALUES*"
+                             Output: "*VALUES*".column1
+(12 rows)
+
+CREATE OR REPLACE VIEW json_array_subquery_view AS
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) ORDER BY i LIMIT 3 RETURNING jsonb);
+\sv json_array_subquery_view
+CREATE OR REPLACE VIEW public.json_array_subquery_view AS
+ SELECT JSON_ARRAY( SELECT foo.i
+           FROM ( VALUES (1), (2), (NULL::integer), (4)) foo(i)
+          ORDER BY foo.i
+         LIMIT 3 RETURNING jsonb) AS "json_array"
+DROP VIEW json_array_subquery_view;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM (VALUES (1), (2), (NULL), (4)) t1(a)
+WHERE JSON_ARRAY(
+    SELECT b FROM (VALUES (1), (2), (3)) t2(b) WHERE b = t1.a
+    RETURNING jsonb
+) = '[]'::jsonb;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Values Scan on "*VALUES*"
+   Output: "*VALUES*".column1
+   Filter: (COALESCE((SubPlan expr_1), '[]'::jsonb) = '[]'::jsonb)
+   SubPlan expr_1
+     ->  Aggregate
+           Output: JSON_ARRAYAGG("*VALUES*_1".column1 RETURNING jsonb)
+           ->  Values Scan on "*VALUES*_1"
+                 Output: "*VALUES*_1".column1
+                 Filter: ("*VALUES*_1".column1 = "*VALUES*".column1)
+(9 rows)
+
+CREATE VIEW json_array_subquery_view AS
+SELECT * FROM (VALUES (1), (2), (NULL), (4)) t1(a)
+WHERE JSON_ARRAY(
+    SELECT b FROM (VALUES (1), (2), (3)) t2(b) WHERE b = t1.a
+    RETURNING jsonb
+) = '[]'::jsonb;
+\sv json_array_subquery_view
+CREATE OR REPLACE VIEW public.json_array_subquery_view AS
+ SELECT a
+   FROM ( VALUES (1), (2), (NULL::integer), (4)) t1(a)
+  WHERE JSON_ARRAY( SELECT t2.b
+           FROM ( VALUES (1), (2), (3)) t2(b)
+          WHERE t2.b = t1.a RETURNING jsonb) = '[]'::jsonb
+DROP VIEW json_array_subquery_view;
+-- JSON_ARRAY(subquery) with RETURNING text
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING text);
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Result
+   Output: COALESCE((InitPlan expr_1).col1, '[]'::text)
+   InitPlan expr_1
+     ->  Aggregate
+           Output: JSON_ARRAYAGG("*VALUES*".column1 RETURNING text)
+           ->  Values Scan on "*VALUES*"
+                 Output: "*VALUES*".column1
+(7 rows)
+
+CREATE VIEW json_array_subquery_view AS
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING text);
+\sv json_array_subquery_view
+CREATE OR REPLACE VIEW public.json_array_subquery_view AS
+ SELECT JSON_ARRAY( SELECT foo.i
+           FROM ( VALUES (1), (2), (NULL::integer), (4)) foo(i) RETURNING text) AS "json_array"
 DROP VIEW json_array_subquery_view;
 -- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT
 create type comp1 as (a int, b date);
index 5b2c46615566f90a11a3485d0d7835353a297178..00ecf6161bfbbcb237b44a4a0f9ed32764d67b75 100644 (file)
@@ -193,6 +193,7 @@ SELECT JSON_ARRAY(JSON_ARRAY('{ "a" : 123 }' RETURNING text));
 SELECT JSON_ARRAY(JSON_ARRAY('{ "a" : 123 }' FORMAT JSON RETURNING text));
 SELECT JSON_ARRAY(JSON_ARRAY('{ "a" : 123 }' FORMAT JSON RETURNING text) FORMAT JSON);
 
+-- JSON_ARRAY(subquery)
 SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i));
 SELECT JSON_ARRAY(SELECT i FROM (VALUES (NULL::int[]), ('{1,2}'), (NULL), (NULL), ('{3,4}'), (NULL)) foo(i));
 SELECT JSON_ARRAY(SELECT i FROM (VALUES (NULL::int[]), ('{1,2}'), (NULL), (NULL), ('{3,4}'), (NULL)) foo(i) RETURNING jsonb);
@@ -201,6 +202,17 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (NULL::int[]), ('{1,2}'), (NULL), (NULL)
 SELECT JSON_ARRAY(SELECT i FROM (VALUES (3), (1), (NULL), (2)) foo(i) ORDER BY i);
 SELECT JSON_ARRAY(WITH x AS (SELECT 1) VALUES (TRUE));
 
+-- JSON_ARRAY(subquery) with empty result set
+SELECT JSON_ARRAY(SELECT 1 WHERE FALSE);
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) WHERE i > 4);
+
+-- JSON_ARRAY(subquery) with a correlated subquery in the WHERE clause
+SELECT * FROM (VALUES (1), (2), (NULL), (4)) t1(a)
+WHERE JSON_ARRAY(
+    SELECT b FROM (VALUES (1), (2), (3)) t2(b) WHERE b = t1.a
+    RETURNING jsonb
+) = '[]'::jsonb;
+
 -- Should fail
 SELECT JSON_ARRAY(SELECT FROM (VALUES (1)) foo(i));
 SELECT JSON_ARRAY(SELECT i, i FROM (VALUES (1)) foo(i));
@@ -384,6 +396,43 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING
 
 \sv json_array_subquery_view
 
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) ORDER BY i LIMIT 3 RETURNING jsonb);
+
+CREATE OR REPLACE VIEW json_array_subquery_view AS
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) ORDER BY i LIMIT 3 RETURNING jsonb);
+
+\sv json_array_subquery_view
+
+DROP VIEW json_array_subquery_view;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM (VALUES (1), (2), (NULL), (4)) t1(a)
+WHERE JSON_ARRAY(
+    SELECT b FROM (VALUES (1), (2), (3)) t2(b) WHERE b = t1.a
+    RETURNING jsonb
+) = '[]'::jsonb;
+
+CREATE VIEW json_array_subquery_view AS
+SELECT * FROM (VALUES (1), (2), (NULL), (4)) t1(a)
+WHERE JSON_ARRAY(
+    SELECT b FROM (VALUES (1), (2), (3)) t2(b) WHERE b = t1.a
+    RETURNING jsonb
+) = '[]'::jsonb;
+
+\sv json_array_subquery_view
+
+DROP VIEW json_array_subquery_view;
+
+-- JSON_ARRAY(subquery) with RETURNING text
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING text);
+
+CREATE VIEW json_array_subquery_view AS
+SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING text);
+
+\sv json_array_subquery_view
+
 DROP VIEW json_array_subquery_view;
 
 -- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT