agora inbox for [email protected]
help / color / mirror / Atom feed[PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely
26+ messages / 9 participants
[nested] [flat]
* [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely
@ 2019-02-20 11:11 Etsuro Fujita <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Etsuro Fujita @ 2019-02-20 11:11 UTC (permalink / raw)
---
contrib/postgres_fdw/deparse.c | 28 +-
.../postgres_fdw/expected/postgres_fdw.out | 182 ++++-----
contrib/postgres_fdw/postgres_fdw.c | 363 +++++++++++++++++-
contrib/postgres_fdw/postgres_fdw.h | 9 +-
4 files changed, 447 insertions(+), 135 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 92a0ab6da5..97dd07bee8 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -167,7 +167,8 @@ static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
deparse_expr_cxt *context);
static void deparseLockingClause(deparse_expr_cxt *context);
-static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
+static void appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context);
static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
@@ -929,8 +930,8 @@ build_tlist_to_deparse(RelOptInfo *foreignrel)
void
deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
List *tlist, List *remote_conds, List *pathkeys,
- bool is_subquery, List **retrieved_attrs,
- List **params_list)
+ bool has_final_sort, bool is_subquery,
+ List **retrieved_attrs, List **params_list)
{
deparse_expr_cxt context;
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
@@ -985,7 +986,7 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
/* Add ORDER BY clause if we found any useful pathkeys */
if (pathkeys)
- appendOrderByClause(pathkeys, &context);
+ appendOrderByClause(pathkeys, has_final_sort, &context);
/* Add any necessary FOR UPDATE/SHARE. */
deparseLockingClause(&context);
@@ -1590,7 +1591,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse the subquery representing the relation. */
appendStringInfoChar(buf, '(');
deparseSelectStmtForRel(buf, root, foreignrel, NIL,
- fpinfo->remote_conds, NIL, true,
+ fpinfo->remote_conds, NIL, false, true,
&retrieved_attrs, params_list);
appendStringInfoChar(buf, ')');
@@ -3109,7 +3110,8 @@ appendGroupByClause(List *tlist, deparse_expr_cxt *context)
* base relation are obtained and deparsed.
*/
static void
-appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
+appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context)
{
ListCell *lcell;
int nestlevel;
@@ -3126,7 +3128,19 @@ appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
PathKey *pathkey = lfirst(lcell);
Expr *em_expr;
- em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+ if (has_final_sort)
+ {
+ /*
+ * By construction, context->foreignrel is the input relation to
+ * the final sort.
+ */
+ em_expr = find_em_expr_for_input_target(context->root,
+ pathkey->pk_eclass,
+ context->foreignrel->reltarget);
+ }
+ else
+ em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+
Assert(em_expr != NULL);
appendStringInfoString(buf, delim);
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 42108bd3d4..ee36acc850 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2552,18 +2552,13 @@ DROP ROLE regress_view_owner;
-- Simple aggregates
explain (verbose, costs off)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
- Result
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), ((sum(c1)) * ((random() <= '1'::double precision))::integer), c2
- -> Sort
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Sort Key: (count(ft1.c6)), (sum(ft1.c1))
- -> Foreign Scan
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7 ORDER BY count(c6) ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(4 rows)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
count | sum | avg | min | max | stddev | sum2
@@ -2621,16 +2616,13 @@ select sum(t1.c1), count(t2.c1) from ft1 t1 inner join ft2 t2 on (t1.c1 = t2.c1)
-- GROUP BY clause having expressions
explain (verbose, costs off)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Sort Key: ((ft1.c2 / 2))
- -> Foreign Scan
- Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1 ORDER BY (c2 / 2) ASC NULLS LAST
+(4 rows)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
?column? | ?column?
@@ -2645,18 +2637,15 @@ select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
-- Aggregates in subquery are pushed down.
explain (verbose, costs off)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
- QUERY PLAN
----------------------------------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------
Aggregate
Output: count(ft1.c2), sum(ft1.c2)
- -> Sort
+ -> Foreign Scan
Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Sort Key: ft1.c2, (sum(ft1.c1))
- -> Foreign Scan
- Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3 ORDER BY c2 ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(6 rows)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
count | sum
@@ -2712,16 +2701,13 @@ select c2 * (random() <= 1)::int as c2 from ft2 group by c2 * (random() <= 1)::i
-- GROUP BY clause in various forms, cardinal, alias and constant expression
explain (verbose, costs off)
select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (count(c2)), c2, 5, 7.0, 9
- Sort Key: ft1.c2
- -> Foreign Scan
- Output: (count(c2)), c2, 5, 7.0, 9
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT count(c2), c2, 5, 7.0, 9 FROM "S 1"."T 1" GROUP BY 2, 3, 5
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT count(c2), c2, 5, 7.0, 9 FROM "S 1"."T 1" GROUP BY 2, 3, 5 ORDER BY c2 ASC NULLS LAST
+(4 rows)
select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2;
w | x | y | z
@@ -2742,16 +2728,13 @@ select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2
-- Also, ORDER BY contains an aggregate function
explain (verbose, costs off)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
- QUERY PLAN
------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, c2, (sum(c1))
- Sort Key: (sum(ft1.c1))
- -> Foreign Scan
- Output: c2, c2, (sum(c1))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2 ORDER BY sum("C 1") ASC NULLS LAST
+(4 rows)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
c2 | c2
@@ -2764,16 +2747,13 @@ select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
-- Testing HAVING clause shippability
explain (verbose, costs off)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, (sum(c1))
- Sort Key: ft2.c2
- -> Foreign Scan
- Output: c2, (sum(c1))
- Relations: Aggregate on (public.ft2)
- Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800))
-(7 rows)
+ Relations: Aggregate on (public.ft2)
+ Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800)) ORDER BY c2 ASC NULLS LAST
+(4 rows)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
c2 | sum
@@ -2823,16 +2803,13 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
-- ORDER BY within aggregate, same column used to order
explain (verbose, costs off)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(c1 ORDER BY c1)), c2
- Sort Key: (array_agg(ft1.c1 ORDER BY ft1.c1))
- -> Foreign Scan
- Output: (array_agg(c1 ORDER BY c1)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2 ORDER BY array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
array_agg
@@ -2869,16 +2846,13 @@ select array_agg(c5 order by c1 desc) from ft2 where c2 = 6 and c1 < 50;
-- DISTINCT within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5)) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2890,16 +2864,13 @@ select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2
-- DISTINCT combined with ORDER BY within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2910,16 +2881,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2931,16 +2899,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4
-- FILTER within aggregate
explain (verbose, costs off)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Sort Key: (sum(ft1.c1) FILTER (WHERE ((ft1.c1 < 100) AND (ft1.c2 > 5))))
- -> Foreign Scan
- Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2 ORDER BY sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))) ASC NULLS LAST
+(4 rows)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
sum
@@ -3339,16 +3304,13 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
-- FULL join with IS NULL check in HAVING
explain (verbose, costs off)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Sort Key: (avg(t1.c1)), (sum(t2.c1))
- -> Foreign Scan
- Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL)))
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL))) ORDER BY avg(r1.c1) ASC NULLS LAST, sum(r2.c1) ASC NULLS LAST
+(4 rows)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
avg | sum
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6b96e7de0a..6174d2be07 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -246,6 +246,25 @@ typedef struct PgFdwAnalyzeState
MemoryContext temp_cxt; /* context for per-tuple temporary data */
} PgFdwAnalyzeState;
+/*
+ * This enum describes what's kept in the fdw_private list for a ForeignPath.
+ * We store:
+ *
+ * 1) Boolean flag showing if the remote query has the final sort
+ */
+enum FdwPathPrivateIndex
+{
+ /* has-final-sort flag (as an integer Value node) */
+ FdwPathPrivateHasFinalSort
+};
+
+/* Struct for extra information passed to estimate_path_cost_size */
+typedef struct
+{
+ PathTarget *target;
+ bool has_final_sort;
+} PgFdwPathExtraData;
+
/*
* Identify the attribute where data conversion fails.
*/
@@ -368,6 +387,7 @@ static void estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost);
static void get_remote_estimate(const char *sql,
@@ -376,6 +396,12 @@ static void get_remote_estimate(const char *sql,
int *width,
Cost *startup_cost,
Cost *total_cost);
+static void adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost);
static bool ec_member_matches_foreign(PlannerInfo *root, RelOptInfo *rel,
EquivalenceClass *ec, EquivalenceMember *em,
void *arg);
@@ -452,6 +478,9 @@ static void add_foreign_grouping_paths(PlannerInfo *root,
RelOptInfo *input_rel,
RelOptInfo *grouped_rel,
GroupPathExtraData *extra);
+static void add_foreign_ordered_paths(PlannerInfo *root,
+ RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel);
static void apply_server_options(PgFdwRelationInfo *fpinfo);
static void apply_table_options(PgFdwRelationInfo *fpinfo);
static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
@@ -637,7 +666,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* values in fpinfo so we don't need to do it again to generate the
* basic foreign path.
*/
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
@@ -668,7 +697,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
set_baserel_size_estimates(root, baserel);
/* Fill in basically-bogus cost estimates for use later. */
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
}
@@ -1102,7 +1131,7 @@ postgresGetForeignPaths(PlannerInfo *root,
/* Get a cost estimate from the remote */
estimate_path_cost_size(root, baserel,
- param_info->ppi_clauses, NIL,
+ param_info->ppi_clauses, NIL, NULL,
&rows, &width,
&startup_cost, &total_cost);
@@ -1149,8 +1178,16 @@ postgresGetForeignPlan(PlannerInfo *root,
List *fdw_recheck_quals = NIL;
List *retrieved_attrs;
StringInfoData sql;
+ bool has_final_sort = false;
ListCell *lc;
+ /*
+ * Get private info created by postgresGetForeignUpperPaths, if any.
+ */
+ if (best_path->fdw_private)
+ has_final_sort = intVal(list_nth(best_path->fdw_private,
+ FdwPathPrivateHasFinalSort));
+
if (IS_SIMPLE_REL(foreignrel))
{
/*
@@ -1299,7 +1336,8 @@ postgresGetForeignPlan(PlannerInfo *root,
initStringInfo(&sql);
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
remote_exprs, best_path->path.pathkeys,
- false, &retrieved_attrs, ¶ms_list);
+ has_final_sort, false,
+ &retrieved_attrs, ¶ms_list);
/* Remember remote_exprs for possible use by postgresPlanDirectModify */
fpinfo->final_remote_exprs = remote_exprs;
@@ -2482,6 +2520,7 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es)
*
* param_join_conds are the parameterization clauses with outer relations.
* pathkeys specify the expected sort order if any for given path being costed.
+ * fpextra specifies post-scan/join processing steps such as the final sort.
*
* The function returns the cost and size estimates in p_row, p_width,
* p_startup_cost and p_total_cost variables.
@@ -2491,6 +2530,7 @@ estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost)
{
@@ -2555,8 +2595,9 @@ estimate_path_cost_size(PlannerInfo *root,
initStringInfo(&sql);
appendStringInfoString(&sql, "EXPLAIN ");
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
- remote_conds, pathkeys, false,
- &retrieved_attrs, NULL);
+ remote_conds, pathkeys,
+ fpextra ? fpextra->has_final_sort : false,
+ false, &retrieved_attrs, NULL);
/* Get the remote estimate */
conn = GetConnection(fpinfo->user, false);
@@ -2833,6 +2874,23 @@ estimate_path_cost_size(PlannerInfo *root,
run_cost += foreignrel->reltarget->cost.per_tuple * rows;
}
+ /*
+ * If this is an UPPERREL_ORDERED step performed on the final
+ * scan/join relation, the costs obtained from the cache wouldn't yet
+ * contain the eval costs for the final scan/join target, which would
+ * have been updated by apply_scanjoin_target_to_paths(); add the eval
+ * costs now.
+ */
+ if (fpextra && !IS_UPPER_REL(foreignrel))
+ {
+ /* The costs should have been obtained from the cache. */
+ Assert(fpinfo->rel_startup_cost >= 0 &&
+ fpinfo->rel_total_cost >= 0);
+
+ startup_cost += foreignrel->reltarget->cost.startup;
+ run_cost += foreignrel->reltarget->cost.per_tuple * rows;
+ }
+
/*
* Without remote estimates, we have no real way to estimate the cost
* of generating sorted output. It could be free if the query plan
@@ -2844,13 +2902,39 @@ estimate_path_cost_size(PlannerInfo *root,
*/
if (pathkeys != NIL)
{
- startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
- run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ if (IS_UPPER_REL(foreignrel))
+ {
+ Assert(fpinfo->stage == UPPERREL_GROUP_AGG);
+ adjust_foreign_grouping_path_cost(root, pathkeys,
+ retrieved_rows, width,
+ &startup_cost, &run_cost);
+ }
+ else
+ {
+ startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ }
}
total_cost = startup_cost + run_cost;
}
+ /*
+ * If this includes an UPPERREL_ORDERED step, the given target, which
+ * would be the final target to be applied to the resulting path, might
+ * have different expressions from the underlying relation's reltarget
+ * (see make_sort_input_target()); adjust tlist eval costs.
+ */
+ if (fpextra && fpextra->target != foreignrel->reltarget)
+ {
+ QualCost oldcost = foreignrel->reltarget->cost;
+ QualCost newcost = fpextra->target->cost;
+
+ startup_cost += newcost.startup - oldcost.startup;
+ total_cost += newcost.startup - oldcost.startup;
+ total_cost += (newcost.per_tuple - oldcost.per_tuple) * rows;
+ }
+
/*
* Cache the costs for scans without any pathkeys or parameterization
* before adding the costs for transferring data from the foreign server.
@@ -2860,7 +2944,7 @@ estimate_path_cost_size(PlannerInfo *root,
* foreign server. This function will be called at least once for every
* foreign relation without pathkeys and parameterization.
*/
- if (pathkeys == NIL && param_join_conds == NIL)
+ if (pathkeys == NIL && param_join_conds == NIL && fpextra == NULL)
{
fpinfo->rel_startup_cost = startup_cost;
fpinfo->rel_total_cost = total_cost;
@@ -2935,6 +3019,57 @@ get_remote_estimate(const char *sql, PGconn *conn,
PG_END_TRY();
}
+/*
+ * Adjust the cost estimates of a foreign grouping path.
+ */
+static void
+adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost)
+{
+ /*
+ * If the GROUP BY clause isn't sort-able, the plan chosen by the remote
+ * side is unlikely to generate properly-sorted output, so it would need
+ * an explicit sort; adjust the given costs with cost_sort(). Likewise,
+ * if the GROUP BY clause is sort-able but isn't a superset of the given
+ * pathkeys, adjust the costs with that function. Otherwise, adjust the
+ * costs by applying the same heuristic as for the scan/join case.
+ */
+ if (!grouping_is_sortable(root->parse->groupClause) ||
+ !pathkeys_contained_in(pathkeys, root->group_pathkeys))
+ {
+ Path sort_path; /* dummy for result of cost_sort */
+
+ cost_sort(&sort_path,
+ root,
+ pathkeys,
+ *p_startup_cost + *p_run_cost,
+ retrieved_rows,
+ width,
+ 0.0,
+ work_mem,
+ -1.0);
+
+ *p_startup_cost = sort_path.startup_cost;
+ *p_run_cost = sort_path.total_cost - sort_path.startup_cost;
+ }
+ else
+ {
+ /*
+ * The default cost seems too large for foreign-grouping cases; add
+ * 1/4th of the default.
+ */
+ double sort_multiplier = 1.0 + (DEFAULT_FDW_SORT_MULTIPLIER
+ - 1.0) * 0.25;
+
+ *p_startup_cost *= sort_multiplier;
+ *p_run_cost *= sort_multiplier;
+ }
+}
+
/*
* Detect whether we want to process an EquivalenceClass member.
*
@@ -4930,7 +5065,7 @@ add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
List *useful_pathkeys = lfirst(lc);
Path *sorted_epq_path;
- estimate_path_cost_size(root, rel, NIL, useful_pathkeys,
+ estimate_path_cost_size(root, rel, NIL, useful_pathkeys, NULL,
&rows, &width, &startup_cost, &total_cost);
/*
@@ -5181,8 +5316,8 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
extra->sjinfo);
/* Estimate costs for bare join relation */
- estimate_path_cost_size(root, joinrel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, joinrel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the joinrel */
joinrel->rows = rows;
joinrel->reltarget->width = width;
@@ -5451,15 +5586,29 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
return;
/* Ignore stages we don't support; and skip any duplicate calls. */
- if (stage != UPPERREL_GROUP_AGG || output_rel->fdw_private)
+ if ((stage != UPPERREL_GROUP_AGG &&
+ stage != UPPERREL_ORDERED) ||
+ output_rel->fdw_private)
return;
fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
+ fpinfo->stage = stage;
fpinfo->pushdown_safe = false;
output_rel->fdw_private = fpinfo;
- add_foreign_grouping_paths(root, input_rel, output_rel,
- (GroupPathExtraData *) extra);
+ switch (stage)
+ {
+ case UPPERREL_GROUP_AGG:
+ add_foreign_grouping_paths(root, input_rel, output_rel,
+ (GroupPathExtraData *) extra);
+ break;
+ case UPPERREL_ORDERED:
+ add_foreign_ordered_paths(root, input_rel, output_rel);
+ break;
+ default:
+ elog(ERROR, "unexpected upper relation: %d", (int) stage);
+ break;
+ }
}
/*
@@ -5529,8 +5678,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
cost_qual_eval(&fpinfo->local_conds_cost, fpinfo->local_conds, root);
/* Estimate the cost of push down */
- estimate_path_cost_size(root, grouped_rel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, grouped_rel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the fpinfo */
fpinfo->rows = rows;
@@ -5553,6 +5702,124 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
add_path(grouped_rel, (Path *) grouppath);
}
+/*
+ * add_foreign_ordered_paths
+ * Add foreign paths for performing the final sort remotely.
+ *
+ * Given input_rel contains the source-data Paths. The paths are added to the
+ * given ordered_rel.
+ */
+static void
+add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel)
+{
+ Query *parse = root->parse;
+ PgFdwRelationInfo *ifpinfo = input_rel->fdw_private;
+ PgFdwRelationInfo *fpinfo = ordered_rel->fdw_private;
+ PgFdwPathExtraData *fpextra;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+ ForeignPath *ordered_path;
+ ListCell *lc;
+
+ /* Shouldn't get here unless the query has ORDER BY */
+ Assert(parse->sortClause);
+
+ /* Save the input_rel as outerrel in fpinfo */
+ fpinfo->outerrel = input_rel;
+
+ /*
+ * Copy foreign table, foreign server, user mapping, FDW options etc.
+ * details from the input relation's fpinfo.
+ */
+ fpinfo->table = ifpinfo->table;
+ fpinfo->server = ifpinfo->server;
+ fpinfo->user = ifpinfo->user;
+ merge_fdw_options(fpinfo, ifpinfo, NULL);
+
+ /*
+ * For now we don't support cases where there are any SRFs in the tlist
+ */
+ if (parse->hasTargetSRFs)
+ return;
+
+ /* Assess if it is safe to push down the final sort */
+ foreach(lc, root->sort_pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
+ Expr *sort_expr;
+
+ /*
+ * is_foreign_expr would detect volatile expressions as well, but
+ * checking ec_has_volatile here saves some cycles.
+ */
+ if (pathkey_ec->ec_has_volatile)
+ return;
+
+ /* Get the sort expression for the pathkey_ec */
+ sort_expr = find_em_expr_for_input_target(root,
+ pathkey_ec,
+ input_rel->reltarget);
+
+ /* If it's unsafe to remote, we cannot push down the final sort */
+ if (!is_foreign_expr(root, input_rel, sort_expr))
+ return;
+ }
+
+ /* Safe to pushdown */
+ fpinfo->pushdown_safe = true;
+
+ /* No work if the core code already generated pre-sorted ForeignPaths */
+ foreach(lc, ordered_rel->pathlist)
+ {
+ Path *path = (Path *) lfirst(lc);
+
+ if (IsA(path, ForeignPath))
+ {
+ Assert(pathkeys_contained_in(root->sort_pathkeys,
+ path->pathkeys));
+ return;
+ }
+ }
+
+ /* Initialize the selectivity and cost of local_conds */
+ fpinfo->local_conds_sel = 1.0;
+ fpinfo->local_conds_cost.startup = 0.0;
+ fpinfo->local_conds_cost.per_tuple = 0.0;
+
+ fpextra = (PgFdwPathExtraData *) palloc0(sizeof(PgFdwPathExtraData));
+ fpextra->target = root->upper_targets[UPPERREL_ORDERED];
+ fpextra->has_final_sort = true;
+
+ /* Estimate the cost of performing the final sort remotely */
+ estimate_path_cost_size(root, input_rel, NIL, root->sort_pathkeys, fpextra,
+ &rows, &width, &startup_cost, &total_cost);
+
+ /*
+ * Build the fdw_private list that will be used by postgresGetForeignPlan.
+ * Items in the list must match order in enum FdwPathPrivateIndex.
+ */
+ fdw_private = list_make1(makeInteger(true));
+
+ /* Create foreign ordering ForeignPath */
+ ordered_path = create_foreign_upper_path(root,
+ input_rel,
+ root->upper_targets[UPPERREL_ORDERED],
+ rows,
+ startup_cost,
+ total_cost,
+ root->sort_pathkeys,
+ NULL, /* no extra plan */
+ fdw_private);
+
+ /* And add it to the ordered rel */
+ add_path(ordered_rel, (Path *) ordered_path);
+}
+
/*
* Create a tuple from the specified row of the PGresult.
*
@@ -5803,3 +6070,65 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
/* We didn't find any suitable equivalence class expression */
return NULL;
}
+
+/*
+ * Find an equivalence class member expression to be computed as a sort column
+ * in the given target.
+ */
+Expr *
+find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target)
+{
+ ListCell *lc1;
+ int i;
+
+ i = 0;
+ foreach(lc1, target->exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc1);
+ Index sgref = get_pathtarget_sortgroupref(target, i);
+ ListCell *lc2;
+
+ /* Ignore non-sort expressions */
+ if (sgref == 0 ||
+ get_sortgroupref_clause_noerr(sgref,
+ root->parse->sortClause) == NULL)
+ {
+ i++;
+ continue;
+ }
+
+ /* We ignore binary-compatible relabeling on both ends */
+ while (expr && IsA(expr, RelabelType))
+ expr = ((RelabelType *) expr)->arg;
+
+ /* Locate an EquivalenceClass member matching this expr, if any */
+ foreach(lc2, ec->ec_members)
+ {
+ EquivalenceMember *em = (EquivalenceMember *) lfirst(lc2);
+ Expr *em_expr;
+
+ /* Don't match constants */
+ if (em->em_is_const)
+ continue;
+
+ /* Ignore child members */
+ if (em->em_is_child)
+ continue;
+
+ /* Match if same expression (after stripping relabel) */
+ em_expr = em->em_expr;
+ while (em_expr && IsA(em_expr, RelabelType))
+ em_expr = ((RelabelType *) em_expr)->arg;
+
+ if (equal(em_expr, expr))
+ return em->em_expr;
+ }
+
+ i++;
+ }
+
+ elog(ERROR, "could not find pathkey item to sort");
+ return NULL; /* keep compiler quiet */
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 3f50103285..936e3f498a 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -92,6 +92,9 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /* Upper relation information */
+ UpperRelationKind stage;
+
/* Grouping information */
List *grouped_tlist;
@@ -175,10 +178,14 @@ extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
List **retrieved_attrs);
extern void deparseStringLiteral(StringInfo buf, const char *val);
extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
+extern Expr *find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target);
extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, List *tlist,
- List *remote_conds, List *pathkeys, bool is_subquery,
+ List *remote_conds, List *pathkeys,
+ bool has_final_sort, bool is_subquery,
List **retrieved_attrs, List **params_list);
extern const char *get_jointype_name(JoinType jointype);
--
2.19.2
--------------080909020109040609010109
Content-Type: text/x-patch;
name="0002-Refactor-create_limit_path-to-share-cost-adjustment-v4.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename*0="0002-Refactor-create_limit_path-to-share-cost-adjustment-v4.";
filename*1="patch"
^ permalink raw reply [nested|flat] 26+ messages in thread
* [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely
@ 2019-03-06 11:27 Etsuro Fujita <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Etsuro Fujita @ 2019-03-06 11:27 UTC (permalink / raw)
---
contrib/postgres_fdw/deparse.c | 28 +-
.../postgres_fdw/expected/postgres_fdw.out | 167 +++-----
contrib/postgres_fdw/postgres_fdw.c | 389 +++++++++++++++++-
contrib/postgres_fdw/postgres_fdw.h | 9 +-
4 files changed, 467 insertions(+), 126 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 92a0ab6da5..97dd07bee8 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -167,7 +167,8 @@ static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
deparse_expr_cxt *context);
static void deparseLockingClause(deparse_expr_cxt *context);
-static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
+static void appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context);
static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
@@ -929,8 +930,8 @@ build_tlist_to_deparse(RelOptInfo *foreignrel)
void
deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
List *tlist, List *remote_conds, List *pathkeys,
- bool is_subquery, List **retrieved_attrs,
- List **params_list)
+ bool has_final_sort, bool is_subquery,
+ List **retrieved_attrs, List **params_list)
{
deparse_expr_cxt context;
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
@@ -985,7 +986,7 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
/* Add ORDER BY clause if we found any useful pathkeys */
if (pathkeys)
- appendOrderByClause(pathkeys, &context);
+ appendOrderByClause(pathkeys, has_final_sort, &context);
/* Add any necessary FOR UPDATE/SHARE. */
deparseLockingClause(&context);
@@ -1590,7 +1591,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse the subquery representing the relation. */
appendStringInfoChar(buf, '(');
deparseSelectStmtForRel(buf, root, foreignrel, NIL,
- fpinfo->remote_conds, NIL, true,
+ fpinfo->remote_conds, NIL, false, true,
&retrieved_attrs, params_list);
appendStringInfoChar(buf, ')');
@@ -3109,7 +3110,8 @@ appendGroupByClause(List *tlist, deparse_expr_cxt *context)
* base relation are obtained and deparsed.
*/
static void
-appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
+appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context)
{
ListCell *lcell;
int nestlevel;
@@ -3126,7 +3128,19 @@ appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
PathKey *pathkey = lfirst(lcell);
Expr *em_expr;
- em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+ if (has_final_sort)
+ {
+ /*
+ * By construction, context->foreignrel is the input relation to
+ * the final sort.
+ */
+ em_expr = find_em_expr_for_input_target(context->root,
+ pathkey->pk_eclass,
+ context->foreignrel->reltarget);
+ }
+ else
+ em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+
Assert(em_expr != NULL);
appendStringInfoString(buf, delim);
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 42108bd3d4..592dd3e05f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2552,18 +2552,13 @@ DROP ROLE regress_view_owner;
-- Simple aggregates
explain (verbose, costs off)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
- Result
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), ((sum(c1)) * ((random() <= '1'::double precision))::integer), c2
- -> Sort
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Sort Key: (count(ft1.c6)), (sum(ft1.c1))
- -> Foreign Scan
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7 ORDER BY count(c6) ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(4 rows)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
count | sum | avg | min | max | stddev | sum2
@@ -2621,16 +2616,13 @@ select sum(t1.c1), count(t2.c1) from ft1 t1 inner join ft2 t2 on (t1.c1 = t2.c1)
-- GROUP BY clause having expressions
explain (verbose, costs off)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Sort Key: ((ft1.c2 / 2))
- -> Foreign Scan
- Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1 ORDER BY (c2 / 2) ASC NULLS LAST
+(4 rows)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
?column? | ?column?
@@ -2645,18 +2637,15 @@ select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
-- Aggregates in subquery are pushed down.
explain (verbose, costs off)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
- QUERY PLAN
----------------------------------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------
Aggregate
Output: count(ft1.c2), sum(ft1.c2)
- -> Sort
+ -> Foreign Scan
Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Sort Key: ft1.c2, (sum(ft1.c1))
- -> Foreign Scan
- Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3 ORDER BY c2 ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(6 rows)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
count | sum
@@ -2742,16 +2731,13 @@ select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2
-- Also, ORDER BY contains an aggregate function
explain (verbose, costs off)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
- QUERY PLAN
------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, c2, (sum(c1))
- Sort Key: (sum(ft1.c1))
- -> Foreign Scan
- Output: c2, c2, (sum(c1))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2 ORDER BY sum("C 1") ASC NULLS LAST
+(4 rows)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
c2 | c2
@@ -2764,16 +2750,13 @@ select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
-- Testing HAVING clause shippability
explain (verbose, costs off)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, (sum(c1))
- Sort Key: ft2.c2
- -> Foreign Scan
- Output: c2, (sum(c1))
- Relations: Aggregate on (public.ft2)
- Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800))
-(7 rows)
+ Relations: Aggregate on (public.ft2)
+ Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800)) ORDER BY c2 ASC NULLS LAST
+(4 rows)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
c2 | sum
@@ -2823,16 +2806,13 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
-- ORDER BY within aggregate, same column used to order
explain (verbose, costs off)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(c1 ORDER BY c1)), c2
- Sort Key: (array_agg(ft1.c1 ORDER BY ft1.c1))
- -> Foreign Scan
- Output: (array_agg(c1 ORDER BY c1)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2 ORDER BY array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
array_agg
@@ -2869,16 +2849,13 @@ select array_agg(c5 order by c1 desc) from ft2 where c2 = 6 and c1 < 50;
-- DISTINCT within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5)) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2890,16 +2867,13 @@ select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2
-- DISTINCT combined with ORDER BY within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2910,16 +2884,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2931,16 +2902,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4
-- FILTER within aggregate
explain (verbose, costs off)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Sort Key: (sum(ft1.c1) FILTER (WHERE ((ft1.c1 < 100) AND (ft1.c2 > 5))))
- -> Foreign Scan
- Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2 ORDER BY sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))) ASC NULLS LAST
+(4 rows)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
sum
@@ -3339,16 +3307,13 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
-- FULL join with IS NULL check in HAVING
explain (verbose, costs off)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Sort Key: (avg(t1.c1)), (sum(t2.c1))
- -> Foreign Scan
- Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL)))
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL))) ORDER BY avg(r1.c1) ASC NULLS LAST, sum(r2.c1) ASC NULLS LAST
+(4 rows)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
avg | sum
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f387fac42..e22472ae3d 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -246,6 +246,25 @@ typedef struct PgFdwAnalyzeState
MemoryContext temp_cxt; /* context for per-tuple temporary data */
} PgFdwAnalyzeState;
+/*
+ * This enum describes what's kept in the fdw_private list for a ForeignPath.
+ * We store:
+ *
+ * 1) Boolean flag showing if the remote query has the final sort
+ */
+enum FdwPathPrivateIndex
+{
+ /* has-final-sort flag (as an integer Value node) */
+ FdwPathPrivateHasFinalSort
+};
+
+/* Struct for extra information passed to estimate_path_cost_size */
+typedef struct
+{
+ PathTarget *target;
+ bool has_final_sort;
+} PgFdwPathExtraData;
+
/*
* Identify the attribute where data conversion fails.
*/
@@ -368,6 +387,7 @@ static void estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost);
static void get_remote_estimate(const char *sql,
@@ -376,6 +396,12 @@ static void get_remote_estimate(const char *sql,
int *width,
Cost *startup_cost,
Cost *total_cost);
+static void adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost);
static bool ec_member_matches_foreign(PlannerInfo *root, RelOptInfo *rel,
EquivalenceClass *ec, EquivalenceMember *em,
void *arg);
@@ -452,6 +478,9 @@ static void add_foreign_grouping_paths(PlannerInfo *root,
RelOptInfo *input_rel,
RelOptInfo *grouped_rel,
GroupPathExtraData *extra);
+static void add_foreign_ordered_paths(PlannerInfo *root,
+ RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel);
static void apply_server_options(PgFdwRelationInfo *fpinfo);
static void apply_table_options(PgFdwRelationInfo *fpinfo);
static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
@@ -637,7 +666,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* values in fpinfo so we don't need to do it again to generate the
* basic foreign path.
*/
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
@@ -668,7 +697,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
set_baserel_size_estimates(root, baserel);
/* Fill in basically-bogus cost estimates for use later. */
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
}
@@ -1102,7 +1131,7 @@ postgresGetForeignPaths(PlannerInfo *root,
/* Get a cost estimate from the remote */
estimate_path_cost_size(root, baserel,
- param_info->ppi_clauses, NIL,
+ param_info->ppi_clauses, NIL, NULL,
&rows, &width,
&startup_cost, &total_cost);
@@ -1149,8 +1178,16 @@ postgresGetForeignPlan(PlannerInfo *root,
List *fdw_recheck_quals = NIL;
List *retrieved_attrs;
StringInfoData sql;
+ bool has_final_sort = false;
ListCell *lc;
+ /*
+ * Get private info created by postgresGetForeignUpperPaths, if any.
+ */
+ if (best_path->fdw_private)
+ has_final_sort = intVal(list_nth(best_path->fdw_private,
+ FdwPathPrivateHasFinalSort));
+
if (IS_SIMPLE_REL(foreignrel))
{
/*
@@ -1299,7 +1336,8 @@ postgresGetForeignPlan(PlannerInfo *root,
initStringInfo(&sql);
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
remote_exprs, best_path->path.pathkeys,
- false, &retrieved_attrs, ¶ms_list);
+ has_final_sort, false,
+ &retrieved_attrs, ¶ms_list);
/* Remember remote_exprs for possible use by postgresPlanDirectModify */
fpinfo->final_remote_exprs = remote_exprs;
@@ -2482,6 +2520,8 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es)
*
* param_join_conds are the parameterization clauses with outer relations.
* pathkeys specify the expected sort order if any for given path being costed.
+ * fpextra specifies additional post-scan/join processing steps such as the
+ * final sort.
*
* The function returns the cost and size estimates in p_row, p_width,
* p_startup_cost and p_total_cost variables.
@@ -2491,6 +2531,7 @@ estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost)
{
@@ -2555,8 +2596,9 @@ estimate_path_cost_size(PlannerInfo *root,
initStringInfo(&sql);
appendStringInfoString(&sql, "EXPLAIN ");
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
- remote_conds, pathkeys, false,
- &retrieved_attrs, NULL);
+ remote_conds, pathkeys,
+ fpextra ? fpextra->has_final_sort : false,
+ false, &retrieved_attrs, NULL);
/* Get the remote estimate */
conn = GetConnection(fpinfo->user, false);
@@ -2632,6 +2674,19 @@ estimate_path_cost_size(PlannerInfo *root,
{
startup_cost = fpinfo->rel_startup_cost;
run_cost = fpinfo->rel_total_cost - fpinfo->rel_startup_cost;
+
+ /*
+ * When we're called from postgresGetForeignUpperPaths() with the
+ * UPPERREL_ORDERED stage, the costs of scanning a foreign base or
+ * join relation obtained from the cache wouldn't yet contain the
+ * eval costs for the final scan/join target that has been updated
+ * by apply_scanjoin_target_to_paths(); add the eval costs now.
+ */
+ if (fpextra && !IS_UPPER_REL(foreignrel))
+ {
+ startup_cost += foreignrel->reltarget->cost.startup;
+ run_cost += foreignrel->reltarget->cost.per_tuple * rows;
+ }
}
else if (IS_JOIN_REL(foreignrel))
{
@@ -2844,13 +2899,39 @@ estimate_path_cost_size(PlannerInfo *root,
*/
if (pathkeys != NIL)
{
- startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
- run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ if (IS_UPPER_REL(foreignrel))
+ {
+ Assert(fpinfo->stage == UPPERREL_GROUP_AGG);
+ adjust_foreign_grouping_path_cost(root, pathkeys,
+ retrieved_rows, width,
+ &startup_cost, &run_cost);
+ }
+ else
+ {
+ startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ }
}
total_cost = startup_cost + run_cost;
}
+ /*
+ * If this includes an UPPERREL_ORDERED step, the given target, which will
+ * be applied to the resulting path, might have different expressions from
+ * the relation's reltarget (see make_sort_input_target()); adjust tlist
+ * eval costs.
+ */
+ if (fpextra && fpextra->target != foreignrel->reltarget)
+ {
+ QualCost oldcost = foreignrel->reltarget->cost;
+ QualCost newcost = fpextra->target->cost;
+
+ startup_cost += newcost.startup - oldcost.startup;
+ total_cost += newcost.startup - oldcost.startup;
+ total_cost += (newcost.per_tuple - oldcost.per_tuple) * rows;
+ }
+
/*
* Cache the costs for scans without any pathkeys or parameterization
* before adding the costs for transferring data from the foreign server.
@@ -2860,7 +2941,7 @@ estimate_path_cost_size(PlannerInfo *root,
* foreign server. This function will be called at least once for every
* foreign relation without pathkeys and parameterization.
*/
- if (pathkeys == NIL && param_join_conds == NIL)
+ if (pathkeys == NIL && param_join_conds == NIL && fpextra == NULL)
{
fpinfo->rel_startup_cost = startup_cost;
fpinfo->rel_total_cost = total_cost;
@@ -2935,6 +3016,57 @@ get_remote_estimate(const char *sql, PGconn *conn,
PG_END_TRY();
}
+/*
+ * Adjust the cost estimates of a foreign grouping path.
+ */
+static void
+adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost)
+{
+ /*
+ * If the GROUP BY clause isn't sort-able, the plan chosen by the remote
+ * side is unlikely to generate properly-sorted output, so it would need
+ * an explicit sort; adjust the given costs with cost_sort(). Likewise,
+ * if the GROUP BY clause is sort-able but isn't a superset of the given
+ * pathkeys, adjust the costs with that function. Otherwise, adjust the
+ * costs by applying the same heuristic as for the scan/join case.
+ */
+ if (!grouping_is_sortable(root->parse->groupClause) ||
+ !pathkeys_contained_in(pathkeys, root->group_pathkeys))
+ {
+ Path sort_path; /* dummy for result of cost_sort */
+
+ cost_sort(&sort_path,
+ root,
+ pathkeys,
+ *p_startup_cost + *p_run_cost,
+ retrieved_rows,
+ width,
+ 0.0,
+ work_mem,
+ -1.0);
+
+ *p_startup_cost = sort_path.startup_cost;
+ *p_run_cost = sort_path.total_cost - sort_path.startup_cost;
+ }
+ else
+ {
+ /*
+ * The default cost seems too large for foreign-grouping cases; add
+ * 1/4th of the default.
+ */
+ double sort_multiplier = 1.0 + (DEFAULT_FDW_SORT_MULTIPLIER
+ - 1.0) * 0.25;
+
+ *p_startup_cost *= sort_multiplier;
+ *p_run_cost *= sort_multiplier;
+ }
+}
+
/*
* Detect whether we want to process an EquivalenceClass member.
*
@@ -4934,7 +5066,7 @@ add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
List *useful_pathkeys = lfirst(lc);
Path *sorted_epq_path;
- estimate_path_cost_size(root, rel, NIL, useful_pathkeys,
+ estimate_path_cost_size(root, rel, NIL, useful_pathkeys, NULL,
&rows, &width, &startup_cost, &total_cost);
/*
@@ -5185,8 +5317,8 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
extra->sjinfo);
/* Estimate costs for bare join relation */
- estimate_path_cost_size(root, joinrel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, joinrel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the joinrel */
joinrel->rows = rows;
joinrel->reltarget->width = width;
@@ -5455,15 +5587,29 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
return;
/* Ignore stages we don't support; and skip any duplicate calls. */
- if (stage != UPPERREL_GROUP_AGG || output_rel->fdw_private)
+ if ((stage != UPPERREL_GROUP_AGG &&
+ stage != UPPERREL_ORDERED) ||
+ output_rel->fdw_private)
return;
fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
+ fpinfo->stage = stage;
fpinfo->pushdown_safe = false;
output_rel->fdw_private = fpinfo;
- add_foreign_grouping_paths(root, input_rel, output_rel,
- (GroupPathExtraData *) extra);
+ switch (stage)
+ {
+ case UPPERREL_GROUP_AGG:
+ add_foreign_grouping_paths(root, input_rel, output_rel,
+ (GroupPathExtraData *) extra);
+ break;
+ case UPPERREL_ORDERED:
+ add_foreign_ordered_paths(root, input_rel, output_rel);
+ break;
+ default:
+ elog(ERROR, "unexpected upper relation: %d", (int) stage);
+ break;
+ }
}
/*
@@ -5533,8 +5679,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
cost_qual_eval(&fpinfo->local_conds_cost, fpinfo->local_conds, root);
/* Estimate the cost of push down */
- estimate_path_cost_size(root, grouped_rel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, grouped_rel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the fpinfo */
fpinfo->rows = rows;
@@ -5542,6 +5688,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
fpinfo->startup_cost = startup_cost;
fpinfo->total_cost = total_cost;
+ grouped_rel->rows = fpinfo->rows;
+
/* Create and add foreign path to the grouping relation. */
grouppath = create_foreign_upper_path(root,
grouped_rel,
@@ -5557,6 +5705,151 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
add_path(grouped_rel, (Path *) grouppath);
}
+/*
+ * add_foreign_ordered_paths
+ * Add foreign paths for performing the final sort remotely.
+ *
+ * Given input_rel contains the source-data Paths. The paths are added to the
+ * given ordered_rel.
+ */
+static void
+add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel)
+{
+ Query *parse = root->parse;
+ PgFdwRelationInfo *ifpinfo = input_rel->fdw_private;
+ PgFdwRelationInfo *fpinfo = ordered_rel->fdw_private;
+ PgFdwPathExtraData *fpextra;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+ ForeignPath *ordered_path;
+ ListCell *lc;
+
+ /* Shouldn't get here unless the query has ORDER BY */
+ Assert(parse->sortClause);
+
+ /* Save the input_rel as outerrel in fpinfo */
+ fpinfo->outerrel = input_rel;
+
+ /*
+ * Copy foreign table, foreign server, user mapping, FDW options etc.
+ * details from the input relation's fpinfo.
+ */
+ fpinfo->table = ifpinfo->table;
+ fpinfo->server = ifpinfo->server;
+ fpinfo->user = ifpinfo->user;
+ merge_fdw_options(fpinfo, ifpinfo, NULL);
+
+ /*
+ * No work if there is an properly-sorted ForeignPath in the ordered_rel's
+ * pathlist.
+ */
+ foreach(lc, ordered_rel->pathlist)
+ {
+ Path *path = (Path *) lfirst(lc);
+
+ /*
+ * apply_scanjoin_target_to_paths() might put ProjectionPath on top of
+ * ForeignPath; look through ProjectionPath and see if the path
+ * underneath it is ForeignPath.
+ */
+ if (IsA(path, ForeignPath) ||
+ (IsA(path, ProjectionPath) &&
+ IsA(((ProjectionPath *) path)->subpath, ForeignPath)))
+ {
+ Assert(pathkeys_contained_in(root->sort_pathkeys,
+ path->pathkeys));
+
+ /* Safe to push down */
+ fpinfo->pushdown_safe = true;
+ return;
+ }
+ }
+
+ /*
+ * No point in further work for the case where the input relation is the
+ * final scan/join relation because we would have already considered
+ * performing the final sort remotely before we get here.
+ */
+ if (!IS_UPPER_REL(input_rel))
+ return;
+
+ /*
+ * We try to create paths below by extending paths for the underlying
+ * base, join, or grouping relation to perform the final sort remotely,
+ * which is stored into the fdw_private list of the resulting paths.
+ */
+
+ /*
+ * For now we don't support cases where there are any SRFs in the tlist
+ */
+ if (parse->hasTargetSRFs)
+ return;
+
+ /* Assess if it is safe to push down the final sort */
+ foreach(lc, root->sort_pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
+ Expr *sort_expr;
+
+ /*
+ * is_foreign_expr would detect volatile expressions as well, but
+ * checking ec_has_volatile here saves some cycles.
+ */
+ if (pathkey_ec->ec_has_volatile)
+ return;
+
+ /* Get the sort expression for the pathkey_ec */
+ sort_expr = find_em_expr_for_input_target(root,
+ pathkey_ec,
+ input_rel->reltarget);
+
+ /* If it's unsafe to remote, we cannot push down the final sort */
+ if (!is_foreign_expr(root, input_rel, sort_expr))
+ return;
+ }
+
+ /* Safe to push down */
+ fpinfo->pushdown_safe = true;
+
+ /* Initialize the selectivity and cost of local_conds */
+ fpinfo->local_conds_sel = 1.0;
+ fpinfo->local_conds_cost.startup = 0.0;
+ fpinfo->local_conds_cost.per_tuple = 0.0;
+
+ fpextra = (PgFdwPathExtraData *) palloc0(sizeof(PgFdwPathExtraData));
+ fpextra->target = root->upper_targets[UPPERREL_ORDERED];
+ fpextra->has_final_sort = true;
+
+ /* Estimate the costs of performing the final sort remotely */
+ estimate_path_cost_size(root, input_rel, NIL, root->sort_pathkeys, fpextra,
+ &rows, &width, &startup_cost, &total_cost);
+
+ /*
+ * Build the fdw_private list that will be used by postgresGetForeignPlan.
+ * Items in the list must match order in enum FdwPathPrivateIndex.
+ */
+ fdw_private = list_make1(makeInteger(true));
+
+ /* Create foreign ordering ForeignPath */
+ ordered_path = create_foreign_upper_path(root,
+ input_rel,
+ root->upper_targets[UPPERREL_ORDERED],
+ rows,
+ startup_cost,
+ total_cost,
+ root->sort_pathkeys,
+ NULL, /* no extra plan */
+ fdw_private);
+
+ /* And add it to the ordered_rel */
+ add_path(ordered_rel, (Path *) ordered_path);
+}
+
/*
* Create a tuple from the specified row of the PGresult.
*
@@ -5807,3 +6100,65 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
/* We didn't find any suitable equivalence class expression */
return NULL;
}
+
+/*
+ * Find an equivalence class member expression to be computed as a sort column
+ * in the given target.
+ */
+Expr *
+find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target)
+{
+ ListCell *lc1;
+ int i;
+
+ i = 0;
+ foreach(lc1, target->exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc1);
+ Index sgref = get_pathtarget_sortgroupref(target, i);
+ ListCell *lc2;
+
+ /* Ignore non-sort expressions */
+ if (sgref == 0 ||
+ get_sortgroupref_clause_noerr(sgref,
+ root->parse->sortClause) == NULL)
+ {
+ i++;
+ continue;
+ }
+
+ /* We ignore binary-compatible relabeling on both ends */
+ while (expr && IsA(expr, RelabelType))
+ expr = ((RelabelType *) expr)->arg;
+
+ /* Locate an EquivalenceClass member matching this expr, if any */
+ foreach(lc2, ec->ec_members)
+ {
+ EquivalenceMember *em = (EquivalenceMember *) lfirst(lc2);
+ Expr *em_expr;
+
+ /* Don't match constants */
+ if (em->em_is_const)
+ continue;
+
+ /* Ignore child members */
+ if (em->em_is_child)
+ continue;
+
+ /* Match if same expression (after stripping relabel) */
+ em_expr = em->em_expr;
+ while (em_expr && IsA(em_expr, RelabelType))
+ em_expr = ((RelabelType *) em_expr)->arg;
+
+ if (equal(em_expr, expr))
+ return em->em_expr;
+ }
+
+ i++;
+ }
+
+ elog(ERROR, "could not find pathkey item to sort");
+ return NULL; /* keep compiler quiet */
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 3f50103285..936e3f498a 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -92,6 +92,9 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /* Upper relation information */
+ UpperRelationKind stage;
+
/* Grouping information */
List *grouped_tlist;
@@ -175,10 +178,14 @@ extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
List **retrieved_attrs);
extern void deparseStringLiteral(StringInfo buf, const char *val);
extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
+extern Expr *find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target);
extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, List *tlist,
- List *remote_conds, List *pathkeys, bool is_subquery,
+ List *remote_conds, List *pathkeys,
+ bool has_final_sort, bool is_subquery,
List **retrieved_attrs, List **params_list);
extern const char *get_jointype_name(JoinType jointype);
--
2.19.2
--------------060600050702040203040200
Content-Type: text/x-patch;
name="v5-0002-Refactor-create_limit_path-to-share-cost-adjustment-.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename*0="v5-0002-Refactor-create_limit_path-to-share-cost-adjustment-";
filename*1=".patch"
^ permalink raw reply [nested|flat] 26+ messages in thread
* [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely
@ 2019-03-07 09:27 Etsuro Fujita <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Etsuro Fujita @ 2019-03-07 09:27 UTC (permalink / raw)
---
contrib/postgres_fdw/deparse.c | 28 +-
.../postgres_fdw/expected/postgres_fdw.out | 167 +++-----
contrib/postgres_fdw/postgres_fdw.c | 382 +++++++++++++++++-
contrib/postgres_fdw/postgres_fdw.h | 9 +-
4 files changed, 460 insertions(+), 126 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 92a0ab6da5..97dd07bee8 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -167,7 +167,8 @@ static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
deparse_expr_cxt *context);
static void deparseLockingClause(deparse_expr_cxt *context);
-static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
+static void appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context);
static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
@@ -929,8 +930,8 @@ build_tlist_to_deparse(RelOptInfo *foreignrel)
void
deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
List *tlist, List *remote_conds, List *pathkeys,
- bool is_subquery, List **retrieved_attrs,
- List **params_list)
+ bool has_final_sort, bool is_subquery,
+ List **retrieved_attrs, List **params_list)
{
deparse_expr_cxt context;
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
@@ -985,7 +986,7 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
/* Add ORDER BY clause if we found any useful pathkeys */
if (pathkeys)
- appendOrderByClause(pathkeys, &context);
+ appendOrderByClause(pathkeys, has_final_sort, &context);
/* Add any necessary FOR UPDATE/SHARE. */
deparseLockingClause(&context);
@@ -1590,7 +1591,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse the subquery representing the relation. */
appendStringInfoChar(buf, '(');
deparseSelectStmtForRel(buf, root, foreignrel, NIL,
- fpinfo->remote_conds, NIL, true,
+ fpinfo->remote_conds, NIL, false, true,
&retrieved_attrs, params_list);
appendStringInfoChar(buf, ')');
@@ -3109,7 +3110,8 @@ appendGroupByClause(List *tlist, deparse_expr_cxt *context)
* base relation are obtained and deparsed.
*/
static void
-appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
+appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context)
{
ListCell *lcell;
int nestlevel;
@@ -3126,7 +3128,19 @@ appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
PathKey *pathkey = lfirst(lcell);
Expr *em_expr;
- em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+ if (has_final_sort)
+ {
+ /*
+ * By construction, context->foreignrel is the input relation to
+ * the final sort.
+ */
+ em_expr = find_em_expr_for_input_target(context->root,
+ pathkey->pk_eclass,
+ context->foreignrel->reltarget);
+ }
+ else
+ em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+
Assert(em_expr != NULL);
appendStringInfoString(buf, delim);
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 42108bd3d4..592dd3e05f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2552,18 +2552,13 @@ DROP ROLE regress_view_owner;
-- Simple aggregates
explain (verbose, costs off)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
- Result
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), ((sum(c1)) * ((random() <= '1'::double precision))::integer), c2
- -> Sort
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Sort Key: (count(ft1.c6)), (sum(ft1.c1))
- -> Foreign Scan
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7 ORDER BY count(c6) ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(4 rows)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
count | sum | avg | min | max | stddev | sum2
@@ -2621,16 +2616,13 @@ select sum(t1.c1), count(t2.c1) from ft1 t1 inner join ft2 t2 on (t1.c1 = t2.c1)
-- GROUP BY clause having expressions
explain (verbose, costs off)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Sort Key: ((ft1.c2 / 2))
- -> Foreign Scan
- Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1 ORDER BY (c2 / 2) ASC NULLS LAST
+(4 rows)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
?column? | ?column?
@@ -2645,18 +2637,15 @@ select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
-- Aggregates in subquery are pushed down.
explain (verbose, costs off)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
- QUERY PLAN
----------------------------------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------
Aggregate
Output: count(ft1.c2), sum(ft1.c2)
- -> Sort
+ -> Foreign Scan
Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Sort Key: ft1.c2, (sum(ft1.c1))
- -> Foreign Scan
- Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3 ORDER BY c2 ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(6 rows)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
count | sum
@@ -2742,16 +2731,13 @@ select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2
-- Also, ORDER BY contains an aggregate function
explain (verbose, costs off)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
- QUERY PLAN
------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, c2, (sum(c1))
- Sort Key: (sum(ft1.c1))
- -> Foreign Scan
- Output: c2, c2, (sum(c1))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2 ORDER BY sum("C 1") ASC NULLS LAST
+(4 rows)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
c2 | c2
@@ -2764,16 +2750,13 @@ select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
-- Testing HAVING clause shippability
explain (verbose, costs off)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, (sum(c1))
- Sort Key: ft2.c2
- -> Foreign Scan
- Output: c2, (sum(c1))
- Relations: Aggregate on (public.ft2)
- Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800))
-(7 rows)
+ Relations: Aggregate on (public.ft2)
+ Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800)) ORDER BY c2 ASC NULLS LAST
+(4 rows)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
c2 | sum
@@ -2823,16 +2806,13 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
-- ORDER BY within aggregate, same column used to order
explain (verbose, costs off)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(c1 ORDER BY c1)), c2
- Sort Key: (array_agg(ft1.c1 ORDER BY ft1.c1))
- -> Foreign Scan
- Output: (array_agg(c1 ORDER BY c1)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2 ORDER BY array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
array_agg
@@ -2869,16 +2849,13 @@ select array_agg(c5 order by c1 desc) from ft2 where c2 = 6 and c1 < 50;
-- DISTINCT within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5)) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2890,16 +2867,13 @@ select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2
-- DISTINCT combined with ORDER BY within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2910,16 +2884,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2931,16 +2902,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4
-- FILTER within aggregate
explain (verbose, costs off)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Sort Key: (sum(ft1.c1) FILTER (WHERE ((ft1.c1 < 100) AND (ft1.c2 > 5))))
- -> Foreign Scan
- Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2 ORDER BY sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))) ASC NULLS LAST
+(4 rows)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
sum
@@ -3339,16 +3307,13 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
-- FULL join with IS NULL check in HAVING
explain (verbose, costs off)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Sort Key: (avg(t1.c1)), (sum(t2.c1))
- -> Foreign Scan
- Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL)))
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL))) ORDER BY avg(r1.c1) ASC NULLS LAST, sum(r2.c1) ASC NULLS LAST
+(4 rows)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
avg | sum
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f387fac42..c342bd319c 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -246,6 +246,25 @@ typedef struct PgFdwAnalyzeState
MemoryContext temp_cxt; /* context for per-tuple temporary data */
} PgFdwAnalyzeState;
+/*
+ * This enum describes what's kept in the fdw_private list for a ForeignPath.
+ * We store:
+ *
+ * 1) Boolean flag showing if the remote query has the final sort
+ */
+enum FdwPathPrivateIndex
+{
+ /* has-final-sort flag (as an integer Value node) */
+ FdwPathPrivateHasFinalSort
+};
+
+/* Struct for extra information passed to estimate_path_cost_size */
+typedef struct
+{
+ PathTarget *target;
+ bool has_final_sort;
+} PgFdwPathExtraData;
+
/*
* Identify the attribute where data conversion fails.
*/
@@ -368,6 +387,7 @@ static void estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost);
static void get_remote_estimate(const char *sql,
@@ -376,6 +396,12 @@ static void get_remote_estimate(const char *sql,
int *width,
Cost *startup_cost,
Cost *total_cost);
+static void adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost);
static bool ec_member_matches_foreign(PlannerInfo *root, RelOptInfo *rel,
EquivalenceClass *ec, EquivalenceMember *em,
void *arg);
@@ -452,6 +478,9 @@ static void add_foreign_grouping_paths(PlannerInfo *root,
RelOptInfo *input_rel,
RelOptInfo *grouped_rel,
GroupPathExtraData *extra);
+static void add_foreign_ordered_paths(PlannerInfo *root,
+ RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel);
static void apply_server_options(PgFdwRelationInfo *fpinfo);
static void apply_table_options(PgFdwRelationInfo *fpinfo);
static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
@@ -637,7 +666,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* values in fpinfo so we don't need to do it again to generate the
* basic foreign path.
*/
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
@@ -668,7 +697,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
set_baserel_size_estimates(root, baserel);
/* Fill in basically-bogus cost estimates for use later. */
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
}
@@ -1102,7 +1131,7 @@ postgresGetForeignPaths(PlannerInfo *root,
/* Get a cost estimate from the remote */
estimate_path_cost_size(root, baserel,
- param_info->ppi_clauses, NIL,
+ param_info->ppi_clauses, NIL, NULL,
&rows, &width,
&startup_cost, &total_cost);
@@ -1149,8 +1178,16 @@ postgresGetForeignPlan(PlannerInfo *root,
List *fdw_recheck_quals = NIL;
List *retrieved_attrs;
StringInfoData sql;
+ bool has_final_sort = false;
ListCell *lc;
+ /*
+ * Get private info created by postgresGetForeignUpperPaths, if any.
+ */
+ if (best_path->fdw_private)
+ has_final_sort = intVal(list_nth(best_path->fdw_private,
+ FdwPathPrivateHasFinalSort));
+
if (IS_SIMPLE_REL(foreignrel))
{
/*
@@ -1299,7 +1336,8 @@ postgresGetForeignPlan(PlannerInfo *root,
initStringInfo(&sql);
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
remote_exprs, best_path->path.pathkeys,
- false, &retrieved_attrs, ¶ms_list);
+ has_final_sort, false,
+ &retrieved_attrs, ¶ms_list);
/* Remember remote_exprs for possible use by postgresPlanDirectModify */
fpinfo->final_remote_exprs = remote_exprs;
@@ -2482,6 +2520,8 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es)
*
* param_join_conds are the parameterization clauses with outer relations.
* pathkeys specify the expected sort order if any for given path being costed.
+ * fpextra specifies additional post-scan/join processing steps such as the
+ * final sort.
*
* The function returns the cost and size estimates in p_row, p_width,
* p_startup_cost and p_total_cost variables.
@@ -2491,6 +2531,7 @@ estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost)
{
@@ -2555,8 +2596,9 @@ estimate_path_cost_size(PlannerInfo *root,
initStringInfo(&sql);
appendStringInfoString(&sql, "EXPLAIN ");
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
- remote_conds, pathkeys, false,
- &retrieved_attrs, NULL);
+ remote_conds, pathkeys,
+ fpextra ? fpextra->has_final_sort : false,
+ false, &retrieved_attrs, NULL);
/* Get the remote estimate */
conn = GetConnection(fpinfo->user, false);
@@ -2632,6 +2674,19 @@ estimate_path_cost_size(PlannerInfo *root,
{
startup_cost = fpinfo->rel_startup_cost;
run_cost = fpinfo->rel_total_cost - fpinfo->rel_startup_cost;
+
+ /*
+ * When we're called from postgresGetForeignUpperPaths() with the
+ * UPPERREL_ORDERED stage, the costs of scanning a foreign base or
+ * join relation obtained from the cache would not yet contain the
+ * eval costs for the final scan/join target that has been updated
+ * by apply_scanjoin_target_to_paths(); add the eval costs now.
+ */
+ if (fpextra && !IS_UPPER_REL(foreignrel))
+ {
+ startup_cost += foreignrel->reltarget->cost.startup;
+ run_cost += foreignrel->reltarget->cost.per_tuple * rows;
+ }
}
else if (IS_JOIN_REL(foreignrel))
{
@@ -2844,13 +2899,39 @@ estimate_path_cost_size(PlannerInfo *root,
*/
if (pathkeys != NIL)
{
- startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
- run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ if (IS_UPPER_REL(foreignrel))
+ {
+ Assert(fpinfo->stage == UPPERREL_GROUP_AGG);
+ adjust_foreign_grouping_path_cost(root, pathkeys,
+ retrieved_rows, width,
+ &startup_cost, &run_cost);
+ }
+ else
+ {
+ startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ }
}
total_cost = startup_cost + run_cost;
}
+ /*
+ * If this includes an UPPERREL_ORDERED step, the given target, which will
+ * be applied to the resulting path, might have different expressions from
+ * the foreignrel's reltarget (see make_sort_input_target()); adjust tlist
+ * eval costs.
+ */
+ if (fpextra && fpextra->target != foreignrel->reltarget)
+ {
+ QualCost oldcost = foreignrel->reltarget->cost;
+ QualCost newcost = fpextra->target->cost;
+
+ startup_cost += newcost.startup - oldcost.startup;
+ total_cost += newcost.startup - oldcost.startup;
+ total_cost += (newcost.per_tuple - oldcost.per_tuple) * rows;
+ }
+
/*
* Cache the costs for scans without any pathkeys or parameterization
* before adding the costs for transferring data from the foreign server.
@@ -2860,7 +2941,7 @@ estimate_path_cost_size(PlannerInfo *root,
* foreign server. This function will be called at least once for every
* foreign relation without pathkeys and parameterization.
*/
- if (pathkeys == NIL && param_join_conds == NIL)
+ if (pathkeys == NIL && param_join_conds == NIL && fpextra == NULL)
{
fpinfo->rel_startup_cost = startup_cost;
fpinfo->rel_total_cost = total_cost;
@@ -2935,6 +3016,57 @@ get_remote_estimate(const char *sql, PGconn *conn,
PG_END_TRY();
}
+/*
+ * Adjust the cost estimates of a foreign grouping path.
+ */
+static void
+adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost)
+{
+ /*
+ * If the GROUP BY clause isn't sort-able, the plan chosen by the remote
+ * side is unlikely to generate properly-sorted output, so it would need
+ * an explicit sort; adjust the given costs with cost_sort(). Likewise,
+ * if the GROUP BY clause is sort-able but isn't a superset of the given
+ * pathkeys, adjust the costs with that function. Otherwise, adjust the
+ * costs by applying the same heuristic as for the scan/join case.
+ */
+ if (!grouping_is_sortable(root->parse->groupClause) ||
+ !pathkeys_contained_in(pathkeys, root->group_pathkeys))
+ {
+ Path sort_path; /* dummy for result of cost_sort */
+
+ cost_sort(&sort_path,
+ root,
+ pathkeys,
+ *p_startup_cost + *p_run_cost,
+ retrieved_rows,
+ width,
+ 0.0,
+ work_mem,
+ -1.0);
+
+ *p_startup_cost = sort_path.startup_cost;
+ *p_run_cost = sort_path.total_cost - sort_path.startup_cost;
+ }
+ else
+ {
+ /*
+ * The default cost seems too large for foreign-grouping cases; add
+ * 1/4th of the default.
+ */
+ double sort_multiplier = 1.0 + (DEFAULT_FDW_SORT_MULTIPLIER
+ - 1.0) * 0.25;
+
+ *p_startup_cost *= sort_multiplier;
+ *p_run_cost *= sort_multiplier;
+ }
+}
+
/*
* Detect whether we want to process an EquivalenceClass member.
*
@@ -4934,7 +5066,7 @@ add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
List *useful_pathkeys = lfirst(lc);
Path *sorted_epq_path;
- estimate_path_cost_size(root, rel, NIL, useful_pathkeys,
+ estimate_path_cost_size(root, rel, NIL, useful_pathkeys, NULL,
&rows, &width, &startup_cost, &total_cost);
/*
@@ -5185,8 +5317,8 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
extra->sjinfo);
/* Estimate costs for bare join relation */
- estimate_path_cost_size(root, joinrel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, joinrel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the joinrel */
joinrel->rows = rows;
joinrel->reltarget->width = width;
@@ -5455,15 +5587,29 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
return;
/* Ignore stages we don't support; and skip any duplicate calls. */
- if (stage != UPPERREL_GROUP_AGG || output_rel->fdw_private)
+ if ((stage != UPPERREL_GROUP_AGG &&
+ stage != UPPERREL_ORDERED) ||
+ output_rel->fdw_private)
return;
fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
+ fpinfo->stage = stage;
fpinfo->pushdown_safe = false;
output_rel->fdw_private = fpinfo;
- add_foreign_grouping_paths(root, input_rel, output_rel,
- (GroupPathExtraData *) extra);
+ switch (stage)
+ {
+ case UPPERREL_GROUP_AGG:
+ add_foreign_grouping_paths(root, input_rel, output_rel,
+ (GroupPathExtraData *) extra);
+ break;
+ case UPPERREL_ORDERED:
+ add_foreign_ordered_paths(root, input_rel, output_rel);
+ break;
+ default:
+ elog(ERROR, "unexpected upper relation: %d", (int) stage);
+ break;
+ }
}
/*
@@ -5533,8 +5679,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
cost_qual_eval(&fpinfo->local_conds_cost, fpinfo->local_conds, root);
/* Estimate the cost of push down */
- estimate_path_cost_size(root, grouped_rel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, grouped_rel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the fpinfo */
fpinfo->rows = rows;
@@ -5542,6 +5688,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
fpinfo->startup_cost = startup_cost;
fpinfo->total_cost = total_cost;
+ grouped_rel->rows = fpinfo->rows;
+
/* Create and add foreign path to the grouping relation. */
grouppath = create_foreign_upper_path(root,
grouped_rel,
@@ -5557,6 +5705,144 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
add_path(grouped_rel, (Path *) grouppath);
}
+/*
+ * add_foreign_ordered_paths
+ * Add foreign paths for performing the final sort remotely.
+ *
+ * Given input_rel contains the source-data Paths. The paths are added to the
+ * given ordered_rel.
+ */
+static void
+add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel)
+{
+ Query *parse = root->parse;
+ PgFdwRelationInfo *ifpinfo = input_rel->fdw_private;
+ PgFdwRelationInfo *fpinfo = ordered_rel->fdw_private;
+ PgFdwPathExtraData *fpextra;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+ ForeignPath *ordered_path;
+ ListCell *lc;
+
+ /* Shouldn't get here unless the query has ORDER BY */
+ Assert(parse->sortClause);
+
+ /*
+ * For now we don't support cases where there are any SRFs in the tlist
+ */
+ if (parse->hasTargetSRFs)
+ return;
+
+ /* Save the input_rel as outerrel in fpinfo */
+ fpinfo->outerrel = input_rel;
+
+ /*
+ * Copy foreign table, foreign server, user mapping, FDW options etc.
+ * details from the input relation's fpinfo.
+ */
+ fpinfo->table = ifpinfo->table;
+ fpinfo->server = ifpinfo->server;
+ fpinfo->user = ifpinfo->user;
+ merge_fdw_options(fpinfo, ifpinfo, NULL);
+
+ /*
+ * No work if there is a properly-sorted ForeignPath in the ordered_rel's
+ * pathlist.
+ */
+ foreach(lc, ordered_rel->pathlist)
+ {
+ Path *path = (Path *) lfirst(lc);
+
+ /*
+ * apply_scanjoin_target_to_paths() might put a ProjectionPath on top
+ * of each of its scan/join paths; look through ProjectionPath and see
+ * if the path underneath it is ForeignPath.
+ */
+ if (IsA(path, ForeignPath) ||
+ (IsA(path, ProjectionPath) &&
+ IsA(((ProjectionPath *) path)->subpath, ForeignPath)))
+ {
+ Assert(pathkeys_contained_in(root->sort_pathkeys,
+ path->pathkeys));
+
+ /* Safe to push down */
+ fpinfo->pushdown_safe = true;
+
+ return;
+ }
+ }
+
+ /*
+ * We try to create paths below by extending paths for the underlying
+ * base, join, or grouping relation to perform the final sort remotely,
+ * which is stored into the fdw_private list of the resulting paths.
+ */
+
+ /* Assess if it is safe to push down the final sort */
+ foreach(lc, root->sort_pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
+ Expr *sort_expr;
+
+ /*
+ * is_foreign_expr would detect volatile expressions as well, but
+ * checking ec_has_volatile here saves some cycles.
+ */
+ if (pathkey_ec->ec_has_volatile)
+ return;
+
+ /* Get the sort expression for the pathkey_ec */
+ sort_expr = find_em_expr_for_input_target(root,
+ pathkey_ec,
+ input_rel->reltarget);
+
+ /* If it's unsafe to remote, we cannot push down the final sort */
+ if (!is_foreign_expr(root, input_rel, sort_expr))
+ return;
+ }
+
+ /* Safe to push down */
+ fpinfo->pushdown_safe = true;
+
+ /* Initialize the selectivity and cost of local_conds */
+ fpinfo->local_conds_sel = 1.0;
+ fpinfo->local_conds_cost.startup = 0.0;
+ fpinfo->local_conds_cost.per_tuple = 0.0;
+
+ fpextra = (PgFdwPathExtraData *) palloc0(sizeof(PgFdwPathExtraData));
+ fpextra->target = root->upper_targets[UPPERREL_ORDERED];
+ fpextra->has_final_sort = true;
+
+ /* Estimate the costs of performing the final sort remotely */
+ estimate_path_cost_size(root, input_rel, NIL, root->sort_pathkeys, fpextra,
+ &rows, &width, &startup_cost, &total_cost);
+
+ /*
+ * Build the fdw_private list that will be used by postgresGetForeignPlan.
+ * Items in the list must match order in enum FdwPathPrivateIndex.
+ */
+ fdw_private = list_make1(makeInteger(true));
+
+ /* Create foreign ordering ForeignPath */
+ ordered_path = create_foreign_upper_path(root,
+ input_rel,
+ root->upper_targets[UPPERREL_ORDERED],
+ rows,
+ startup_cost,
+ total_cost,
+ root->sort_pathkeys,
+ NULL, /* no extra plan */
+ fdw_private);
+
+ /* And add it to the ordered_rel */
+ add_path(ordered_rel, (Path *) ordered_path);
+}
+
/*
* Create a tuple from the specified row of the PGresult.
*
@@ -5807,3 +6093,65 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
/* We didn't find any suitable equivalence class expression */
return NULL;
}
+
+/*
+ * Find an equivalence class member expression to be computed as a sort column
+ * in the given target.
+ */
+Expr *
+find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target)
+{
+ ListCell *lc1;
+ int i;
+
+ i = 0;
+ foreach(lc1, target->exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc1);
+ Index sgref = get_pathtarget_sortgroupref(target, i);
+ ListCell *lc2;
+
+ /* Ignore non-sort expressions */
+ if (sgref == 0 ||
+ get_sortgroupref_clause_noerr(sgref,
+ root->parse->sortClause) == NULL)
+ {
+ i++;
+ continue;
+ }
+
+ /* We ignore binary-compatible relabeling on both ends */
+ while (expr && IsA(expr, RelabelType))
+ expr = ((RelabelType *) expr)->arg;
+
+ /* Locate an EquivalenceClass member matching this expr, if any */
+ foreach(lc2, ec->ec_members)
+ {
+ EquivalenceMember *em = (EquivalenceMember *) lfirst(lc2);
+ Expr *em_expr;
+
+ /* Don't match constants */
+ if (em->em_is_const)
+ continue;
+
+ /* Ignore child members */
+ if (em->em_is_child)
+ continue;
+
+ /* Match if same expression (after stripping relabel) */
+ em_expr = em->em_expr;
+ while (em_expr && IsA(em_expr, RelabelType))
+ em_expr = ((RelabelType *) em_expr)->arg;
+
+ if (equal(em_expr, expr))
+ return em->em_expr;
+ }
+
+ i++;
+ }
+
+ elog(ERROR, "could not find pathkey item to sort");
+ return NULL; /* keep compiler quiet */
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 3f50103285..936e3f498a 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -92,6 +92,9 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /* Upper relation information */
+ UpperRelationKind stage;
+
/* Grouping information */
List *grouped_tlist;
@@ -175,10 +178,14 @@ extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
List **retrieved_attrs);
extern void deparseStringLiteral(StringInfo buf, const char *val);
extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
+extern Expr *find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target);
extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, List *tlist,
- List *remote_conds, List *pathkeys, bool is_subquery,
+ List *remote_conds, List *pathkeys,
+ bool has_final_sort, bool is_subquery,
List **retrieved_attrs, List **params_list);
extern const char *get_jointype_name(JoinType jointype);
--
2.19.2
--------------050108040401020008040409
Content-Type: text/x-patch;
name="v6-0002-Refactor-create_limit_path-to-share-cost-adjustment-.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename*0="v6-0002-Refactor-create_limit_path-to-share-cost-adjustment-";
filename*1=".patch"
^ permalink raw reply [nested|flat] 26+ messages in thread
* [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely
@ 2019-03-20 10:19 Etsuro Fujita <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Etsuro Fujita @ 2019-03-20 10:19 UTC (permalink / raw)
---
contrib/postgres_fdw/deparse.c | 28 +-
.../postgres_fdw/expected/postgres_fdw.out | 167 +++-----
contrib/postgres_fdw/postgres_fdw.c | 388 ++++++++++++++++--
contrib/postgres_fdw/postgres_fdw.h | 12 +-
4 files changed, 459 insertions(+), 136 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 92a0ab6da5..97dd07bee8 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -167,7 +167,8 @@ static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
deparse_expr_cxt *context);
static void deparseLockingClause(deparse_expr_cxt *context);
-static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
+static void appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context);
static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
@@ -929,8 +930,8 @@ build_tlist_to_deparse(RelOptInfo *foreignrel)
void
deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
List *tlist, List *remote_conds, List *pathkeys,
- bool is_subquery, List **retrieved_attrs,
- List **params_list)
+ bool has_final_sort, bool is_subquery,
+ List **retrieved_attrs, List **params_list)
{
deparse_expr_cxt context;
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
@@ -985,7 +986,7 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
/* Add ORDER BY clause if we found any useful pathkeys */
if (pathkeys)
- appendOrderByClause(pathkeys, &context);
+ appendOrderByClause(pathkeys, has_final_sort, &context);
/* Add any necessary FOR UPDATE/SHARE. */
deparseLockingClause(&context);
@@ -1590,7 +1591,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse the subquery representing the relation. */
appendStringInfoChar(buf, '(');
deparseSelectStmtForRel(buf, root, foreignrel, NIL,
- fpinfo->remote_conds, NIL, true,
+ fpinfo->remote_conds, NIL, false, true,
&retrieved_attrs, params_list);
appendStringInfoChar(buf, ')');
@@ -3109,7 +3110,8 @@ appendGroupByClause(List *tlist, deparse_expr_cxt *context)
* base relation are obtained and deparsed.
*/
static void
-appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
+appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context)
{
ListCell *lcell;
int nestlevel;
@@ -3126,7 +3128,19 @@ appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
PathKey *pathkey = lfirst(lcell);
Expr *em_expr;
- em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+ if (has_final_sort)
+ {
+ /*
+ * By construction, context->foreignrel is the input relation to
+ * the final sort.
+ */
+ em_expr = find_em_expr_for_input_target(context->root,
+ pathkey->pk_eclass,
+ context->foreignrel->reltarget);
+ }
+ else
+ em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+
Assert(em_expr != NULL);
appendStringInfoString(buf, delim);
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 42108bd3d4..592dd3e05f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2552,18 +2552,13 @@ DROP ROLE regress_view_owner;
-- Simple aggregates
explain (verbose, costs off)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
- Result
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), ((sum(c1)) * ((random() <= '1'::double precision))::integer), c2
- -> Sort
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Sort Key: (count(ft1.c6)), (sum(ft1.c1))
- -> Foreign Scan
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7 ORDER BY count(c6) ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(4 rows)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
count | sum | avg | min | max | stddev | sum2
@@ -2621,16 +2616,13 @@ select sum(t1.c1), count(t2.c1) from ft1 t1 inner join ft2 t2 on (t1.c1 = t2.c1)
-- GROUP BY clause having expressions
explain (verbose, costs off)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Sort Key: ((ft1.c2 / 2))
- -> Foreign Scan
- Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1 ORDER BY (c2 / 2) ASC NULLS LAST
+(4 rows)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
?column? | ?column?
@@ -2645,18 +2637,15 @@ select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
-- Aggregates in subquery are pushed down.
explain (verbose, costs off)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
- QUERY PLAN
----------------------------------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------
Aggregate
Output: count(ft1.c2), sum(ft1.c2)
- -> Sort
+ -> Foreign Scan
Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Sort Key: ft1.c2, (sum(ft1.c1))
- -> Foreign Scan
- Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3 ORDER BY c2 ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(6 rows)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
count | sum
@@ -2742,16 +2731,13 @@ select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2
-- Also, ORDER BY contains an aggregate function
explain (verbose, costs off)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
- QUERY PLAN
------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, c2, (sum(c1))
- Sort Key: (sum(ft1.c1))
- -> Foreign Scan
- Output: c2, c2, (sum(c1))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2 ORDER BY sum("C 1") ASC NULLS LAST
+(4 rows)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
c2 | c2
@@ -2764,16 +2750,13 @@ select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
-- Testing HAVING clause shippability
explain (verbose, costs off)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, (sum(c1))
- Sort Key: ft2.c2
- -> Foreign Scan
- Output: c2, (sum(c1))
- Relations: Aggregate on (public.ft2)
- Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800))
-(7 rows)
+ Relations: Aggregate on (public.ft2)
+ Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800)) ORDER BY c2 ASC NULLS LAST
+(4 rows)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
c2 | sum
@@ -2823,16 +2806,13 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
-- ORDER BY within aggregate, same column used to order
explain (verbose, costs off)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(c1 ORDER BY c1)), c2
- Sort Key: (array_agg(ft1.c1 ORDER BY ft1.c1))
- -> Foreign Scan
- Output: (array_agg(c1 ORDER BY c1)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2 ORDER BY array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
array_agg
@@ -2869,16 +2849,13 @@ select array_agg(c5 order by c1 desc) from ft2 where c2 = 6 and c1 < 50;
-- DISTINCT within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5)) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2890,16 +2867,13 @@ select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2
-- DISTINCT combined with ORDER BY within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2910,16 +2884,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2931,16 +2902,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4
-- FILTER within aggregate
explain (verbose, costs off)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Sort Key: (sum(ft1.c1) FILTER (WHERE ((ft1.c1 < 100) AND (ft1.c2 > 5))))
- -> Foreign Scan
- Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2 ORDER BY sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))) ASC NULLS LAST
+(4 rows)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
sum
@@ -3339,16 +3307,13 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
-- FULL join with IS NULL check in HAVING
explain (verbose, costs off)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Sort Key: (avg(t1.c1)), (sum(t2.c1))
- -> Foreign Scan
- Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL)))
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL))) ORDER BY avg(r1.c1) ASC NULLS LAST, sum(r2.c1) ASC NULLS LAST
+(4 rows)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
avg | sum
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f387fac42..4208bf6baa 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -246,6 +246,25 @@ typedef struct PgFdwAnalyzeState
MemoryContext temp_cxt; /* context for per-tuple temporary data */
} PgFdwAnalyzeState;
+/*
+ * This enum describes what's kept in the fdw_private list for a ForeignPath.
+ * We store:
+ *
+ * 1) Boolean flag showing if the remote query has the final sort
+ */
+enum FdwPathPrivateIndex
+{
+ /* has-final-sort flag (as an integer Value node) */
+ FdwPathPrivateHasFinalSort
+};
+
+/* Struct for extra information passed to estimate_path_cost_size() */
+typedef struct
+{
+ PathTarget *target;
+ bool has_final_sort;
+} PgFdwPathExtraData;
+
/*
* Identify the attribute where data conversion fails.
*/
@@ -368,6 +387,7 @@ static void estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost);
static void get_remote_estimate(const char *sql,
@@ -376,6 +396,12 @@ static void get_remote_estimate(const char *sql,
int *width,
Cost *startup_cost,
Cost *total_cost);
+static void adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost);
static bool ec_member_matches_foreign(PlannerInfo *root, RelOptInfo *rel,
EquivalenceClass *ec, EquivalenceMember *em,
void *arg);
@@ -452,6 +478,9 @@ static void add_foreign_grouping_paths(PlannerInfo *root,
RelOptInfo *input_rel,
RelOptInfo *grouped_rel,
GroupPathExtraData *extra);
+static void add_foreign_ordered_paths(PlannerInfo *root,
+ RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel);
static void apply_server_options(PgFdwRelationInfo *fpinfo);
static void apply_table_options(PgFdwRelationInfo *fpinfo);
static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
@@ -637,7 +666,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* values in fpinfo so we don't need to do it again to generate the
* basic foreign path.
*/
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
@@ -668,7 +697,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
set_baserel_size_estimates(root, baserel);
/* Fill in basically-bogus cost estimates for use later. */
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
}
@@ -827,6 +856,7 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
* Pushing the query_pathkeys to the remote server is always worth
* considering, because it might let us avoid a local sort.
*/
+ fpinfo->qp_is_pushdown_safe = false;
if (root->query_pathkeys)
{
bool query_pathkeys_ok = true;
@@ -857,7 +887,10 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
}
if (query_pathkeys_ok)
+ {
useful_pathkeys_list = list_make1(list_copy(root->query_pathkeys));
+ fpinfo->qp_is_pushdown_safe = true;
+ }
}
/*
@@ -1102,7 +1135,7 @@ postgresGetForeignPaths(PlannerInfo *root,
/* Get a cost estimate from the remote */
estimate_path_cost_size(root, baserel,
- param_info->ppi_clauses, NIL,
+ param_info->ppi_clauses, NIL, NULL,
&rows, &width,
&startup_cost, &total_cost);
@@ -1149,8 +1182,16 @@ postgresGetForeignPlan(PlannerInfo *root,
List *fdw_recheck_quals = NIL;
List *retrieved_attrs;
StringInfoData sql;
+ bool has_final_sort = false;
ListCell *lc;
+ /*
+ * Get FDW private data created by postgresGetForeignUpperPaths(), if any.
+ */
+ if (best_path->fdw_private)
+ has_final_sort = intVal(list_nth(best_path->fdw_private,
+ FdwPathPrivateHasFinalSort));
+
if (IS_SIMPLE_REL(foreignrel))
{
/*
@@ -1299,7 +1340,8 @@ postgresGetForeignPlan(PlannerInfo *root,
initStringInfo(&sql);
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
remote_exprs, best_path->path.pathkeys,
- false, &retrieved_attrs, ¶ms_list);
+ has_final_sort, false,
+ &retrieved_attrs, ¶ms_list);
/* Remember remote_exprs for possible use by postgresPlanDirectModify */
fpinfo->final_remote_exprs = remote_exprs;
@@ -2482,6 +2524,8 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es)
*
* param_join_conds are the parameterization clauses with outer relations.
* pathkeys specify the expected sort order if any for given path being costed.
+ * fpextra specifies additional post-scan/join-processing steps such as the
+ * final sort.
*
* The function returns the cost and size estimates in p_row, p_width,
* p_startup_cost and p_total_cost variables.
@@ -2491,6 +2535,7 @@ estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost)
{
@@ -2555,8 +2600,9 @@ estimate_path_cost_size(PlannerInfo *root,
initStringInfo(&sql);
appendStringInfoString(&sql, "EXPLAIN ");
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
- remote_conds, pathkeys, false,
- &retrieved_attrs, NULL);
+ remote_conds, pathkeys,
+ fpextra ? fpextra->has_final_sort : false,
+ false, &retrieved_attrs, NULL);
/* Get the remote estimate */
conn = GetConnection(fpinfo->user, false);
@@ -2624,9 +2670,9 @@ estimate_path_cost_size(PlannerInfo *root,
/*
* We will come here again and again with different set of pathkeys
- * that caller wants to cost. We don't need to calculate the cost of
- * bare scan each time. Instead, use the costs if we have cached them
- * already.
+ * that caller wants to cost. We don't need to calculate the costs of
+ * the underlying scan, join, or grouping each time. Instead, use the
+ * costs if we have cached them already.
*/
if (fpinfo->rel_startup_cost >= 0 && fpinfo->rel_total_cost >= 0)
{
@@ -2844,23 +2890,51 @@ estimate_path_cost_size(PlannerInfo *root,
*/
if (pathkeys != NIL)
{
- startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
- run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ if (IS_UPPER_REL(foreignrel))
+ {
+ Assert(fpinfo->stage == UPPERREL_GROUP_AGG);
+ adjust_foreign_grouping_path_cost(root, pathkeys,
+ retrieved_rows, width,
+ &startup_cost, &run_cost);
+ }
+ else
+ {
+ startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ }
}
total_cost = startup_cost + run_cost;
}
/*
- * Cache the costs for scans without any pathkeys or parameterization
- * before adding the costs for transferring data from the foreign server.
- * These costs are useful for costing the join between this relation and
- * another foreign relation or to calculate the costs of paths with
- * pathkeys for this relation, when the costs can not be obtained from the
- * foreign server. This function will be called at least once for every
- * foreign relation without pathkeys and parameterization.
+ * If this includes the final sort step, the given target, which will be
+ * applied to the resulting path, might have different expressions from
+ * the foreignrel's reltarget (see make_sort_input_target()); adjust tlist
+ * eval costs.
*/
- if (pathkeys == NIL && param_join_conds == NIL)
+ if (fpextra && fpextra->target != foreignrel->reltarget)
+ {
+ QualCost oldcost = foreignrel->reltarget->cost;
+ QualCost newcost = fpextra->target->cost;
+
+ startup_cost += newcost.startup - oldcost.startup;
+ total_cost += newcost.startup - oldcost.startup;
+ total_cost += (newcost.per_tuple - oldcost.per_tuple) * rows;
+ }
+
+ /*
+ * Cache the costs for scans, joins, or groupings without any
+ * parameterization, pathkeys, or additional post-scan/join-processing
+ * steps, before adding the costs for transferring data from the foreign
+ * server. These costs are useful for costing remote joins involving this
+ * relation or costing other remote processing for this relation such as
+ * remote sort, when the costs can not be obtained from the foreign
+ * server. This function will be called at least once for every foreign
+ * relation without any parameterization, pathkeys, or additional
+ * post-scan/join-processing steps.
+ */
+ if (pathkeys == NIL && param_join_conds == NIL && fpextra == NULL)
{
fpinfo->rel_startup_cost = startup_cost;
fpinfo->rel_total_cost = total_cost;
@@ -2935,6 +3009,57 @@ get_remote_estimate(const char *sql, PGconn *conn,
PG_END_TRY();
}
+/*
+ * Adjust the cost estimates of a foreign grouping path.
+ */
+static void
+adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost)
+{
+ /*
+ * If the GROUP BY clause isn't sort-able, the plan chosen by the remote
+ * side is unlikely to generate properly-sorted output, so it would need
+ * an explicit sort; adjust the given costs with cost_sort(). Likewise,
+ * if the GROUP BY clause is sort-able but isn't a superset of the given
+ * pathkeys, adjust the costs with that function. Otherwise, adjust the
+ * costs by applying the same heuristic as for the scan or join case.
+ */
+ if (!grouping_is_sortable(root->parse->groupClause) ||
+ !pathkeys_contained_in(pathkeys, root->group_pathkeys))
+ {
+ Path sort_path; /* dummy for result of cost_sort */
+
+ cost_sort(&sort_path,
+ root,
+ pathkeys,
+ *p_startup_cost + *p_run_cost,
+ retrieved_rows,
+ width,
+ 0.0,
+ work_mem,
+ -1.0);
+
+ *p_startup_cost = sort_path.startup_cost;
+ *p_run_cost = sort_path.total_cost - sort_path.startup_cost;
+ }
+ else
+ {
+ /*
+ * The default extra cost seems too large for foreign-grouping cases;
+ * add 1/4th of that default.
+ */
+ double sort_multiplier = 1.0 + (DEFAULT_FDW_SORT_MULTIPLIER
+ - 1.0) * 0.25;
+
+ *p_startup_cost *= sort_multiplier;
+ *p_run_cost *= sort_multiplier;
+ }
+}
+
/*
* Detect whether we want to process an EquivalenceClass member.
*
@@ -4934,7 +5059,7 @@ add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
List *useful_pathkeys = lfirst(lc);
Path *sorted_epq_path;
- estimate_path_cost_size(root, rel, NIL, useful_pathkeys,
+ estimate_path_cost_size(root, rel, NIL, useful_pathkeys, NULL,
&rows, &width, &startup_cost, &total_cost);
/*
@@ -5185,8 +5310,8 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
extra->sjinfo);
/* Estimate costs for bare join relation */
- estimate_path_cost_size(root, joinrel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, joinrel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the joinrel */
joinrel->rows = rows;
joinrel->reltarget->width = width;
@@ -5455,15 +5580,29 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
return;
/* Ignore stages we don't support; and skip any duplicate calls. */
- if (stage != UPPERREL_GROUP_AGG || output_rel->fdw_private)
+ if ((stage != UPPERREL_GROUP_AGG &&
+ stage != UPPERREL_ORDERED) ||
+ output_rel->fdw_private)
return;
fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
+ fpinfo->stage = stage;
fpinfo->pushdown_safe = false;
output_rel->fdw_private = fpinfo;
- add_foreign_grouping_paths(root, input_rel, output_rel,
- (GroupPathExtraData *) extra);
+ switch (stage)
+ {
+ case UPPERREL_GROUP_AGG:
+ add_foreign_grouping_paths(root, input_rel, output_rel,
+ (GroupPathExtraData *) extra);
+ break;
+ case UPPERREL_ORDERED:
+ add_foreign_ordered_paths(root, input_rel, output_rel);
+ break;
+ default:
+ elog(ERROR, "unexpected upper relation: %d", (int) stage);
+ break;
+ }
}
/*
@@ -5533,8 +5672,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
cost_qual_eval(&fpinfo->local_conds_cost, fpinfo->local_conds, root);
/* Estimate the cost of push down */
- estimate_path_cost_size(root, grouped_rel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, grouped_rel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the fpinfo */
fpinfo->rows = rows;
@@ -5542,6 +5681,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
fpinfo->startup_cost = startup_cost;
fpinfo->total_cost = total_cost;
+ grouped_rel->rows = fpinfo->rows;
+
/* Create and add foreign path to the grouping relation. */
grouppath = create_foreign_upper_path(root,
grouped_rel,
@@ -5557,6 +5698,137 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
add_path(grouped_rel, (Path *) grouppath);
}
+/*
+ * add_foreign_ordered_paths
+ * Add foreign paths for performing the final sort remotely.
+ *
+ * Given input_rel contains the source-data Paths. The paths are added to the
+ * given ordered_rel.
+ */
+static void
+add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel)
+{
+ Query *parse = root->parse;
+ PgFdwRelationInfo *ifpinfo = input_rel->fdw_private;
+ PgFdwRelationInfo *fpinfo = ordered_rel->fdw_private;
+ PgFdwPathExtraData *fpextra;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+ ForeignPath *ordered_path;
+ ListCell *lc;
+
+ /* Shouldn't get here unless the query has ORDER BY */
+ Assert(parse->sortClause);
+
+ /* We don't support cases where there are any SRFs in the targetlist. */
+ if (parse->hasTargetSRFs)
+ return;
+
+ /* Save the input_rel as outerrel in fpinfo */
+ fpinfo->outerrel = input_rel;
+
+ /*
+ * Copy foreign table, foreign server, user mapping, FDW options etc.
+ * details from the input relation's fpinfo.
+ */
+ fpinfo->table = ifpinfo->table;
+ fpinfo->server = ifpinfo->server;
+ fpinfo->user = ifpinfo->user;
+ merge_fdw_options(fpinfo, ifpinfo, NULL);
+
+ /*
+ * If the input_rel is a base or join relation, we would already have
+ * considered pushing down the final sort to the remote server when
+ * creating pre-sorted foreign paths for that relation, because the
+ * query_pathkeys is set to the root->sort_pathkeys in that case (see
+ * standard_qp_callback()).
+ */
+ if (input_rel->reloptkind == RELOPT_BASEREL ||
+ input_rel->reloptkind == RELOPT_JOINREL)
+ {
+ Assert(root->query_pathkeys == root->sort_pathkeys);
+
+ fpinfo->pushdown_safe = ifpinfo->qp_is_pushdown_safe;
+
+ return;
+ }
+
+ /* The input_rel should be a grouping relation */
+ Assert(input_rel->reloptkind == RELOPT_UPPER_REL);
+ Assert(ifpinfo->stage == UPPERREL_GROUP_AGG);
+
+ /*
+ * We try to create a path below by extending a simple foreign path for
+ * the underlying grouping relation to perform the final sort remotely,
+ * which is stored into the fdw_private list of the resulting path.
+ */
+
+ /* Assess if it is safe to push down the final sort */
+ foreach(lc, root->sort_pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
+ Expr *sort_expr;
+
+ /*
+ * is_foreign_expr would detect volatile expressions as well, but
+ * checking ec_has_volatile here saves some cycles.
+ */
+ if (pathkey_ec->ec_has_volatile)
+ return;
+
+ /* Get the sort expression for the pathkey_ec */
+ sort_expr = find_em_expr_for_input_target(root,
+ pathkey_ec,
+ input_rel->reltarget);
+
+ /* If it's unsafe to remote, we cannot push down the final sort */
+ if (!is_foreign_expr(root, input_rel, sort_expr))
+ return;
+ }
+
+ /* Safe to push down */
+ fpinfo->pushdown_safe = true;
+
+ /* Initialize the selectivity and cost of local_conds */
+ fpinfo->local_conds_sel = 1.0;
+ fpinfo->local_conds_cost.startup = 0.0;
+ fpinfo->local_conds_cost.per_tuple = 0.0;
+
+ /* Construct PgFdwPathExtraData */
+ fpextra = (PgFdwPathExtraData *) palloc0(sizeof(PgFdwPathExtraData));
+ fpextra->target = root->upper_targets[UPPERREL_ORDERED];
+ fpextra->has_final_sort = true;
+
+ /* Estimate the costs of performing the final sort remotely */
+ estimate_path_cost_size(root, input_rel, NIL, root->sort_pathkeys, fpextra,
+ &rows, &width, &startup_cost, &total_cost);
+
+ /*
+ * Build the fdw_private list that will be used by postgresGetForeignPlan.
+ * Items in the list must match order in enum FdwPathPrivateIndex.
+ */
+ fdw_private = list_make1(makeInteger(true));
+
+ /* Create foreign ordering ForeignPath */
+ ordered_path = create_foreign_upper_path(root,
+ input_rel,
+ root->upper_targets[UPPERREL_ORDERED],
+ rows,
+ startup_cost,
+ total_cost,
+ root->sort_pathkeys,
+ NULL, /* no extra plan */
+ fdw_private);
+
+ /* and add it to the ordered_rel */
+ add_path(ordered_rel, (Path *) ordered_path);
+}
+
/*
* Create a tuple from the specified row of the PGresult.
*
@@ -5807,3 +6079,65 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
/* We didn't find any suitable equivalence class expression */
return NULL;
}
+
+/*
+ * Find an equivalence class member expression to be computed as a sort column
+ * in the given target.
+ */
+Expr *
+find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target)
+{
+ ListCell *lc1;
+ int i;
+
+ i = 0;
+ foreach(lc1, target->exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc1);
+ Index sgref = get_pathtarget_sortgroupref(target, i);
+ ListCell *lc2;
+
+ /* Ignore non-sort expressions */
+ if (sgref == 0 ||
+ get_sortgroupref_clause_noerr(sgref,
+ root->parse->sortClause) == NULL)
+ {
+ i++;
+ continue;
+ }
+
+ /* We ignore binary-compatible relabeling on both ends */
+ while (expr && IsA(expr, RelabelType))
+ expr = ((RelabelType *) expr)->arg;
+
+ /* Locate an EquivalenceClass member matching this expr, if any */
+ foreach(lc2, ec->ec_members)
+ {
+ EquivalenceMember *em = (EquivalenceMember *) lfirst(lc2);
+ Expr *em_expr;
+
+ /* Don't match constants */
+ if (em->em_is_const)
+ continue;
+
+ /* Ignore child members */
+ if (em->em_is_child)
+ continue;
+
+ /* Match if same expression (after stripping relabel) */
+ em_expr = em->em_expr;
+ while (em_expr && IsA(em_expr, RelabelType))
+ em_expr = ((RelabelType *) em_expr)->arg;
+
+ if (equal(em_expr, expr))
+ return em->em_expr;
+ }
+
+ i++;
+ }
+
+ elog(ERROR, "could not find pathkey item to sort");
+ return NULL; /* keep compiler quiet */
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 3f50103285..e9cbbe6234 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -49,6 +49,9 @@ typedef struct PgFdwRelationInfo
/* Bitmap of attr numbers we need to fetch from the remote server. */
Bitmapset *attrs_used;
+ /* True means that the query_pathkeys is safe to push down */
+ bool qp_is_pushdown_safe;
+
/* Cost and selectivity of local_conds. */
QualCost local_conds_cost;
Selectivity local_conds_sel;
@@ -92,6 +95,9 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /* Upper relation information */
+ UpperRelationKind stage;
+
/* Grouping information */
List *grouped_tlist;
@@ -175,10 +181,14 @@ extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
List **retrieved_attrs);
extern void deparseStringLiteral(StringInfo buf, const char *val);
extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
+extern Expr *find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target);
extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, List *tlist,
- List *remote_conds, List *pathkeys, bool is_subquery,
+ List *remote_conds, List *pathkeys,
+ bool has_final_sort, bool is_subquery,
List **retrieved_attrs, List **params_list);
extern const char *get_jointype_name(JoinType jointype);
--
2.19.2
--------------080806040701020705050304
Content-Type: text/x-patch;
name="v7-0002-Refactor-create_limit_path-to-share-cost-adjustment-.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename*0="v7-0002-Refactor-create_limit_path-to-share-cost-adjustment-";
filename*1=".patch"
^ permalink raw reply [nested|flat] 26+ messages in thread
* [PATCH 1/4] postgres_fdw: Perform UPPERREL_ORDERED step remotely
@ 2019-03-27 07:10 Etsuro Fujita <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Etsuro Fujita @ 2019-03-27 07:10 UTC (permalink / raw)
---
contrib/postgres_fdw/deparse.c | 28 +-
.../postgres_fdw/expected/postgres_fdw.out | 167 +++-----
contrib/postgres_fdw/postgres_fdw.c | 390 ++++++++++++++++--
contrib/postgres_fdw/postgres_fdw.h | 12 +-
4 files changed, 461 insertions(+), 136 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 92a0ab6da5..97dd07bee8 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -167,7 +167,8 @@ static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
deparse_expr_cxt *context);
static void deparseLockingClause(deparse_expr_cxt *context);
-static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
+static void appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context);
static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
@@ -929,8 +930,8 @@ build_tlist_to_deparse(RelOptInfo *foreignrel)
void
deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
List *tlist, List *remote_conds, List *pathkeys,
- bool is_subquery, List **retrieved_attrs,
- List **params_list)
+ bool has_final_sort, bool is_subquery,
+ List **retrieved_attrs, List **params_list)
{
deparse_expr_cxt context;
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
@@ -985,7 +986,7 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
/* Add ORDER BY clause if we found any useful pathkeys */
if (pathkeys)
- appendOrderByClause(pathkeys, &context);
+ appendOrderByClause(pathkeys, has_final_sort, &context);
/* Add any necessary FOR UPDATE/SHARE. */
deparseLockingClause(&context);
@@ -1590,7 +1591,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse the subquery representing the relation. */
appendStringInfoChar(buf, '(');
deparseSelectStmtForRel(buf, root, foreignrel, NIL,
- fpinfo->remote_conds, NIL, true,
+ fpinfo->remote_conds, NIL, false, true,
&retrieved_attrs, params_list);
appendStringInfoChar(buf, ')');
@@ -3109,7 +3110,8 @@ appendGroupByClause(List *tlist, deparse_expr_cxt *context)
* base relation are obtained and deparsed.
*/
static void
-appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
+appendOrderByClause(List *pathkeys, bool has_final_sort,
+ deparse_expr_cxt *context)
{
ListCell *lcell;
int nestlevel;
@@ -3126,7 +3128,19 @@ appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
PathKey *pathkey = lfirst(lcell);
Expr *em_expr;
- em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+ if (has_final_sort)
+ {
+ /*
+ * By construction, context->foreignrel is the input relation to
+ * the final sort.
+ */
+ em_expr = find_em_expr_for_input_target(context->root,
+ pathkey->pk_eclass,
+ context->foreignrel->reltarget);
+ }
+ else
+ em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
+
Assert(em_expr != NULL);
appendStringInfoString(buf, delim);
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 9ad9035c5d..9532a19056 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2552,18 +2552,13 @@ DROP ROLE regress_view_owner;
-- Simple aggregates
explain (verbose, costs off)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
- Result
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), ((sum(c1)) * ((random() <= '1'::double precision))::integer), c2
- -> Sort
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Sort Key: (count(ft1.c6)), (sum(ft1.c1))
- -> Foreign Scan
- Output: (count(c6)), (sum(c1)), (avg(c1)), (min(c2)), (max(c1)), (stddev(c2)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT count(c6), sum("C 1"), avg("C 1"), min(c2), max("C 1"), stddev(c2), c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) GROUP BY 7 ORDER BY count(c6) ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(4 rows)
select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2;
count | sum | avg | min | max | stddev | sum2
@@ -2621,16 +2616,13 @@ select sum(t1.c1), count(t2.c1) from ft1 t1 inner join ft2 t2 on (t1.c1 = t2.c1)
-- GROUP BY clause having expressions
explain (verbose, costs off)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Sort Key: ((ft1.c2 / 2))
- -> Foreign Scan
- Output: ((c2 / 2)), ((sum(c2) * (c2 / 2)))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT (c2 / 2), (sum(c2) * (c2 / 2)) FROM "S 1"."T 1" GROUP BY 1 ORDER BY (c2 / 2) ASC NULLS LAST
+(4 rows)
select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
?column? | ?column?
@@ -2645,18 +2637,15 @@ select c2/2, sum(c2) * (c2/2) from ft1 group by c2/2 order by c2/2;
-- Aggregates in subquery are pushed down.
explain (verbose, costs off)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
- QUERY PLAN
----------------------------------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------
Aggregate
Output: count(ft1.c2), sum(ft1.c2)
- -> Sort
+ -> Foreign Scan
Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Sort Key: ft1.c2, (sum(ft1.c1))
- -> Foreign Scan
- Output: ft1.c2, (sum(ft1.c1)), (sqrt((ft1.c1)::double precision))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3
-(9 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, sum("C 1"), sqrt("C 1") FROM "S 1"."T 1" GROUP BY 1, 3 ORDER BY c2 ASC NULLS LAST, sum("C 1") ASC NULLS LAST
+(6 rows)
select count(x.a), sum(x.a) from (select c2 a, sum(c1) b from ft1 group by c2, sqrt(c1) order by 1, 2) x;
count | sum
@@ -2742,16 +2731,13 @@ select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2
-- Also, ORDER BY contains an aggregate function
explain (verbose, costs off)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
- QUERY PLAN
------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, c2, (sum(c1))
- Sort Key: (sum(ft1.c1))
- -> Foreign Scan
- Output: c2, c2, (sum(c1))
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT c2, c2, sum("C 1") FROM "S 1"."T 1" WHERE ((c2 > 6)) GROUP BY 1, 2 ORDER BY sum("C 1") ASC NULLS LAST
+(4 rows)
select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
c2 | c2
@@ -2764,16 +2750,13 @@ select c2, c2 from ft1 where c2 > 6 group by 1, 2 order by sum(c1);
-- Testing HAVING clause shippability
explain (verbose, costs off)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: c2, (sum(c1))
- Sort Key: ft2.c2
- -> Foreign Scan
- Output: c2, (sum(c1))
- Relations: Aggregate on (public.ft2)
- Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800))
-(7 rows)
+ Relations: Aggregate on (public.ft2)
+ Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 HAVING ((avg("C 1") < 500::numeric)) AND ((sum("C 1") < 49800)) ORDER BY c2 ASC NULLS LAST
+(4 rows)
select c2, sum(c1) from ft2 group by c2 having avg(c1) < 500 and sum(c1) < 49800 order by c2;
c2 | sum
@@ -2823,16 +2806,13 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
-- ORDER BY within aggregate, same column used to order
explain (verbose, costs off)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(c1 ORDER BY c1)), c2
- Sort Key: (array_agg(ft1.c1 ORDER BY ft1.c1))
- -> Foreign Scan
- Output: (array_agg(c1 ORDER BY c1)), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST), c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) GROUP BY 2 ORDER BY array_agg("C 1" ORDER BY "C 1" ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(c1 order by c1) from ft1 where c1 < 100 group by c2 order by 1;
array_agg
@@ -2869,16 +2849,13 @@ select array_agg(c5 order by c1 desc) from ft2 where c2 = 6 and c1 < 50;
-- DISTINCT within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5)), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5)) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2890,16 +2867,13 @@ select array_agg(distinct (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2
-- DISTINCT combined with ORDER BY within aggregate
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5)))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5))), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) ASC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2910,16 +2884,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5) from ft4 t1 full join ft
explain (verbose, costs off)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Sort Key: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST))
- -> Foreign Scan
- Output: (array_agg(DISTINCT (t1.c1 % 5) ORDER BY (t1.c1 % 5) DESC NULLS LAST)), ((t2.c1 % 3))
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST), (r2.c1 % 3) FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) WHERE (((r1.c1 < 20) OR ((r1.c1 IS NULL) AND (r2.c1 < 5)))) GROUP BY 2 ORDER BY array_agg(DISTINCT (r1.c1 % 5) ORDER BY ((r1.c1 % 5)) DESC NULLS LAST) ASC NULLS LAST
+(4 rows)
select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) where t1.c1 < 20 or (t1.c1 is null and t2.c1 < 5) group by (t2.c1)%3 order by 1;
array_agg
@@ -2931,16 +2902,13 @@ select array_agg(distinct (t1.c1)%5 order by (t1.c1)%5 desc nulls last) from ft4
-- FILTER within aggregate
explain (verbose, costs off)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
- QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Sort Key: (sum(ft1.c1) FILTER (WHERE ((ft1.c1 < 100) AND (ft1.c2 > 5))))
- -> Foreign Scan
- Output: (sum(c1) FILTER (WHERE ((c1 < 100) AND (c2 > 5)))), c2
- Relations: Aggregate on (public.ft1)
- Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2
-(7 rows)
+ Relations: Aggregate on (public.ft1)
+ Remote SQL: SELECT sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))), c2 FROM "S 1"."T 1" GROUP BY 2 ORDER BY sum("C 1") FILTER (WHERE (("C 1" < 100) AND (c2 > 5))) ASC NULLS LAST
+(4 rows)
select sum(c1) filter (where c1 < 100 and c2 > 5) from ft1 group by c2 order by 1 nulls last;
sum
@@ -3339,16 +3307,13 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
-- FULL join with IS NULL check in HAVING
explain (verbose, costs off)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
- QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Sort Key: (avg(t1.c1)), (sum(t2.c1))
- -> Foreign Scan
- Output: (avg(t1.c1)), (sum(t2.c1)), t2.c1
- Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
- Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL)))
-(7 rows)
+ Relations: Aggregate on ((public.ft4 t1) FULL JOIN (public.ft5 t2))
+ Remote SQL: SELECT avg(r1.c1), sum(r2.c1), r2.c1 FROM ("S 1"."T 3" r1 FULL JOIN "S 1"."T 4" r2 ON (((r1.c1 = r2.c1)))) GROUP BY 3 HAVING ((((avg(r1.c1) IS NULL) AND (sum(r2.c1) < 10)) OR (sum(r2.c1) IS NULL))) ORDER BY avg(r1.c1) ASC NULLS LAST, sum(r2.c1) ASC NULLS LAST
+(4 rows)
select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
avg | sum
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f387fac42..7bd30333f0 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -246,6 +246,25 @@ typedef struct PgFdwAnalyzeState
MemoryContext temp_cxt; /* context for per-tuple temporary data */
} PgFdwAnalyzeState;
+/*
+ * This enum describes what's kept in the fdw_private list for a ForeignPath.
+ * We store:
+ *
+ * 1) Boolean flag showing if the remote query has the final sort
+ */
+enum FdwPathPrivateIndex
+{
+ /* has-final-sort flag (as an integer Value node) */
+ FdwPathPrivateHasFinalSort
+};
+
+/* Struct for extra information passed to estimate_path_cost_size() */
+typedef struct
+{
+ PathTarget *target;
+ bool has_final_sort;
+} PgFdwPathExtraData;
+
/*
* Identify the attribute where data conversion fails.
*/
@@ -368,6 +387,7 @@ static void estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost);
static void get_remote_estimate(const char *sql,
@@ -376,6 +396,12 @@ static void get_remote_estimate(const char *sql,
int *width,
Cost *startup_cost,
Cost *total_cost);
+static void adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost);
static bool ec_member_matches_foreign(PlannerInfo *root, RelOptInfo *rel,
EquivalenceClass *ec, EquivalenceMember *em,
void *arg);
@@ -452,6 +478,9 @@ static void add_foreign_grouping_paths(PlannerInfo *root,
RelOptInfo *input_rel,
RelOptInfo *grouped_rel,
GroupPathExtraData *extra);
+static void add_foreign_ordered_paths(PlannerInfo *root,
+ RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel);
static void apply_server_options(PgFdwRelationInfo *fpinfo);
static void apply_table_options(PgFdwRelationInfo *fpinfo);
static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
@@ -637,7 +666,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* values in fpinfo so we don't need to do it again to generate the
* basic foreign path.
*/
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
@@ -668,7 +697,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
set_baserel_size_estimates(root, baserel);
/* Fill in basically-bogus cost estimates for use later. */
- estimate_path_cost_size(root, baserel, NIL, NIL,
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
&fpinfo->rows, &fpinfo->width,
&fpinfo->startup_cost, &fpinfo->total_cost);
}
@@ -827,6 +856,7 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
* Pushing the query_pathkeys to the remote server is always worth
* considering, because it might let us avoid a local sort.
*/
+ fpinfo->qp_is_pushdown_safe = false;
if (root->query_pathkeys)
{
bool query_pathkeys_ok = true;
@@ -857,7 +887,10 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
}
if (query_pathkeys_ok)
+ {
useful_pathkeys_list = list_make1(list_copy(root->query_pathkeys));
+ fpinfo->qp_is_pushdown_safe = true;
+ }
}
/*
@@ -1102,7 +1135,7 @@ postgresGetForeignPaths(PlannerInfo *root,
/* Get a cost estimate from the remote */
estimate_path_cost_size(root, baserel,
- param_info->ppi_clauses, NIL,
+ param_info->ppi_clauses, NIL, NULL,
&rows, &width,
&startup_cost, &total_cost);
@@ -1149,8 +1182,16 @@ postgresGetForeignPlan(PlannerInfo *root,
List *fdw_recheck_quals = NIL;
List *retrieved_attrs;
StringInfoData sql;
+ bool has_final_sort = false;
ListCell *lc;
+ /*
+ * Get FDW private data created by postgresGetForeignUpperPaths(), if any.
+ */
+ if (best_path->fdw_private)
+ has_final_sort = intVal(list_nth(best_path->fdw_private,
+ FdwPathPrivateHasFinalSort));
+
if (IS_SIMPLE_REL(foreignrel))
{
/*
@@ -1299,7 +1340,8 @@ postgresGetForeignPlan(PlannerInfo *root,
initStringInfo(&sql);
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
remote_exprs, best_path->path.pathkeys,
- false, &retrieved_attrs, ¶ms_list);
+ has_final_sort, false,
+ &retrieved_attrs, ¶ms_list);
/* Remember remote_exprs for possible use by postgresPlanDirectModify */
fpinfo->final_remote_exprs = remote_exprs;
@@ -2482,6 +2524,8 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es)
*
* param_join_conds are the parameterization clauses with outer relations.
* pathkeys specify the expected sort order if any for given path being costed.
+ * fpextra specifies additional post-scan/join-processing steps such as the
+ * final sort.
*
* The function returns the cost and size estimates in p_row, p_width,
* p_startup_cost and p_total_cost variables.
@@ -2491,6 +2535,7 @@ estimate_path_cost_size(PlannerInfo *root,
RelOptInfo *foreignrel,
List *param_join_conds,
List *pathkeys,
+ PgFdwPathExtraData *fpextra,
double *p_rows, int *p_width,
Cost *p_startup_cost, Cost *p_total_cost)
{
@@ -2555,8 +2600,9 @@ estimate_path_cost_size(PlannerInfo *root,
initStringInfo(&sql);
appendStringInfoString(&sql, "EXPLAIN ");
deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
- remote_conds, pathkeys, false,
- &retrieved_attrs, NULL);
+ remote_conds, pathkeys,
+ fpextra ? fpextra->has_final_sort : false,
+ false, &retrieved_attrs, NULL);
/* Get the remote estimate */
conn = GetConnection(fpinfo->user, false);
@@ -2624,9 +2670,9 @@ estimate_path_cost_size(PlannerInfo *root,
/*
* We will come here again and again with different set of pathkeys
- * that caller wants to cost. We don't need to calculate the cost of
- * bare scan each time. Instead, use the costs if we have cached them
- * already.
+ * that caller wants to cost. We don't need to calculate the costs of
+ * the underlying scan, join, or grouping each time. Instead, use the
+ * costs if we have cached them already.
*/
if (fpinfo->rel_startup_cost >= 0 && fpinfo->rel_total_cost >= 0)
{
@@ -2844,23 +2890,52 @@ estimate_path_cost_size(PlannerInfo *root,
*/
if (pathkeys != NIL)
{
- startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
- run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ if (IS_UPPER_REL(foreignrel))
+ {
+ Assert(foreignrel->reloptkind == RELOPT_UPPER_REL &&
+ fpinfo->stage == UPPERREL_GROUP_AGG);
+ adjust_foreign_grouping_path_cost(root, pathkeys,
+ retrieved_rows, width,
+ &startup_cost, &run_cost);
+ }
+ else
+ {
+ startup_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ run_cost *= DEFAULT_FDW_SORT_MULTIPLIER;
+ }
}
total_cost = startup_cost + run_cost;
}
/*
- * Cache the costs for scans without any pathkeys or parameterization
- * before adding the costs for transferring data from the foreign server.
- * These costs are useful for costing the join between this relation and
- * another foreign relation or to calculate the costs of paths with
- * pathkeys for this relation, when the costs can not be obtained from the
- * foreign server. This function will be called at least once for every
- * foreign relation without pathkeys and parameterization.
+ * If this includes the final sort step, the given target, which will be
+ * applied to the resulting path, might have different expressions from
+ * the foreignrel's reltarget (see make_sort_input_target()); adjust tlist
+ * eval costs.
*/
- if (pathkeys == NIL && param_join_conds == NIL)
+ if (fpextra && fpextra->target != foreignrel->reltarget)
+ {
+ QualCost oldcost = foreignrel->reltarget->cost;
+ QualCost newcost = fpextra->target->cost;
+
+ startup_cost += newcost.startup - oldcost.startup;
+ total_cost += newcost.startup - oldcost.startup;
+ total_cost += (newcost.per_tuple - oldcost.per_tuple) * rows;
+ }
+
+ /*
+ * Cache the costs for scans, joins, or groupings without any
+ * parameterization, pathkeys, or additional post-scan/join-processing
+ * steps, before adding the costs for transferring data from the foreign
+ * server. These costs are useful for costing remote joins involving this
+ * relation or costing other remote operations for this relation such as
+ * remote sorts, when the costs can not be obtained from the foreign
+ * server. This function will be called at least once for every foreign
+ * relation without any parameterization, pathkeys, or additional
+ * post-scan/join-processing steps.
+ */
+ if (pathkeys == NIL && param_join_conds == NIL && fpextra == NULL)
{
fpinfo->rel_startup_cost = startup_cost;
fpinfo->rel_total_cost = total_cost;
@@ -2935,6 +3010,57 @@ get_remote_estimate(const char *sql, PGconn *conn,
PG_END_TRY();
}
+/*
+ * Adjust the cost estimates of a foreign grouping path.
+ */
+static void
+adjust_foreign_grouping_path_cost(PlannerInfo *root,
+ List *pathkeys,
+ double retrieved_rows,
+ double width,
+ Cost *p_startup_cost,
+ Cost *p_run_cost)
+{
+ /*
+ * If the GROUP BY clause isn't sort-able, the plan chosen by the remote
+ * side is unlikely to generate properly-sorted output, so it would need
+ * an explicit sort; adjust the given costs with cost_sort(). Likewise,
+ * if the GROUP BY clause is sort-able but isn't a superset of the given
+ * pathkeys, adjust the costs with that function. Otherwise, adjust the
+ * costs by applying the same heuristic as for the scan or join case.
+ */
+ if (!grouping_is_sortable(root->parse->groupClause) ||
+ !pathkeys_contained_in(pathkeys, root->group_pathkeys))
+ {
+ Path sort_path; /* dummy for result of cost_sort */
+
+ cost_sort(&sort_path,
+ root,
+ pathkeys,
+ *p_startup_cost + *p_run_cost,
+ retrieved_rows,
+ width,
+ 0.0,
+ work_mem,
+ -1.0);
+
+ *p_startup_cost = sort_path.startup_cost;
+ *p_run_cost = sort_path.total_cost - sort_path.startup_cost;
+ }
+ else
+ {
+ /*
+ * The default extra cost seems too large for foreign-grouping cases;
+ * add 1/4th of that default.
+ */
+ double sort_multiplier = 1.0 + (DEFAULT_FDW_SORT_MULTIPLIER
+ - 1.0) * 0.25;
+
+ *p_startup_cost *= sort_multiplier;
+ *p_run_cost *= sort_multiplier;
+ }
+}
+
/*
* Detect whether we want to process an EquivalenceClass member.
*
@@ -4934,7 +5060,7 @@ add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
List *useful_pathkeys = lfirst(lc);
Path *sorted_epq_path;
- estimate_path_cost_size(root, rel, NIL, useful_pathkeys,
+ estimate_path_cost_size(root, rel, NIL, useful_pathkeys, NULL,
&rows, &width, &startup_cost, &total_cost);
/*
@@ -5185,8 +5311,8 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
extra->sjinfo);
/* Estimate costs for bare join relation */
- estimate_path_cost_size(root, joinrel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, joinrel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the joinrel */
joinrel->rows = rows;
joinrel->reltarget->width = width;
@@ -5455,15 +5581,29 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
return;
/* Ignore stages we don't support; and skip any duplicate calls. */
- if (stage != UPPERREL_GROUP_AGG || output_rel->fdw_private)
+ if ((stage != UPPERREL_GROUP_AGG &&
+ stage != UPPERREL_ORDERED) ||
+ output_rel->fdw_private)
return;
fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
fpinfo->pushdown_safe = false;
+ fpinfo->stage = stage;
output_rel->fdw_private = fpinfo;
- add_foreign_grouping_paths(root, input_rel, output_rel,
- (GroupPathExtraData *) extra);
+ switch (stage)
+ {
+ case UPPERREL_GROUP_AGG:
+ add_foreign_grouping_paths(root, input_rel, output_rel,
+ (GroupPathExtraData *) extra);
+ break;
+ case UPPERREL_ORDERED:
+ add_foreign_ordered_paths(root, input_rel, output_rel);
+ break;
+ default:
+ elog(ERROR, "unexpected upper relation: %d", (int) stage);
+ break;
+ }
}
/*
@@ -5533,8 +5673,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
cost_qual_eval(&fpinfo->local_conds_cost, fpinfo->local_conds, root);
/* Estimate the cost of push down */
- estimate_path_cost_size(root, grouped_rel, NIL, NIL, &rows,
- &width, &startup_cost, &total_cost);
+ estimate_path_cost_size(root, grouped_rel, NIL, NIL, NULL,
+ &rows, &width, &startup_cost, &total_cost);
/* Now update this information in the fpinfo */
fpinfo->rows = rows;
@@ -5542,6 +5682,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
fpinfo->startup_cost = startup_cost;
fpinfo->total_cost = total_cost;
+ grouped_rel->rows = fpinfo->rows;
+
/* Create and add foreign path to the grouping relation. */
grouppath = create_foreign_upper_path(root,
grouped_rel,
@@ -5557,6 +5699,138 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
add_path(grouped_rel, (Path *) grouppath);
}
+/*
+ * add_foreign_ordered_paths
+ * Add foreign paths for performing the final sort remotely.
+ *
+ * Given input_rel contains the source-data Paths. The paths are added to the
+ * given ordered_rel.
+ */
+static void
+add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel,
+ RelOptInfo *ordered_rel)
+{
+ Query *parse = root->parse;
+ PgFdwRelationInfo *ifpinfo = input_rel->fdw_private;
+ PgFdwRelationInfo *fpinfo = ordered_rel->fdw_private;
+ PgFdwPathExtraData *fpextra;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+ ForeignPath *ordered_path;
+ ListCell *lc;
+
+ /* Shouldn't get here unless the query has ORDER BY */
+ Assert(parse->sortClause);
+
+ /* We don't support cases where there are any SRFs in the targetlist */
+ if (parse->hasTargetSRFs)
+ return;
+
+ /* Save the input_rel as outerrel in fpinfo */
+ fpinfo->outerrel = input_rel;
+
+ /*
+ * Copy foreign table, foreign server, user mapping, FDW options etc.
+ * details from the input relation's fpinfo.
+ */
+ fpinfo->table = ifpinfo->table;
+ fpinfo->server = ifpinfo->server;
+ fpinfo->user = ifpinfo->user;
+ merge_fdw_options(fpinfo, ifpinfo, NULL);
+
+ /*
+ * If the input_rel is a base or join relation, we would already have
+ * considered pushing down the final sort to the remote server when
+ * creating pre-sorted foreign paths for that relation, because the
+ * query_pathkeys is set to the root->sort_pathkeys in that case (see
+ * standard_qp_callback()).
+ */
+ if (input_rel->reloptkind == RELOPT_BASEREL ||
+ input_rel->reloptkind == RELOPT_JOINREL)
+ {
+ Assert(root->query_pathkeys == root->sort_pathkeys);
+
+ /* Safe to push down if the query_pathkeys is safe to push down */
+ fpinfo->pushdown_safe = ifpinfo->qp_is_pushdown_safe;
+
+ return;
+ }
+
+ /* The input_rel should be a grouping relation */
+ Assert(input_rel->reloptkind == RELOPT_UPPER_REL &&
+ ifpinfo->stage == UPPERREL_GROUP_AGG);
+
+ /*
+ * We try to create a path below by extending a simple foreign path for
+ * the underlying grouping relation to perform the final sort remotely,
+ * which is stored into the fdw_private list of the resulting path.
+ */
+
+ /* Assess if it is safe to push down the final sort */
+ foreach(lc, root->sort_pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
+ Expr *sort_expr;
+
+ /*
+ * is_foreign_expr would detect volatile expressions as well, but
+ * checking ec_has_volatile here saves some cycles.
+ */
+ if (pathkey_ec->ec_has_volatile)
+ return;
+
+ /* Get the sort expression for the pathkey_ec */
+ sort_expr = find_em_expr_for_input_target(root,
+ pathkey_ec,
+ input_rel->reltarget);
+
+ /* If it's unsafe to remote, we cannot push down the final sort */
+ if (!is_foreign_expr(root, input_rel, sort_expr))
+ return;
+ }
+
+ /* Safe to push down */
+ fpinfo->pushdown_safe = true;
+
+ /* Initialize the selectivity and cost of local_conds */
+ fpinfo->local_conds_sel = 1.0;
+ fpinfo->local_conds_cost.startup = 0.0;
+ fpinfo->local_conds_cost.per_tuple = 0.0;
+
+ /* Construct PgFdwPathExtraData */
+ fpextra = (PgFdwPathExtraData *) palloc0(sizeof(PgFdwPathExtraData));
+ fpextra->target = root->upper_targets[UPPERREL_ORDERED];
+ fpextra->has_final_sort = true;
+
+ /* Estimate the costs of performing the final sort remotely */
+ estimate_path_cost_size(root, input_rel, NIL, root->sort_pathkeys, fpextra,
+ &rows, &width, &startup_cost, &total_cost);
+
+ /*
+ * Build the fdw_private list that will be used by postgresGetForeignPlan.
+ * Items in the list must match order in enum FdwPathPrivateIndex.
+ */
+ fdw_private = list_make1(makeInteger(true));
+
+ /* Create foreign ordering ForeignPath */
+ ordered_path = create_foreign_upper_path(root,
+ input_rel,
+ root->upper_targets[UPPERREL_ORDERED],
+ rows,
+ startup_cost,
+ total_cost,
+ root->sort_pathkeys,
+ NULL, /* no extra plan */
+ fdw_private);
+
+ /* and add it to the ordered_rel */
+ add_path(ordered_rel, (Path *) ordered_path);
+}
+
/*
* Create a tuple from the specified row of the PGresult.
*
@@ -5807,3 +6081,65 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
/* We didn't find any suitable equivalence class expression */
return NULL;
}
+
+/*
+ * Find an equivalence class member expression to be computed as a sort column
+ * in the given target.
+ */
+Expr *
+find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target)
+{
+ ListCell *lc1;
+ int i;
+
+ i = 0;
+ foreach(lc1, target->exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc1);
+ Index sgref = get_pathtarget_sortgroupref(target, i);
+ ListCell *lc2;
+
+ /* Ignore non-sort expressions */
+ if (sgref == 0 ||
+ get_sortgroupref_clause_noerr(sgref,
+ root->parse->sortClause) == NULL)
+ {
+ i++;
+ continue;
+ }
+
+ /* We ignore binary-compatible relabeling on both ends */
+ while (expr && IsA(expr, RelabelType))
+ expr = ((RelabelType *) expr)->arg;
+
+ /* Locate an EquivalenceClass member matching this expr, if any */
+ foreach(lc2, ec->ec_members)
+ {
+ EquivalenceMember *em = (EquivalenceMember *) lfirst(lc2);
+ Expr *em_expr;
+
+ /* Don't match constants */
+ if (em->em_is_const)
+ continue;
+
+ /* Ignore child members */
+ if (em->em_is_child)
+ continue;
+
+ /* Match if same expression (after stripping relabel) */
+ em_expr = em->em_expr;
+ while (em_expr && IsA(em_expr, RelabelType))
+ em_expr = ((RelabelType *) em_expr)->arg;
+
+ if (equal(em_expr, expr))
+ return em->em_expr;
+ }
+
+ i++;
+ }
+
+ elog(ERROR, "could not find pathkey item to sort");
+ return NULL; /* keep compiler quiet */
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 3f50103285..e9cbbe6234 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -49,6 +49,9 @@ typedef struct PgFdwRelationInfo
/* Bitmap of attr numbers we need to fetch from the remote server. */
Bitmapset *attrs_used;
+ /* True means that the query_pathkeys is safe to push down */
+ bool qp_is_pushdown_safe;
+
/* Cost and selectivity of local_conds. */
QualCost local_conds_cost;
Selectivity local_conds_sel;
@@ -92,6 +95,9 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /* Upper relation information */
+ UpperRelationKind stage;
+
/* Grouping information */
List *grouped_tlist;
@@ -175,10 +181,14 @@ extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
List **retrieved_attrs);
extern void deparseStringLiteral(StringInfo buf, const char *val);
extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
+extern Expr *find_em_expr_for_input_target(PlannerInfo *root,
+ EquivalenceClass *ec,
+ PathTarget *target);
extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, List *tlist,
- List *remote_conds, List *pathkeys, bool is_subquery,
+ List *remote_conds, List *pathkeys,
+ bool has_final_sort, bool is_subquery,
List **retrieved_attrs, List **params_list);
extern const char *get_jointype_name(JoinType jointype);
--
2.19.2
--------------060101040300010105040500
Content-Type: text/x-patch;
name="v8-0002-postgres_fdw-Modify-regression-test-case-for-EPQ-pla.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename*0="v8-0002-postgres_fdw-Modify-regression-test-case-for-EPQ-pla";
filename*1=".patch"
^ permalink raw reply [nested|flat] 26+ messages in thread
* POC: PLpgSQL FOREACH IN JSON ARRAY
@ 2026-02-28 07:10 Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
0 siblings, 2 replies; 26+ messages in thread
From: Pavel Stehule @ 2026-02-28 07:10 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>
Hi
I wrote PoC for previously proposed plpgsql statement FOREACH IN JSON ARRAY
It looks like:
do $$
declare x int;
begin
foreach x in json array '[1,2,3,4]'
loop
raise notice 'x: %', x;
end loop;
end;
$$
do $$
declare x int; y int;
begin
foreach x, y in json array '[{"x": 100, "y": 1000}, {"y": 1000, "x":
100}]'
loop
raise notice 'x: %, y: %', x, y;
end loop;
end
$$
My first motivation for this patch is performance. This is faster (3 - 4 x)
than using FOR IN SELECT FROM json_array_elements, because there is no
overhead of SQL executor. Second motivation is a little bit better
readability, because inside plpgsql' statements we have info about used
variables and we can use it.
The behavior is very similar to FOREACH IN ARRAY with one significant
difference - the values of JSON objects are assigned to the composite
variable or lists of variables by names (not by position). It made this
decision because jsonb doesn't preserve the position of the field in
object, and then assignment based on position cannot work.
The code is relatively short now - about 400 lines +/- and the code is
simple without risks.
There are some open questions - mainly if default mode for mapping json
fields to plpgsql variables should be in lax or strict mode. Now, it is
something between (cast errors are raised) - it is consistent
with jsonb_populate_record - but it should not be the final design. I
cannot say what is better - currently implemented behavior is consistent
with common plpgsql behaviour, but SQL/JSON is different. I can imagine
that default behaviour will be lax, and with some optional clauses we can
push behave to strict mode. I have no strong opinion about it. Maybe I
prefer the current "strict" behaviour a little bit, because it is more
"safe", but it is only my personal opinion. But again, I have no strong
opinion about this question and I very much invite any discussion about it.
This is proof of concept patch - casting between plpgsql arrays and json
arrays is not supported, documentation and regress tests are minimalistic,
but it is good enough for testing and good enough for decision, if this
feature is wanted or not (or if it needs some modifications).
This is a new feature (and proprietary feature). There should not be any
compatibility issues.
What do you think about this feature?
Regards
Pavel
Attachments:
[text/x-patch] v20260228-1-0001-FOREACH-scalar-IN-JSON-ARRAY.patch (28.0K, ../../CAFj8pRD9Jjv+m=6DP6vWWn5NnZeTPH9eoPFCnA1JE5hRKRDMxQ@mail.gmail.com/3-v20260228-1-0001-FOREACH-scalar-IN-JSON-ARRAY.patch)
download | inline diff:
From c9f041e5408351961425eabeef161c4b72b93d1c Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN JSON ARRAY
this patch introduce FOREACH scalar_var IN JSON ARRAY. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN JSON ARRAY '[1,2,3.14]'
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN JSON ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 63 ++++
src/backend/utils/adt/jsonb_util.c | 1 +
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 119 +++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_comp.c | 4 +-
src/pl/plpgsql/src/pl_exec.c | 296 +++++++++++++++++-
src/pl/plpgsql/src/pl_funcs.c | 29 ++
src/pl/plpgsql/src/pl_gram.y | 39 ++-
src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 +
src/pl/plpgsql/src/plpgsql.h | 35 ++-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 89 ++++++
12 files changed, 669 insertions(+), 10 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..c11f44676bf 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2780,6 +2780,69 @@ NOTICE: row = {10,11,12}
</para>
</sect2>
+ <sect2 id="plpgsql-foreach-json-array">
+ <title>Looping through JSON arrays</title>
+
+ <para>
+ The <literal>FOREACH</literal> loop is much like a <literal>FOREACH</literal> loop,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of an JSON array value
+ (expression is internaly casted to jsonb type).
+
+<synopsis>
+<optional> <<<replaceable>label</replaceable>>> </optional>
+FOREACH <replaceable>target</replaceable> IN JSON ARRAY <replaceable>expression</replaceable> LOOP
+ <replaceable>statements</replaceable>
+END LOOP <optional> <replaceable>label</replaceable> </optional>;
+</synopsis>
+ </para>
+
+ <para>
+ Target can be scalar variable, composite variable or list of
+ scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+
+ <para>
+ Assigning to PLpgSQL array variables is not supported.
+ </para>
+ </sect2>
+
<sect2 id="plpgsql-error-trapping">
<title>Trapping Errors</title>
diff --git a/src/backend/utils/adt/jsonb_util.c b/src/backend/utils/adt/jsonb_util.c
index 91fb9ea09bf..7513b1198dd 100644
--- a/src/backend/utils/adt/jsonb_util.c
+++ b/src/backend/utils/adt/jsonb_util.c
@@ -18,6 +18,7 @@
#include "common/hashfn.h"
#include "miscadmin.h"
#include "port/pg_bitutils.h"
+#include "utils/builtins.h"
#include "utils/date.h"
#include "utils/datetime.h"
#include "utils/datum.h"
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..43ae84412f1
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,119 @@
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index 5ecc7766757..8fa60976cfd 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -1253,6 +1253,7 @@ make_datum_param(PLpgSQL_expr *expr, int dno, int location)
PLpgSQL_datum *datum;
Param *param;
MemoryContext oldcontext;
+ char *refname;
/* see comment in resolve_column_ref */
estate = expr->func->cur_estate;
@@ -1273,7 +1274,8 @@ make_datum_param(PLpgSQL_expr *expr, int dno, int location)
datum,
¶m->paramtype,
¶m->paramtypmod,
- ¶m->paramcollid);
+ ¶m->paramcollid,
+ &refname);
param->location = location;
return (Node *) param;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 84552e32c87..3383b42c39a 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -41,6 +41,8 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
@@ -305,6 +307,8 @@ static int exec_stmt_forc(PLpgSQL_execstate *estate,
PLpgSQL_stmt_forc *stmt);
static int exec_stmt_foreach_a(PLpgSQL_execstate *estate,
PLpgSQL_stmt_foreach_a *stmt);
+static int exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt);
static int exec_stmt_open(PLpgSQL_execstate *estate,
PLpgSQL_stmt_open *stmt);
static int exec_stmt_fetch(PLpgSQL_execstate *estate,
@@ -2075,6 +2079,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
rc = exec_stmt_foreach_a(estate, (PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ rc = exec_stmt_foreach_json_a(estate, (PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
+
case PLPGSQL_STMT_EXIT:
rc = exec_stmt_exit(estate, (PLpgSQL_stmt_exit *) stmt);
break;
@@ -2995,6 +3003,240 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
}
+/*
+ * Convert JsonbValue to Datum that can be assigned to PLpgSQL_var.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ if (expected_typid == JSONBOID)
+ {
+ *typid = JSONBOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(JsonbValueToJsonb(jbv));
+ }
+ else if (expected_typid == JSONOID)
+ {
+ Jsonb *jsonb;
+ char *str;
+
+ /* serialize JsonValue to JSON text */
+ jsonb = JsonbValueToJsonb(jbv);
+ str = JsonbToCString(NULL, &jsonb->root, VARSIZE(jsonb));
+
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text(str));
+ }
+ else if (jbv->type == jbvNull)
+ {
+ *typid = expected_typid;
+ *typmod = -1;
+ *isnull = true;
+
+ return (Datum) 0;
+ }
+ else if (jbv->type == jbvString)
+ {
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text_with_len(jbv->val.string.val, jbv->val.string.len));
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ *typid = NUMERICOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(jbv->val.numeric);
+ }
+ else if (jbv->type == jbvBool)
+ {
+ *typid = BOOLOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return BoolGetDatum(jbv->val.boolean);
+ }
+ else
+ {
+ Jsonb *jsonb;
+ Datum result;
+
+ jsonb = JsonbValueToJsonb(jbv);
+ result = json_populate_type(PointerGetDatum(jsonb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+
+ return result;
+ }
+}
+
+/* ----------
+ * exec_stmt_foreach_json_a Loop over elements in json array
+ *
+ * When target is a composite, then target is populated like json_to_populate_record.
+ * jsonb doesn't preserve attribute order, so position based mapping between
+ * target and source can be possibly dangerous (with unexpected behave).
+ * ----------
+ */
+static int
+exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ Oid exprtypeid;
+ int32 exprtypmod;
+ Datum exprdatum;
+ PLpgSQL_datum *loop_var;
+ Oid loop_var_typid;
+ int32 loop_var_typmod;
+ Oid loop_var_collation;
+ char *loop_var_name;
+ Jsonb *jb;
+ JsonbIterator *it;
+ JsonbValue jbv;
+ JsonbIteratorToken r;
+ MemoryContext stmt_mcontext;
+ MemoryContext oldcontext;
+ MemoryContext tmp_cxt;
+ bool found = false;
+ bool isnull;
+ bool skipNested = false;
+ int rc = PLPGSQL_RC_OK;
+ void *cache = NULL;
+
+ /* get the value of the expression */
+ exprdatum = exec_eval_expr(estate, stmt->expr, &isnull,
+ &exprtypeid, &exprtypmod);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("FOREACH expression must not be null")));
+
+ /*
+ * Do as much as possible of the code below in stmt_mcontext, to avoid any
+ * leaks from called subroutines. We need a private stmt_mcontext since
+ * we'll be calling arbitrary statement code.
+ */
+ stmt_mcontext = get_stmt_mcontext(estate);
+ push_stmt_mcontext(estate);
+ oldcontext = MemoryContextSwitchTo(stmt_mcontext);
+
+ tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "FOREACH IN JSON ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* cast to jsonb */
+ exprdatum = exec_cast_value(estate, exprdatum, &isnull,
+ exprtypeid, exprtypmod,
+ JSONBOID, -1);
+
+ Assert(!isnull);
+
+ /*
+ * We must copy the array into stmt_mcontext, else it will disappear in
+ * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
+ * while running the loop body, so we have little choice.
+ */
+ jb = DatumGetJsonbPCopy(exprdatum);
+
+ /* Clean up any leftover temporary memory */
+ exec_eval_cleanup(estate);
+
+ /*
+ * This is compatible with jsonb_array_element. SQL/JSON functions are not
+ * too strict like PostgreSQL proprietary (old json) functions. In SQL/JSON
+ * a scalar is equal to one element array. The basic question is if FOREACH
+ * should be more restrictive like old JSON function, or less restrictive
+ * like SQL/JSON functions.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot extract elements from a scalar")));
+ else if (!JB_ROOT_IS_ARRAY(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot extract elements from an object")));
+
+ /* Set up the loop variable and see if it is of an array type */
+ loop_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, loop_var,
+ &loop_var_typid, &loop_var_typmod,
+ &loop_var_collation, &loop_var_name);
+
+ it = JsonbIteratorInit(&jb->root);
+
+ while ((r = JsonbIteratorNext(&it, &jbv, skipNested)) != WJB_DONE)
+ {
+ skipNested = true;
+
+ if (r == WJB_ELEM)
+ {
+ Datum val;
+ Oid valtypid;
+ int32 valtypmod;
+ bool valisnull;
+
+ MemoryContextSwitchTo(tmp_cxt);
+
+ val = JsonbValueToDatum(&jbv,
+ &valtypid, &valtypmod, &valisnull,
+ loop_var_typid, loop_var_typmod,
+ &cache, stmt_mcontext);
+
+ /* exec_assign_value and exec_stmts must run in the main context */
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Assign current element/slice to the loop variable */
+ exec_assign_value(estate, loop_var, val,
+ valisnull, valtypid, valtypmod);
+
+ MemoryContextReset(tmp_cxt);
+
+ /*
+ * Execute the statements
+ */
+ rc = exec_stmts(estate, stmt->body);
+
+ LOOP_RC_PROCESSING(stmt->label, break);
+
+ MemoryContextSwitchTo(stmt_mcontext);
+ }
+ }
+
+ /* Restore memory context state */
+ MemoryContextSwitchTo(oldcontext);
+ pop_stmt_mcontext(estate);
+
+ /* Release temporary memory, including the array value */
+ MemoryContextReset(stmt_mcontext);
+
+ /*
+ * Set the FOUND variable to indicate the result of executing the loop
+ * (namely, whether we looped one or more times). This must be set here so
+ * that it does not interfere with the value of the FOUND variable inside
+ * the loop processing itself.
+ */
+ exec_set_found(estate, found);
+
+ return rc;
+}
+
/* ----------
* exec_stmt_foreach_a Loop over elements or slices of an array
*
@@ -5522,7 +5764,8 @@ plpgsql_exec_get_datum_type(PLpgSQL_execstate *estate,
void
plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
PLpgSQL_datum *datum,
- Oid *typeId, int32 *typMod, Oid *collation)
+ Oid *typeId, int32 *typMod, Oid *collation,
+ char **refname)
{
switch (datum->dtype)
{
@@ -5534,6 +5777,54 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
*typeId = var->datatype->typoid;
*typMod = var->datatype->atttypmod;
*collation = var->datatype->collation;
+ *refname = var->refname;
+ break;
+ }
+
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+ char *varname;
+
+ /*
+ * We cannot to use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation, &varname);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ varname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ *refname = row->refname;
break;
}
@@ -5556,6 +5847,7 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
}
/* composite types are never collatable */
*collation = InvalidOid;
+ *refname = rec->refname;
break;
}
@@ -5593,6 +5885,7 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
*typeId = recfield->finfo.ftypeid;
*typMod = recfield->finfo.ftypmod;
*collation = recfield->finfo.fcollation;
+ *refname = recfield->fieldname;
break;
}
@@ -5601,6 +5894,7 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
*typeId = InvalidOid; /* keep compiler quiet */
*typMod = -1;
*collation = InvalidOid;
+ *refname = NULL;
break;
}
}
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index 92cd9116c0e..7511fab7e68 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -253,6 +253,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
return _("FOR over cursor");
case PLPGSQL_STMT_FOREACH_A:
return _("FOREACH over array");
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ return _("FOREACH over json array");
case PLPGSQL_STMT_EXIT:
return ((PLpgSQL_stmt_exit *) stmt)->is_exit ? "EXIT" : "CONTINUE";
case PLPGSQL_STMT_RETURN:
@@ -467,6 +469,14 @@ plpgsql_statement_tree_walker_impl(PLpgSQL_stmt *stmt,
{
PLpgSQL_stmt_foreach_a *fstmt = (PLpgSQL_stmt_foreach_a *) stmt;
+ E_WALK(fstmt->expr);
+ S_LIST_WALK(fstmt->body);
+ break;
+ }
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ {
+ PLpgSQL_stmt_foreach_json_a *fstmt = (PLpgSQL_stmt_foreach_json_a *) stmt;
+
E_WALK(fstmt->expr);
S_LIST_WALK(fstmt->body);
break;
@@ -795,6 +805,7 @@ static void dump_fori(PLpgSQL_stmt_fori *stmt);
static void dump_fors(PLpgSQL_stmt_fors *stmt);
static void dump_forc(PLpgSQL_stmt_forc *stmt);
static void dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt);
+static void dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt);
static void dump_exit(PLpgSQL_stmt_exit *stmt);
static void dump_return(PLpgSQL_stmt_return *stmt);
static void dump_return_next(PLpgSQL_stmt_return_next *stmt);
@@ -861,6 +872,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
case PLPGSQL_STMT_FOREACH_A:
dump_foreach_a((PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ dump_foreach_json_a((PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
case PLPGSQL_STMT_EXIT:
dump_exit((PLpgSQL_stmt_exit *) stmt);
break;
@@ -1157,6 +1171,21 @@ dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt)
printf(" ENDFOREACHA");
}
+static void
+dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ dump_ind();
+ printf("FOREACHA var %d ", stmt->varno);
+ printf("IN JSON ARRAY ");
+ dump_expr(stmt->expr);
+ printf("\n");
+
+ dump_stmts(stmt->body);
+
+ dump_ind();
+ printf(" ENDFOREACHA");
+}
+
static void
dump_open(PLpgSQL_stmt_open *stmt)
{
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 5009e59a78f..23b465b10d5 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -178,6 +178,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
PLpgSQL_diag_item *diagitem;
PLpgSQL_stmt_fetch *fetch;
PLpgSQL_case_when *casewhen;
+ PLpgSQL_stmt_foreach *foreach;
}
%type <declhdr> decl_sect
@@ -220,6 +221,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%type <casewhen> case_when
%type <list> case_when_list opt_case_else
+%type <foreach> foreach_type
%type <boolean> getdiag_area_opt
%type <list> getdiag_list
@@ -341,6 +343,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%token <keyword> K_PRINT_STRICT_PARAMS
%token <keyword> K_PRIOR
%token <keyword> K_QUERY
+%token <keyword> K_JSON
%token <keyword> K_RAISE
%token <keyword> K_RELATIVE
%token <keyword> K_RETURN
@@ -1671,16 +1674,29 @@ for_variable : T_DATUM
}
;
-stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN K_ARRAY expr_until_loop loop_body
+stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN foreach_type expr_until_loop loop_body
{
- PLpgSQL_stmt_foreach_a *new;
+ PLpgSQL_stmt_foreach *new;
- new = palloc0_object(PLpgSQL_stmt_foreach_a);
- new->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ new = $6;
new->lineno = plpgsql_location_to_lineno(@2, yyscanner);
new->stmtid = ++plpgsql_curr_compile->nstatements;
new->label = $1;
- new->slice = $4;
+
+ if ($4 > 0)
+ {
+ /* slicing is supported only by FOREACH IN ARRAY */
+ if (new->cmd_type == PLPGSQL_STMT_FOREACH_A)
+ {
+ ((PLpgSQL_stmt_foreach_a *) new)->slice = $4;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("not zero slice is allowed only for arrays"),
+ parser_errposition(@4)));
+ }
+
new->expr = $7;
new->body = $8.stmts;
@@ -1719,6 +1735,19 @@ foreach_slice :
}
;
+foreach_type :
+ K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ }
+ | K_JSON K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_json_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_JSON_A;
+ }
+ ;
+
stmt_exit : exit_type opt_label opt_exitcond
{
PLpgSQL_stmt_exit *new;
diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
index 6379e86c8cb..d7588d3b4ad 100644
--- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
@@ -69,6 +69,7 @@ PG_KEYWORD("import", K_IMPORT)
PG_KEYWORD("info", K_INFO)
PG_KEYWORD("insert", K_INSERT)
PG_KEYWORD("is", K_IS)
+PG_KEYWORD("json", K_JSON)
PG_KEYWORD("last", K_LAST)
PG_KEYWORD("log", K_LOG)
PG_KEYWORD("merge", K_MERGE)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..4a1e9ba65bc 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -112,6 +112,7 @@ typedef enum PLpgSQL_stmt_type
PLPGSQL_STMT_FORS,
PLPGSQL_STMT_FORC,
PLPGSQL_STMT_FOREACH_A,
+ PLPGSQL_STMT_FOREACH_JSON_A,
PLPGSQL_STMT_EXIT,
PLPGSQL_STMT_RETURN,
PLPGSQL_STMT_RETURN_NEXT,
@@ -766,6 +767,20 @@ typedef struct PLpgSQL_stmt_dynfors
List *params; /* USING expressions */
} PLpgSQL_stmt_dynfors;
+/*
+ * FOREACH loop (ancestor IN ARRAY and IN JSON ARRAY loop)
+ */
+typedef struct PLpgSQL_stmt_foreach
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* set expression */
+ List *body; /* List of statements */
+} PLpgSQL_stmt_foreach;
+
/*
* FOREACH item in array loop
*/
@@ -776,11 +791,27 @@ typedef struct PLpgSQL_stmt_foreach_a
unsigned int stmtid;
char *label;
int varno; /* loop target variable */
- int slice; /* slice dimension, or 0 */
PLpgSQL_expr *expr; /* array expression */
List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+ int slice; /* slice dimension, or 0 */
} PLpgSQL_stmt_foreach_a;
+/*
+ * FOREACH item in array loop
+ */
+typedef struct PLpgSQL_stmt_foreach_json_a
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* array expression */
+ List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+} PLpgSQL_stmt_foreach_json_a;
+
/*
* OPEN a curvar
*/
@@ -1274,7 +1305,7 @@ extern PGDLLEXPORT Oid plpgsql_exec_get_datum_type(PLpgSQL_execstate *estate,
extern void plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
PLpgSQL_datum *datum,
Oid *typeId, int32 *typMod,
- Oid *collation);
+ Oid *collation, char **refname);
/*
* Functions for namespace handling in pl_funcs.c
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..7e7abfe2568
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,89 @@
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-01 05:44 ` Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
1 sibling, 1 reply; 26+ messages in thread
From: Pavel Stehule @ 2026-03-01 05:44 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>
Hi
small update
Now assignment to plpgsql array variable and json array works. Some minor
code cleaning + more regress tests.
Regards
Pavel
Attachments:
[text/x-patch] v20260301-2-0001-FOREACH-scalar-IN-JSON-ARRAY.patch (28.2K, ../../CAFj8pRCBUtakcgBF2uZ-+2rq9Kzw-p0V1i6B73gfiVjwKct3AQ@mail.gmail.com/3-v20260301-2-0001-FOREACH-scalar-IN-JSON-ARRAY.patch)
download | inline diff:
From b0556c08fe031027ebd10dff8f9d4df5f18ac286 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN JSON ARRAY
this patch introduce FOREACH scalar_var IN JSON ARRAY. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN JSON ARRAY '[1,2,3.14]'
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN JSON ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 59 ++++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 192 ++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 287 ++++++++++++++++++
src/pl/plpgsql/src/pl_funcs.c | 29 ++
src/pl/plpgsql/src/pl_gram.y | 39 ++-
src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 +
src/pl/plpgsql/src/plpgsql.h | 36 ++-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 159 ++++++++++
10 files changed, 797 insertions(+), 8 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..034fbd8bd45 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2780,6 +2780,65 @@ NOTICE: row = {10,11,12}
</para>
</sect2>
+ <sect2 id="plpgsql-foreach-json-array">
+ <title>Looping through JSON arrays</title>
+
+ <para>
+ The <literal>FOREACH</literal> loop is much like a <literal>FOREACH</literal> loop,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of an JSON array value
+ (expression is internaly casted to jsonb type).
+
+<synopsis>
+<optional> <<<replaceable>label</replaceable>>> </optional>
+FOREACH <replaceable>target</replaceable> IN JSON ARRAY <replaceable>expression</replaceable> LOOP
+ <replaceable>statements</replaceable>
+END LOOP <optional> <replaceable>label</replaceable> </optional>;
+</synopsis>
+ </para>
+
+ <para>
+ Target can be scalar variable, composite variable or list of
+ scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+ </sect2>
+
<sect2 id="plpgsql-error-trapping">
<title>Trapping Errors</title>
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..bc366e78df3
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,192 @@
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 84552e32c87..f988a1d74c4 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -41,6 +41,8 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
@@ -305,6 +307,8 @@ static int exec_stmt_forc(PLpgSQL_execstate *estate,
PLpgSQL_stmt_forc *stmt);
static int exec_stmt_foreach_a(PLpgSQL_execstate *estate,
PLpgSQL_stmt_foreach_a *stmt);
+static int exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt);
static int exec_stmt_open(PLpgSQL_execstate *estate,
PLpgSQL_stmt_open *stmt);
static int exec_stmt_fetch(PLpgSQL_execstate *estate,
@@ -2075,6 +2079,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
rc = exec_stmt_foreach_a(estate, (PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ rc = exec_stmt_foreach_json_a(estate, (PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
+
case PLPGSQL_STMT_EXIT:
rc = exec_stmt_exit(estate, (PLpgSQL_stmt_exit *) stmt);
break;
@@ -2995,6 +3003,240 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
}
+/*
+ * Convert JsonbValue to Datum that can be assigned to PLpgSQL_var.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ if (expected_typid == JSONBOID)
+ {
+ *typid = JSONBOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(JsonbValueToJsonb(jbv));
+ }
+ else if (expected_typid == JSONOID)
+ {
+ Jsonb *jsonb;
+ char *str;
+
+ /* serialize JsonValue to JSON text */
+ jsonb = JsonbValueToJsonb(jbv);
+ str = JsonbToCString(NULL, &jsonb->root, VARSIZE(jsonb));
+
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text(str));
+ }
+ else if (jbv->type == jbvNull)
+ {
+ *typid = expected_typid;
+ *typmod = -1;
+ *isnull = true;
+
+ return (Datum) 0;
+ }
+ else if (jbv->type == jbvString)
+ {
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len));
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ *typid = NUMERICOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(jbv->val.numeric);
+ }
+ else if (jbv->type == jbvBool)
+ {
+ *typid = BOOLOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return BoolGetDatum(jbv->val.boolean);
+ }
+ else
+ {
+ Jsonb *jsonb;
+ Datum result;
+
+ jsonb = JsonbValueToJsonb(jbv);
+ result = json_populate_type(PointerGetDatum(jsonb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+
+ return result;
+ }
+}
+
+/* ----------
+ * exec_stmt_foreach_json_a Loop over elements in json array
+ *
+ * When target is a composite, then target is populated like json_to_populate_record.
+ * jsonb doesn't preserve attribute order, so position based mapping between
+ * target and source can be possibly dangerous (with unexpected behave).
+ * ----------
+ */
+static int
+exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ Oid exprtypeid;
+ int32 exprtypmod;
+ Datum exprdatum;
+ PLpgSQL_datum *loop_var;
+ Oid loop_var_typid;
+ int32 loop_var_typmod;
+ Oid loop_var_collation;
+ Jsonb *jb;
+ JsonbIterator *it;
+ JsonbValue jbv;
+ JsonbIteratorToken r;
+ MemoryContext stmt_mcontext;
+ MemoryContext oldcontext;
+ MemoryContext tmp_cxt;
+ bool found = false;
+ bool isnull;
+ bool skipNested = false;
+ int rc = PLPGSQL_RC_OK;
+ void *cache = NULL;
+
+ /* get the value of the expression */
+ exprdatum = exec_eval_expr(estate, stmt->expr, &isnull,
+ &exprtypeid, &exprtypmod);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("FOREACH expression must not be null")));
+
+ /*
+ * Do as much as possible of the code below in stmt_mcontext, to avoid any
+ * leaks from called subroutines. We need a private stmt_mcontext since
+ * we'll be calling arbitrary statement code.
+ */
+ stmt_mcontext = get_stmt_mcontext(estate);
+ push_stmt_mcontext(estate);
+ oldcontext = MemoryContextSwitchTo(stmt_mcontext);
+
+ tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "FOREACH IN JSON ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* cast to jsonb */
+ exprdatum = exec_cast_value(estate, exprdatum, &isnull,
+ exprtypeid, exprtypmod,
+ JSONBOID, -1);
+
+ Assert(!isnull);
+
+ /*
+ * We must copy the array into stmt_mcontext, else it will disappear in
+ * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
+ * while running the loop body, so we have little choice.
+ */
+ jb = DatumGetJsonbPCopy(exprdatum);
+
+ /* Clean up any leftover temporary memory */
+ exec_eval_cleanup(estate);
+
+ /*
+ * This is compatible with jsonb_array_element. SQL/JSON functions are not
+ * too strict like PostgreSQL proprietary (old json) functions. In SQL/JSON
+ * a scalar is equal to one element array. The basic question is if FOREACH
+ * should be more restrictive like old JSON function, or less restrictive
+ * like SQL/JSON functions.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot extract elements from a scalar")));
+ else if (!JB_ROOT_IS_ARRAY(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot extract elements from an object")));
+
+ /* Set up the loop variable and see if it is of an array type */
+ loop_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, loop_var,
+ &loop_var_typid, &loop_var_typmod,
+ &loop_var_collation);
+
+ it = JsonbIteratorInit(&jb->root);
+
+ while ((r = JsonbIteratorNext(&it, &jbv, skipNested)) != WJB_DONE)
+ {
+ skipNested = true;
+
+ if (r == WJB_ELEM)
+ {
+ Datum val;
+ Oid valtypid;
+ int32 valtypmod;
+ bool valisnull;
+
+ MemoryContextSwitchTo(tmp_cxt);
+
+ val = JsonbValueToDatum(&jbv,
+ &valtypid, &valtypmod, &valisnull,
+ loop_var_typid, loop_var_typmod,
+ &cache, stmt_mcontext);
+
+ /* exec_assign_value and exec_stmts must run in the main context */
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Assign current element/slice to the loop variable */
+ exec_assign_value(estate, loop_var, val,
+ valisnull, valtypid, valtypmod);
+
+ MemoryContextReset(tmp_cxt);
+
+ /*
+ * Execute the statements
+ */
+ rc = exec_stmts(estate, stmt->body);
+
+ LOOP_RC_PROCESSING(stmt->label, break);
+
+ MemoryContextSwitchTo(stmt_mcontext);
+ }
+ }
+
+ /* Restore memory context state */
+ MemoryContextSwitchTo(oldcontext);
+ pop_stmt_mcontext(estate);
+
+ /* Release temporary memory, including the array value */
+ MemoryContextReset(stmt_mcontext);
+
+ /*
+ * Set the FOUND variable to indicate the result of executing the loop
+ * (namely, whether we looped one or more times). This must be set here so
+ * that it does not interfere with the value of the FOUND variable inside
+ * the loop processing itself.
+ */
+ exec_set_found(estate, found);
+
+ return rc;
+}
+
/* ----------
* exec_stmt_foreach_a Loop over elements or slices of an array
*
@@ -5537,6 +5779,51 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot to use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index 92cd9116c0e..7511fab7e68 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -253,6 +253,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
return _("FOR over cursor");
case PLPGSQL_STMT_FOREACH_A:
return _("FOREACH over array");
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ return _("FOREACH over json array");
case PLPGSQL_STMT_EXIT:
return ((PLpgSQL_stmt_exit *) stmt)->is_exit ? "EXIT" : "CONTINUE";
case PLPGSQL_STMT_RETURN:
@@ -467,6 +469,14 @@ plpgsql_statement_tree_walker_impl(PLpgSQL_stmt *stmt,
{
PLpgSQL_stmt_foreach_a *fstmt = (PLpgSQL_stmt_foreach_a *) stmt;
+ E_WALK(fstmt->expr);
+ S_LIST_WALK(fstmt->body);
+ break;
+ }
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ {
+ PLpgSQL_stmt_foreach_json_a *fstmt = (PLpgSQL_stmt_foreach_json_a *) stmt;
+
E_WALK(fstmt->expr);
S_LIST_WALK(fstmt->body);
break;
@@ -795,6 +805,7 @@ static void dump_fori(PLpgSQL_stmt_fori *stmt);
static void dump_fors(PLpgSQL_stmt_fors *stmt);
static void dump_forc(PLpgSQL_stmt_forc *stmt);
static void dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt);
+static void dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt);
static void dump_exit(PLpgSQL_stmt_exit *stmt);
static void dump_return(PLpgSQL_stmt_return *stmt);
static void dump_return_next(PLpgSQL_stmt_return_next *stmt);
@@ -861,6 +872,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
case PLPGSQL_STMT_FOREACH_A:
dump_foreach_a((PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ dump_foreach_json_a((PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
case PLPGSQL_STMT_EXIT:
dump_exit((PLpgSQL_stmt_exit *) stmt);
break;
@@ -1157,6 +1171,21 @@ dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt)
printf(" ENDFOREACHA");
}
+static void
+dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ dump_ind();
+ printf("FOREACHA var %d ", stmt->varno);
+ printf("IN JSON ARRAY ");
+ dump_expr(stmt->expr);
+ printf("\n");
+
+ dump_stmts(stmt->body);
+
+ dump_ind();
+ printf(" ENDFOREACHA");
+}
+
static void
dump_open(PLpgSQL_stmt_open *stmt)
{
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 5009e59a78f..23b465b10d5 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -178,6 +178,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
PLpgSQL_diag_item *diagitem;
PLpgSQL_stmt_fetch *fetch;
PLpgSQL_case_when *casewhen;
+ PLpgSQL_stmt_foreach *foreach;
}
%type <declhdr> decl_sect
@@ -220,6 +221,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%type <casewhen> case_when
%type <list> case_when_list opt_case_else
+%type <foreach> foreach_type
%type <boolean> getdiag_area_opt
%type <list> getdiag_list
@@ -341,6 +343,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%token <keyword> K_PRINT_STRICT_PARAMS
%token <keyword> K_PRIOR
%token <keyword> K_QUERY
+%token <keyword> K_JSON
%token <keyword> K_RAISE
%token <keyword> K_RELATIVE
%token <keyword> K_RETURN
@@ -1671,16 +1674,29 @@ for_variable : T_DATUM
}
;
-stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN K_ARRAY expr_until_loop loop_body
+stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN foreach_type expr_until_loop loop_body
{
- PLpgSQL_stmt_foreach_a *new;
+ PLpgSQL_stmt_foreach *new;
- new = palloc0_object(PLpgSQL_stmt_foreach_a);
- new->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ new = $6;
new->lineno = plpgsql_location_to_lineno(@2, yyscanner);
new->stmtid = ++plpgsql_curr_compile->nstatements;
new->label = $1;
- new->slice = $4;
+
+ if ($4 > 0)
+ {
+ /* slicing is supported only by FOREACH IN ARRAY */
+ if (new->cmd_type == PLPGSQL_STMT_FOREACH_A)
+ {
+ ((PLpgSQL_stmt_foreach_a *) new)->slice = $4;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("not zero slice is allowed only for arrays"),
+ parser_errposition(@4)));
+ }
+
new->expr = $7;
new->body = $8.stmts;
@@ -1719,6 +1735,19 @@ foreach_slice :
}
;
+foreach_type :
+ K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ }
+ | K_JSON K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_json_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_JSON_A;
+ }
+ ;
+
stmt_exit : exit_type opt_label opt_exitcond
{
PLpgSQL_stmt_exit *new;
diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
index 6379e86c8cb..d7588d3b4ad 100644
--- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
@@ -69,6 +69,7 @@ PG_KEYWORD("import", K_IMPORT)
PG_KEYWORD("info", K_INFO)
PG_KEYWORD("insert", K_INSERT)
PG_KEYWORD("is", K_IS)
+PG_KEYWORD("json", K_JSON)
PG_KEYWORD("last", K_LAST)
PG_KEYWORD("log", K_LOG)
PG_KEYWORD("merge", K_MERGE)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..c57b1da9b95 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -112,6 +112,7 @@ typedef enum PLpgSQL_stmt_type
PLPGSQL_STMT_FORS,
PLPGSQL_STMT_FORC,
PLPGSQL_STMT_FOREACH_A,
+ PLPGSQL_STMT_FOREACH_JSON_A,
PLPGSQL_STMT_EXIT,
PLPGSQL_STMT_RETURN,
PLPGSQL_STMT_RETURN_NEXT,
@@ -299,6 +300,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +446,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
@@ -766,6 +768,20 @@ typedef struct PLpgSQL_stmt_dynfors
List *params; /* USING expressions */
} PLpgSQL_stmt_dynfors;
+/*
+ * FOREACH loop (ancestor IN ARRAY and IN JSON ARRAY loop)
+ */
+typedef struct PLpgSQL_stmt_foreach
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* set expression */
+ List *body; /* List of statements */
+} PLpgSQL_stmt_foreach;
+
/*
* FOREACH item in array loop
*/
@@ -776,11 +792,27 @@ typedef struct PLpgSQL_stmt_foreach_a
unsigned int stmtid;
char *label;
int varno; /* loop target variable */
- int slice; /* slice dimension, or 0 */
PLpgSQL_expr *expr; /* array expression */
List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+ int slice; /* slice dimension, or 0 */
} PLpgSQL_stmt_foreach_a;
+/*
+ * FOREACH item in array loop
+ */
+typedef struct PLpgSQL_stmt_foreach_json_a
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* array expression */
+ List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+} PLpgSQL_stmt_foreach_json_a;
+
/*
* OPEN a curvar
*/
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..33acb8f924c
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,159 @@
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-01 19:40 ` Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Pavel Stehule @ 2026-03-01 19:40 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>
ne 1. 3. 2026 v 6:44 odesílatel Pavel Stehule <[email protected]>
napsal:
> Hi
>
> small update
>
> Now assignment to plpgsql array variable and json array works. Some minor
> code cleaning + more regress tests.
>
fix uninitialized argument of json_populate_type function
regards
Pavel
> Regards
>
> Pavel
>
>
Attachments:
[text/x-patch] v20260301-3-0001-FOREACH-scalar-IN-JSON-ARRAY.patch (28.3K, ../../CAFj8pRA7TLOTuY-Dry2SaSpaPaqUJpsJke8Bm2SyZaVh2p5y4Q@mail.gmail.com/3-v20260301-3-0001-FOREACH-scalar-IN-JSON-ARRAY.patch)
download | inline diff:
From 00267f26fe213d2459672fd16acbea73cc91641c Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN JSON ARRAY
this patch introduce FOREACH scalar_var IN JSON ARRAY. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN JSON ARRAY '[1,2,3.14]'
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN JSON ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 59 ++++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 192 ++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 290 ++++++++++++++++++
src/pl/plpgsql/src/pl_funcs.c | 29 ++
src/pl/plpgsql/src/pl_gram.y | 39 ++-
src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 +
src/pl/plpgsql/src/plpgsql.h | 36 ++-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 159 ++++++++++
10 files changed, 800 insertions(+), 8 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..034fbd8bd45 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2780,6 +2780,65 @@ NOTICE: row = {10,11,12}
</para>
</sect2>
+ <sect2 id="plpgsql-foreach-json-array">
+ <title>Looping through JSON arrays</title>
+
+ <para>
+ The <literal>FOREACH</literal> loop is much like a <literal>FOREACH</literal> loop,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of an JSON array value
+ (expression is internaly casted to jsonb type).
+
+<synopsis>
+<optional> <<<replaceable>label</replaceable>>> </optional>
+FOREACH <replaceable>target</replaceable> IN JSON ARRAY <replaceable>expression</replaceable> LOOP
+ <replaceable>statements</replaceable>
+END LOOP <optional> <replaceable>label</replaceable> </optional>;
+</synopsis>
+ </para>
+
+ <para>
+ Target can be scalar variable, composite variable or list of
+ scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+ </sect2>
+
<sect2 id="plpgsql-error-trapping">
<title>Trapping Errors</title>
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..bc366e78df3
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,192 @@
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 84552e32c87..267bbabee6a 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -41,6 +41,8 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
@@ -305,6 +307,8 @@ static int exec_stmt_forc(PLpgSQL_execstate *estate,
PLpgSQL_stmt_forc *stmt);
static int exec_stmt_foreach_a(PLpgSQL_execstate *estate,
PLpgSQL_stmt_foreach_a *stmt);
+static int exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt);
static int exec_stmt_open(PLpgSQL_execstate *estate,
PLpgSQL_stmt_open *stmt);
static int exec_stmt_fetch(PLpgSQL_execstate *estate,
@@ -2075,6 +2079,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
rc = exec_stmt_foreach_a(estate, (PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ rc = exec_stmt_foreach_json_a(estate, (PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
+
case PLPGSQL_STMT_EXIT:
rc = exec_stmt_exit(estate, (PLpgSQL_stmt_exit *) stmt);
break;
@@ -2995,6 +3003,243 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
}
+/*
+ * Convert JsonbValue to Datum that can be assigned to PLpgSQL_var.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ if (expected_typid == JSONBOID)
+ {
+ *typid = JSONBOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(JsonbValueToJsonb(jbv));
+ }
+ else if (expected_typid == JSONOID)
+ {
+ Jsonb *jsonb;
+ char *str;
+
+ /* serialize JsonValue to JSON text */
+ jsonb = JsonbValueToJsonb(jbv);
+ str = JsonbToCString(NULL, &jsonb->root, VARSIZE(jsonb));
+
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text(str));
+ }
+ else if (jbv->type == jbvNull)
+ {
+ *typid = expected_typid;
+ *typmod = -1;
+ *isnull = true;
+
+ return (Datum) 0;
+ }
+ else if (jbv->type == jbvString)
+ {
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len));
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ *typid = NUMERICOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(jbv->val.numeric);
+ }
+ else if (jbv->type == jbvBool)
+ {
+ *typid = BOOLOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return BoolGetDatum(jbv->val.boolean);
+ }
+ else
+ {
+ Jsonb *jsonb;
+ Datum result;
+
+ /* isnull in json_populate_type is inout argument */
+ *isnull = false;
+
+ jsonb = JsonbValueToJsonb(jbv);
+ result = json_populate_type(PointerGetDatum(jsonb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+
+ return result;
+ }
+}
+
+/* ----------
+ * exec_stmt_foreach_json_a Loop over elements in json array
+ *
+ * When target is a composite, then target is populated like json_to_populate_record.
+ * jsonb doesn't preserve attribute order, so position based mapping between
+ * target and source can be possibly dangerous (with unexpected behave).
+ * ----------
+ */
+static int
+exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ Oid exprtypeid;
+ int32 exprtypmod;
+ Datum exprdatum;
+ PLpgSQL_datum *loop_var;
+ Oid loop_var_typid;
+ int32 loop_var_typmod;
+ Oid loop_var_collation;
+ Jsonb *jb;
+ JsonbIterator *it;
+ JsonbValue jbv;
+ JsonbIteratorToken r;
+ MemoryContext stmt_mcontext;
+ MemoryContext oldcontext;
+ MemoryContext tmp_cxt;
+ bool found = false;
+ bool isnull;
+ bool skipNested = false;
+ int rc = PLPGSQL_RC_OK;
+ void *cache = NULL;
+
+ /* get the value of the expression */
+ exprdatum = exec_eval_expr(estate, stmt->expr, &isnull,
+ &exprtypeid, &exprtypmod);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("FOREACH expression must not be null")));
+
+ /*
+ * Do as much as possible of the code below in stmt_mcontext, to avoid any
+ * leaks from called subroutines. We need a private stmt_mcontext since
+ * we'll be calling arbitrary statement code.
+ */
+ stmt_mcontext = get_stmt_mcontext(estate);
+ push_stmt_mcontext(estate);
+ oldcontext = MemoryContextSwitchTo(stmt_mcontext);
+
+ tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "FOREACH IN JSON ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* cast to jsonb */
+ exprdatum = exec_cast_value(estate, exprdatum, &isnull,
+ exprtypeid, exprtypmod,
+ JSONBOID, -1);
+
+ Assert(!isnull);
+
+ /*
+ * We must copy the array into stmt_mcontext, else it will disappear in
+ * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
+ * while running the loop body, so we have little choice.
+ */
+ jb = DatumGetJsonbPCopy(exprdatum);
+
+ /* Clean up any leftover temporary memory */
+ exec_eval_cleanup(estate);
+
+ /*
+ * This is compatible with jsonb_array_element. SQL/JSON functions are not
+ * too strict like PostgreSQL proprietary (old json) functions. In SQL/JSON
+ * a scalar is equal to one element array. The basic question is if FOREACH
+ * should be more restrictive like old JSON function, or less restrictive
+ * like SQL/JSON functions.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot extract elements from a scalar")));
+ else if (!JB_ROOT_IS_ARRAY(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot extract elements from an object")));
+
+ /* Set up the loop variable and see if it is of an array type */
+ loop_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, loop_var,
+ &loop_var_typid, &loop_var_typmod,
+ &loop_var_collation);
+
+ it = JsonbIteratorInit(&jb->root);
+
+ while ((r = JsonbIteratorNext(&it, &jbv, skipNested)) != WJB_DONE)
+ {
+ skipNested = true;
+
+ if (r == WJB_ELEM)
+ {
+ Datum val;
+ Oid valtypid;
+ int32 valtypmod;
+ bool valisnull;
+
+ MemoryContextSwitchTo(tmp_cxt);
+
+ val = JsonbValueToDatum(&jbv,
+ &valtypid, &valtypmod, &valisnull,
+ loop_var_typid, loop_var_typmod,
+ &cache, stmt_mcontext);
+
+ /* exec_assign_value and exec_stmts must run in the main context */
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Assign current element/slice to the loop variable */
+ exec_assign_value(estate, loop_var, val,
+ valisnull, valtypid, valtypmod);
+
+ MemoryContextReset(tmp_cxt);
+
+ /*
+ * Execute the statements
+ */
+ rc = exec_stmts(estate, stmt->body);
+
+ LOOP_RC_PROCESSING(stmt->label, break);
+
+ MemoryContextSwitchTo(stmt_mcontext);
+ }
+ }
+
+ /* Restore memory context state */
+ MemoryContextSwitchTo(oldcontext);
+ pop_stmt_mcontext(estate);
+
+ /* Release temporary memory, including the array value */
+ MemoryContextReset(stmt_mcontext);
+
+ /*
+ * Set the FOUND variable to indicate the result of executing the loop
+ * (namely, whether we looped one or more times). This must be set here so
+ * that it does not interfere with the value of the FOUND variable inside
+ * the loop processing itself.
+ */
+ exec_set_found(estate, found);
+
+ return rc;
+}
+
/* ----------
* exec_stmt_foreach_a Loop over elements or slices of an array
*
@@ -5537,6 +5782,51 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot to use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index 92cd9116c0e..7511fab7e68 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -253,6 +253,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
return _("FOR over cursor");
case PLPGSQL_STMT_FOREACH_A:
return _("FOREACH over array");
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ return _("FOREACH over json array");
case PLPGSQL_STMT_EXIT:
return ((PLpgSQL_stmt_exit *) stmt)->is_exit ? "EXIT" : "CONTINUE";
case PLPGSQL_STMT_RETURN:
@@ -467,6 +469,14 @@ plpgsql_statement_tree_walker_impl(PLpgSQL_stmt *stmt,
{
PLpgSQL_stmt_foreach_a *fstmt = (PLpgSQL_stmt_foreach_a *) stmt;
+ E_WALK(fstmt->expr);
+ S_LIST_WALK(fstmt->body);
+ break;
+ }
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ {
+ PLpgSQL_stmt_foreach_json_a *fstmt = (PLpgSQL_stmt_foreach_json_a *) stmt;
+
E_WALK(fstmt->expr);
S_LIST_WALK(fstmt->body);
break;
@@ -795,6 +805,7 @@ static void dump_fori(PLpgSQL_stmt_fori *stmt);
static void dump_fors(PLpgSQL_stmt_fors *stmt);
static void dump_forc(PLpgSQL_stmt_forc *stmt);
static void dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt);
+static void dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt);
static void dump_exit(PLpgSQL_stmt_exit *stmt);
static void dump_return(PLpgSQL_stmt_return *stmt);
static void dump_return_next(PLpgSQL_stmt_return_next *stmt);
@@ -861,6 +872,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
case PLPGSQL_STMT_FOREACH_A:
dump_foreach_a((PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ dump_foreach_json_a((PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
case PLPGSQL_STMT_EXIT:
dump_exit((PLpgSQL_stmt_exit *) stmt);
break;
@@ -1157,6 +1171,21 @@ dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt)
printf(" ENDFOREACHA");
}
+static void
+dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ dump_ind();
+ printf("FOREACHA var %d ", stmt->varno);
+ printf("IN JSON ARRAY ");
+ dump_expr(stmt->expr);
+ printf("\n");
+
+ dump_stmts(stmt->body);
+
+ dump_ind();
+ printf(" ENDFOREACHA");
+}
+
static void
dump_open(PLpgSQL_stmt_open *stmt)
{
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 5009e59a78f..23b465b10d5 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -178,6 +178,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
PLpgSQL_diag_item *diagitem;
PLpgSQL_stmt_fetch *fetch;
PLpgSQL_case_when *casewhen;
+ PLpgSQL_stmt_foreach *foreach;
}
%type <declhdr> decl_sect
@@ -220,6 +221,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%type <casewhen> case_when
%type <list> case_when_list opt_case_else
+%type <foreach> foreach_type
%type <boolean> getdiag_area_opt
%type <list> getdiag_list
@@ -341,6 +343,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%token <keyword> K_PRINT_STRICT_PARAMS
%token <keyword> K_PRIOR
%token <keyword> K_QUERY
+%token <keyword> K_JSON
%token <keyword> K_RAISE
%token <keyword> K_RELATIVE
%token <keyword> K_RETURN
@@ -1671,16 +1674,29 @@ for_variable : T_DATUM
}
;
-stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN K_ARRAY expr_until_loop loop_body
+stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN foreach_type expr_until_loop loop_body
{
- PLpgSQL_stmt_foreach_a *new;
+ PLpgSQL_stmt_foreach *new;
- new = palloc0_object(PLpgSQL_stmt_foreach_a);
- new->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ new = $6;
new->lineno = plpgsql_location_to_lineno(@2, yyscanner);
new->stmtid = ++plpgsql_curr_compile->nstatements;
new->label = $1;
- new->slice = $4;
+
+ if ($4 > 0)
+ {
+ /* slicing is supported only by FOREACH IN ARRAY */
+ if (new->cmd_type == PLPGSQL_STMT_FOREACH_A)
+ {
+ ((PLpgSQL_stmt_foreach_a *) new)->slice = $4;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("not zero slice is allowed only for arrays"),
+ parser_errposition(@4)));
+ }
+
new->expr = $7;
new->body = $8.stmts;
@@ -1719,6 +1735,19 @@ foreach_slice :
}
;
+foreach_type :
+ K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ }
+ | K_JSON K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_json_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_JSON_A;
+ }
+ ;
+
stmt_exit : exit_type opt_label opt_exitcond
{
PLpgSQL_stmt_exit *new;
diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
index 6379e86c8cb..d7588d3b4ad 100644
--- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
@@ -69,6 +69,7 @@ PG_KEYWORD("import", K_IMPORT)
PG_KEYWORD("info", K_INFO)
PG_KEYWORD("insert", K_INSERT)
PG_KEYWORD("is", K_IS)
+PG_KEYWORD("json", K_JSON)
PG_KEYWORD("last", K_LAST)
PG_KEYWORD("log", K_LOG)
PG_KEYWORD("merge", K_MERGE)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..c57b1da9b95 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -112,6 +112,7 @@ typedef enum PLpgSQL_stmt_type
PLPGSQL_STMT_FORS,
PLPGSQL_STMT_FORC,
PLPGSQL_STMT_FOREACH_A,
+ PLPGSQL_STMT_FOREACH_JSON_A,
PLPGSQL_STMT_EXIT,
PLPGSQL_STMT_RETURN,
PLPGSQL_STMT_RETURN_NEXT,
@@ -299,6 +300,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +446,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
@@ -766,6 +768,20 @@ typedef struct PLpgSQL_stmt_dynfors
List *params; /* USING expressions */
} PLpgSQL_stmt_dynfors;
+/*
+ * FOREACH loop (ancestor IN ARRAY and IN JSON ARRAY loop)
+ */
+typedef struct PLpgSQL_stmt_foreach
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* set expression */
+ List *body; /* List of statements */
+} PLpgSQL_stmt_foreach;
+
/*
* FOREACH item in array loop
*/
@@ -776,11 +792,27 @@ typedef struct PLpgSQL_stmt_foreach_a
unsigned int stmtid;
char *label;
int varno; /* loop target variable */
- int slice; /* slice dimension, or 0 */
PLpgSQL_expr *expr; /* array expression */
List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+ int slice; /* slice dimension, or 0 */
} PLpgSQL_stmt_foreach_a;
+/*
+ * FOREACH item in array loop
+ */
+typedef struct PLpgSQL_stmt_foreach_json_a
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* array expression */
+ List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+} PLpgSQL_stmt_foreach_json_a;
+
/*
* OPEN a curvar
*/
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..33acb8f924c
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,159 @@
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-03 07:42 ` Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Jim Jones @ 2026-03-03 07:42 UTC (permalink / raw)
To: Pavel Stehule <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi Pavel,
I quickly tested the patch, and I also could observe a ~3x performance
improvement!
A few first impressions:
## in exec_stmt_foreach_json_a the boolean variable found is declared as
false, bit its value is never set to true until exec_set_found() is called:
/*
* Set the FOUND variable to indicate the result of executing the loop
* (namely, whether we looped one or more times). This must be set here so
* that it does not interfere with the value of the FOUND variable inside
* the loop processing itself.
*/
exec_set_found(estate, found);
Test:
DO $$
DECLARE
x int;
BEGIN
FOREACH x IN JSON ARRAY '[1,2,3]'
LOOP
RAISE NOTICE 'x: %', x;
END LOOP;
IF FOUND THEN
RAISE NOTICE 'FOUND is true';
ELSE
RAISE NOTICE 'FOUND is false';
END IF;
END;
$$;
NOTICE: x: 1
NOTICE: x: 2
NOTICE: x: 3
NOTICE: FOUND is false
## Suggestion in the plpgsql.sgml
The <literal>FOREACH</literal> loop is much like a
<literal>FOREACH</literal> loop,
to
"much like a regular <literal>FOREACH</literal> loop over arrays"
## Typo in comment
/*
* We cannot to use fieldnames for tupdescentry, because
* these names can be suffixed by name of row variable.
...
We cannot to use > We cannot use
## Nit pick
These error messages are not wrong, but IMO a errhint/errdetail could
add some value here:
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("cannot extract elements from a scalar")));
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("cannot extract elements from an object")));
Something like this perhaps?
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("cannot extract elements from a scalar"),
errhint("FOREACH IN JSON ARRAY requires an array value.")));
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("FOREACH expression must evaluate to a JSON array"),
errdetail("Cannot iterate over a scalar value.")));
Thanks for the patch!
Best, Jim
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
@ 2026-03-03 13:45 ` Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Pavel Stehule @ 2026-03-03 13:45 UTC (permalink / raw)
To: Jim Jones <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
Hi
út 3. 3. 2026 v 8:43 odesílatel Jim Jones <[email protected]>
napsal:
> Hi Pavel,
>
> I quickly tested the patch, and I also could observe a ~3x performance
> improvement!
>
> A few first impressions:
>
> ## in exec_stmt_foreach_json_a the boolean variable found is declared as
> false, bit its value is never set to true until exec_set_found() is called:
>
> /*
> * Set the FOUND variable to indicate the result of executing the loop
> * (namely, whether we looped one or more times). This must be set here so
> * that it does not interfere with the value of the FOUND variable inside
> * the loop processing itself.
> */
> exec_set_found(estate, found);
>
>
> Test:
>
>
> DO $$
> DECLARE
> x int;
> BEGIN
> FOREACH x IN JSON ARRAY '[1,2,3]'
> LOOP
> RAISE NOTICE 'x: %', x;
> END LOOP;
>
> IF FOUND THEN
> RAISE NOTICE 'FOUND is true';
> ELSE
> RAISE NOTICE 'FOUND is false';
> END IF;
> END;
> $$;
> NOTICE: x: 1
> NOTICE: x: 2
> NOTICE: x: 3
> NOTICE: FOUND is false
>
>
fixed + regress tests
>
> ## Suggestion in the plpgsql.sgml
>
> The <literal>FOREACH</literal> loop is much like a
> <literal>FOREACH</literal> loop,
>
> to
>
> "much like a regular <literal>FOREACH</literal> loop over arrays"
>
done
>
> ## Typo in comment
>
> /*
> * We cannot to use fieldnames for tupdescentry, because
> * these names can be suffixed by name of row variable.
> ...
>
> We cannot to use > We cannot use
>
fixed
>
>
> ## Nit pick
>
> These error messages are not wrong, but IMO a errhint/errdetail could
> add some value here:
>
> ereport(ERROR,
> (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> errmsg("cannot extract elements from a scalar")));
>
> ereport(ERROR,
> (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> errmsg("cannot extract elements from an object")));
>
> Something like this perhaps?
>
> ereport(ERROR,
> (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> errmsg("cannot extract elements from a scalar"),
> errhint("FOREACH IN JSON ARRAY requires an array value.")));
>
> ereport(ERROR,
> (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> errmsg("FOREACH expression must evaluate to a JSON array"),
> errdetail("Cannot iterate over a scalar value.")));
>
>
I rewrote it to
if (JB_ROOT_IS_SCALAR(jb))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("FOREACH expression must evaluate to a JSON array"),
errhint("Cannot iterate over a scalar value.")));
else if (JB_ROOT_IS_OBJECT(jb))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("FOREACH expression must evaluate to a JSON array"),
errdetail("Cannot iterate over a object value.")));
Assert(JB_ROOT_IS_ARRAY(jb));
+ regress tests
>
> Thanks for the patch!
>
Thank you for check
Regards
Pavel
>
> Best, Jim
>
Attachments:
[text/x-patch] v20260303-4-0001-FOREACH-scalar-IN-JSON-ARRAY.patch (30.7K, ../../CAFj8pRDi-7S7cWgtsvdHm8vvhvd8xHQ7tdXymBfBQQgpYangPQ@mail.gmail.com/3-v20260303-4-0001-FOREACH-scalar-IN-JSON-ARRAY.patch)
download | inline diff:
From 899ee1689d9dc537c57a8e74e04088d638cc3ccc Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN JSON ARRAY
this patch introduce FOREACH scalar_var IN JSON ARRAY. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN JSON ARRAY '[1,2,3.14]'
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN JSON ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 60 ++++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 253 +++++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 296 ++++++++++++++++++
src/pl/plpgsql/src/pl_funcs.c | 29 ++
src/pl/plpgsql/src/pl_gram.y | 39 ++-
src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 +
src/pl/plpgsql/src/plpgsql.h | 36 ++-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 214 +++++++++++++
10 files changed, 923 insertions(+), 8 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..ad12d2191aa 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2780,6 +2780,66 @@ NOTICE: row = {10,11,12}
</para>
</sect2>
+ <sect2 id="plpgsql-foreach-json-array">
+ <title>Looping through JSON arrays</title>
+
+ <para>
+ The <literal>FOREACH</literal> loop is much like a regular
+ <literal>FOREACH</literal> loop over arrays,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of an JSON array value
+ (expression is internaly casted to jsonb type).
+
+<synopsis>
+<optional> <<<replaceable>label</replaceable>>> </optional>
+FOREACH <replaceable>target</replaceable> IN JSON ARRAY <replaceable>expression</replaceable> LOOP
+ <replaceable>statements</replaceable>
+END LOOP <optional> <replaceable>label</replaceable> </optional>;
+</synopsis>
+ </para>
+
+ <para>
+ Target can be scalar variable, composite variable or list of
+ scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION scan_rows(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT scan_rows('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+ </sect2>
+
<sect2 id="plpgsql-error-trapping">
<title>Trapping Errors</title>
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..b24e73277f1
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,253 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in json array NULL -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must not be null
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x numeric;
+begin
+ foreach x in json array '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+HINT: Cannot iterate over a scalar value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x numeric;
+begin
+ foreach x in json array '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+DETAIL: Cannot iterate over a object value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: 10
+NOTICE: FOUND: t
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: FOUND: f
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 84552e32c87..036390f07a4 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -41,6 +41,8 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
@@ -305,6 +307,8 @@ static int exec_stmt_forc(PLpgSQL_execstate *estate,
PLpgSQL_stmt_forc *stmt);
static int exec_stmt_foreach_a(PLpgSQL_execstate *estate,
PLpgSQL_stmt_foreach_a *stmt);
+static int exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt);
static int exec_stmt_open(PLpgSQL_execstate *estate,
PLpgSQL_stmt_open *stmt);
static int exec_stmt_fetch(PLpgSQL_execstate *estate,
@@ -2075,6 +2079,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
rc = exec_stmt_foreach_a(estate, (PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ rc = exec_stmt_foreach_json_a(estate, (PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
+
case PLPGSQL_STMT_EXIT:
rc = exec_stmt_exit(estate, (PLpgSQL_stmt_exit *) stmt);
break;
@@ -2995,6 +3003,249 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
}
+/*
+ * Convert JsonbValue to Datum that can be assigned to PLpgSQL_var.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ if (expected_typid == JSONBOID)
+ {
+ *typid = JSONBOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(JsonbValueToJsonb(jbv));
+ }
+ else if (expected_typid == JSONOID)
+ {
+ Jsonb *jsonb;
+ char *str;
+
+ /* serialize JsonValue to JSON text */
+ jsonb = JsonbValueToJsonb(jbv);
+ str = JsonbToCString(NULL, &jsonb->root, VARSIZE(jsonb));
+
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text(str));
+ }
+ else if (jbv->type == jbvNull)
+ {
+ *typid = expected_typid;
+ *typmod = -1;
+ *isnull = true;
+
+ return (Datum) 0;
+ }
+ else if (jbv->type == jbvString)
+ {
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len));
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ *typid = NUMERICOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(jbv->val.numeric);
+ }
+ else if (jbv->type == jbvBool)
+ {
+ *typid = BOOLOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return BoolGetDatum(jbv->val.boolean);
+ }
+ else
+ {
+ Jsonb *jsonb;
+ Datum result;
+
+ /* isnull in json_populate_type is inout argument */
+ *isnull = false;
+
+ jsonb = JsonbValueToJsonb(jbv);
+ result = json_populate_type(PointerGetDatum(jsonb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+
+ return result;
+ }
+}
+
+/* ----------
+ * exec_stmt_foreach_json_a Loop over elements in json array
+ *
+ * When target is a composite, then target is populated like json_to_populate_record.
+ * jsonb doesn't preserve attribute order, so position based mapping between
+ * target and source can be possibly dangerous (with unexpected behave).
+ * ----------
+ */
+static int
+exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ Oid exprtypeid;
+ int32 exprtypmod;
+ Datum exprdatum;
+ PLpgSQL_datum *loop_var;
+ Oid loop_var_typid;
+ int32 loop_var_typmod;
+ Oid loop_var_collation;
+ Jsonb *jb;
+ JsonbIterator *it;
+ JsonbValue jbv;
+ JsonbIteratorToken r;
+ MemoryContext stmt_mcontext;
+ MemoryContext oldcontext;
+ MemoryContext tmp_cxt;
+ bool found = false;
+ bool isnull;
+ bool skipNested = false;
+ int rc = PLPGSQL_RC_OK;
+ void *cache = NULL;
+
+ /* get the value of the expression */
+ exprdatum = exec_eval_expr(estate, stmt->expr, &isnull,
+ &exprtypeid, &exprtypmod);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("FOREACH expression must not be null")));
+
+ /*
+ * Do as much as possible of the code below in stmt_mcontext, to avoid any
+ * leaks from called subroutines. We need a private stmt_mcontext since
+ * we'll be calling arbitrary statement code.
+ */
+ stmt_mcontext = get_stmt_mcontext(estate);
+ push_stmt_mcontext(estate);
+ oldcontext = MemoryContextSwitchTo(stmt_mcontext);
+
+ tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "FOREACH IN JSON ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* cast to jsonb */
+ exprdatum = exec_cast_value(estate, exprdatum, &isnull,
+ exprtypeid, exprtypmod,
+ JSONBOID, -1);
+
+ Assert(!isnull);
+
+ /*
+ * We must copy the array into stmt_mcontext, else it will disappear in
+ * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
+ * while running the loop body, so we have little choice.
+ */
+ jb = DatumGetJsonbPCopy(exprdatum);
+
+ /* Clean up any leftover temporary memory */
+ exec_eval_cleanup(estate);
+
+ /*
+ * This is compatible with jsonb_array_element. SQL/JSON functions are not
+ * too strict like PostgreSQL proprietary (old json) functions. In SQL/JSON
+ * a scalar is equal to one element array. The basic question is if FOREACH
+ * should be more restrictive like old JSON function, or less restrictive
+ * like SQL/JSON functions.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errhint("Cannot iterate over a scalar value.")));
+ else if (JB_ROOT_IS_OBJECT(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errdetail("Cannot iterate over a object value.")));
+
+ Assert(JB_ROOT_IS_ARRAY(jb));
+
+ /* Set up the loop variable and see if it is of an array type */
+ loop_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, loop_var,
+ &loop_var_typid, &loop_var_typmod,
+ &loop_var_collation);
+
+ it = JsonbIteratorInit(&jb->root);
+
+ while ((r = JsonbIteratorNext(&it, &jbv, skipNested)) != WJB_DONE)
+ {
+ skipNested = true;
+
+ if (r == WJB_ELEM)
+ {
+ Datum val;
+ Oid valtypid;
+ int32 valtypmod;
+ bool valisnull;
+
+ found = true; /* looped at least once */
+
+ MemoryContextSwitchTo(tmp_cxt);
+
+ val = JsonbValueToDatum(&jbv,
+ &valtypid, &valtypmod, &valisnull,
+ loop_var_typid, loop_var_typmod,
+ &cache, stmt_mcontext);
+
+ /* exec_assign_value and exec_stmts must run in the main context */
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Assign current element/slice to the loop variable */
+ exec_assign_value(estate, loop_var, val,
+ valisnull, valtypid, valtypmod);
+
+ MemoryContextReset(tmp_cxt);
+
+ /*
+ * Execute the statements
+ */
+ rc = exec_stmts(estate, stmt->body);
+
+ LOOP_RC_PROCESSING(stmt->label, break);
+
+ MemoryContextSwitchTo(stmt_mcontext);
+ }
+ }
+
+ /* Restore memory context state */
+ MemoryContextSwitchTo(oldcontext);
+ pop_stmt_mcontext(estate);
+
+ /* Release temporary memory, including the array value */
+ MemoryContextReset(stmt_mcontext);
+
+ /*
+ * Set the FOUND variable to indicate the result of executing the loop
+ * (namely, whether we looped one or more times). This must be set here so
+ * that it does not interfere with the value of the FOUND variable inside
+ * the loop processing itself.
+ */
+ exec_set_found(estate, found);
+
+ return rc;
+}
+
/* ----------
* exec_stmt_foreach_a Loop over elements or slices of an array
*
@@ -5537,6 +5788,51 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index 92cd9116c0e..7511fab7e68 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -253,6 +253,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
return _("FOR over cursor");
case PLPGSQL_STMT_FOREACH_A:
return _("FOREACH over array");
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ return _("FOREACH over json array");
case PLPGSQL_STMT_EXIT:
return ((PLpgSQL_stmt_exit *) stmt)->is_exit ? "EXIT" : "CONTINUE";
case PLPGSQL_STMT_RETURN:
@@ -467,6 +469,14 @@ plpgsql_statement_tree_walker_impl(PLpgSQL_stmt *stmt,
{
PLpgSQL_stmt_foreach_a *fstmt = (PLpgSQL_stmt_foreach_a *) stmt;
+ E_WALK(fstmt->expr);
+ S_LIST_WALK(fstmt->body);
+ break;
+ }
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ {
+ PLpgSQL_stmt_foreach_json_a *fstmt = (PLpgSQL_stmt_foreach_json_a *) stmt;
+
E_WALK(fstmt->expr);
S_LIST_WALK(fstmt->body);
break;
@@ -795,6 +805,7 @@ static void dump_fori(PLpgSQL_stmt_fori *stmt);
static void dump_fors(PLpgSQL_stmt_fors *stmt);
static void dump_forc(PLpgSQL_stmt_forc *stmt);
static void dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt);
+static void dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt);
static void dump_exit(PLpgSQL_stmt_exit *stmt);
static void dump_return(PLpgSQL_stmt_return *stmt);
static void dump_return_next(PLpgSQL_stmt_return_next *stmt);
@@ -861,6 +872,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
case PLPGSQL_STMT_FOREACH_A:
dump_foreach_a((PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ dump_foreach_json_a((PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
case PLPGSQL_STMT_EXIT:
dump_exit((PLpgSQL_stmt_exit *) stmt);
break;
@@ -1157,6 +1171,21 @@ dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt)
printf(" ENDFOREACHA");
}
+static void
+dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ dump_ind();
+ printf("FOREACHA var %d ", stmt->varno);
+ printf("IN JSON ARRAY ");
+ dump_expr(stmt->expr);
+ printf("\n");
+
+ dump_stmts(stmt->body);
+
+ dump_ind();
+ printf(" ENDFOREACHA");
+}
+
static void
dump_open(PLpgSQL_stmt_open *stmt)
{
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 5009e59a78f..23b465b10d5 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -178,6 +178,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
PLpgSQL_diag_item *diagitem;
PLpgSQL_stmt_fetch *fetch;
PLpgSQL_case_when *casewhen;
+ PLpgSQL_stmt_foreach *foreach;
}
%type <declhdr> decl_sect
@@ -220,6 +221,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%type <casewhen> case_when
%type <list> case_when_list opt_case_else
+%type <foreach> foreach_type
%type <boolean> getdiag_area_opt
%type <list> getdiag_list
@@ -341,6 +343,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%token <keyword> K_PRINT_STRICT_PARAMS
%token <keyword> K_PRIOR
%token <keyword> K_QUERY
+%token <keyword> K_JSON
%token <keyword> K_RAISE
%token <keyword> K_RELATIVE
%token <keyword> K_RETURN
@@ -1671,16 +1674,29 @@ for_variable : T_DATUM
}
;
-stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN K_ARRAY expr_until_loop loop_body
+stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN foreach_type expr_until_loop loop_body
{
- PLpgSQL_stmt_foreach_a *new;
+ PLpgSQL_stmt_foreach *new;
- new = palloc0_object(PLpgSQL_stmt_foreach_a);
- new->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ new = $6;
new->lineno = plpgsql_location_to_lineno(@2, yyscanner);
new->stmtid = ++plpgsql_curr_compile->nstatements;
new->label = $1;
- new->slice = $4;
+
+ if ($4 > 0)
+ {
+ /* slicing is supported only by FOREACH IN ARRAY */
+ if (new->cmd_type == PLPGSQL_STMT_FOREACH_A)
+ {
+ ((PLpgSQL_stmt_foreach_a *) new)->slice = $4;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("not zero slice is allowed only for arrays"),
+ parser_errposition(@4)));
+ }
+
new->expr = $7;
new->body = $8.stmts;
@@ -1719,6 +1735,19 @@ foreach_slice :
}
;
+foreach_type :
+ K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ }
+ | K_JSON K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_json_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_JSON_A;
+ }
+ ;
+
stmt_exit : exit_type opt_label opt_exitcond
{
PLpgSQL_stmt_exit *new;
diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
index 6379e86c8cb..d7588d3b4ad 100644
--- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
@@ -69,6 +69,7 @@ PG_KEYWORD("import", K_IMPORT)
PG_KEYWORD("info", K_INFO)
PG_KEYWORD("insert", K_INSERT)
PG_KEYWORD("is", K_IS)
+PG_KEYWORD("json", K_JSON)
PG_KEYWORD("last", K_LAST)
PG_KEYWORD("log", K_LOG)
PG_KEYWORD("merge", K_MERGE)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..c57b1da9b95 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -112,6 +112,7 @@ typedef enum PLpgSQL_stmt_type
PLPGSQL_STMT_FORS,
PLPGSQL_STMT_FORC,
PLPGSQL_STMT_FOREACH_A,
+ PLPGSQL_STMT_FOREACH_JSON_A,
PLPGSQL_STMT_EXIT,
PLPGSQL_STMT_RETURN,
PLPGSQL_STMT_RETURN_NEXT,
@@ -299,6 +300,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +446,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
@@ -766,6 +768,20 @@ typedef struct PLpgSQL_stmt_dynfors
List *params; /* USING expressions */
} PLpgSQL_stmt_dynfors;
+/*
+ * FOREACH loop (ancestor IN ARRAY and IN JSON ARRAY loop)
+ */
+typedef struct PLpgSQL_stmt_foreach
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* set expression */
+ List *body; /* List of statements */
+} PLpgSQL_stmt_foreach;
+
/*
* FOREACH item in array loop
*/
@@ -776,11 +792,27 @@ typedef struct PLpgSQL_stmt_foreach_a
unsigned int stmtid;
char *label;
int varno; /* loop target variable */
- int slice; /* slice dimension, or 0 */
PLpgSQL_expr *expr; /* array expression */
List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+ int slice; /* slice dimension, or 0 */
} PLpgSQL_stmt_foreach_a;
+/*
+ * FOREACH item in array loop
+ */
+typedef struct PLpgSQL_stmt_foreach_json_a
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* array expression */
+ List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+} PLpgSQL_stmt_foreach_json_a;
+
/*
* OPEN a curvar
*/
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..0e5c7106d1e
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,214 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in json array NULL -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in json array '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in json array '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-04 11:35 ` Jim Jones <[email protected]>
2026-03-04 18:50 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Jim Jones @ 2026-03-04 11:35 UTC (permalink / raw)
To: Pavel Stehule <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
I reviewed the code I have nothing to add at this point. LGTM!
The tests touch a lot of different scenarios, but for the sake of
completeness I'd like to suggest adding these three cases:
-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
DO $$
DECLARE x int;
BEGIN
FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
LOOP
EXIT WHEN x = 3;
RAISE NOTICE '%', x;
END LOOP;
END;
$$;
DO $$
DECLARE x int;
BEGIN
FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
LOOP
CONTINUE WHEN x % 2 = 0;
RAISE NOTICE '%', x;
END LOOP;
END;
$$;
-- Variable instead of string
DO $$
DECLARE x int; arr jsonb;
BEGIN
SELECT jsonb_agg(i) INTO arr
FROM generate_series(1,3) i;
FOREACH x IN JSON ARRAY arr
LOOP
RAISE NOTICE '%', x;
END LOOP;
END;
$$;
Thanks!
Best, Jim
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
@ 2026-03-04 18:50 ` Pavel Stehule <[email protected]>
2026-03-09 06:03 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Shreeya Sharma <[email protected]>
2026-03-09 06:11 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Shreeya Sharma <[email protected]>
2026-03-12 03:54 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Chao Li <[email protected]>
0 siblings, 3 replies; 26+ messages in thread
From: Pavel Stehule @ 2026-03-04 18:50 UTC (permalink / raw)
To: Jim Jones <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
Hi
st 4. 3. 2026 v 12:35 odesílatel Jim Jones <[email protected]>
napsal:
> I reviewed the code I have nothing to add at this point. LGTM!
>
> The tests touch a lot of different scenarios, but for the sake of
> completeness I'd like to suggest adding these three cases:
>
> -- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
> DO $$
> DECLARE x int;
> BEGIN
> FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
> LOOP
> EXIT WHEN x = 3;
> RAISE NOTICE '%', x;
> END LOOP;
> END;
> $$;
>
> DO $$
> DECLARE x int;
> BEGIN
> FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
> LOOP
> CONTINUE WHEN x % 2 = 0;
> RAISE NOTICE '%', x;
> END LOOP;
> END;
> $$;
>
>
> -- Variable instead of string
> DO $$
> DECLARE x int; arr jsonb;
> BEGIN
> SELECT jsonb_agg(i) INTO arr
> FROM generate_series(1,3) i;
>
> FOREACH x IN JSON ARRAY arr
> LOOP
> RAISE NOTICE '%', x;
> END LOOP;
> END;
> $$;
>
>
I merged these examples to tests
Thank you for review
Regards
Pavel
>
> Thanks!
>
> Best, Jim
>
Attachments:
[text/x-patch] v20260304-5-0001-FOREACH-scalar-IN-JSON-ARRAY.patch (32.0K, ../../CAFj8pRA-GKmYig4A23mUyCxEquYPK_t2acg0s_KHWhadh_w5MA@mail.gmail.com/3-v20260304-5-0001-FOREACH-scalar-IN-JSON-ARRAY.patch)
download | inline diff:
From b4fb57ca7e20ddef5454f1742bb14670f4ab495c Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN JSON ARRAY
this patch introduce FOREACH scalar_var IN JSON ARRAY. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN JSON ARRAY '[1,2,3.14]'
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN JSON ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 60 ++++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 297 ++++++++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 296 +++++++++++++++++
src/pl/plpgsql/src/pl_funcs.c | 29 ++
src/pl/plpgsql/src/pl_gram.y | 39 ++-
src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 +
src/pl/plpgsql/src/plpgsql.h | 36 ++-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 253 +++++++++++++++
10 files changed, 1006 insertions(+), 8 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..761b3cd9d35 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2780,6 +2780,66 @@ NOTICE: row = {10,11,12}
</para>
</sect2>
+ <sect2 id="plpgsql-foreach-json-array">
+ <title>Looping through JSON arrays</title>
+
+ <para>
+ The <literal>FOREACH</literal> loop is much like a regular
+ <literal>FOREACH</literal> loop over arrays,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of a JSON array value
+ (expression is internally casted to jsonb type).
+
+<synopsis>
+<optional> <<<replaceable>label</replaceable>>> </optional>
+FOREACH <replaceable>target</replaceable> IN JSON ARRAY <replaceable>expression</replaceable> LOOP
+ <replaceable>statements</replaceable>
+END LOOP <optional> <replaceable>label</replaceable> </optional>;
+</synopsis>
+ </para>
+
+ <para>
+ The target can be a scalar variable, a composite variable, or a list
+ of scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION print_elements(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_elements('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION print_fields(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_fields('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+ </sect2>
+
<sect2 id="plpgsql-error-trapping">
<title>Trapping Errors</title>
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..6d39560bade
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,297 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in json array NULL -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must not be null
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x numeric;
+begin
+ foreach x in json array '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+HINT: Cannot iterate over a scalar value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x numeric;
+begin
+ foreach x in json array '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+DETAIL: Cannot iterate over a object value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: 10
+NOTICE: FOUND: t
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: FOUND: f
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 3
+NOTICE: 5
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in json array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 84552e32c87..036390f07a4 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -41,6 +41,8 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
@@ -305,6 +307,8 @@ static int exec_stmt_forc(PLpgSQL_execstate *estate,
PLpgSQL_stmt_forc *stmt);
static int exec_stmt_foreach_a(PLpgSQL_execstate *estate,
PLpgSQL_stmt_foreach_a *stmt);
+static int exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt);
static int exec_stmt_open(PLpgSQL_execstate *estate,
PLpgSQL_stmt_open *stmt);
static int exec_stmt_fetch(PLpgSQL_execstate *estate,
@@ -2075,6 +2079,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
rc = exec_stmt_foreach_a(estate, (PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ rc = exec_stmt_foreach_json_a(estate, (PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
+
case PLPGSQL_STMT_EXIT:
rc = exec_stmt_exit(estate, (PLpgSQL_stmt_exit *) stmt);
break;
@@ -2995,6 +3003,249 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
}
+/*
+ * Convert JsonbValue to Datum that can be assigned to PLpgSQL_var.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ if (expected_typid == JSONBOID)
+ {
+ *typid = JSONBOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(JsonbValueToJsonb(jbv));
+ }
+ else if (expected_typid == JSONOID)
+ {
+ Jsonb *jsonb;
+ char *str;
+
+ /* serialize JsonValue to JSON text */
+ jsonb = JsonbValueToJsonb(jbv);
+ str = JsonbToCString(NULL, &jsonb->root, VARSIZE(jsonb));
+
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text(str));
+ }
+ else if (jbv->type == jbvNull)
+ {
+ *typid = expected_typid;
+ *typmod = -1;
+ *isnull = true;
+
+ return (Datum) 0;
+ }
+ else if (jbv->type == jbvString)
+ {
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len));
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ *typid = NUMERICOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(jbv->val.numeric);
+ }
+ else if (jbv->type == jbvBool)
+ {
+ *typid = BOOLOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return BoolGetDatum(jbv->val.boolean);
+ }
+ else
+ {
+ Jsonb *jsonb;
+ Datum result;
+
+ /* isnull in json_populate_type is inout argument */
+ *isnull = false;
+
+ jsonb = JsonbValueToJsonb(jbv);
+ result = json_populate_type(PointerGetDatum(jsonb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+
+ return result;
+ }
+}
+
+/* ----------
+ * exec_stmt_foreach_json_a Loop over elements in json array
+ *
+ * When target is a composite, then target is populated like json_to_populate_record.
+ * jsonb doesn't preserve attribute order, so position based mapping between
+ * target and source can be possibly dangerous (with unexpected behave).
+ * ----------
+ */
+static int
+exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ Oid exprtypeid;
+ int32 exprtypmod;
+ Datum exprdatum;
+ PLpgSQL_datum *loop_var;
+ Oid loop_var_typid;
+ int32 loop_var_typmod;
+ Oid loop_var_collation;
+ Jsonb *jb;
+ JsonbIterator *it;
+ JsonbValue jbv;
+ JsonbIteratorToken r;
+ MemoryContext stmt_mcontext;
+ MemoryContext oldcontext;
+ MemoryContext tmp_cxt;
+ bool found = false;
+ bool isnull;
+ bool skipNested = false;
+ int rc = PLPGSQL_RC_OK;
+ void *cache = NULL;
+
+ /* get the value of the expression */
+ exprdatum = exec_eval_expr(estate, stmt->expr, &isnull,
+ &exprtypeid, &exprtypmod);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("FOREACH expression must not be null")));
+
+ /*
+ * Do as much as possible of the code below in stmt_mcontext, to avoid any
+ * leaks from called subroutines. We need a private stmt_mcontext since
+ * we'll be calling arbitrary statement code.
+ */
+ stmt_mcontext = get_stmt_mcontext(estate);
+ push_stmt_mcontext(estate);
+ oldcontext = MemoryContextSwitchTo(stmt_mcontext);
+
+ tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "FOREACH IN JSON ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* cast to jsonb */
+ exprdatum = exec_cast_value(estate, exprdatum, &isnull,
+ exprtypeid, exprtypmod,
+ JSONBOID, -1);
+
+ Assert(!isnull);
+
+ /*
+ * We must copy the array into stmt_mcontext, else it will disappear in
+ * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
+ * while running the loop body, so we have little choice.
+ */
+ jb = DatumGetJsonbPCopy(exprdatum);
+
+ /* Clean up any leftover temporary memory */
+ exec_eval_cleanup(estate);
+
+ /*
+ * This is compatible with jsonb_array_element. SQL/JSON functions are not
+ * too strict like PostgreSQL proprietary (old json) functions. In SQL/JSON
+ * a scalar is equal to one element array. The basic question is if FOREACH
+ * should be more restrictive like old JSON function, or less restrictive
+ * like SQL/JSON functions.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errhint("Cannot iterate over a scalar value.")));
+ else if (JB_ROOT_IS_OBJECT(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errdetail("Cannot iterate over a object value.")));
+
+ Assert(JB_ROOT_IS_ARRAY(jb));
+
+ /* Set up the loop variable and see if it is of an array type */
+ loop_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, loop_var,
+ &loop_var_typid, &loop_var_typmod,
+ &loop_var_collation);
+
+ it = JsonbIteratorInit(&jb->root);
+
+ while ((r = JsonbIteratorNext(&it, &jbv, skipNested)) != WJB_DONE)
+ {
+ skipNested = true;
+
+ if (r == WJB_ELEM)
+ {
+ Datum val;
+ Oid valtypid;
+ int32 valtypmod;
+ bool valisnull;
+
+ found = true; /* looped at least once */
+
+ MemoryContextSwitchTo(tmp_cxt);
+
+ val = JsonbValueToDatum(&jbv,
+ &valtypid, &valtypmod, &valisnull,
+ loop_var_typid, loop_var_typmod,
+ &cache, stmt_mcontext);
+
+ /* exec_assign_value and exec_stmts must run in the main context */
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Assign current element/slice to the loop variable */
+ exec_assign_value(estate, loop_var, val,
+ valisnull, valtypid, valtypmod);
+
+ MemoryContextReset(tmp_cxt);
+
+ /*
+ * Execute the statements
+ */
+ rc = exec_stmts(estate, stmt->body);
+
+ LOOP_RC_PROCESSING(stmt->label, break);
+
+ MemoryContextSwitchTo(stmt_mcontext);
+ }
+ }
+
+ /* Restore memory context state */
+ MemoryContextSwitchTo(oldcontext);
+ pop_stmt_mcontext(estate);
+
+ /* Release temporary memory, including the array value */
+ MemoryContextReset(stmt_mcontext);
+
+ /*
+ * Set the FOUND variable to indicate the result of executing the loop
+ * (namely, whether we looped one or more times). This must be set here so
+ * that it does not interfere with the value of the FOUND variable inside
+ * the loop processing itself.
+ */
+ exec_set_found(estate, found);
+
+ return rc;
+}
+
/* ----------
* exec_stmt_foreach_a Loop over elements or slices of an array
*
@@ -5537,6 +5788,51 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index 92cd9116c0e..7511fab7e68 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -253,6 +253,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
return _("FOR over cursor");
case PLPGSQL_STMT_FOREACH_A:
return _("FOREACH over array");
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ return _("FOREACH over json array");
case PLPGSQL_STMT_EXIT:
return ((PLpgSQL_stmt_exit *) stmt)->is_exit ? "EXIT" : "CONTINUE";
case PLPGSQL_STMT_RETURN:
@@ -467,6 +469,14 @@ plpgsql_statement_tree_walker_impl(PLpgSQL_stmt *stmt,
{
PLpgSQL_stmt_foreach_a *fstmt = (PLpgSQL_stmt_foreach_a *) stmt;
+ E_WALK(fstmt->expr);
+ S_LIST_WALK(fstmt->body);
+ break;
+ }
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ {
+ PLpgSQL_stmt_foreach_json_a *fstmt = (PLpgSQL_stmt_foreach_json_a *) stmt;
+
E_WALK(fstmt->expr);
S_LIST_WALK(fstmt->body);
break;
@@ -795,6 +805,7 @@ static void dump_fori(PLpgSQL_stmt_fori *stmt);
static void dump_fors(PLpgSQL_stmt_fors *stmt);
static void dump_forc(PLpgSQL_stmt_forc *stmt);
static void dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt);
+static void dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt);
static void dump_exit(PLpgSQL_stmt_exit *stmt);
static void dump_return(PLpgSQL_stmt_return *stmt);
static void dump_return_next(PLpgSQL_stmt_return_next *stmt);
@@ -861,6 +872,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
case PLPGSQL_STMT_FOREACH_A:
dump_foreach_a((PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ dump_foreach_json_a((PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
case PLPGSQL_STMT_EXIT:
dump_exit((PLpgSQL_stmt_exit *) stmt);
break;
@@ -1157,6 +1171,21 @@ dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt)
printf(" ENDFOREACHA");
}
+static void
+dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ dump_ind();
+ printf("FOREACHA var %d ", stmt->varno);
+ printf("IN JSON ARRAY ");
+ dump_expr(stmt->expr);
+ printf("\n");
+
+ dump_stmts(stmt->body);
+
+ dump_ind();
+ printf(" ENDFOREACHA");
+}
+
static void
dump_open(PLpgSQL_stmt_open *stmt)
{
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 5009e59a78f..23b465b10d5 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -178,6 +178,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
PLpgSQL_diag_item *diagitem;
PLpgSQL_stmt_fetch *fetch;
PLpgSQL_case_when *casewhen;
+ PLpgSQL_stmt_foreach *foreach;
}
%type <declhdr> decl_sect
@@ -220,6 +221,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%type <casewhen> case_when
%type <list> case_when_list opt_case_else
+%type <foreach> foreach_type
%type <boolean> getdiag_area_opt
%type <list> getdiag_list
@@ -341,6 +343,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%token <keyword> K_PRINT_STRICT_PARAMS
%token <keyword> K_PRIOR
%token <keyword> K_QUERY
+%token <keyword> K_JSON
%token <keyword> K_RAISE
%token <keyword> K_RELATIVE
%token <keyword> K_RETURN
@@ -1671,16 +1674,29 @@ for_variable : T_DATUM
}
;
-stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN K_ARRAY expr_until_loop loop_body
+stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN foreach_type expr_until_loop loop_body
{
- PLpgSQL_stmt_foreach_a *new;
+ PLpgSQL_stmt_foreach *new;
- new = palloc0_object(PLpgSQL_stmt_foreach_a);
- new->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ new = $6;
new->lineno = plpgsql_location_to_lineno(@2, yyscanner);
new->stmtid = ++plpgsql_curr_compile->nstatements;
new->label = $1;
- new->slice = $4;
+
+ if ($4 > 0)
+ {
+ /* slicing is supported only by FOREACH IN ARRAY */
+ if (new->cmd_type == PLPGSQL_STMT_FOREACH_A)
+ {
+ ((PLpgSQL_stmt_foreach_a *) new)->slice = $4;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("not zero slice is allowed only for arrays"),
+ parser_errposition(@4)));
+ }
+
new->expr = $7;
new->body = $8.stmts;
@@ -1719,6 +1735,19 @@ foreach_slice :
}
;
+foreach_type :
+ K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ }
+ | K_JSON K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_json_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_JSON_A;
+ }
+ ;
+
stmt_exit : exit_type opt_label opt_exitcond
{
PLpgSQL_stmt_exit *new;
diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
index 6379e86c8cb..d7588d3b4ad 100644
--- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
@@ -69,6 +69,7 @@ PG_KEYWORD("import", K_IMPORT)
PG_KEYWORD("info", K_INFO)
PG_KEYWORD("insert", K_INSERT)
PG_KEYWORD("is", K_IS)
+PG_KEYWORD("json", K_JSON)
PG_KEYWORD("last", K_LAST)
PG_KEYWORD("log", K_LOG)
PG_KEYWORD("merge", K_MERGE)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..c57b1da9b95 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -112,6 +112,7 @@ typedef enum PLpgSQL_stmt_type
PLPGSQL_STMT_FORS,
PLPGSQL_STMT_FORC,
PLPGSQL_STMT_FOREACH_A,
+ PLPGSQL_STMT_FOREACH_JSON_A,
PLPGSQL_STMT_EXIT,
PLPGSQL_STMT_RETURN,
PLPGSQL_STMT_RETURN_NEXT,
@@ -299,6 +300,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +446,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
@@ -766,6 +768,20 @@ typedef struct PLpgSQL_stmt_dynfors
List *params; /* USING expressions */
} PLpgSQL_stmt_dynfors;
+/*
+ * FOREACH loop (ancestor IN ARRAY and IN JSON ARRAY loop)
+ */
+typedef struct PLpgSQL_stmt_foreach
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* set expression */
+ List *body; /* List of statements */
+} PLpgSQL_stmt_foreach;
+
/*
* FOREACH item in array loop
*/
@@ -776,11 +792,27 @@ typedef struct PLpgSQL_stmt_foreach_a
unsigned int stmtid;
char *label;
int varno; /* loop target variable */
- int slice; /* slice dimension, or 0 */
PLpgSQL_expr *expr; /* array expression */
List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+ int slice; /* slice dimension, or 0 */
} PLpgSQL_stmt_foreach_a;
+/*
+ * FOREACH item in array loop
+ */
+typedef struct PLpgSQL_stmt_foreach_json_a
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* array expression */
+ List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+} PLpgSQL_stmt_foreach_json_a;
+
/*
* OPEN a curvar
*/
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..0dde8d6fba4
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,253 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in json array NULL -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in json array '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in json array '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
+
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in json array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-04 18:50 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-09 06:03 ` Shreeya Sharma <[email protected]>
2 siblings, 0 replies; 26+ messages in thread
From: Shreeya Sharma @ 2026-03-09 06:03 UTC (permalink / raw)
To: [email protected]; +Cc: Pavel Stehule <[email protected]>
Status: SUCCESS
Applied against: /Users/shreeyasharma/postgresql (Current HEAD)
Build status: PASS
Test status: PASS
Notes:
- Compilation passed: True
- Regression tests (make check) passed: True
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-04 18:50 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-09 06:11 ` Shreeya Sharma <[email protected]>
2026-03-09 06:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2 siblings, 1 reply; 26+ messages in thread
From: Shreeya Sharma @ 2026-03-09 06:11 UTC (permalink / raw)
To: [email protected]; +Cc: Pavel Stehule <[email protected]>
The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, passed
Spec compliant: tested, passed
Documentation: tested, passed
The changes are good
The new status of this patch is: Ready for Committer
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-04 18:50 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-09 06:11 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Shreeya Sharma <[email protected]>
@ 2026-03-09 06:44 ` Pavel Stehule <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Pavel Stehule @ 2026-03-09 06:44 UTC (permalink / raw)
To: Shreeya Sharma <[email protected]>; +Cc: [email protected]
po 9. 3. 2026 v 7:12 odesílatel Shreeya Sharma <[email protected]>
napsal:
> The following review has been posted through the commitfest application:
> make installcheck-world: tested, passed
> Implements feature: tested, passed
> Spec compliant: tested, passed
> Documentation: tested, passed
>
> The changes are good
>
> The new status of this patch is: Ready for Committer
>
Thank you very much
Regards
Pavel
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-04 18:50 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-12 03:54 ` Chao Li <[email protected]>
2026-03-12 10:38 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2 siblings, 1 reply; 26+ messages in thread
From: Chao Li @ 2026-03-12 03:54 UTC (permalink / raw)
To: Pavel Stehule <[email protected]>; +Cc: Jim Jones <[email protected]>; PostgreSQL Hackers <[email protected]>
> On Mar 5, 2026, at 02:50, Pavel Stehule <[email protected]> wrote:
>
> Hi
>
> st 4. 3. 2026 v 12:35 odesílatel Jim Jones <[email protected]> napsal:
> I reviewed the code I have nothing to add at this point. LGTM!
>
> The tests touch a lot of different scenarios, but for the sake of
> completeness I'd like to suggest adding these three cases:
>
> -- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
> DO $$
> DECLARE x int;
> BEGIN
> FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
> LOOP
> EXIT WHEN x = 3;
> RAISE NOTICE '%', x;
> END LOOP;
> END;
> $$;
>
> DO $$
> DECLARE x int;
> BEGIN
> FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
> LOOP
> CONTINUE WHEN x % 2 = 0;
> RAISE NOTICE '%', x;
> END LOOP;
> END;
> $$;
>
>
> -- Variable instead of string
> DO $$
> DECLARE x int; arr jsonb;
> BEGIN
> SELECT jsonb_agg(i) INTO arr
> FROM generate_series(1,3) i;
>
> FOREACH x IN JSON ARRAY arr
> LOOP
> RAISE NOTICE '%', x;
> END LOOP;
> END;
> $$;
>
>
> I merged these examples to tests
>
> Thank you for review
>
> Regards
>
> Pavel
>
> Thanks!
>
> Best, Jim
> <v20260304-5-0001-FOREACH-scalar-IN-JSON-ARRAY.patch>
I just reviewed and tested the patch. Here comes my comments:
1 - pl_gram.y
```
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("not zero slice is allowed only for arrays"),
+ parser_errposition(@4)));
```
* () around errcode and errmsg are no longer needed. This comment is general, and I saw other ereport() also use () in this patch.
* parser_errposition should have the same indention as errmsg.
2 - pl_exec.c
```
+ errdetail("Cannot iterate over a object value.")));
```
Typo: a -> an
3 - pl_exec.c
```
+ tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "FOREACH IN JSON ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
```
Do we need to destroy tmp_cxt after the loop?
4 Looks like record type of loop var is not supported:
```
evantest=# do $$
declare
r record;
begin
foreach r in json array '[{"x":1,"y":"hi"},{"x":2,"y":"hello"}]’
loop
raise notice 'x: %, y: %', r.x, r.y;
end loop;
end;
$$;
ERROR: record type has not been registered
CONTEXT: PL/pgSQL function inline_code_block line 5 at FOREACH over json array
```
So, I want to check if you intentionally don’t want to support that or just missed that? If it’s not supported, then maybe document that.
5 I tried that composite type of loop var is supported, maybe add a test case for that. What I tested:
```
create type t_foreach_json_row as (
x int,
y text,
z numeric
);
do $$
declare
r t_foreach_json_row;
begin
foreach r in json array
'[{"x":1,"y":"one","z":1.5},
{"x":2,"y":"two"},
{"y":"three","z":3.14},
{}]'
loop
raise notice 'x=%, y=%, z=%', r.x, r.y, r.z;
end loop;
end;
$$;
drop type t_foreach_json_row;
```
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-04 18:50 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-12 03:54 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Chao Li <[email protected]>
@ 2026-03-12 10:38 ` Pavel Stehule <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Pavel Stehule @ 2026-03-12 10:38 UTC (permalink / raw)
To: Chao Li <[email protected]>; +Cc: Jim Jones <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi
čt 12. 3. 2026 v 4:55 odesílatel Chao Li <[email protected]> napsal:
>
>
> > On Mar 5, 2026, at 02:50, Pavel Stehule <[email protected]> wrote:
> >
> > Hi
> >
> > st 4. 3. 2026 v 12:35 odesílatel Jim Jones <[email protected]>
> napsal:
> > I reviewed the code I have nothing to add at this point. LGTM!
> >
> > The tests touch a lot of different scenarios, but for the sake of
> > completeness I'd like to suggest adding these three cases:
> >
> > -- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
> > DO $$
> > DECLARE x int;
> > BEGIN
> > FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
> > LOOP
> > EXIT WHEN x = 3;
> > RAISE NOTICE '%', x;
> > END LOOP;
> > END;
> > $$;
> >
> > DO $$
> > DECLARE x int;
> > BEGIN
> > FOREACH x IN JSON ARRAY '[1,2,3,4,5]'
> > LOOP
> > CONTINUE WHEN x % 2 = 0;
> > RAISE NOTICE '%', x;
> > END LOOP;
> > END;
> > $$;
> >
> >
> > -- Variable instead of string
> > DO $$
> > DECLARE x int; arr jsonb;
> > BEGIN
> > SELECT jsonb_agg(i) INTO arr
> > FROM generate_series(1,3) i;
> >
> > FOREACH x IN JSON ARRAY arr
> > LOOP
> > RAISE NOTICE '%', x;
> > END LOOP;
> > END;
> > $$;
> >
> >
> > I merged these examples to tests
> >
> > Thank you for review
> >
> > Regards
> >
> > Pavel
> >
> > Thanks!
> >
> > Best, Jim
> > <v20260304-5-0001-FOREACH-scalar-IN-JSON-ARRAY.patch>
>
> I just reviewed and tested the patch. Here comes my comments:
>
> 1 - pl_gram.y
> ```
> +
> ereport(ERROR,
> +
> (errcode(ERRCODE_SYNTAX_ERROR),
> +
> errmsg("not zero slice is allowed only for arrays"),
> +
> parser_errposition(@4)));
> ```
>
> * () around errcode and errmsg are no longer needed. This comment is
> general, and I saw other ereport() also use () in this patch.
> * parser_errposition should have the same indention as errmsg.
>
fixed
>
> 2 - pl_exec.c
> ```
> + errdetail("Cannot iterate over a object
> value.")));
> ```
>
> Typo: a -> an
>
fixed
>
> 3 - pl_exec.c
> ```
> + tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
> +
> "FOREACH IN JSON ARRAY temporary cxt",
> +
> ALLOCSET_DEFAULT_SIZES);
> ```
>
> Do we need to destroy tmp_cxt after the loop?
>
no - it is cleaned by MemoryContextReset(stmt_mcontext);
>
> 4 Looks like record type of loop var is not supported:
> ```
> evantest=# do $$
> declare
> r record;
> begin
> foreach r in json array '[{"x":1,"y":"hi"},{"x":2,"y":"hello"}]’
> loop
> raise notice 'x: %, y: %', r.x, r.y;
> end loop;
> end;
> $$;
> ERROR: record type has not been registered
> CONTEXT: PL/pgSQL function inline_code_block line 5 at FOREACH over json
> array
> ```
>
> So, I want to check if you intentionally don’t want to support that or
> just missed that? If it’s not supported, then maybe document that.
>
It is intentional at this moment (and I think so it will be in future too).
For reading fields from a json object I use the json_populate_type
function, and this function needs a known tupdesc. Generally JSON objects
have no fixed structure, and when a record's variable has no assigned type,
then we have to create new tupdesc for each value. This can be possibly
slow and memory expensive. Probably - I never tested this case. It is valid
use case, but it can be solved in later - and the support will be more
invasive - requires support in json_populate_type
I enhanced doc
<para>
The target variable can be of type RECORD, but the real structure has
to be
assigned before usage in FOREACH statement.
</para>
>
> 5 I tried that composite type of loop var is supported, maybe add a test
> case for that. What I tested:
> ```
> create type t_foreach_json_row as (
> x int,
> y text,
> z numeric
> );
>
> do $$
> declare
> r t_foreach_json_row;
> begin
> foreach r in json array
> '[{"x":1,"y":"one","z":1.5},
> {"x":2,"y":"two"},
> {"y":"three","z":3.14},
> {}]'
> loop
> raise notice 'x=%, y=%, z=%', r.x, r.y, r.z;
> end loop;
> end;
> $$;
>
> drop type t_foreach_json_row;
> ```
>
it is there already
create type t3 as (x int, y numeric, z varchar);
do $$
declare c t3;
begin
foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10,
"y":3.14}]'
loop
raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
end loop;
end;
$$;
assigned updated version -
I'll try to modify this patch like Tom proposed in the next version. But
the fundament behavior should be same
Thank you for check and testing
Regards
Pavel
>
> Best regards,
> --
> Chao Li (Evan)
> HighGo Software Co., Ltd.
> https://www.highgo.com/
>
>
>
>
>
Attachments:
[text/x-patch] v20260312-6-0001-FOREACH-scalar-IN-JSON-ARRAY.patch (32.2K, ../../CAFj8pRBUd1Ojs8O+cJY3qmruuM=QPby_XTJtL9d6YtUX15OvTQ@mail.gmail.com/3-v20260312-6-0001-FOREACH-scalar-IN-JSON-ARRAY.patch)
download | inline diff:
From 0798d478ddbfa1676ace2dafe8bd8441423a56a4 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN JSON ARRAY
this patch introduce FOREACH scalar_var IN JSON ARRAY. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN JSON ARRAY '[1,2,3.14]'
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN JSON ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 65 ++++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 297 ++++++++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 296 +++++++++++++++++
src/pl/plpgsql/src/pl_funcs.c | 29 ++
src/pl/plpgsql/src/pl_gram.y | 39 ++-
src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 +
src/pl/plpgsql/src/plpgsql.h | 36 ++-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 253 +++++++++++++++
10 files changed, 1011 insertions(+), 8 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..4a27e013f2a 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2780,6 +2780,71 @@ NOTICE: row = {10,11,12}
</para>
</sect2>
+ <sect2 id="plpgsql-foreach-json-array">
+ <title>Looping through JSON arrays</title>
+
+ <para>
+ The <literal>FOREACH</literal> loop is much like a regular
+ <literal>FOREACH</literal> loop over arrays,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of a JSON array value
+ (expression is internally casted to jsonb type).
+
+<synopsis>
+<optional> <<<replaceable>label</replaceable>>> </optional>
+FOREACH <replaceable>target</replaceable> IN JSON ARRAY <replaceable>expression</replaceable> LOOP
+ <replaceable>statements</replaceable>
+END LOOP <optional> <replaceable>label</replaceable> </optional>;
+</synopsis>
+ </para>
+
+ <para>
+ The target can be a scalar variable, a composite variable, or a list
+ of scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION print_elements(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_elements('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION print_fields(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN JSON ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_fields('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+
+ <para>
+ The target variable can be of type <literal>RECORD</literal>, but the real structure has to be
+ assigned before usage in the <literal>FOREACH</literal> statement.
+ </para>
+ </sect2>
+
<sect2 id="plpgsql-error-trapping">
<title>Trapping Errors</title>
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..090da983848
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,297 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in json array NULL -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must not be null
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x numeric;
+begin
+ foreach x in json array '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+HINT: Cannot iterate over a scalar value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x numeric;
+begin
+ foreach x in json array '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+DETAIL: Cannot iterate over an object value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: 10
+NOTICE: FOUND: t
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: FOUND: f
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over json array
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 3
+NOTICE: 5
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in json array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 84552e32c87..2e108d7e918 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -41,6 +41,8 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
@@ -305,6 +307,8 @@ static int exec_stmt_forc(PLpgSQL_execstate *estate,
PLpgSQL_stmt_forc *stmt);
static int exec_stmt_foreach_a(PLpgSQL_execstate *estate,
PLpgSQL_stmt_foreach_a *stmt);
+static int exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt);
static int exec_stmt_open(PLpgSQL_execstate *estate,
PLpgSQL_stmt_open *stmt);
static int exec_stmt_fetch(PLpgSQL_execstate *estate,
@@ -2075,6 +2079,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
rc = exec_stmt_foreach_a(estate, (PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ rc = exec_stmt_foreach_json_a(estate, (PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
+
case PLPGSQL_STMT_EXIT:
rc = exec_stmt_exit(estate, (PLpgSQL_stmt_exit *) stmt);
break;
@@ -2995,6 +3003,249 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
}
+/*
+ * Convert JsonbValue to Datum that can be assigned to PLpgSQL_var.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ if (expected_typid == JSONBOID)
+ {
+ *typid = JSONBOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(JsonbValueToJsonb(jbv));
+ }
+ else if (expected_typid == JSONOID)
+ {
+ Jsonb *jsonb;
+ char *str;
+
+ /* serialize JsonValue to JSON text */
+ jsonb = JsonbValueToJsonb(jbv);
+ str = JsonbToCString(NULL, &jsonb->root, VARSIZE(jsonb));
+
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text(str));
+ }
+ else if (jbv->type == jbvNull)
+ {
+ *typid = expected_typid;
+ *typmod = -1;
+ *isnull = true;
+
+ return (Datum) 0;
+ }
+ else if (jbv->type == jbvString)
+ {
+ *typid = TEXTOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len));
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ *typid = NUMERICOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return PointerGetDatum(jbv->val.numeric);
+ }
+ else if (jbv->type == jbvBool)
+ {
+ *typid = BOOLOID;
+ *typmod = -1;
+ *isnull = false;
+
+ return BoolGetDatum(jbv->val.boolean);
+ }
+ else
+ {
+ Jsonb *jsonb;
+ Datum result;
+
+ /* isnull in json_populate_type is inout argument */
+ *isnull = false;
+
+ jsonb = JsonbValueToJsonb(jbv);
+ result = json_populate_type(PointerGetDatum(jsonb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+
+ return result;
+ }
+}
+
+/* ----------
+ * exec_stmt_foreach_json_a Loop over elements in json array
+ *
+ * When target is a composite, then target is populated like json_to_populate_record.
+ * jsonb doesn't preserve attribute order, so position based mapping between
+ * target and source can be possibly dangerous (with unexpected behave).
+ * ----------
+ */
+static int
+exec_stmt_foreach_json_a(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ Oid exprtypeid;
+ int32 exprtypmod;
+ Datum exprdatum;
+ PLpgSQL_datum *loop_var;
+ Oid loop_var_typid;
+ int32 loop_var_typmod;
+ Oid loop_var_collation;
+ Jsonb *jb;
+ JsonbIterator *it;
+ JsonbValue jbv;
+ JsonbIteratorToken r;
+ MemoryContext stmt_mcontext;
+ MemoryContext oldcontext;
+ MemoryContext tmp_cxt;
+ bool found = false;
+ bool isnull;
+ bool skipNested = false;
+ int rc = PLPGSQL_RC_OK;
+ void *cache = NULL;
+
+ /* get the value of the expression */
+ exprdatum = exec_eval_expr(estate, stmt->expr, &isnull,
+ &exprtypeid, &exprtypmod);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("FOREACH expression must not be null")));
+
+ /*
+ * Do as much as possible of the code below in stmt_mcontext, to avoid any
+ * leaks from called subroutines. We need a private stmt_mcontext since
+ * we'll be calling arbitrary statement code.
+ */
+ stmt_mcontext = get_stmt_mcontext(estate);
+ push_stmt_mcontext(estate);
+ oldcontext = MemoryContextSwitchTo(stmt_mcontext);
+
+ tmp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "FOREACH IN JSON ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* cast to jsonb */
+ exprdatum = exec_cast_value(estate, exprdatum, &isnull,
+ exprtypeid, exprtypmod,
+ JSONBOID, -1);
+
+ Assert(!isnull);
+
+ /*
+ * We must copy the array into stmt_mcontext, else it will disappear in
+ * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
+ * while running the loop body, so we have little choice.
+ */
+ jb = DatumGetJsonbPCopy(exprdatum);
+
+ /* Clean up any leftover temporary memory */
+ exec_eval_cleanup(estate);
+
+ /*
+ * This is compatible with jsonb_array_element. SQL/JSON functions are not
+ * too strict like PostgreSQL proprietary (old json) functions. In SQL/JSON
+ * a scalar is equal to one element array. The basic question is if FOREACH
+ * should be more restrictive like old JSON function, or less restrictive
+ * like SQL/JSON functions.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errhint("Cannot iterate over a scalar value.")));
+ else if (JB_ROOT_IS_OBJECT(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errdetail("Cannot iterate over an object value.")));
+
+ Assert(JB_ROOT_IS_ARRAY(jb));
+
+ /* Set up the loop variable and see if it is of an array type */
+ loop_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, loop_var,
+ &loop_var_typid, &loop_var_typmod,
+ &loop_var_collation);
+
+ it = JsonbIteratorInit(&jb->root);
+
+ while ((r = JsonbIteratorNext(&it, &jbv, skipNested)) != WJB_DONE)
+ {
+ skipNested = true;
+
+ if (r == WJB_ELEM)
+ {
+ Datum val;
+ Oid valtypid;
+ int32 valtypmod;
+ bool valisnull;
+
+ found = true; /* looped at least once */
+
+ MemoryContextSwitchTo(tmp_cxt);
+
+ val = JsonbValueToDatum(&jbv,
+ &valtypid, &valtypmod, &valisnull,
+ loop_var_typid, loop_var_typmod,
+ &cache, stmt_mcontext);
+
+ /* exec_assign_value and exec_stmts must run in the main context */
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Assign current element/slice to the loop variable */
+ exec_assign_value(estate, loop_var, val,
+ valisnull, valtypid, valtypmod);
+
+ MemoryContextReset(tmp_cxt);
+
+ /*
+ * Execute the statements
+ */
+ rc = exec_stmts(estate, stmt->body);
+
+ LOOP_RC_PROCESSING(stmt->label, break);
+
+ MemoryContextSwitchTo(stmt_mcontext);
+ }
+ }
+
+ /* Restore memory context state */
+ MemoryContextSwitchTo(oldcontext);
+ pop_stmt_mcontext(estate);
+
+ /* Release temporary memory, including the array value */
+ MemoryContextReset(stmt_mcontext);
+
+ /*
+ * Set the FOUND variable to indicate the result of executing the loop
+ * (namely, whether we looped one or more times). This must be set here so
+ * that it does not interfere with the value of the FOUND variable inside
+ * the loop processing itself.
+ */
+ exec_set_found(estate, found);
+
+ return rc;
+}
+
/* ----------
* exec_stmt_foreach_a Loop over elements or slices of an array
*
@@ -5537,6 +5788,51 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index 92cd9116c0e..7511fab7e68 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -253,6 +253,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
return _("FOR over cursor");
case PLPGSQL_STMT_FOREACH_A:
return _("FOREACH over array");
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ return _("FOREACH over json array");
case PLPGSQL_STMT_EXIT:
return ((PLpgSQL_stmt_exit *) stmt)->is_exit ? "EXIT" : "CONTINUE";
case PLPGSQL_STMT_RETURN:
@@ -467,6 +469,14 @@ plpgsql_statement_tree_walker_impl(PLpgSQL_stmt *stmt,
{
PLpgSQL_stmt_foreach_a *fstmt = (PLpgSQL_stmt_foreach_a *) stmt;
+ E_WALK(fstmt->expr);
+ S_LIST_WALK(fstmt->body);
+ break;
+ }
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ {
+ PLpgSQL_stmt_foreach_json_a *fstmt = (PLpgSQL_stmt_foreach_json_a *) stmt;
+
E_WALK(fstmt->expr);
S_LIST_WALK(fstmt->body);
break;
@@ -795,6 +805,7 @@ static void dump_fori(PLpgSQL_stmt_fori *stmt);
static void dump_fors(PLpgSQL_stmt_fors *stmt);
static void dump_forc(PLpgSQL_stmt_forc *stmt);
static void dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt);
+static void dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt);
static void dump_exit(PLpgSQL_stmt_exit *stmt);
static void dump_return(PLpgSQL_stmt_return *stmt);
static void dump_return_next(PLpgSQL_stmt_return_next *stmt);
@@ -861,6 +872,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
case PLPGSQL_STMT_FOREACH_A:
dump_foreach_a((PLpgSQL_stmt_foreach_a *) stmt);
break;
+ case PLPGSQL_STMT_FOREACH_JSON_A:
+ dump_foreach_json_a((PLpgSQL_stmt_foreach_json_a *) stmt);
+ break;
case PLPGSQL_STMT_EXIT:
dump_exit((PLpgSQL_stmt_exit *) stmt);
break;
@@ -1157,6 +1171,21 @@ dump_foreach_a(PLpgSQL_stmt_foreach_a *stmt)
printf(" ENDFOREACHA");
}
+static void
+dump_foreach_json_a(PLpgSQL_stmt_foreach_json_a *stmt)
+{
+ dump_ind();
+ printf("FOREACHA var %d ", stmt->varno);
+ printf("IN JSON ARRAY ");
+ dump_expr(stmt->expr);
+ printf("\n");
+
+ dump_stmts(stmt->body);
+
+ dump_ind();
+ printf(" ENDFOREACHA");
+}
+
static void
dump_open(PLpgSQL_stmt_open *stmt)
{
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 5009e59a78f..c760e04d66b 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -178,6 +178,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
PLpgSQL_diag_item *diagitem;
PLpgSQL_stmt_fetch *fetch;
PLpgSQL_case_when *casewhen;
+ PLpgSQL_stmt_foreach *foreach;
}
%type <declhdr> decl_sect
@@ -220,6 +221,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%type <casewhen> case_when
%type <list> case_when_list opt_case_else
+%type <foreach> foreach_type
%type <boolean> getdiag_area_opt
%type <list> getdiag_list
@@ -341,6 +343,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%token <keyword> K_PRINT_STRICT_PARAMS
%token <keyword> K_PRIOR
%token <keyword> K_QUERY
+%token <keyword> K_JSON
%token <keyword> K_RAISE
%token <keyword> K_RELATIVE
%token <keyword> K_RETURN
@@ -1671,16 +1674,29 @@ for_variable : T_DATUM
}
;
-stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN K_ARRAY expr_until_loop loop_body
+stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN foreach_type expr_until_loop loop_body
{
- PLpgSQL_stmt_foreach_a *new;
+ PLpgSQL_stmt_foreach *new;
- new = palloc0_object(PLpgSQL_stmt_foreach_a);
- new->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ new = $6;
new->lineno = plpgsql_location_to_lineno(@2, yyscanner);
new->stmtid = ++plpgsql_curr_compile->nstatements;
new->label = $1;
- new->slice = $4;
+
+ if ($4 > 0)
+ {
+ /* slicing is supported only by FOREACH IN ARRAY */
+ if (new->cmd_type == PLPGSQL_STMT_FOREACH_A)
+ {
+ ((PLpgSQL_stmt_foreach_a *) new)->slice = $4;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("not zero slice is allowed only for arrays"),
+ parser_errposition(@4)));
+ }
+
new->expr = $7;
new->body = $8.stmts;
@@ -1719,6 +1735,19 @@ foreach_slice :
}
;
+foreach_type :
+ K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_A;
+ }
+ | K_JSON K_ARRAY
+ {
+ $$ = (PLpgSQL_stmt_foreach *) palloc0_object(PLpgSQL_stmt_foreach_json_a);
+ $$->cmd_type = PLPGSQL_STMT_FOREACH_JSON_A;
+ }
+ ;
+
stmt_exit : exit_type opt_label opt_exitcond
{
PLpgSQL_stmt_exit *new;
diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
index 6379e86c8cb..d7588d3b4ad 100644
--- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
@@ -69,6 +69,7 @@ PG_KEYWORD("import", K_IMPORT)
PG_KEYWORD("info", K_INFO)
PG_KEYWORD("insert", K_INSERT)
PG_KEYWORD("is", K_IS)
+PG_KEYWORD("json", K_JSON)
PG_KEYWORD("last", K_LAST)
PG_KEYWORD("log", K_LOG)
PG_KEYWORD("merge", K_MERGE)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..c57b1da9b95 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -112,6 +112,7 @@ typedef enum PLpgSQL_stmt_type
PLPGSQL_STMT_FORS,
PLPGSQL_STMT_FORC,
PLPGSQL_STMT_FOREACH_A,
+ PLPGSQL_STMT_FOREACH_JSON_A,
PLPGSQL_STMT_EXIT,
PLPGSQL_STMT_RETURN,
PLPGSQL_STMT_RETURN_NEXT,
@@ -299,6 +300,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +446,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
@@ -766,6 +768,20 @@ typedef struct PLpgSQL_stmt_dynfors
List *params; /* USING expressions */
} PLpgSQL_stmt_dynfors;
+/*
+ * FOREACH loop (ancestor IN ARRAY and IN JSON ARRAY loop)
+ */
+typedef struct PLpgSQL_stmt_foreach
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* set expression */
+ List *body; /* List of statements */
+} PLpgSQL_stmt_foreach;
+
/*
* FOREACH item in array loop
*/
@@ -776,11 +792,27 @@ typedef struct PLpgSQL_stmt_foreach_a
unsigned int stmtid;
char *label;
int varno; /* loop target variable */
- int slice; /* slice dimension, or 0 */
PLpgSQL_expr *expr; /* array expression */
List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+ int slice; /* slice dimension, or 0 */
} PLpgSQL_stmt_foreach_a;
+/*
+ * FOREACH item in array loop
+ */
+typedef struct PLpgSQL_stmt_foreach_json_a
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+ unsigned int stmtid;
+ char *label;
+ int varno; /* loop target variable */
+ PLpgSQL_expr *expr; /* array expression */
+ List *body; /* List of statements */
+ /* end of fields that must match PLpgSQL_stmt_foreach */
+} PLpgSQL_stmt_foreach_json_a;
+
/*
* OPEN a curvar
*/
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..0dde8d6fba4
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,253 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in json array NULL -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in json array '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in json array '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in json array '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in json array '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in json array '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in json array '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in json array '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in json array '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in json array '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in json array '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in json array '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
+
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x int;
+begin
+ foreach x in json array '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in json array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-11 20:57 ` Peter Eisentraut <[email protected]>
2026-03-12 04:30 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Tom Lane <[email protected]>
1 sibling, 1 reply; 26+ messages in thread
From: Peter Eisentraut @ 2026-03-11 20:57 UTC (permalink / raw)
To: Pavel Stehule <[email protected]>; PostgreSQL Hackers <[email protected]>
On 28.02.26 08:10, Pavel Stehule wrote:
> I wrote PoC for previously proposed plpgsql statement FOREACH IN JSON ARRAY
Maybe this could be written in such a way that it doesn't hardcode JSON
arrays specifically, but a type could have an iteration helper function
that would feed this feature?
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
@ 2026-03-12 04:30 ` Tom Lane <[email protected]>
2026-03-12 07:00 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Tom Lane @ 2026-03-12 04:30 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Pavel Stehule <[email protected]>; PostgreSQL Hackers <[email protected]>
Peter Eisentraut <[email protected]> writes:
> Maybe this could be written in such a way that it doesn't hardcode JSON
> arrays specifically, but a type could have an iteration helper function
> that would feed this feature?
+1. ISTM that this feature would make sense for subscriptable types,
so one way to shoehorn it into the system without a lot of new overhead
could be to extend struct SubscriptRoutines to offer optional support
function(s) for iterating through all the elements of a subscriptable
object.
regards, tom lane
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
2026-03-12 04:30 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Tom Lane <[email protected]>
@ 2026-03-12 07:00 ` Pavel Stehule <[email protected]>
2026-03-17 06:58 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Pavel Stehule @ 2026-03-12 07:00 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi
čt 12. 3. 2026 v 5:30 odesílatel Tom Lane <[email protected]> napsal:
> Peter Eisentraut <[email protected]> writes:
> > Maybe this could be written in such a way that it doesn't hardcode JSON
> > arrays specifically, but a type could have an iteration helper function
> > that would feed this feature?
>
> +1. ISTM that this feature would make sense for subscriptable types,
> so one way to shoehorn it into the system without a lot of new overhead
> could be to extend struct SubscriptRoutines to offer optional support
> function(s) for iterating through all the elements of a subscriptable
> object.
>
I'll try to write second patch in this way
Regards
Pavel
>
> regards, tom lane
>
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
2026-03-12 04:30 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Tom Lane <[email protected]>
2026-03-12 07:00 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-17 06:58 ` Pavel Stehule <[email protected]>
2026-03-21 18:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Pavel Stehule @ 2026-03-17 06:58 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi
čt 12. 3. 2026 v 8:00 odesílatel Pavel Stehule <[email protected]>
napsal:
> Hi
>
> čt 12. 3. 2026 v 5:30 odesílatel Tom Lane <[email protected]> napsal:
>
>> Peter Eisentraut <[email protected]> writes:
>> > Maybe this could be written in such a way that it doesn't hardcode JSON
>> > arrays specifically, but a type could have an iteration helper function
>> > that would feed this feature?
>>
>> +1. ISTM that this feature would make sense for subscriptable types,
>> so one way to shoehorn it into the system without a lot of new overhead
>> could be to extend struct SubscriptRoutines to offer optional support
>> function(s) for iterating through all the elements of a subscriptable
>> object.
>>
>
>
attached patch do this - new interface has two
methods: CreateForeachAIterator and iterate
diff --git a/src/include/nodes/subscripting.h
b/src/include/nodes/subscripting.h
index 301f21dac2f..08bfe59ede4 100644
--- a/src/include/nodes/subscripting.h
+++ b/src/include/nodes/subscripting.h
@@ -154,6 +154,32 @@ typedef void (*SubscriptExecSetup) (const
SubscriptingRef *sbsref,
SubscriptingRefState *sbsrefstate,
SubscriptExecSteps *methods);
+typedef struct _ForeachAIterator ForeachAIterator;
+
+/*
+ * ForeachAIiterator is used by PLpgSQL FOREACH IN ARRAY statement.
+ * Input value should not be null, and inside CreateForeachAIterator
+ * routine must be copied to current (statement) context. "iterate"
+ * routine is called under short life memory context, that is resetted
+ * after any call.
+ */
+struct _ForeachAIterator
+{
+ bool (*iterate) (ForeachAIterator *self,
+ Datum *value,
+ bool *isnull,
+ Oid *typid,
+ int32 *typmod);
+ /* Private fields might appear beyond this point... */
+};
+
+typedef ForeachAIterator * (*CreateForeachAIterator) (Datum value,
+ Oid typid,
+ int32 typmod,
+ int slice,
+ Oid target_typid,
+ int32 target_typmod);
+
/* Struct returned by the SQL-visible subscript handler function */
typedef struct SubscriptRoutines
{
@@ -163,6 +189,9 @@ typedef struct SubscriptRoutines
bool fetch_leakproof; /* is fetch SubscriptingRef leakproof?
*/
bool store_leakproof; /* is assignment SubscriptingRef
* leakproof? */
+
+ /* returns iterator used by PL/pgSQL FOREACH statement */
+ CreateForeachAIterator create_foreach_a_iterator;
} SubscriptRoutines;
#endif /* SUBSCRIPTING_H */
Regards
Pavel
>
>
> Regards
>
> Pavel
>
>
>>
>> regards, tom lane
>>
>
Attachments:
[text/x-patch] v20260317-7-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch (37.0K, ../../CAFj8pRDHdO9bLM4=8=p0MF8rNpSKdw-F_=zG=YxWui8S4vn5RQ@mail.gmail.com/3-v20260317-7-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch)
download | inline diff:
From b7b6509381a7d6b4629b3b25ebb4a189b1eb34be Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN ARRAY jsonb_expr
this patch introduce support FOREACH scalar_var IN ARRAY expr, when the
result of the expression can be Jsonb. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN ARRAY '[1,2,3.14]'::jsonb
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'::jsonb
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
Because we use "old" syntax - FOREACH IN ARRAY, I prefer "old" behaviour,
that is more similar to iteration over an array.
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 62 +++-
src/backend/utils/adt/arraysubs.c | 97 +++++-
src/backend/utils/adt/jsonbsubs.c | 181 ++++++++++-
src/include/nodes/subscripting.h | 29 ++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 297 ++++++++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 189 +++++------
src/pl/plpgsql/src/plpgsql.h | 3 +-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 252 +++++++++++++++
10 files changed, 1016 insertions(+), 97 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..6bc9fb02e1e 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2697,12 +2697,12 @@ END LOOP <optional> <replaceable>label</replaceable> </optional>;
</sect2>
<sect2 id="plpgsql-foreach-array">
- <title>Looping through Arrays</title>
+ <title>Looping through Arrays or Jsonb arrays</title>
<para>
The <literal>FOREACH</literal> loop is much like a <literal>FOR</literal> loop,
but instead of iterating through the rows returned by an SQL query,
- it iterates through the elements of an array value.
+ it iterates through the elements of an array value or of jsonb array value.
(In general, <literal>FOREACH</literal> is meant for looping through
components of a composite-valued expression; variants for looping
through composites besides arrays may be added in future.)
@@ -2778,6 +2778,64 @@ NOTICE: row = {7,8,9}
NOTICE: row = {10,11,12}
</programlisting>
</para>
+
+ <para>
+ The <literal>SLICE</literal> higher than zero cannot be used when iterates
+ through jsonb arrays.
+ </para>
+
+ <para>
+ The <literal>FOREACH</literal> loop over jsonb arrays uses
+ same syntax like <literal>FOREACH</literal> loop over arrays,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of a Jsonb array value.
+ </para>
+
+ <para>
+ The target can be a scalar variable, a composite variable, or a list
+ of scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION print_elements(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_elements('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION print_fields(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_fields('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+
+ <para>
+ The target variable can be of type <literal>RECORD</literal>, but the real structure has to be
+ assigned before usage in the <literal>FOREACH</literal> statement.
+ </para>
</sect2>
<sect2 id="plpgsql-error-trapping">
diff --git a/src/backend/utils/adt/arraysubs.c b/src/backend/utils/adt/arraysubs.c
index 2bf9e9509fb..a42248ed633 100644
--- a/src/backend/utils/adt/arraysubs.c
+++ b/src/backend/utils/adt/arraysubs.c
@@ -23,6 +23,7 @@
#include "parser/parse_coerce.h"
#include "parser/parse_expr.h"
#include "utils/array.h"
+#include "utils/builtins.h"
#include "utils/fmgrprotos.h"
#include "utils/lsyscache.h"
@@ -46,6 +47,96 @@ typedef struct ArraySubWorkspace
int lowerindex[MAXDIM];
} ArraySubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ ArrayIterator it;
+ Oid result_typid;
+ int32 result_typmod;
+} ForeachAArrayIterState;
+
+static bool
+foreach_a_array_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAArrayIterState *iter = (ForeachAArrayIterState *) self;
+
+ *typid = iter->result_typid;
+ *typmod = iter->result_typmod;
+
+ return array_iterate(iter->it, value, isnull);
+}
+
+/*
+ * Used by plpgsql FOREACH IN ARRAY when input expression is an array
+ */
+static ForeachAIterator *
+create_foreach_a_array_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAArrayIterState *iter = palloc0(sizeof(ForeachAArrayIterState));
+ ArrayType *arr;
+ Oid target_elem_typid;
+
+ /* check the type of the expression - must be an array */
+ if (!OidIsValid(get_element_type(typid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH expression must yield an array, not type %s",
+ format_type_be(typid))));
+
+ /*
+ * We must copy the array into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ arr = DatumGetArrayTypePCopy(value);
+
+ /* Slice dimension must be less than or equal to array dimension */
+ if (slice < 0 || slice > ARR_NDIM(arr))
+ ereport(ERROR,
+ (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+ errmsg("slice dimension (%d) is out of the valid range 0..%d",
+ slice, ARR_NDIM(arr))));
+
+ /*
+ * Sanity-check the target type. We don't try very hard here, and
+ * should not be too picky since it's possible that exec_assign_value can
+ * coerce values of different types. But it seems worthwhile to complain
+ * if the array-ness of the loop variable is not right.
+ */
+ target_elem_typid = get_element_type(target_typid);
+
+ if (slice > 0 && target_elem_typid == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH ... SLICE loop variable must be of an array type")));
+ if (slice == 0 && target_elem_typid != InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH loop variable must not be of an array type")));
+
+ /* Identify iterator result type */
+ if (slice > 0)
+ {
+ /* When slicing, nominal type of result is same as array type */
+ iter->result_typid = typid;
+ iter->result_typmod = typmod;
+ }
+ else
+ {
+ /* Without slicing, results are individual array elements */
+ iter->result_typid = ARR_ELEMTYPE(arr);
+ iter->result_typmod = typmod;
+ }
+
+ /* Create an iterator to step through the array */
+ iter->it = array_create_iterator(arr, slice, NULL);
+
+ iter->pub.iterate = foreach_a_array_iterate;
+
+ return (ForeachAIterator *) iter;
+}
/*
* Finish parse analysis of a SubscriptingRef expression for an array.
@@ -545,7 +636,8 @@ array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
@@ -572,7 +664,8 @@ raw_array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c
index f2745b29a3f..ad2edf525de 100644
--- a/src/backend/utils/adt/jsonbsubs.c
+++ b/src/backend/utils/adt/jsonbsubs.c
@@ -22,6 +22,7 @@
#include "parser/parse_expr.h"
#include "utils/builtins.h"
#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
/* SubscriptingRefState.workspace for jsonb subscripting execution */
@@ -33,6 +34,17 @@ typedef struct JsonbSubWorkspace
Datum *index; /* Subscript values in Datum format */
} JsonbSubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ JsonbIterator *it;
+ bool skip_nested;
+ Oid target_typid;
+ int32 target_typmod;
+
+ MemoryContext cache_mcxt;
+ void *cache;
+} ForeachAJsonbIterState;
/*
* Finish parse analysis of a SubscriptingRef expression for a jsonb.
@@ -394,6 +406,172 @@ jsonb_exec_setup(const SubscriptingRef *sbsref,
methods->sbs_fetch_old = jsonb_subscript_fetch_old;
}
+/*
+ * Convert JsonbValue to Datum. This function is used in
+ * generic array iterator, that is used by FOREACH plpgsql
+ * statement. Against other cases, the result should not be
+ * necessary of expected_typid, because the value can be
+ * converted later when the value is assigned to PL/pgSQL
+ * variable. This can be more effective than generic IO
+ * cast used by json_populate_type.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ Datum result;
+
+ *isnull = false;
+ *typmod = -1;
+
+ /*
+ * These types can holds JSON null, so must be processed
+ * before processing jbvNull. We don't want to convert
+ * JSON null, to SQL null, when targer is of JSON.
+ */
+ if (expected_typid == JSONBOID || expected_typid == JSONOID)
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ if (expected_typid == JSONOID)
+ {
+ char *str;
+
+ str = JsonbToCString(NULL, &jb->root, VARSIZE(jb));
+ result = PointerGetDatum(cstring_to_text(str));
+ }
+ else
+ result = PointerGetDatum(jb);
+
+ *typid = expected_typid;
+ }
+
+ /*
+ * For special cases we can skip conversion to Jsonb
+ * and possibly IO cast.
+ */
+ else if (jbv->type == jbvNull)
+ {
+ result = (Datum) 0;
+ *isnull = true;
+ *typid = expected_typid;
+ }
+ else if (jbv->type == jbvString)
+ {
+ text *txt = cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len);
+
+ result = PointerGetDatum(txt);
+ *typid = TEXTOID;
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ result = PointerGetDatum(jbv->val.numeric);
+ *typid = NUMERICOID;
+ }
+ else if (jbv->type == jbvBool)
+ {
+ result = BoolGetDatum(jbv->val.boolean);
+ *typid = BOOLOID;
+ }
+
+ /* generic conversion */
+ else
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ result = json_populate_type(PointerGetDatum(jb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+ }
+
+ return result;
+}
+
+static bool
+foreach_a_jsonb_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAJsonbIterState *iter = (ForeachAJsonbIterState *) self;
+ JsonbIteratorToken r;
+ JsonbValue jbv;
+
+ while ((r = JsonbIteratorNext(&iter->it, &jbv, iter->skip_nested)) != WJB_DONE)
+ {
+ iter->skip_nested = true;
+
+ if (r == WJB_ELEM)
+ {
+ *value = JsonbValueToDatum(&jbv, typid, typmod, isnull,
+ iter->target_typid, iter->target_typmod,
+ &iter->cache, iter->cache_mcxt);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Create foreach array iterator for jsonb array
+ */
+static ForeachAIterator *
+create_foreach_a_jsonb_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAJsonbIterState *iter = palloc0(sizeof(ForeachAJsonbIterState));
+ Jsonb *jb;
+
+ if (typid != JSONBOID)
+ elog(ERROR, "unexpected source type");
+
+ if (slice > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("jsonb array iterator doesn't support slicing")));
+
+ /*
+ * We must copy the JSON into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ jb = DatumGetJsonbPCopy(value);
+
+ /*
+ * Jsonb iterator is designed like jsonb_array_element. Input value
+ * must be json array.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errhint("Cannot iterate over a scalar value.")));
+ else if (JB_ROOT_IS_OBJECT(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errdetail("Cannot iterate over an object value.")));
+
+ Assert(JB_ROOT_IS_ARRAY(jb));
+
+ iter->it = JsonbIteratorInit(&jb->root);
+
+ iter->target_typid = target_typid;
+ iter->target_typmod = target_typmod;
+ iter->cache_mcxt = CurrentMemoryContext;
+
+ iter->pub.iterate = foreach_a_jsonb_iterate;
+
+ return (ForeachAIterator *) iter;
+}
+
/*
* jsonb_subscript_handler
* Subscripting handler for jsonb.
@@ -407,7 +585,8 @@ jsonb_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = jsonb_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_jsonb_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/include/nodes/subscripting.h b/src/include/nodes/subscripting.h
index 301f21dac2f..08bfe59ede4 100644
--- a/src/include/nodes/subscripting.h
+++ b/src/include/nodes/subscripting.h
@@ -154,6 +154,32 @@ typedef void (*SubscriptExecSetup) (const SubscriptingRef *sbsref,
SubscriptingRefState *sbsrefstate,
SubscriptExecSteps *methods);
+typedef struct _ForeachAIterator ForeachAIterator;
+
+/*
+ * ForeachAIiterator is used by PLpgSQL FOREACH IN ARRAY statement.
+ * Input value should not be null, and inside CreateForeachAIterator
+ * routine must be copied to current (statement) context. "iterate"
+ * routine is called under short life memory context, that is resetted
+ * after any call.
+ */
+struct _ForeachAIterator
+{
+ bool (*iterate) (ForeachAIterator *self,
+ Datum *value,
+ bool *isnull,
+ Oid *typid,
+ int32 *typmod);
+ /* Private fields might appear beyond this point... */
+};
+
+typedef ForeachAIterator * (*CreateForeachAIterator) (Datum value,
+ Oid typid,
+ int32 typmod,
+ int slice,
+ Oid target_typid,
+ int32 target_typmod);
+
/* Struct returned by the SQL-visible subscript handler function */
typedef struct SubscriptRoutines
{
@@ -163,6 +189,9 @@ typedef struct SubscriptRoutines
bool fetch_leakproof; /* is fetch SubscriptingRef leakproof? */
bool store_leakproof; /* is assignment SubscriptingRef
* leakproof? */
+
+ /* returns iterator used by PL/pgSQL FOREACH statement */
+ CreateForeachAIterator create_foreach_a_iterator;
} SubscriptRoutines;
#endif /* SUBSCRIPTING_H */
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..ddc571f3c79
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,297 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must not be null
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+HINT: Cannot iterate over a scalar value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+DETAIL: Cannot iterate over an object value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: 10
+NOTICE: FOUND: t
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: FOUND: f
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 3
+NOTICE: 5
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 84552e32c87..331d64119c1 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -29,6 +29,7 @@
#include "mb/stringinfo_mb.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
+#include "nodes/subscripting.h"
#include "nodes/supportnodes.h"
#include "optimizer/optimizer.h"
#include "parser/parse_coerce.h"
@@ -2994,40 +2995,49 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
return rc;
}
-
/* ----------
- * exec_stmt_foreach_a Loop over elements or slices of an array
- *
- * When looping over elements, the loop variable is the same type that the
- * array stores (eg: integer), when looping through slices, the loop variable
- * is an array of size and dimensions to match the size of the slice.
- * ----------
+ * exec_stmt_foreach_a Loop over elements in an array or jsonb array
+ * ----------
*/
static int
exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
{
- ArrayType *arr;
- Oid arrtype;
- int32 arrtypmod;
- PLpgSQL_datum *loop_var;
- Oid loop_var_elem_type;
- bool found = false;
+ Datum expr;
+ Oid expr_typid;
+ int32 expr_typmod;
+ bool isnull;
+ PLpgSQL_datum *target_var;
+ Oid target_typid;
+ int32 target_typmod;
+ Oid target_collation;
+ Datum value;
+ Oid typid;
+ int32 typmod;
int rc = PLPGSQL_RC_OK;
+ const struct SubscriptRoutines *sbroutines;
+ ForeachAIterator *iterator;
MemoryContext stmt_mcontext;
+ MemoryContext tmp_cxt;
MemoryContext oldcontext;
- ArrayIterator array_iterator;
- Oid iterator_result_type;
- int32 iterator_result_typmod;
- Datum value;
- bool isnull;
+ bool found = false;
- /* get the value of the array expression */
- value = exec_eval_expr(estate, stmt->expr, &isnull, &arrtype, &arrtypmod);
+ /* get the value of the expression */
+ expr = exec_eval_expr(estate, stmt->expr, &isnull,
+ &expr_typid, &expr_typmod);
if (isnull)
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("FOREACH expression must not be null")));
+ sbroutines = getSubscriptingRoutines(expr_typid, NULL);
+ if (!sbroutines)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot iterate over type %s because it does not support subscripting",
+ format_type_be(expr_typid))));
+
+ Assert(sbroutines->create_foreach_a_iterator);
+
/*
* Do as much as possible of the code below in stmt_mcontext, to avoid any
* leaks from called subroutines. We need a private stmt_mcontext since
@@ -3037,79 +3047,34 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
push_stmt_mcontext(estate);
oldcontext = MemoryContextSwitchTo(stmt_mcontext);
- /* check the type of the expression - must be an array */
- if (!OidIsValid(get_element_type(arrtype)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH expression must yield an array, not type %s",
- format_type_be(arrtype))));
+ /* Set up the target (loop) variable */
+ target_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, target_var,
+ &target_typid, &target_typmod,
+ &target_collation);
/*
- * We must copy the array into stmt_mcontext, else it will disappear in
- * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
- * while running the loop body, so we have little choice.
+ * inside iterator constroctor, the expr should be copied to
+ * current memory context (stmt_mcontext). Without it, it will be released
+ * by next exec_eval_cleanup. The iterator constructor should
+ * be called under stmt memory context.
*/
- arr = DatumGetArrayTypePCopy(value);
+ iterator = sbroutines->create_foreach_a_iterator(expr,
+ expr_typid, expr_typmod,
+ stmt->slice, target_typid,
+ target_typmod);
/* Clean up any leftover temporary memory */
exec_eval_cleanup(estate);
- /* Slice dimension must be less than or equal to array dimension */
- if (stmt->slice < 0 || stmt->slice > ARR_NDIM(arr))
- ereport(ERROR,
- (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
- errmsg("slice dimension (%d) is out of the valid range 0..%d",
- stmt->slice, ARR_NDIM(arr))));
-
- /* Set up the loop variable and see if it is of an array type */
- loop_var = estate->datums[stmt->varno];
- if (loop_var->dtype == PLPGSQL_DTYPE_REC ||
- loop_var->dtype == PLPGSQL_DTYPE_ROW)
- {
- /*
- * Record/row variable is certainly not of array type, and might not
- * be initialized at all yet, so don't try to get its type
- */
- loop_var_elem_type = InvalidOid;
- }
- else
- loop_var_elem_type = get_element_type(plpgsql_exec_get_datum_type(estate,
- loop_var));
+ tmp_cxt = AllocSetContextCreate(stmt_mcontext,
+ "FOREACH IN ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
- /*
- * Sanity-check the loop variable type. We don't try very hard here, and
- * should not be too picky since it's possible that exec_assign_value can
- * coerce values of different types. But it seems worthwhile to complain
- * if the array-ness of the loop variable is not right.
- */
- if (stmt->slice > 0 && loop_var_elem_type == InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH ... SLICE loop variable must be of an array type")));
- if (stmt->slice == 0 && loop_var_elem_type != InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH loop variable must not be of an array type")));
+ MemoryContextSwitchTo(tmp_cxt);
- /* Create an iterator to step through the array */
- array_iterator = array_create_iterator(arr, stmt->slice, NULL);
-
- /* Identify iterator result type */
- if (stmt->slice > 0)
- {
- /* When slicing, nominal type of result is same as array type */
- iterator_result_type = arrtype;
- iterator_result_typmod = arrtypmod;
- }
- else
- {
- /* Without slicing, results are individual array elements */
- iterator_result_type = ARR_ELEMTYPE(arr);
- iterator_result_typmod = arrtypmod;
- }
-
- /* Iterate over the array elements or slices */
- while (array_iterate(array_iterator, &value, &isnull))
+ while (iterator->iterate(iterator, &value, &isnull, &typid, &typmod))
{
found = true; /* looped at least once */
@@ -3117,12 +3082,9 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
MemoryContextSwitchTo(oldcontext);
/* Assign current element/slice to the loop variable */
- exec_assign_value(estate, loop_var, value, isnull,
- iterator_result_type, iterator_result_typmod);
+ exec_assign_value(estate, target_var, value, isnull, typid, typmod);
- /* In slice case, value is temporary; must free it to avoid leakage */
- if (stmt->slice > 0)
- pfree(DatumGetPointer(value));
+ MemoryContextReset(tmp_cxt);
/*
* Execute the statements
@@ -3131,7 +3093,7 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
LOOP_RC_PROCESSING(stmt->label, break);
- MemoryContextSwitchTo(stmt_mcontext);
+ MemoryContextSwitchTo(tmp_cxt);
}
/* Restore memory context state */
@@ -5537,6 +5499,53 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ TupleDescFinalize(row->rowtupdesc);
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..5cbdb4ecd9d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -299,6 +299,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +445,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..a64004417af
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,252 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
+
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
2026-03-12 04:30 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Tom Lane <[email protected]>
2026-03-12 07:00 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-17 06:58 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-03-21 18:40 ` Pavel Stehule <[email protected]>
2026-05-26 06:51 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Pavel Stehule @ 2026-03-21 18:40 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi
only rebase
Regards
Pavel
Attachments:
[text/x-patch] v20260321-8-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch (37.0K, ../../CAFj8pRAHrHmpPmmFfm6B5ig3GzWfVVSh_3CofcQYM8utrSQoFQ@mail.gmail.com/3-v20260321-8-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch)
download | inline diff:
From a08a1205caaf0b546b7f21a1a6251551db2a0196 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN ARRAY jsonb_expr
this patch introduce support FOREACH scalar_var IN ARRAY expr, when the
result of the expression can be Jsonb. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN ARRAY '[1,2,3.14]'::jsonb
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'::jsonb
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
Because we use "old" syntax - FOREACH IN ARRAY, I prefer "old" behaviour,
that is more similar to iteration over an array.
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 62 +++-
src/backend/utils/adt/arraysubs.c | 97 +++++-
src/backend/utils/adt/jsonbsubs.c | 181 ++++++++++-
src/include/nodes/subscripting.h | 29 ++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 297 ++++++++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 189 +++++------
src/pl/plpgsql/src/plpgsql.h | 3 +-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 252 +++++++++++++++
10 files changed, 1016 insertions(+), 97 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..6bc9fb02e1e 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2697,12 +2697,12 @@ END LOOP <optional> <replaceable>label</replaceable> </optional>;
</sect2>
<sect2 id="plpgsql-foreach-array">
- <title>Looping through Arrays</title>
+ <title>Looping through Arrays or Jsonb arrays</title>
<para>
The <literal>FOREACH</literal> loop is much like a <literal>FOR</literal> loop,
but instead of iterating through the rows returned by an SQL query,
- it iterates through the elements of an array value.
+ it iterates through the elements of an array value or of jsonb array value.
(In general, <literal>FOREACH</literal> is meant for looping through
components of a composite-valued expression; variants for looping
through composites besides arrays may be added in future.)
@@ -2778,6 +2778,64 @@ NOTICE: row = {7,8,9}
NOTICE: row = {10,11,12}
</programlisting>
</para>
+
+ <para>
+ The <literal>SLICE</literal> higher than zero cannot be used when iterates
+ through jsonb arrays.
+ </para>
+
+ <para>
+ The <literal>FOREACH</literal> loop over jsonb arrays uses
+ same syntax like <literal>FOREACH</literal> loop over arrays,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of a Jsonb array value.
+ </para>
+
+ <para>
+ The target can be a scalar variable, a composite variable, or a list
+ of scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION print_elements(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_elements('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION print_fields(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_fields('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+
+ <para>
+ The target variable can be of type <literal>RECORD</literal>, but the real structure has to be
+ assigned before usage in the <literal>FOREACH</literal> statement.
+ </para>
</sect2>
<sect2 id="plpgsql-error-trapping">
diff --git a/src/backend/utils/adt/arraysubs.c b/src/backend/utils/adt/arraysubs.c
index 2bf9e9509fb..a42248ed633 100644
--- a/src/backend/utils/adt/arraysubs.c
+++ b/src/backend/utils/adt/arraysubs.c
@@ -23,6 +23,7 @@
#include "parser/parse_coerce.h"
#include "parser/parse_expr.h"
#include "utils/array.h"
+#include "utils/builtins.h"
#include "utils/fmgrprotos.h"
#include "utils/lsyscache.h"
@@ -46,6 +47,96 @@ typedef struct ArraySubWorkspace
int lowerindex[MAXDIM];
} ArraySubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ ArrayIterator it;
+ Oid result_typid;
+ int32 result_typmod;
+} ForeachAArrayIterState;
+
+static bool
+foreach_a_array_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAArrayIterState *iter = (ForeachAArrayIterState *) self;
+
+ *typid = iter->result_typid;
+ *typmod = iter->result_typmod;
+
+ return array_iterate(iter->it, value, isnull);
+}
+
+/*
+ * Used by plpgsql FOREACH IN ARRAY when input expression is an array
+ */
+static ForeachAIterator *
+create_foreach_a_array_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAArrayIterState *iter = palloc0(sizeof(ForeachAArrayIterState));
+ ArrayType *arr;
+ Oid target_elem_typid;
+
+ /* check the type of the expression - must be an array */
+ if (!OidIsValid(get_element_type(typid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH expression must yield an array, not type %s",
+ format_type_be(typid))));
+
+ /*
+ * We must copy the array into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ arr = DatumGetArrayTypePCopy(value);
+
+ /* Slice dimension must be less than or equal to array dimension */
+ if (slice < 0 || slice > ARR_NDIM(arr))
+ ereport(ERROR,
+ (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+ errmsg("slice dimension (%d) is out of the valid range 0..%d",
+ slice, ARR_NDIM(arr))));
+
+ /*
+ * Sanity-check the target type. We don't try very hard here, and
+ * should not be too picky since it's possible that exec_assign_value can
+ * coerce values of different types. But it seems worthwhile to complain
+ * if the array-ness of the loop variable is not right.
+ */
+ target_elem_typid = get_element_type(target_typid);
+
+ if (slice > 0 && target_elem_typid == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH ... SLICE loop variable must be of an array type")));
+ if (slice == 0 && target_elem_typid != InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH loop variable must not be of an array type")));
+
+ /* Identify iterator result type */
+ if (slice > 0)
+ {
+ /* When slicing, nominal type of result is same as array type */
+ iter->result_typid = typid;
+ iter->result_typmod = typmod;
+ }
+ else
+ {
+ /* Without slicing, results are individual array elements */
+ iter->result_typid = ARR_ELEMTYPE(arr);
+ iter->result_typmod = typmod;
+ }
+
+ /* Create an iterator to step through the array */
+ iter->it = array_create_iterator(arr, slice, NULL);
+
+ iter->pub.iterate = foreach_a_array_iterate;
+
+ return (ForeachAIterator *) iter;
+}
/*
* Finish parse analysis of a SubscriptingRef expression for an array.
@@ -545,7 +636,8 @@ array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
@@ -572,7 +664,8 @@ raw_array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c
index f2745b29a3f..ad2edf525de 100644
--- a/src/backend/utils/adt/jsonbsubs.c
+++ b/src/backend/utils/adt/jsonbsubs.c
@@ -22,6 +22,7 @@
#include "parser/parse_expr.h"
#include "utils/builtins.h"
#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
/* SubscriptingRefState.workspace for jsonb subscripting execution */
@@ -33,6 +34,17 @@ typedef struct JsonbSubWorkspace
Datum *index; /* Subscript values in Datum format */
} JsonbSubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ JsonbIterator *it;
+ bool skip_nested;
+ Oid target_typid;
+ int32 target_typmod;
+
+ MemoryContext cache_mcxt;
+ void *cache;
+} ForeachAJsonbIterState;
/*
* Finish parse analysis of a SubscriptingRef expression for a jsonb.
@@ -394,6 +406,172 @@ jsonb_exec_setup(const SubscriptingRef *sbsref,
methods->sbs_fetch_old = jsonb_subscript_fetch_old;
}
+/*
+ * Convert JsonbValue to Datum. This function is used in
+ * generic array iterator, that is used by FOREACH plpgsql
+ * statement. Against other cases, the result should not be
+ * necessary of expected_typid, because the value can be
+ * converted later when the value is assigned to PL/pgSQL
+ * variable. This can be more effective than generic IO
+ * cast used by json_populate_type.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ Datum result;
+
+ *isnull = false;
+ *typmod = -1;
+
+ /*
+ * These types can holds JSON null, so must be processed
+ * before processing jbvNull. We don't want to convert
+ * JSON null, to SQL null, when targer is of JSON.
+ */
+ if (expected_typid == JSONBOID || expected_typid == JSONOID)
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ if (expected_typid == JSONOID)
+ {
+ char *str;
+
+ str = JsonbToCString(NULL, &jb->root, VARSIZE(jb));
+ result = PointerGetDatum(cstring_to_text(str));
+ }
+ else
+ result = PointerGetDatum(jb);
+
+ *typid = expected_typid;
+ }
+
+ /*
+ * For special cases we can skip conversion to Jsonb
+ * and possibly IO cast.
+ */
+ else if (jbv->type == jbvNull)
+ {
+ result = (Datum) 0;
+ *isnull = true;
+ *typid = expected_typid;
+ }
+ else if (jbv->type == jbvString)
+ {
+ text *txt = cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len);
+
+ result = PointerGetDatum(txt);
+ *typid = TEXTOID;
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ result = PointerGetDatum(jbv->val.numeric);
+ *typid = NUMERICOID;
+ }
+ else if (jbv->type == jbvBool)
+ {
+ result = BoolGetDatum(jbv->val.boolean);
+ *typid = BOOLOID;
+ }
+
+ /* generic conversion */
+ else
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ result = json_populate_type(PointerGetDatum(jb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+ }
+
+ return result;
+}
+
+static bool
+foreach_a_jsonb_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAJsonbIterState *iter = (ForeachAJsonbIterState *) self;
+ JsonbIteratorToken r;
+ JsonbValue jbv;
+
+ while ((r = JsonbIteratorNext(&iter->it, &jbv, iter->skip_nested)) != WJB_DONE)
+ {
+ iter->skip_nested = true;
+
+ if (r == WJB_ELEM)
+ {
+ *value = JsonbValueToDatum(&jbv, typid, typmod, isnull,
+ iter->target_typid, iter->target_typmod,
+ &iter->cache, iter->cache_mcxt);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Create foreach array iterator for jsonb array
+ */
+static ForeachAIterator *
+create_foreach_a_jsonb_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAJsonbIterState *iter = palloc0(sizeof(ForeachAJsonbIterState));
+ Jsonb *jb;
+
+ if (typid != JSONBOID)
+ elog(ERROR, "unexpected source type");
+
+ if (slice > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("jsonb array iterator doesn't support slicing")));
+
+ /*
+ * We must copy the JSON into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ jb = DatumGetJsonbPCopy(value);
+
+ /*
+ * Jsonb iterator is designed like jsonb_array_element. Input value
+ * must be json array.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errhint("Cannot iterate over a scalar value.")));
+ else if (JB_ROOT_IS_OBJECT(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errdetail("Cannot iterate over an object value.")));
+
+ Assert(JB_ROOT_IS_ARRAY(jb));
+
+ iter->it = JsonbIteratorInit(&jb->root);
+
+ iter->target_typid = target_typid;
+ iter->target_typmod = target_typmod;
+ iter->cache_mcxt = CurrentMemoryContext;
+
+ iter->pub.iterate = foreach_a_jsonb_iterate;
+
+ return (ForeachAIterator *) iter;
+}
+
/*
* jsonb_subscript_handler
* Subscripting handler for jsonb.
@@ -407,7 +585,8 @@ jsonb_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = jsonb_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_jsonb_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/include/nodes/subscripting.h b/src/include/nodes/subscripting.h
index 301f21dac2f..08bfe59ede4 100644
--- a/src/include/nodes/subscripting.h
+++ b/src/include/nodes/subscripting.h
@@ -154,6 +154,32 @@ typedef void (*SubscriptExecSetup) (const SubscriptingRef *sbsref,
SubscriptingRefState *sbsrefstate,
SubscriptExecSteps *methods);
+typedef struct _ForeachAIterator ForeachAIterator;
+
+/*
+ * ForeachAIiterator is used by PLpgSQL FOREACH IN ARRAY statement.
+ * Input value should not be null, and inside CreateForeachAIterator
+ * routine must be copied to current (statement) context. "iterate"
+ * routine is called under short life memory context, that is resetted
+ * after any call.
+ */
+struct _ForeachAIterator
+{
+ bool (*iterate) (ForeachAIterator *self,
+ Datum *value,
+ bool *isnull,
+ Oid *typid,
+ int32 *typmod);
+ /* Private fields might appear beyond this point... */
+};
+
+typedef ForeachAIterator * (*CreateForeachAIterator) (Datum value,
+ Oid typid,
+ int32 typmod,
+ int slice,
+ Oid target_typid,
+ int32 target_typmod);
+
/* Struct returned by the SQL-visible subscript handler function */
typedef struct SubscriptRoutines
{
@@ -163,6 +189,9 @@ typedef struct SubscriptRoutines
bool fetch_leakproof; /* is fetch SubscriptingRef leakproof? */
bool store_leakproof; /* is assignment SubscriptingRef
* leakproof? */
+
+ /* returns iterator used by PL/pgSQL FOREACH statement */
+ CreateForeachAIterator create_foreach_a_iterator;
} SubscriptRoutines;
#endif /* SUBSCRIPTING_H */
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..ddc571f3c79
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,297 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must not be null
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+HINT: Cannot iterate over a scalar value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+DETAIL: Cannot iterate over an object value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: 10
+NOTICE: FOUND: t
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: FOUND: f
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 3
+NOTICE: 5
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 45d667428f4..9ed346f13ab 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -29,6 +29,7 @@
#include "mb/stringinfo_mb.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
+#include "nodes/subscripting.h"
#include "nodes/supportnodes.h"
#include "optimizer/optimizer.h"
#include "parser/parse_coerce.h"
@@ -3026,40 +3027,49 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
return rc;
}
-
/* ----------
- * exec_stmt_foreach_a Loop over elements or slices of an array
- *
- * When looping over elements, the loop variable is the same type that the
- * array stores (eg: integer), when looping through slices, the loop variable
- * is an array of size and dimensions to match the size of the slice.
- * ----------
+ * exec_stmt_foreach_a Loop over elements in an array or jsonb array
+ * ----------
*/
static int
exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
{
- ArrayType *arr;
- Oid arrtype;
- int32 arrtypmod;
- PLpgSQL_datum *loop_var;
- Oid loop_var_elem_type;
- bool found = false;
+ Datum expr;
+ Oid expr_typid;
+ int32 expr_typmod;
+ bool isnull;
+ PLpgSQL_datum *target_var;
+ Oid target_typid;
+ int32 target_typmod;
+ Oid target_collation;
+ Datum value;
+ Oid typid;
+ int32 typmod;
int rc = PLPGSQL_RC_OK;
+ const struct SubscriptRoutines *sbroutines;
+ ForeachAIterator *iterator;
MemoryContext stmt_mcontext;
+ MemoryContext tmp_cxt;
MemoryContext oldcontext;
- ArrayIterator array_iterator;
- Oid iterator_result_type;
- int32 iterator_result_typmod;
- Datum value;
- bool isnull;
+ bool found = false;
- /* get the value of the array expression */
- value = exec_eval_expr(estate, stmt->expr, &isnull, &arrtype, &arrtypmod);
+ /* get the value of the expression */
+ expr = exec_eval_expr(estate, stmt->expr, &isnull,
+ &expr_typid, &expr_typmod);
if (isnull)
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("FOREACH expression must not be null")));
+ sbroutines = getSubscriptingRoutines(expr_typid, NULL);
+ if (!sbroutines)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot iterate over type %s because it does not support subscripting",
+ format_type_be(expr_typid))));
+
+ Assert(sbroutines->create_foreach_a_iterator);
+
/*
* Do as much as possible of the code below in stmt_mcontext, to avoid any
* leaks from called subroutines. We need a private stmt_mcontext since
@@ -3069,79 +3079,34 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
push_stmt_mcontext(estate);
oldcontext = MemoryContextSwitchTo(stmt_mcontext);
- /* check the type of the expression - must be an array */
- if (!OidIsValid(get_element_type(arrtype)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH expression must yield an array, not type %s",
- format_type_be(arrtype))));
+ /* Set up the target (loop) variable */
+ target_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, target_var,
+ &target_typid, &target_typmod,
+ &target_collation);
/*
- * We must copy the array into stmt_mcontext, else it will disappear in
- * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
- * while running the loop body, so we have little choice.
+ * inside iterator constroctor, the expr should be copied to
+ * current memory context (stmt_mcontext). Without it, it will be released
+ * by next exec_eval_cleanup. The iterator constructor should
+ * be called under stmt memory context.
*/
- arr = DatumGetArrayTypePCopy(value);
+ iterator = sbroutines->create_foreach_a_iterator(expr,
+ expr_typid, expr_typmod,
+ stmt->slice, target_typid,
+ target_typmod);
/* Clean up any leftover temporary memory */
exec_eval_cleanup(estate);
- /* Slice dimension must be less than or equal to array dimension */
- if (stmt->slice < 0 || stmt->slice > ARR_NDIM(arr))
- ereport(ERROR,
- (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
- errmsg("slice dimension (%d) is out of the valid range 0..%d",
- stmt->slice, ARR_NDIM(arr))));
-
- /* Set up the loop variable and see if it is of an array type */
- loop_var = estate->datums[stmt->varno];
- if (loop_var->dtype == PLPGSQL_DTYPE_REC ||
- loop_var->dtype == PLPGSQL_DTYPE_ROW)
- {
- /*
- * Record/row variable is certainly not of array type, and might not
- * be initialized at all yet, so don't try to get its type
- */
- loop_var_elem_type = InvalidOid;
- }
- else
- loop_var_elem_type = get_element_type(plpgsql_exec_get_datum_type(estate,
- loop_var));
+ tmp_cxt = AllocSetContextCreate(stmt_mcontext,
+ "FOREACH IN ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
- /*
- * Sanity-check the loop variable type. We don't try very hard here, and
- * should not be too picky since it's possible that exec_assign_value can
- * coerce values of different types. But it seems worthwhile to complain
- * if the array-ness of the loop variable is not right.
- */
- if (stmt->slice > 0 && loop_var_elem_type == InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH ... SLICE loop variable must be of an array type")));
- if (stmt->slice == 0 && loop_var_elem_type != InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH loop variable must not be of an array type")));
+ MemoryContextSwitchTo(tmp_cxt);
- /* Create an iterator to step through the array */
- array_iterator = array_create_iterator(arr, stmt->slice, NULL);
-
- /* Identify iterator result type */
- if (stmt->slice > 0)
- {
- /* When slicing, nominal type of result is same as array type */
- iterator_result_type = arrtype;
- iterator_result_typmod = arrtypmod;
- }
- else
- {
- /* Without slicing, results are individual array elements */
- iterator_result_type = ARR_ELEMTYPE(arr);
- iterator_result_typmod = arrtypmod;
- }
-
- /* Iterate over the array elements or slices */
- while (array_iterate(array_iterator, &value, &isnull))
+ while (iterator->iterate(iterator, &value, &isnull, &typid, &typmod))
{
found = true; /* looped at least once */
@@ -3149,12 +3114,9 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
MemoryContextSwitchTo(oldcontext);
/* Assign current element/slice to the loop variable */
- exec_assign_value(estate, loop_var, value, isnull,
- iterator_result_type, iterator_result_typmod);
+ exec_assign_value(estate, target_var, value, isnull, typid, typmod);
- /* In slice case, value is temporary; must free it to avoid leakage */
- if (stmt->slice > 0)
- pfree(DatumGetPointer(value));
+ MemoryContextReset(tmp_cxt);
/*
* Execute the statements
@@ -3163,7 +3125,7 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
LOOP_RC_PROCESSING(stmt->label, break);
- MemoryContextSwitchTo(stmt_mcontext);
+ MemoryContextSwitchTo(tmp_cxt);
}
/* Restore memory context state */
@@ -5637,6 +5599,53 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ TupleDescFinalize(row->rowtupdesc);
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..5cbdb4ecd9d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -299,6 +299,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +445,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..a64004417af
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,252 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
+
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
--
2.53.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
2026-03-12 04:30 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Tom Lane <[email protected]>
2026-03-12 07:00 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-17 06:58 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-21 18:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-05-26 06:51 ` Pavel Stehule <[email protected]>
2026-06-16 07:52 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
0 siblings, 1 reply; 26+ messages in thread
From: Pavel Stehule @ 2026-05-26 06:51 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi
fresh rebase
Regards
Pavel
Attachments:
[text/x-patch] v20260526-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch (37.0K, ../../CAFj8pRAS5_1ObWfM-eNP6ei60bdLw884BpAa=Z7bAmw9WiSxFQ@mail.gmail.com/3-v20260526-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch)
download | inline diff:
From 78a4eb85d8a4cf9b2aaa6082bcbadc46a7472e60 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN ARRAY jsonb_expr
this patch introduce support FOREACH scalar_var IN ARRAY expr, when the
result of the expression can be Jsonb. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN ARRAY '[1,2,3.14]'::jsonb
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'::jsonb
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
Because we use "old" syntax - FOREACH IN ARRAY, I prefer "old" behaviour,
that is more similar to iteration over an array.
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 62 +++-
src/backend/utils/adt/arraysubs.c | 97 +++++-
src/backend/utils/adt/jsonbsubs.c | 181 ++++++++++-
src/include/nodes/subscripting.h | 29 ++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 297 ++++++++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 189 +++++------
src/pl/plpgsql/src/plpgsql.h | 3 +-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 252 +++++++++++++++
10 files changed, 1016 insertions(+), 97 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..6bc9fb02e1e 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2697,12 +2697,12 @@ END LOOP <optional> <replaceable>label</replaceable> </optional>;
</sect2>
<sect2 id="plpgsql-foreach-array">
- <title>Looping through Arrays</title>
+ <title>Looping through Arrays or Jsonb arrays</title>
<para>
The <literal>FOREACH</literal> loop is much like a <literal>FOR</literal> loop,
but instead of iterating through the rows returned by an SQL query,
- it iterates through the elements of an array value.
+ it iterates through the elements of an array value or of jsonb array value.
(In general, <literal>FOREACH</literal> is meant for looping through
components of a composite-valued expression; variants for looping
through composites besides arrays may be added in future.)
@@ -2778,6 +2778,64 @@ NOTICE: row = {7,8,9}
NOTICE: row = {10,11,12}
</programlisting>
</para>
+
+ <para>
+ The <literal>SLICE</literal> higher than zero cannot be used when iterates
+ through jsonb arrays.
+ </para>
+
+ <para>
+ The <literal>FOREACH</literal> loop over jsonb arrays uses
+ same syntax like <literal>FOREACH</literal> loop over arrays,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of a Jsonb array value.
+ </para>
+
+ <para>
+ The target can be a scalar variable, a composite variable, or a list
+ of scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION print_elements(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_elements('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION print_fields(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_fields('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+
+ <para>
+ The target variable can be of type <literal>RECORD</literal>, but the real structure has to be
+ assigned before usage in the <literal>FOREACH</literal> statement.
+ </para>
</sect2>
<sect2 id="plpgsql-error-trapping">
diff --git a/src/backend/utils/adt/arraysubs.c b/src/backend/utils/adt/arraysubs.c
index 2bf9e9509fb..a42248ed633 100644
--- a/src/backend/utils/adt/arraysubs.c
+++ b/src/backend/utils/adt/arraysubs.c
@@ -23,6 +23,7 @@
#include "parser/parse_coerce.h"
#include "parser/parse_expr.h"
#include "utils/array.h"
+#include "utils/builtins.h"
#include "utils/fmgrprotos.h"
#include "utils/lsyscache.h"
@@ -46,6 +47,96 @@ typedef struct ArraySubWorkspace
int lowerindex[MAXDIM];
} ArraySubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ ArrayIterator it;
+ Oid result_typid;
+ int32 result_typmod;
+} ForeachAArrayIterState;
+
+static bool
+foreach_a_array_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAArrayIterState *iter = (ForeachAArrayIterState *) self;
+
+ *typid = iter->result_typid;
+ *typmod = iter->result_typmod;
+
+ return array_iterate(iter->it, value, isnull);
+}
+
+/*
+ * Used by plpgsql FOREACH IN ARRAY when input expression is an array
+ */
+static ForeachAIterator *
+create_foreach_a_array_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAArrayIterState *iter = palloc0(sizeof(ForeachAArrayIterState));
+ ArrayType *arr;
+ Oid target_elem_typid;
+
+ /* check the type of the expression - must be an array */
+ if (!OidIsValid(get_element_type(typid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH expression must yield an array, not type %s",
+ format_type_be(typid))));
+
+ /*
+ * We must copy the array into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ arr = DatumGetArrayTypePCopy(value);
+
+ /* Slice dimension must be less than or equal to array dimension */
+ if (slice < 0 || slice > ARR_NDIM(arr))
+ ereport(ERROR,
+ (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+ errmsg("slice dimension (%d) is out of the valid range 0..%d",
+ slice, ARR_NDIM(arr))));
+
+ /*
+ * Sanity-check the target type. We don't try very hard here, and
+ * should not be too picky since it's possible that exec_assign_value can
+ * coerce values of different types. But it seems worthwhile to complain
+ * if the array-ness of the loop variable is not right.
+ */
+ target_elem_typid = get_element_type(target_typid);
+
+ if (slice > 0 && target_elem_typid == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH ... SLICE loop variable must be of an array type")));
+ if (slice == 0 && target_elem_typid != InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH loop variable must not be of an array type")));
+
+ /* Identify iterator result type */
+ if (slice > 0)
+ {
+ /* When slicing, nominal type of result is same as array type */
+ iter->result_typid = typid;
+ iter->result_typmod = typmod;
+ }
+ else
+ {
+ /* Without slicing, results are individual array elements */
+ iter->result_typid = ARR_ELEMTYPE(arr);
+ iter->result_typmod = typmod;
+ }
+
+ /* Create an iterator to step through the array */
+ iter->it = array_create_iterator(arr, slice, NULL);
+
+ iter->pub.iterate = foreach_a_array_iterate;
+
+ return (ForeachAIterator *) iter;
+}
/*
* Finish parse analysis of a SubscriptingRef expression for an array.
@@ -545,7 +636,8 @@ array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
@@ -572,7 +664,8 @@ raw_array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c
index f2745b29a3f..ad2edf525de 100644
--- a/src/backend/utils/adt/jsonbsubs.c
+++ b/src/backend/utils/adt/jsonbsubs.c
@@ -22,6 +22,7 @@
#include "parser/parse_expr.h"
#include "utils/builtins.h"
#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
/* SubscriptingRefState.workspace for jsonb subscripting execution */
@@ -33,6 +34,17 @@ typedef struct JsonbSubWorkspace
Datum *index; /* Subscript values in Datum format */
} JsonbSubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ JsonbIterator *it;
+ bool skip_nested;
+ Oid target_typid;
+ int32 target_typmod;
+
+ MemoryContext cache_mcxt;
+ void *cache;
+} ForeachAJsonbIterState;
/*
* Finish parse analysis of a SubscriptingRef expression for a jsonb.
@@ -394,6 +406,172 @@ jsonb_exec_setup(const SubscriptingRef *sbsref,
methods->sbs_fetch_old = jsonb_subscript_fetch_old;
}
+/*
+ * Convert JsonbValue to Datum. This function is used in
+ * generic array iterator, that is used by FOREACH plpgsql
+ * statement. Against other cases, the result should not be
+ * necessary of expected_typid, because the value can be
+ * converted later when the value is assigned to PL/pgSQL
+ * variable. This can be more effective than generic IO
+ * cast used by json_populate_type.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ Datum result;
+
+ *isnull = false;
+ *typmod = -1;
+
+ /*
+ * These types can holds JSON null, so must be processed
+ * before processing jbvNull. We don't want to convert
+ * JSON null, to SQL null, when targer is of JSON.
+ */
+ if (expected_typid == JSONBOID || expected_typid == JSONOID)
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ if (expected_typid == JSONOID)
+ {
+ char *str;
+
+ str = JsonbToCString(NULL, &jb->root, VARSIZE(jb));
+ result = PointerGetDatum(cstring_to_text(str));
+ }
+ else
+ result = PointerGetDatum(jb);
+
+ *typid = expected_typid;
+ }
+
+ /*
+ * For special cases we can skip conversion to Jsonb
+ * and possibly IO cast.
+ */
+ else if (jbv->type == jbvNull)
+ {
+ result = (Datum) 0;
+ *isnull = true;
+ *typid = expected_typid;
+ }
+ else if (jbv->type == jbvString)
+ {
+ text *txt = cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len);
+
+ result = PointerGetDatum(txt);
+ *typid = TEXTOID;
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ result = PointerGetDatum(jbv->val.numeric);
+ *typid = NUMERICOID;
+ }
+ else if (jbv->type == jbvBool)
+ {
+ result = BoolGetDatum(jbv->val.boolean);
+ *typid = BOOLOID;
+ }
+
+ /* generic conversion */
+ else
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ result = json_populate_type(PointerGetDatum(jb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+ }
+
+ return result;
+}
+
+static bool
+foreach_a_jsonb_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAJsonbIterState *iter = (ForeachAJsonbIterState *) self;
+ JsonbIteratorToken r;
+ JsonbValue jbv;
+
+ while ((r = JsonbIteratorNext(&iter->it, &jbv, iter->skip_nested)) != WJB_DONE)
+ {
+ iter->skip_nested = true;
+
+ if (r == WJB_ELEM)
+ {
+ *value = JsonbValueToDatum(&jbv, typid, typmod, isnull,
+ iter->target_typid, iter->target_typmod,
+ &iter->cache, iter->cache_mcxt);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Create foreach array iterator for jsonb array
+ */
+static ForeachAIterator *
+create_foreach_a_jsonb_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAJsonbIterState *iter = palloc0(sizeof(ForeachAJsonbIterState));
+ Jsonb *jb;
+
+ if (typid != JSONBOID)
+ elog(ERROR, "unexpected source type");
+
+ if (slice > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("jsonb array iterator doesn't support slicing")));
+
+ /*
+ * We must copy the JSON into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ jb = DatumGetJsonbPCopy(value);
+
+ /*
+ * Jsonb iterator is designed like jsonb_array_element. Input value
+ * must be json array.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errhint("Cannot iterate over a scalar value.")));
+ else if (JB_ROOT_IS_OBJECT(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errdetail("Cannot iterate over an object value.")));
+
+ Assert(JB_ROOT_IS_ARRAY(jb));
+
+ iter->it = JsonbIteratorInit(&jb->root);
+
+ iter->target_typid = target_typid;
+ iter->target_typmod = target_typmod;
+ iter->cache_mcxt = CurrentMemoryContext;
+
+ iter->pub.iterate = foreach_a_jsonb_iterate;
+
+ return (ForeachAIterator *) iter;
+}
+
/*
* jsonb_subscript_handler
* Subscripting handler for jsonb.
@@ -407,7 +585,8 @@ jsonb_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = jsonb_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_jsonb_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/include/nodes/subscripting.h b/src/include/nodes/subscripting.h
index 301f21dac2f..08bfe59ede4 100644
--- a/src/include/nodes/subscripting.h
+++ b/src/include/nodes/subscripting.h
@@ -154,6 +154,32 @@ typedef void (*SubscriptExecSetup) (const SubscriptingRef *sbsref,
SubscriptingRefState *sbsrefstate,
SubscriptExecSteps *methods);
+typedef struct _ForeachAIterator ForeachAIterator;
+
+/*
+ * ForeachAIiterator is used by PLpgSQL FOREACH IN ARRAY statement.
+ * Input value should not be null, and inside CreateForeachAIterator
+ * routine must be copied to current (statement) context. "iterate"
+ * routine is called under short life memory context, that is resetted
+ * after any call.
+ */
+struct _ForeachAIterator
+{
+ bool (*iterate) (ForeachAIterator *self,
+ Datum *value,
+ bool *isnull,
+ Oid *typid,
+ int32 *typmod);
+ /* Private fields might appear beyond this point... */
+};
+
+typedef ForeachAIterator * (*CreateForeachAIterator) (Datum value,
+ Oid typid,
+ int32 typmod,
+ int slice,
+ Oid target_typid,
+ int32 target_typmod);
+
/* Struct returned by the SQL-visible subscript handler function */
typedef struct SubscriptRoutines
{
@@ -163,6 +189,9 @@ typedef struct SubscriptRoutines
bool fetch_leakproof; /* is fetch SubscriptingRef leakproof? */
bool store_leakproof; /* is assignment SubscriptingRef
* leakproof? */
+
+ /* returns iterator used by PL/pgSQL FOREACH statement */
+ CreateForeachAIterator create_foreach_a_iterator;
} SubscriptRoutines;
#endif /* SUBSCRIPTING_H */
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..ddc571f3c79
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,297 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must not be null
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+HINT: Cannot iterate over a scalar value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+DETAIL: Cannot iterate over an object value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: 10
+NOTICE: FOUND: t
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: FOUND: f
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 3
+NOTICE: 5
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..1c1d98157df 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -29,6 +29,7 @@
#include "mb/stringinfo_mb.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
+#include "nodes/subscripting.h"
#include "nodes/supportnodes.h"
#include "optimizer/optimizer.h"
#include "parser/parse_coerce.h"
@@ -3026,40 +3027,49 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
return rc;
}
-
/* ----------
- * exec_stmt_foreach_a Loop over elements or slices of an array
- *
- * When looping over elements, the loop variable is the same type that the
- * array stores (eg: integer), when looping through slices, the loop variable
- * is an array of size and dimensions to match the size of the slice.
- * ----------
+ * exec_stmt_foreach_a Loop over elements in an array or jsonb array
+ * ----------
*/
static int
exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
{
- ArrayType *arr;
- Oid arrtype;
- int32 arrtypmod;
- PLpgSQL_datum *loop_var;
- Oid loop_var_elem_type;
- bool found = false;
+ Datum expr;
+ Oid expr_typid;
+ int32 expr_typmod;
+ bool isnull;
+ PLpgSQL_datum *target_var;
+ Oid target_typid;
+ int32 target_typmod;
+ Oid target_collation;
+ Datum value;
+ Oid typid;
+ int32 typmod;
int rc = PLPGSQL_RC_OK;
+ const struct SubscriptRoutines *sbroutines;
+ ForeachAIterator *iterator;
MemoryContext stmt_mcontext;
+ MemoryContext tmp_cxt;
MemoryContext oldcontext;
- ArrayIterator array_iterator;
- Oid iterator_result_type;
- int32 iterator_result_typmod;
- Datum value;
- bool isnull;
+ bool found = false;
- /* get the value of the array expression */
- value = exec_eval_expr(estate, stmt->expr, &isnull, &arrtype, &arrtypmod);
+ /* get the value of the expression */
+ expr = exec_eval_expr(estate, stmt->expr, &isnull,
+ &expr_typid, &expr_typmod);
if (isnull)
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("FOREACH expression must not be null")));
+ sbroutines = getSubscriptingRoutines(expr_typid, NULL);
+ if (!sbroutines)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot iterate over type %s because it does not support subscripting",
+ format_type_be(expr_typid))));
+
+ Assert(sbroutines->create_foreach_a_iterator);
+
/*
* Do as much as possible of the code below in stmt_mcontext, to avoid any
* leaks from called subroutines. We need a private stmt_mcontext since
@@ -3069,79 +3079,34 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
push_stmt_mcontext(estate);
oldcontext = MemoryContextSwitchTo(stmt_mcontext);
- /* check the type of the expression - must be an array */
- if (!OidIsValid(get_element_type(arrtype)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH expression must yield an array, not type %s",
- format_type_be(arrtype))));
+ /* Set up the target (loop) variable */
+ target_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, target_var,
+ &target_typid, &target_typmod,
+ &target_collation);
/*
- * We must copy the array into stmt_mcontext, else it will disappear in
- * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
- * while running the loop body, so we have little choice.
+ * inside iterator constroctor, the expr should be copied to
+ * current memory context (stmt_mcontext). Without it, it will be released
+ * by next exec_eval_cleanup. The iterator constructor should
+ * be called under stmt memory context.
*/
- arr = DatumGetArrayTypePCopy(value);
+ iterator = sbroutines->create_foreach_a_iterator(expr,
+ expr_typid, expr_typmod,
+ stmt->slice, target_typid,
+ target_typmod);
/* Clean up any leftover temporary memory */
exec_eval_cleanup(estate);
- /* Slice dimension must be less than or equal to array dimension */
- if (stmt->slice < 0 || stmt->slice > ARR_NDIM(arr))
- ereport(ERROR,
- (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
- errmsg("slice dimension (%d) is out of the valid range 0..%d",
- stmt->slice, ARR_NDIM(arr))));
-
- /* Set up the loop variable and see if it is of an array type */
- loop_var = estate->datums[stmt->varno];
- if (loop_var->dtype == PLPGSQL_DTYPE_REC ||
- loop_var->dtype == PLPGSQL_DTYPE_ROW)
- {
- /*
- * Record/row variable is certainly not of array type, and might not
- * be initialized at all yet, so don't try to get its type
- */
- loop_var_elem_type = InvalidOid;
- }
- else
- loop_var_elem_type = get_element_type(plpgsql_exec_get_datum_type(estate,
- loop_var));
+ tmp_cxt = AllocSetContextCreate(stmt_mcontext,
+ "FOREACH IN ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
- /*
- * Sanity-check the loop variable type. We don't try very hard here, and
- * should not be too picky since it's possible that exec_assign_value can
- * coerce values of different types. But it seems worthwhile to complain
- * if the array-ness of the loop variable is not right.
- */
- if (stmt->slice > 0 && loop_var_elem_type == InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH ... SLICE loop variable must be of an array type")));
- if (stmt->slice == 0 && loop_var_elem_type != InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH loop variable must not be of an array type")));
+ MemoryContextSwitchTo(tmp_cxt);
- /* Create an iterator to step through the array */
- array_iterator = array_create_iterator(arr, stmt->slice, NULL);
-
- /* Identify iterator result type */
- if (stmt->slice > 0)
- {
- /* When slicing, nominal type of result is same as array type */
- iterator_result_type = arrtype;
- iterator_result_typmod = arrtypmod;
- }
- else
- {
- /* Without slicing, results are individual array elements */
- iterator_result_type = ARR_ELEMTYPE(arr);
- iterator_result_typmod = arrtypmod;
- }
-
- /* Iterate over the array elements or slices */
- while (array_iterate(array_iterator, &value, &isnull))
+ while (iterator->iterate(iterator, &value, &isnull, &typid, &typmod))
{
found = true; /* looped at least once */
@@ -3149,12 +3114,9 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
MemoryContextSwitchTo(oldcontext);
/* Assign current element/slice to the loop variable */
- exec_assign_value(estate, loop_var, value, isnull,
- iterator_result_type, iterator_result_typmod);
+ exec_assign_value(estate, target_var, value, isnull, typid, typmod);
- /* In slice case, value is temporary; must free it to avoid leakage */
- if (stmt->slice > 0)
- pfree(DatumGetPointer(value));
+ MemoryContextReset(tmp_cxt);
/*
* Execute the statements
@@ -3163,7 +3125,7 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
LOOP_RC_PROCESSING(stmt->label, break);
- MemoryContextSwitchTo(stmt_mcontext);
+ MemoryContextSwitchTo(tmp_cxt);
}
/* Restore memory context state */
@@ -5639,6 +5601,53 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ TupleDescFinalize(row->rowtupdesc);
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..5cbdb4ecd9d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -299,6 +299,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +445,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..a64004417af
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,252 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
+
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
--
2.54.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* Re: POC: PLpgSQL FOREACH IN JSON ARRAY
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
2026-03-12 04:30 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Tom Lane <[email protected]>
2026-03-12 07:00 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-17 06:58 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-21 18:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-05-26 06:51 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
@ 2026-06-16 07:52 ` Pavel Stehule <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Pavel Stehule @ 2026-06-16 07:52 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi
rebase
Regards
Pavel
Attachments:
[text/x-patch] v20260616-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch (37.0K, ../../CAFj8pRDT5LvSMNxorBROZk9Ma41Z_7_0w6Ynq3rn9X6dTocbaQ@mail.gmail.com/3-v20260616-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch)
download | inline diff:
From c4581a4ae2025cf2240b5bda4d4d753fbb3166ea Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 23 Feb 2026 12:53:44 +0100
Subject: [PATCH] FOREACH scalar IN ARRAY jsonb_expr
this patch introduce support FOREACH scalar_var IN ARRAY expr, when the
result of the expression can be Jsonb. The design is based
on behave of jsonb_array_elements functions. In this case, FOREACH enforce
casting to target type (because we know target type) and try to reduce
IO casting. Attention: IO casting can be more strict, then casting based
on cast functions.
DECLARE t int;
BEGIN
-- this can work because we use cast numeric -> int
FOREACH t IN ARRAY '[1,2,3.14]'::jsonb
LOOP
-- this fails, because IO cast is used, and integer input function
-- allows only digits
FOREAC t IN JSON ARRAY '[1,2,3,"3.14"]'::jsonb
LOOP
Conceptual question is if casting should be strict like "old" PostgreSQL
json function or lax as "new" SQL/JSON functions? I can imagine lax mode
as default with possibility to switch to strict mode (this is not implemented
now):
FOREACH t IN ARRAY '[1,2,3]' ERROR ON EMPTY ERROR ON ERROR
LOOP
...
Because we use "old" syntax - FOREACH IN ARRAY, I prefer "old" behaviour,
that is more similar to iteration over an array.
The performance (best case for iteration over 1000 fields array) is about
4x better than when FOR IN SELECT jsonb_array_elements is used.
---
doc/src/sgml/plpgsql.sgml | 62 +++-
src/backend/utils/adt/arraysubs.c | 97 +++++-
src/backend/utils/adt/jsonbsubs.c | 181 ++++++++++-
src/include/nodes/subscripting.h | 29 ++
src/pl/plpgsql/src/Makefile | 2 +-
.../plpgsql/src/expected/plpgsql_foreach.out | 297 ++++++++++++++++++
src/pl/plpgsql/src/meson.build | 1 +
src/pl/plpgsql/src/pl_exec.c | 189 +++++------
src/pl/plpgsql/src/plpgsql.h | 3 +-
src/pl/plpgsql/src/sql/plpgsql_foreach.sql | 252 +++++++++++++++
10 files changed, 1016 insertions(+), 97 deletions(-)
create mode 100644 src/pl/plpgsql/src/expected/plpgsql_foreach.out
create mode 100644 src/pl/plpgsql/src/sql/plpgsql_foreach.sql
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..6bc9fb02e1e 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -2697,12 +2697,12 @@ END LOOP <optional> <replaceable>label</replaceable> </optional>;
</sect2>
<sect2 id="plpgsql-foreach-array">
- <title>Looping through Arrays</title>
+ <title>Looping through Arrays or Jsonb arrays</title>
<para>
The <literal>FOREACH</literal> loop is much like a <literal>FOR</literal> loop,
but instead of iterating through the rows returned by an SQL query,
- it iterates through the elements of an array value.
+ it iterates through the elements of an array value or of jsonb array value.
(In general, <literal>FOREACH</literal> is meant for looping through
components of a composite-valued expression; variants for looping
through composites besides arrays may be added in future.)
@@ -2778,6 +2778,64 @@ NOTICE: row = {7,8,9}
NOTICE: row = {10,11,12}
</programlisting>
</para>
+
+ <para>
+ The <literal>SLICE</literal> higher than zero cannot be used when iterates
+ through jsonb arrays.
+ </para>
+
+ <para>
+ The <literal>FOREACH</literal> loop over jsonb arrays uses
+ same syntax like <literal>FOREACH</literal> loop over arrays,
+ but instead of iterating through elements of the array,
+ it iterates through the elements of a Jsonb array value.
+ </para>
+
+ <para>
+ The target can be a scalar variable, a composite variable, or a list
+ of scalar variables. When variable is not scalar, then assigned value
+ should be a JSON object and the JSON attributes are assigned by names.
+
+<programlisting>
+CREATE FUNCTION print_elements(jsonb) RETURNS void AS $$
+DECLARE
+ x int;
+BEGIN
+ FOREACH x IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'row = %', x;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_elements('[1,2,3]');
+NOTICE: row = 1
+NOTICE: row = 2
+NOTICE: row = 3
+
+CREATE FUNCTION print_fields(jsonb) RETURNS void AS $$
+DECLARE
+ x int; y varchar;
+BEGIN
+ FOREACH x, y IN ARRAY $1
+ LOOP
+ RAISE NOTICE 'x: %, y: %', x, y;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT print_fields('[{},{"x":10},{"y":"Hi"},{"y":"Hi", "x":1000}]');
+NOTICE: x: <NULL>, y: <NULL>
+NOTICE: x: 10, y: <NULL>
+NOTICE: x: <NULL>, y: Hi
+NOTICE: x: 1000, y: Hi
+</programlisting>
+ </para>
+
+ <para>
+ The target variable can be of type <literal>RECORD</literal>, but the real structure has to be
+ assigned before usage in the <literal>FOREACH</literal> statement.
+ </para>
</sect2>
<sect2 id="plpgsql-error-trapping">
diff --git a/src/backend/utils/adt/arraysubs.c b/src/backend/utils/adt/arraysubs.c
index 2bf9e9509fb..a42248ed633 100644
--- a/src/backend/utils/adt/arraysubs.c
+++ b/src/backend/utils/adt/arraysubs.c
@@ -23,6 +23,7 @@
#include "parser/parse_coerce.h"
#include "parser/parse_expr.h"
#include "utils/array.h"
+#include "utils/builtins.h"
#include "utils/fmgrprotos.h"
#include "utils/lsyscache.h"
@@ -46,6 +47,96 @@ typedef struct ArraySubWorkspace
int lowerindex[MAXDIM];
} ArraySubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ ArrayIterator it;
+ Oid result_typid;
+ int32 result_typmod;
+} ForeachAArrayIterState;
+
+static bool
+foreach_a_array_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAArrayIterState *iter = (ForeachAArrayIterState *) self;
+
+ *typid = iter->result_typid;
+ *typmod = iter->result_typmod;
+
+ return array_iterate(iter->it, value, isnull);
+}
+
+/*
+ * Used by plpgsql FOREACH IN ARRAY when input expression is an array
+ */
+static ForeachAIterator *
+create_foreach_a_array_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAArrayIterState *iter = palloc0(sizeof(ForeachAArrayIterState));
+ ArrayType *arr;
+ Oid target_elem_typid;
+
+ /* check the type of the expression - must be an array */
+ if (!OidIsValid(get_element_type(typid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH expression must yield an array, not type %s",
+ format_type_be(typid))));
+
+ /*
+ * We must copy the array into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ arr = DatumGetArrayTypePCopy(value);
+
+ /* Slice dimension must be less than or equal to array dimension */
+ if (slice < 0 || slice > ARR_NDIM(arr))
+ ereport(ERROR,
+ (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+ errmsg("slice dimension (%d) is out of the valid range 0..%d",
+ slice, ARR_NDIM(arr))));
+
+ /*
+ * Sanity-check the target type. We don't try very hard here, and
+ * should not be too picky since it's possible that exec_assign_value can
+ * coerce values of different types. But it seems worthwhile to complain
+ * if the array-ness of the loop variable is not right.
+ */
+ target_elem_typid = get_element_type(target_typid);
+
+ if (slice > 0 && target_elem_typid == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH ... SLICE loop variable must be of an array type")));
+ if (slice == 0 && target_elem_typid != InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH loop variable must not be of an array type")));
+
+ /* Identify iterator result type */
+ if (slice > 0)
+ {
+ /* When slicing, nominal type of result is same as array type */
+ iter->result_typid = typid;
+ iter->result_typmod = typmod;
+ }
+ else
+ {
+ /* Without slicing, results are individual array elements */
+ iter->result_typid = ARR_ELEMTYPE(arr);
+ iter->result_typmod = typmod;
+ }
+
+ /* Create an iterator to step through the array */
+ iter->it = array_create_iterator(arr, slice, NULL);
+
+ iter->pub.iterate = foreach_a_array_iterate;
+
+ return (ForeachAIterator *) iter;
+}
/*
* Finish parse analysis of a SubscriptingRef expression for an array.
@@ -545,7 +636,8 @@ array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
@@ -572,7 +664,8 @@ raw_array_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = array_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_array_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c
index f2745b29a3f..ad2edf525de 100644
--- a/src/backend/utils/adt/jsonbsubs.c
+++ b/src/backend/utils/adt/jsonbsubs.c
@@ -22,6 +22,7 @@
#include "parser/parse_expr.h"
#include "utils/builtins.h"
#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
/* SubscriptingRefState.workspace for jsonb subscripting execution */
@@ -33,6 +34,17 @@ typedef struct JsonbSubWorkspace
Datum *index; /* Subscript values in Datum format */
} JsonbSubWorkspace;
+typedef struct
+{
+ ForeachAIterator pub;
+ JsonbIterator *it;
+ bool skip_nested;
+ Oid target_typid;
+ int32 target_typmod;
+
+ MemoryContext cache_mcxt;
+ void *cache;
+} ForeachAJsonbIterState;
/*
* Finish parse analysis of a SubscriptingRef expression for a jsonb.
@@ -394,6 +406,172 @@ jsonb_exec_setup(const SubscriptingRef *sbsref,
methods->sbs_fetch_old = jsonb_subscript_fetch_old;
}
+/*
+ * Convert JsonbValue to Datum. This function is used in
+ * generic array iterator, that is used by FOREACH plpgsql
+ * statement. Against other cases, the result should not be
+ * necessary of expected_typid, because the value can be
+ * converted later when the value is assigned to PL/pgSQL
+ * variable. This can be more effective than generic IO
+ * cast used by json_populate_type.
+ */
+static Datum
+JsonbValueToDatum(JsonbValue *jbv,
+ Oid *typid, int32 *typmod, bool *isnull,
+ Oid expected_typid, int32 expected_typmod,
+ void **cache, MemoryContext mcxt)
+{
+ Datum result;
+
+ *isnull = false;
+ *typmod = -1;
+
+ /*
+ * These types can holds JSON null, so must be processed
+ * before processing jbvNull. We don't want to convert
+ * JSON null, to SQL null, when targer is of JSON.
+ */
+ if (expected_typid == JSONBOID || expected_typid == JSONOID)
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ if (expected_typid == JSONOID)
+ {
+ char *str;
+
+ str = JsonbToCString(NULL, &jb->root, VARSIZE(jb));
+ result = PointerGetDatum(cstring_to_text(str));
+ }
+ else
+ result = PointerGetDatum(jb);
+
+ *typid = expected_typid;
+ }
+
+ /*
+ * For special cases we can skip conversion to Jsonb
+ * and possibly IO cast.
+ */
+ else if (jbv->type == jbvNull)
+ {
+ result = (Datum) 0;
+ *isnull = true;
+ *typid = expected_typid;
+ }
+ else if (jbv->type == jbvString)
+ {
+ text *txt = cstring_to_text_with_len(jbv->val.string.val,
+ jbv->val.string.len);
+
+ result = PointerGetDatum(txt);
+ *typid = TEXTOID;
+ }
+ else if (jbv->type == jbvNumeric)
+ {
+ result = PointerGetDatum(jbv->val.numeric);
+ *typid = NUMERICOID;
+ }
+ else if (jbv->type == jbvBool)
+ {
+ result = BoolGetDatum(jbv->val.boolean);
+ *typid = BOOLOID;
+ }
+
+ /* generic conversion */
+ else
+ {
+ Jsonb *jb = JsonbValueToJsonb(jbv);
+
+ result = json_populate_type(PointerGetDatum(jb), JSONBOID,
+ expected_typid, expected_typmod,
+ cache, mcxt,
+ isnull, false, NULL);
+
+ *typid = expected_typid;
+ *typmod = expected_typmod;
+ }
+
+ return result;
+}
+
+static bool
+foreach_a_jsonb_iterate(ForeachAIterator *self,
+ Datum *value, bool *isnull,
+ Oid *typid, int32 *typmod)
+{
+ ForeachAJsonbIterState *iter = (ForeachAJsonbIterState *) self;
+ JsonbIteratorToken r;
+ JsonbValue jbv;
+
+ while ((r = JsonbIteratorNext(&iter->it, &jbv, iter->skip_nested)) != WJB_DONE)
+ {
+ iter->skip_nested = true;
+
+ if (r == WJB_ELEM)
+ {
+ *value = JsonbValueToDatum(&jbv, typid, typmod, isnull,
+ iter->target_typid, iter->target_typmod,
+ &iter->cache, iter->cache_mcxt);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Create foreach array iterator for jsonb array
+ */
+static ForeachAIterator *
+create_foreach_a_jsonb_iterator(Datum value, Oid typid, int32 typmod,
+ int slice, Oid target_typid, int32 target_typmod)
+{
+ ForeachAJsonbIterState *iter = palloc0(sizeof(ForeachAJsonbIterState));
+ Jsonb *jb;
+
+ if (typid != JSONBOID)
+ elog(ERROR, "unexpected source type");
+
+ if (slice > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("jsonb array iterator doesn't support slicing")));
+
+ /*
+ * We must copy the JSON into current context, because input expression
+ * is evaluated in context cleaned by exec_eval_cleanup.
+ */
+ jb = DatumGetJsonbPCopy(value);
+
+ /*
+ * Jsonb iterator is designed like jsonb_array_element. Input value
+ * must be json array.
+ */
+ if (JB_ROOT_IS_SCALAR(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errhint("Cannot iterate over a scalar value.")));
+ else if (JB_ROOT_IS_OBJECT(jb))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("FOREACH expression must evaluate to a JSON array"),
+ errdetail("Cannot iterate over an object value.")));
+
+ Assert(JB_ROOT_IS_ARRAY(jb));
+
+ iter->it = JsonbIteratorInit(&jb->root);
+
+ iter->target_typid = target_typid;
+ iter->target_typmod = target_typmod;
+ iter->cache_mcxt = CurrentMemoryContext;
+
+ iter->pub.iterate = foreach_a_jsonb_iterate;
+
+ return (ForeachAIterator *) iter;
+}
+
/*
* jsonb_subscript_handler
* Subscripting handler for jsonb.
@@ -407,7 +585,8 @@ jsonb_subscript_handler(PG_FUNCTION_ARGS)
.exec_setup = jsonb_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
- .store_leakproof = false /* ... but assignment throws error */
+ .store_leakproof = false, /* ... but assignment throws error */
+ .create_foreach_a_iterator = create_foreach_a_jsonb_iterator
};
PG_RETURN_POINTER(&sbsroutines);
diff --git a/src/include/nodes/subscripting.h b/src/include/nodes/subscripting.h
index 301f21dac2f..08bfe59ede4 100644
--- a/src/include/nodes/subscripting.h
+++ b/src/include/nodes/subscripting.h
@@ -154,6 +154,32 @@ typedef void (*SubscriptExecSetup) (const SubscriptingRef *sbsref,
SubscriptingRefState *sbsrefstate,
SubscriptExecSteps *methods);
+typedef struct _ForeachAIterator ForeachAIterator;
+
+/*
+ * ForeachAIiterator is used by PLpgSQL FOREACH IN ARRAY statement.
+ * Input value should not be null, and inside CreateForeachAIterator
+ * routine must be copied to current (statement) context. "iterate"
+ * routine is called under short life memory context, that is resetted
+ * after any call.
+ */
+struct _ForeachAIterator
+{
+ bool (*iterate) (ForeachAIterator *self,
+ Datum *value,
+ bool *isnull,
+ Oid *typid,
+ int32 *typmod);
+ /* Private fields might appear beyond this point... */
+};
+
+typedef ForeachAIterator * (*CreateForeachAIterator) (Datum value,
+ Oid typid,
+ int32 typmod,
+ int slice,
+ Oid target_typid,
+ int32 target_typmod);
+
/* Struct returned by the SQL-visible subscript handler function */
typedef struct SubscriptRoutines
{
@@ -163,6 +189,9 @@ typedef struct SubscriptRoutines
bool fetch_leakproof; /* is fetch SubscriptingRef leakproof? */
bool store_leakproof; /* is assignment SubscriptingRef
* leakproof? */
+
+ /* returns iterator used by PL/pgSQL FOREACH statement */
+ CreateForeachAIterator create_foreach_a_iterator;
} SubscriptRoutines;
#endif /* SUBSCRIPTING_H */
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..5bd0cf31dfc 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,7 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
- plpgsql_trap plpgsql_trigger plpgsql_varprops
+ plpgsql_trap plpgsql_trigger plpgsql_varprops plpgsql_foreach
# where to find gen_keywordlist.pl and subsidiary files
TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_foreach.out b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
new file mode 100644
index 00000000000..ddc571f3c79
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_foreach.out
@@ -0,0 +1,297 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must not be null
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+HINT: Cannot iterate over a scalar value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+ERROR: FOREACH expression must evaluate to a JSON array
+DETAIL: Cannot iterate over an object value.
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: <NULL>
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3
+NOTICE: <NULL>
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: 10
+NOTICE: FOUND: t
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+NOTICE: FOUND: f
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+ERROR: invalid input syntax for type integer: "3.14"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOREACH over array
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+NOTICE: true
+NOTICE: false
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 10
+NOTICE: 20
+NOTICE: 30
+NOTICE: 3.14
+NOTICE: null
+NOTICE: "Hi"
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+create type t3 as (x int, y numeric, z varchar);
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+NOTICE: x: <NULL>, y: <NULL>, z: <NULL>
+NOTICE: x: <NULL>, y: <NULL>, z: Hi
+NOTICE: x: <NULL>, y: 3.14, z: <NULL>
+NOTICE: x: 10, y: 3.14, z: Hi
+drop type t3;
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3
+NOTICE: 4 5 6
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+NOTICE: Hi Hello
+NOTICE: Hello Hi
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+create type t2 as (x int[], y varchar);
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+NOTICE: 1 2 3, y: Hi
+NOTICE: 4 5 6, y: Hi
+drop type t2;
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 3
+NOTICE: 5
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 6ff27006cfc..609eed7a28d 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
'plpgsql_trap',
'plpgsql_trigger',
'plpgsql_varprops',
+ 'plpgsql_foreach',
],
},
}
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..1c1d98157df 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -29,6 +29,7 @@
#include "mb/stringinfo_mb.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
+#include "nodes/subscripting.h"
#include "nodes/supportnodes.h"
#include "optimizer/optimizer.h"
#include "parser/parse_coerce.h"
@@ -3026,40 +3027,49 @@ exec_stmt_forc(PLpgSQL_execstate *estate, PLpgSQL_stmt_forc *stmt)
return rc;
}
-
/* ----------
- * exec_stmt_foreach_a Loop over elements or slices of an array
- *
- * When looping over elements, the loop variable is the same type that the
- * array stores (eg: integer), when looping through slices, the loop variable
- * is an array of size and dimensions to match the size of the slice.
- * ----------
+ * exec_stmt_foreach_a Loop over elements in an array or jsonb array
+ * ----------
*/
static int
exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
{
- ArrayType *arr;
- Oid arrtype;
- int32 arrtypmod;
- PLpgSQL_datum *loop_var;
- Oid loop_var_elem_type;
- bool found = false;
+ Datum expr;
+ Oid expr_typid;
+ int32 expr_typmod;
+ bool isnull;
+ PLpgSQL_datum *target_var;
+ Oid target_typid;
+ int32 target_typmod;
+ Oid target_collation;
+ Datum value;
+ Oid typid;
+ int32 typmod;
int rc = PLPGSQL_RC_OK;
+ const struct SubscriptRoutines *sbroutines;
+ ForeachAIterator *iterator;
MemoryContext stmt_mcontext;
+ MemoryContext tmp_cxt;
MemoryContext oldcontext;
- ArrayIterator array_iterator;
- Oid iterator_result_type;
- int32 iterator_result_typmod;
- Datum value;
- bool isnull;
+ bool found = false;
- /* get the value of the array expression */
- value = exec_eval_expr(estate, stmt->expr, &isnull, &arrtype, &arrtypmod);
+ /* get the value of the expression */
+ expr = exec_eval_expr(estate, stmt->expr, &isnull,
+ &expr_typid, &expr_typmod);
if (isnull)
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("FOREACH expression must not be null")));
+ sbroutines = getSubscriptingRoutines(expr_typid, NULL);
+ if (!sbroutines)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot iterate over type %s because it does not support subscripting",
+ format_type_be(expr_typid))));
+
+ Assert(sbroutines->create_foreach_a_iterator);
+
/*
* Do as much as possible of the code below in stmt_mcontext, to avoid any
* leaks from called subroutines. We need a private stmt_mcontext since
@@ -3069,79 +3079,34 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
push_stmt_mcontext(estate);
oldcontext = MemoryContextSwitchTo(stmt_mcontext);
- /* check the type of the expression - must be an array */
- if (!OidIsValid(get_element_type(arrtype)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH expression must yield an array, not type %s",
- format_type_be(arrtype))));
+ /* Set up the target (loop) variable */
+ target_var = estate->datums[stmt->varno];
+
+ plpgsql_exec_get_datum_type_info(estate, target_var,
+ &target_typid, &target_typmod,
+ &target_collation);
/*
- * We must copy the array into stmt_mcontext, else it will disappear in
- * exec_eval_cleanup. This is annoying, but cleanup will certainly happen
- * while running the loop body, so we have little choice.
+ * inside iterator constroctor, the expr should be copied to
+ * current memory context (stmt_mcontext). Without it, it will be released
+ * by next exec_eval_cleanup. The iterator constructor should
+ * be called under stmt memory context.
*/
- arr = DatumGetArrayTypePCopy(value);
+ iterator = sbroutines->create_foreach_a_iterator(expr,
+ expr_typid, expr_typmod,
+ stmt->slice, target_typid,
+ target_typmod);
/* Clean up any leftover temporary memory */
exec_eval_cleanup(estate);
- /* Slice dimension must be less than or equal to array dimension */
- if (stmt->slice < 0 || stmt->slice > ARR_NDIM(arr))
- ereport(ERROR,
- (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
- errmsg("slice dimension (%d) is out of the valid range 0..%d",
- stmt->slice, ARR_NDIM(arr))));
-
- /* Set up the loop variable and see if it is of an array type */
- loop_var = estate->datums[stmt->varno];
- if (loop_var->dtype == PLPGSQL_DTYPE_REC ||
- loop_var->dtype == PLPGSQL_DTYPE_ROW)
- {
- /*
- * Record/row variable is certainly not of array type, and might not
- * be initialized at all yet, so don't try to get its type
- */
- loop_var_elem_type = InvalidOid;
- }
- else
- loop_var_elem_type = get_element_type(plpgsql_exec_get_datum_type(estate,
- loop_var));
+ tmp_cxt = AllocSetContextCreate(stmt_mcontext,
+ "FOREACH IN ARRAY temporary cxt",
+ ALLOCSET_DEFAULT_SIZES);
- /*
- * Sanity-check the loop variable type. We don't try very hard here, and
- * should not be too picky since it's possible that exec_assign_value can
- * coerce values of different types. But it seems worthwhile to complain
- * if the array-ness of the loop variable is not right.
- */
- if (stmt->slice > 0 && loop_var_elem_type == InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH ... SLICE loop variable must be of an array type")));
- if (stmt->slice == 0 && loop_var_elem_type != InvalidOid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("FOREACH loop variable must not be of an array type")));
+ MemoryContextSwitchTo(tmp_cxt);
- /* Create an iterator to step through the array */
- array_iterator = array_create_iterator(arr, stmt->slice, NULL);
-
- /* Identify iterator result type */
- if (stmt->slice > 0)
- {
- /* When slicing, nominal type of result is same as array type */
- iterator_result_type = arrtype;
- iterator_result_typmod = arrtypmod;
- }
- else
- {
- /* Without slicing, results are individual array elements */
- iterator_result_type = ARR_ELEMTYPE(arr);
- iterator_result_typmod = arrtypmod;
- }
-
- /* Iterate over the array elements or slices */
- while (array_iterate(array_iterator, &value, &isnull))
+ while (iterator->iterate(iterator, &value, &isnull, &typid, &typmod))
{
found = true; /* looped at least once */
@@ -3149,12 +3114,9 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
MemoryContextSwitchTo(oldcontext);
/* Assign current element/slice to the loop variable */
- exec_assign_value(estate, loop_var, value, isnull,
- iterator_result_type, iterator_result_typmod);
+ exec_assign_value(estate, target_var, value, isnull, typid, typmod);
- /* In slice case, value is temporary; must free it to avoid leakage */
- if (stmt->slice > 0)
- pfree(DatumGetPointer(value));
+ MemoryContextReset(tmp_cxt);
/*
* Execute the statements
@@ -3163,7 +3125,7 @@ exec_stmt_foreach_a(PLpgSQL_execstate *estate, PLpgSQL_stmt_foreach_a *stmt)
LOOP_RC_PROCESSING(stmt->label, break);
- MemoryContextSwitchTo(stmt_mcontext);
+ MemoryContextSwitchTo(tmp_cxt);
}
/* Restore memory context state */
@@ -5639,6 +5601,53 @@ plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) datum;
+
+ if (!row->rowtupdesc)
+ {
+ int i;
+
+ row->rowtupdesc = CreateTemplateTupleDesc(row->nfields);
+
+ for (i = 0; i < row->nfields; i++)
+ {
+ PLpgSQL_datum *var = estate->datums[row->varnos[i]];
+ Oid vartypid;
+ int32 vartypmod;
+ Oid varcollation;
+
+ /*
+ * We cannot use fieldnames for tupdescentry, because
+ * these names can be suffixed by name of row variable.
+ * Unfortunately, the PLpgSQL_recfield is not casted to
+ * PLpgSQL_variable.
+ */
+ plpgsql_exec_get_datum_type_info(estate, var,
+ &vartypid, &vartypmod,
+ &varcollation);
+
+ TupleDescInitEntry(row->rowtupdesc, i + 1,
+ var->refname, vartypid, vartypmod,
+ 0);
+ TupleDescInitEntryCollation(row->rowtupdesc, i + 1,
+ varcollation);
+ }
+
+ TupleDescFinalize(row->rowtupdesc);
+
+ /* Make sure we have a valid type/typmod setting */
+ BlessTupleDesc(row->rowtupdesc);
+ }
+
+ *typeId = row->rowtupdesc->tdtypeid;
+ *typMod = row->rowtupdesc->tdtypmod;
+ /* composite types are never collatable */
+ *collation = InvalidOid;
+ break;
+ }
+
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..5cbdb4ecd9d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -299,6 +299,7 @@ typedef struct PLpgSQL_datum
{
PLpgSQL_datum_type dtype;
int dno;
+ char *refname;
} PLpgSQL_datum;
/*
@@ -444,9 +445,9 @@ typedef struct PLpgSQL_recfield
{
PLpgSQL_datum_type dtype;
int dno;
+ char *fieldname; /* name of field */
/* end of PLpgSQL_datum fields */
- char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
int nextfield; /* dno of next child, or -1 if none */
uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
diff --git a/src/pl/plpgsql/src/sql/plpgsql_foreach.sql b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
new file mode 100644
index 00000000000..a64004417af
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_foreach.sql
@@ -0,0 +1,252 @@
+-- input must be a JSON array
+do $$
+declare x numeric;
+begin
+ foreach x in array NULL::jsonb -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '10' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '{}' -- fail
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to numeric
+do $$
+declare x numeric;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- numeric to int by cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[10]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- test of FOUND variable
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[]'
+ loop
+ raise notice '%', x;
+ end loop;
+ raise notice 'FOUND: %', found;
+end;
+$$;
+
+-- conversion "3.14" to int should to fail due IO cast
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '["10",20,30,"3.14"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x boolean;
+begin
+ foreach x in array jsonb '[true, false]'
+ loop
+ if x then
+ raise notice 'true';
+ else
+ raise notice 'false';
+ end if;
+ end loop;
+end;
+$$;
+
+-- jsonb to jsonb
+do $$
+declare x jsonb;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- jsonb to json
+do $$
+declare x json;
+begin
+ foreach x in array jsonb '[10,20,30,3.14, null, "Hi"]'
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- iteration over composites
+do $$
+declare x int; y numeric; z varchar;
+begin
+ foreach x, y, z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', x, y, z;
+ end loop;
+end;
+$$;
+
+create type t3 as (x int, y numeric, z varchar);
+
+do $$
+declare c t3;
+begin
+ foreach c in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+do $$
+declare c t3;
+begin
+ foreach c.x, c.y, c.z in array jsonb '[{}, {"z":"Hi"}, {"y": 3.14}, {"z":"Hi", "x":10, "y":3.14}]'
+ loop
+ raise notice 'x: %, y: %, z: %', c.x, c.y, c.z;
+ end loop;
+end;
+$$;
+
+drop type t3;
+
+-- target can be a array
+do $$
+declare x int[];
+begin
+ foreach x in array jsonb '[[1,2,3],[4,5,6]]'
+ loop
+ raise notice '% % %', x[1], x[2], x[3];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x varchar[];
+begin
+ foreach x in array jsonb '[["Hi","Hello"],["Hello","Hi"]]'
+ loop
+ raise notice '% %', x[1], x[2];
+ end loop;
+end;
+$$;
+
+do $$
+declare x int[]; y varchar;
+begin
+ foreach x, y in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', x[1], x[2], x[3], y;
+ end loop;
+end;
+$$;
+
+create type t2 as (x int[], y varchar);
+
+do $$
+declare c t2;
+begin
+ foreach c in array jsonb '[{"x":[1,2,3], "y":"Hi"}, {"x":[4,5,6], "y":"Hi"}]'
+ loop
+ raise notice '% % %, y: %', c.x[1], c.x[2], c.x[3], c.y;
+ end loop;
+end;
+$$;
+
+drop type t2;
+
+-- EXIT and CONTINUE can be triggered by LOOP_RC_PROCESSING
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ exit when x = 3;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+do $$
+declare x int;
+begin
+ foreach x in array jsonb '[1,2,3,4,5]'
+ loop
+ continue when x % 2 = 0;
+ raise notice '%', x;
+ end loop;
+end;
+$$;
+
+-- Variable instead of string
+DO $$
+declare
+ x int;
+ arr jsonb;
+begin
+ select jsonb_agg(i) into arr
+ from generate_series(1,3) g(i);
+
+ foreach x in array arr
+ loop
+ raise notice '%', x;
+ end loop;
+end;
+$$;
--
2.54.0
^ permalink raw reply [nested|flat] 26+ messages in thread
* [PATCH 1/2] REPACK: do not require the user to have REPLICATION
@ 2026-04-20 09:38 Álvaro Herrera <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Álvaro Herrera @ 2026-04-20 09:38 UTC (permalink / raw)
Although REPACK (CONCURRENTLY) uses replication slots, there is no
concern that the slot will leak data of other users, because the
MAINTAIN privilege on the table is required anyway; requiring
REPLICATION is user-unfriendly without providing any actual protection.
A related aspect is that the REPLICATION attribute is not needed to
prevent REPACK from stealing slots from logical replication, since
commit e76d8c749c31 made REPACK use a separate pool of replication
slots.
Because there are now successful concurrent repack runs in the
regression tests, we're forced to run test_plan_advice under
wal_level=replica.
Author: Antonin Houska <[email protected]>
Reported-by: Justin Pryzby <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/aeJHPNmL4vVy3oPw@pryzbyj2023
---
src/backend/commands/repack_worker.c | 1 -
.../test_plan_advice/t/001_replan_regress.pl | 1 +
src/test/regress/expected/cluster.out | 20 +++++++++++++++++--
src/test/regress/sql/cluster.sql | 11 ++++++++--
4 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c
index b17edd771e2..e4a4860805b 100644
--- a/src/backend/commands/repack_worker.c
+++ b/src/backend/commands/repack_worker.c
@@ -214,7 +214,6 @@ repack_setup_logical_decoding(Oid relid)
/*
* Make sure we can use logical decoding.
*/
- CheckSlotPermissions();
CheckLogicalDecodingRequirements(true);
/*
diff --git a/src/test/modules/test_plan_advice/t/001_replan_regress.pl b/src/test/modules/test_plan_advice/t/001_replan_regress.pl
index 38ffa4d11ae..452b179a665 100644
--- a/src/test/modules/test_plan_advice/t/001_replan_regress.pl
+++ b/src/test/modules/test_plan_advice/t/001_replan_regress.pl
@@ -18,6 +18,7 @@ $node->init();
# Set up our desired configuration.
$node->append_conf('postgresql.conf', <<EOM);
shared_preload_libraries='test_plan_advice'
+wal_level=replica
pg_plan_advice.always_explain_supplied_advice=false
pg_plan_advice.feedback_warnings=true
EOM
diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out
index 6127b215a86..e17bc91fae1 100644
--- a/src/test/regress/expected/cluster.out
+++ b/src/test/regress/expected/cluster.out
@@ -543,15 +543,17 @@ ERROR: REPACK (CONCURRENTLY) is not supported for partitioned tables
HINT: Consider running the command on individual partitions.
DROP TABLE clstrpart;
-- Ownership of partitions is checked
-CREATE TABLE ptnowner(i int unique) PARTITION BY LIST (i);
+CREATE TABLE ptnowner(i int unique not null) PARTITION BY LIST (i);
CREATE INDEX ptnowner_i_idx ON ptnowner(i);
CREATE TABLE ptnowner1 PARTITION OF ptnowner FOR VALUES IN (1);
-CREATE ROLE regress_ptnowner;
+CREATE ROLE regress_ptnowner LOGIN;
CREATE TABLE ptnowner2 PARTITION OF ptnowner FOR VALUES IN (2);
ALTER TABLE ptnowner1 OWNER TO regress_ptnowner;
SET SESSION AUTHORIZATION regress_ptnowner;
CLUSTER ptnowner USING ptnowner_i_idx;
ERROR: permission denied for table ptnowner
+ALTER TABLE ptnowner1 REPLICA IDENTITY USING INDEX ptnowner1_i_key;
+REPACK (CONCURRENTLY) ptnowner1;
RESET SESSION AUTHORIZATION;
ALTER TABLE ptnowner OWNER TO regress_ptnowner;
CREATE TEMP TABLE ptnowner_oldnodes AS
@@ -560,6 +562,11 @@ CREATE TEMP TABLE ptnowner_oldnodes AS
SET SESSION AUTHORIZATION regress_ptnowner;
CLUSTER ptnowner USING ptnowner_i_idx;
WARNING: permission denied to execute CLUSTER on "ptnowner2", skipping it
+-- still can't repack without a replica identity
+ALTER TABLE ptnowner1 REPLICA IDENTITY DEFAULT;
+REPACK (CONCURRENTLY) ptnowner1;
+ERROR: cannot process relation "ptnowner1"
+HINT: Relation "ptnowner1" has no identity index.
RESET SESSION AUTHORIZATION;
SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a
JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C";
@@ -570,6 +577,15 @@ SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a
ptnowner2 | t
(3 rows)
+SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a
+ JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C";
+ relname | ?column?
+-----------+----------
+ ptnowner | t
+ ptnowner1 | f
+ ptnowner2 | t
+(3 rows)
+
DROP TABLE ptnowner;
DROP ROLE regress_ptnowner;
-- Test CLUSTER with external tuplesorting
diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluster.sql
index d14063a9683..1f471a8821a 100644
--- a/src/test/regress/sql/cluster.sql
+++ b/src/test/regress/sql/cluster.sql
@@ -254,14 +254,16 @@ REPACK (CONCURRENTLY) clstrpart;
DROP TABLE clstrpart;
-- Ownership of partitions is checked
-CREATE TABLE ptnowner(i int unique) PARTITION BY LIST (i);
+CREATE TABLE ptnowner(i int unique not null) PARTITION BY LIST (i);
CREATE INDEX ptnowner_i_idx ON ptnowner(i);
CREATE TABLE ptnowner1 PARTITION OF ptnowner FOR VALUES IN (1);
-CREATE ROLE regress_ptnowner;
+CREATE ROLE regress_ptnowner LOGIN;
CREATE TABLE ptnowner2 PARTITION OF ptnowner FOR VALUES IN (2);
ALTER TABLE ptnowner1 OWNER TO regress_ptnowner;
SET SESSION AUTHORIZATION regress_ptnowner;
CLUSTER ptnowner USING ptnowner_i_idx;
+ALTER TABLE ptnowner1 REPLICA IDENTITY USING INDEX ptnowner1_i_key;
+REPACK (CONCURRENTLY) ptnowner1;
RESET SESSION AUTHORIZATION;
ALTER TABLE ptnowner OWNER TO regress_ptnowner;
CREATE TEMP TABLE ptnowner_oldnodes AS
@@ -269,7 +271,12 @@ CREATE TEMP TABLE ptnowner_oldnodes AS
JOIN pg_class AS c ON c.oid=tree.relid;
SET SESSION AUTHORIZATION regress_ptnowner;
CLUSTER ptnowner USING ptnowner_i_idx;
+-- still can't repack without a replica identity
+ALTER TABLE ptnowner1 REPLICA IDENTITY DEFAULT;
+REPACK (CONCURRENTLY) ptnowner1;
RESET SESSION AUTHORIZATION;
+SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a
+ JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C";
SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a
JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C";
DROP TABLE ptnowner;
--
2.47.3
--m6zy3l65ushq557m
Content-Type: text/x-diff; charset=utf-8
Content-Disposition: attachment;
filename="0002-REPACK-do-not-require-LOGIN-privileges.patch"
^ permalink raw reply [nested|flat] 26+ messages in thread
* [PATCH v1 1/1] Add list of major features to the v19 release notes.
@ 2026-07-01 21:33 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 26+ messages in thread
From: Nathan Bossart @ 2026-07-01 21:33 UTC (permalink / raw)
---
doc/src/sgml/release-19.sgml | 79 +++++++++++++++++++++++++++++++++++-
1 file changed, 78 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/release-19.sgml b/doc/src/sgml/release-19.sgml
index d8d3758d5ba..00c620b283c 100644
--- a/doc/src/sgml/release-19.sgml
+++ b/doc/src/sgml/release-19.sgml
@@ -19,7 +19,84 @@
<itemizedlist>
<listitem>
- <para><emphasis>fill in later</emphasis></para>
+ <para>
+ Support for
+ <link linkend="ddl-property-graphs">property graph queries</link>
+ (<acronym>SQL/PGQ</acronym>).
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ A new <link linkend="sql-repack"><command>REPACK</command></link> command
+ that reclaims disk space and reorganizes table contents, combining the
+ functionality of the existing <command>VACUUM FULL</command> and
+ <command>CLUSTER</command> commands. Its <literal>CONCURRENTLY</literal>
+ option allows repacking without blocking reads and writes to the table.
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ Logical replication now
+ <link linkend="logical-replication-sequences">replicates sequence values</link>,
+ and it can be enabled without a server restart when
+ <xref linkend="guc-wal-level"/> is set to <literal>replica</literal>.
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ Autovacuum can now use
+ <link linkend="guc-autovacuum-max-parallel-workers">multiple worker processes</link>
+ to vacuum a single table in parallel, and a
+ <link linkend="autovacuum-priority">new scoring system</link> prioritizes
+ the tables that most need vacuuming or analyzing.
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ Faster performance in many areas, including automatic scaling of the
+ number of <link linkend="guc-io-max-workers">I/O worker processes</link>,
+ quicker foreign-key checks, and further planning and execution
+ optimizations.
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ Data checksums can now be
+ <link linkend="checksums-online-enable-disable">enabled or disabled while the database is running</link>.
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ A new <link linkend="sql-wait-for"><command>WAIT FOR</command></link>
+ command that lets an application pause until a standby has replayed
+ changes up to a chosen point, thereby supporting
+ <quote>read-your-writes</quote> query patterns on standbys.
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ <xref linkend="sql-update"/> and <xref linkend="sql-delete"/> can now
+ change or remove data for just part of a time range via the new
+ <literal>FOR PORTION OF</literal> clause.
+ </para>
+ </listitem>
+
+ <listitem>
+ <para>
+ A new
+ <link linkend="pgplanadvice"><application>pg_plan_advice</application></link>
+ extension for stabilizing and controlling the query planner's decisions,
+ together with the companion
+ <link linkend="pgstashadvice"><application>pg_stash_advice</application></link>
+ extension that applies this advice automatically based on the query.
+ </para>
</listitem>
</itemizedlist>
--
2.50.1 (Apple Git-155)
--Dy4bd/k8yrlB0eLC--
^ permalink raw reply [nested|flat] 26+ messages in thread
end of thread, other threads:[~2026-07-01 21:33 UTC | newest]
Thread overview: 26+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-02-20 11:11 [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely Etsuro Fujita <[email protected]>
2019-03-06 11:27 [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely Etsuro Fujita <[email protected]>
2019-03-07 09:27 [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely Etsuro Fujita <[email protected]>
2019-03-20 10:19 [PATCH 1/3] postgres_fdw: Perform UPPERREL_ORDERED step remotely Etsuro Fujita <[email protected]>
2019-03-27 07:10 [PATCH 1/4] postgres_fdw: Perform UPPERREL_ORDERED step remotely Etsuro Fujita <[email protected]>
2026-02-28 07:10 POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 05:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-01 19:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-03 07:42 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-03 13:45 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-04 11:35 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Jim Jones <[email protected]>
2026-03-04 18:50 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-09 06:03 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Shreeya Sharma <[email protected]>
2026-03-09 06:11 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Shreeya Sharma <[email protected]>
2026-03-09 06:44 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-12 03:54 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Chao Li <[email protected]>
2026-03-12 10:38 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-11 20:57 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Peter Eisentraut <[email protected]>
2026-03-12 04:30 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Tom Lane <[email protected]>
2026-03-12 07:00 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-17 06:58 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-03-21 18:40 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-05-26 06:51 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-06-16 07:52 ` Re: POC: PLpgSQL FOREACH IN JSON ARRAY Pavel Stehule <[email protected]>
2026-04-20 09:38 [PATCH 1/2] REPACK: do not require the user to have REPLICATION Álvaro Herrera <[email protected]>
2026-07-01 21:33 [PATCH v1 1/1] Add list of major features to the v19 release notes. Nathan Bossart <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox