diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml index 4cd338fe6e3..3d97e2b5375 100644 --- a/doc/src/sgml/func/func-json.sgml +++ b/doc/src/sgml/func/func-json.sgml @@ -620,7 +620,8 @@ which must be a SELECT query returning a single column. If ABSENT ON NULL is specified, NULL values are ignored. This is always the case if a - query_expression is used. + query_expression is used. If the query returns + no rows, an empty JSON array is returned. json_array(1,true,json '{"a":null}') diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7edbd5b7225..f968ac68314 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -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 */ diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index fcf6d7fff2a..cd86311bb0b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -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: diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index f535f3b9351..c3c7aa29720 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -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; } /* diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index c781cdc84d3..75b77bb39f1 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -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) { diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 1602962dbe1..8d5924a32f2 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202604061 +#define CATALOG_VERSION_NO 202605011 #endif diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfc946c20b..7977ee24783 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -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; diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index f3be69838bf..a14936a4e6e 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -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); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 5b2c4661556..00ecf6161bf 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -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