This is an automated email from the ASF dual-hosted git repository.

maxyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry.git


The following commit(s) were added to refs/heads/main by this push:
     new cf4f56aaf76 Fix readable CTE with SELECT INTO clause.
cf4f56aaf76 is described below

commit cf4f56aaf76f8fb6c2c13bb406f110a11c9f31b0
Author: hanwei <[email protected]>
AuthorDate: Fri Oct 17 10:46:59 2025 +0800

    Fix readable CTE with SELECT INTO clause.
    
    In https://github.com/apache/cloudberry/pull/1215 bugfix, it add a check
    for writable CTE with SELECT INTO clause. It will invoke
    `transformWithClause` twice, even it's readable CTE. But Cloudberry
    currently only support one WITH clause per query level, so it will lead
    to crash.
    
    So we can give up invoking `transformWithClause` function when check and
    iterate through each CTE to verify if it is writable.
    
    At the same time, we add comprehensive test cases to validate.
---
 src/backend/parser/analyze.c                 | 38 +++++++++------
 src/test/regress/expected/with.out           | 73 ++++++++++++++++++++++++++++
 src/test/regress/expected/with_optimizer.out | 73 ++++++++++++++++++++++++++++
 src/test/regress/sql/with.sql                | 49 +++++++++++++++++++
 4 files changed, 218 insertions(+), 15 deletions(-)

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 552d2e1aa28..d8091c6c4f4 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -343,21 +343,29 @@ transformOptionalSelectInto(ParseState *pstate, Node 
*parseTree)
 
                        if (stmt->withClause)
                        {
-                               /*
-                                * Just transform to check p_hasModifyingCTE, 
cte list will be transformed inside SELECT stmt.
-                                */
-                               transformWithClause(pstate, stmt->withClause);
-                               /*
-                                * Since Cloudberry currently only support a 
single writer gang, only one
-                                * writable clause is permitted per CTE. Once 
we get flexible gangs
-                                * with more than one writer gang we can lift 
this restriction.
-                                */
-                               if (pstate->p_hasModifyingCTE)
-                                       ereport(ERROR,
-                                                       
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                                                        errmsg("writable CTE 
queries cannot be used with writable queries"),
-                                                        errdetail("Apache 
Cloudberry currently only support CTEs with one writable clause, called in a 
non-writable context."),
-                                                        errhint("Rewrite the 
query to only include one writable clause.")));
+                               ListCell   *lc;
+                               foreach(lc, stmt->withClause->ctes)
+                               {
+                                       CommonTableExpr *cte = (CommonTableExpr 
*) lfirst(lc);
+                                       if (!IsA(cte->ctequery, SelectStmt))
+                                       {
+                                               /* must be a data-modifying 
statement */
+                                               Assert(IsA(cte->ctequery, 
InsertStmt) ||
+                                                          IsA(cte->ctequery, 
UpdateStmt) ||
+                                                          IsA(cte->ctequery, 
DeleteStmt));
+
+                                               /*
+                                                * Since Cloudberry currently 
only support a single writer gang, only one
+                                                * writable clause is permitted 
per CTE. Once we get flexible gangs
+                                                * with more than one writer 
gang we can lift this restriction.
+                                                */
+                                               ereport(ERROR,
+                                                               
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                                                
errmsg("writable CTE queries cannot be used with writable queries"),
+                                                                
errdetail("Apache Cloudberry currently only support CTEs with one writable 
clause, called in a non-writable context."),
+                                                                
errhint("Rewrite the query to only include one writable clause.")));
+                                       }
+                               }
                        }
                }
        }
diff --git a/src/test/regress/expected/with.out 
b/src/test/regress/expected/with.out
index ca8771a01fc..6c643fec8d4 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3264,3 +3264,76 @@ ERROR:  writable CTE queries cannot be used with 
writable queries
 DETAIL:  Apache Cloudberry currently only support CTEs with one writable 
clause, called in a non-writable context.
 HINT:  Rewrite the query to only include one writable clause.
 DROP TABLE t_w_cte_relp;
+--
+-- readable CTE with SELECT INTO clause.
+--
+WITH sel AS (
+  SELECT 1 as a
+)
+SELECT a INTO t_r_cte FROM sel;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_r_cte;
+ a 
+---
+ 1
+(1 row)
+
+DROP TABLE t_r_cte;
+--
+-- Multiple readable CTEs with SELECT INTO
+--
+WITH cte1 AS (SELECT 1 as a),
+     cte2 AS (SELECT 2 as b)
+SELECT a, b INTO t_multi_r_cte FROM cte1, cte2;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_multi_r_cte;
+ a | b 
+---+---
+ 1 | 2
+(1 row)
+
+DROP TABLE t_multi_r_cte;
+--
+-- Nested SELECT in CTE with SELECT INTO
+--
+WITH nested_cte AS (
+  SELECT * FROM (SELECT 100 as val, 'test'::text as name) subq
+)
+SELECT val, name INTO t_nested_r_cte FROM nested_cte;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_nested_r_cte;
+ val | name 
+-----+------
+ 100 | test
+(1 row)
+
+DROP TABLE t_nested_r_cte;
+--
+-- CTE with JOIN and SELECT INTO
+--
+WITH cte1 AS (SELECT 1 as id, 'foo' as val),
+     cte2 AS (SELECT 1 as id, 'bar' as val)
+SELECT cte1.id, cte1.val as val1, cte2.val as val2
+INTO t_join_r_cte
+FROM cte1 JOIN cte2 ON cte1.id = cte2.id;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_join_r_cte;
+ id | val1 | val2 
+----+------+------
+  1 | foo  | bar
+(1 row)
+
+DROP TABLE t_join_r_cte;
+--
+-- Verify mixed readable and writable CTEs blocked (negative test)
+--
+CREATE TABLE t_mixed_test (a int);
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause -- Using column named 'a' 
as the Apache Cloudberry data distribution key for this table.
+HINT:  The 'DISTRIBUTED BY' clause determines the distribution of data. Make 
sure column(s) chosen are the optimal data distribution key to minimize skew.
+WITH read_cte AS (SELECT 1 as x),
+     write_cte AS (INSERT INTO t_mixed_test VALUES (1) RETURNING *)
+SELECT x INTO t_should_fail2 FROM read_cte;  -- should fail
+ERROR:  writable CTE queries cannot be used with writable queries
+DETAIL:  Apache Cloudberry currently only support CTEs with one writable 
clause, called in a non-writable context.
+HINT:  Rewrite the query to only include one writable clause.
+DROP TABLE t_mixed_test;
diff --git a/src/test/regress/expected/with_optimizer.out 
b/src/test/regress/expected/with_optimizer.out
index ca81f919f2a..76404cfde44 100644
--- a/src/test/regress/expected/with_optimizer.out
+++ b/src/test/regress/expected/with_optimizer.out
@@ -3284,3 +3284,76 @@ ERROR:  writable CTE queries cannot be used with 
writable queries
 DETAIL:  Apache Cloudberry currently only support CTEs with one writable 
clause, called in a non-writable context.
 HINT:  Rewrite the query to only include one writable clause.
 DROP TABLE t_w_cte_relp;
+--
+-- readable CTE with SELECT INTO clause.
+--
+WITH sel AS (
+  SELECT 1 as a
+)
+SELECT a INTO t_r_cte FROM sel;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_r_cte;
+ a 
+---
+ 1
+(1 row)
+
+DROP TABLE t_r_cte;
+--
+-- Multiple readable CTEs with SELECT INTO
+--
+WITH cte1 AS (SELECT 1 as a),
+     cte2 AS (SELECT 2 as b)
+SELECT a, b INTO t_multi_r_cte FROM cte1, cte2;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_multi_r_cte;
+ a | b 
+---+---
+ 1 | 2
+(1 row)
+
+DROP TABLE t_multi_r_cte;
+--
+-- Nested SELECT in CTE with SELECT INTO
+--
+WITH nested_cte AS (
+  SELECT * FROM (SELECT 100 as val, 'test'::text as name) subq
+)
+SELECT val, name INTO t_nested_r_cte FROM nested_cte;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_nested_r_cte;
+ val | name 
+-----+------
+ 100 | test
+(1 row)
+
+DROP TABLE t_nested_r_cte;
+--
+-- CTE with JOIN and SELECT INTO
+--
+WITH cte1 AS (SELECT 1 as id, 'foo' as val),
+     cte2 AS (SELECT 1 as id, 'bar' as val)
+SELECT cte1.id, cte1.val as val1, cte2.val as val2
+INTO t_join_r_cte
+FROM cte1 JOIN cte2 ON cte1.id = cte2.id;
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause. Creating a NULL policy 
entry.
+SELECT * FROM t_join_r_cte;
+ id | val1 | val2 
+----+------+------
+  1 | foo  | bar
+(1 row)
+
+DROP TABLE t_join_r_cte;
+--
+-- Verify mixed readable and writable CTEs blocked (negative test)
+--
+CREATE TABLE t_mixed_test (a int);
+NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause -- Using column named 'a' 
as the Apache Cloudberry data distribution key for this table.
+HINT:  The 'DISTRIBUTED BY' clause determines the distribution of data. Make 
sure column(s) chosen are the optimal data distribution key to minimize skew.
+WITH read_cte AS (SELECT 1 as x),
+     write_cte AS (INSERT INTO t_mixed_test VALUES (1) RETURNING *)
+SELECT x INTO t_should_fail2 FROM read_cte;  -- should fail
+ERROR:  writable CTE queries cannot be used with writable queries
+DETAIL:  Apache Cloudberry currently only support CTEs with one writable 
clause, called in a non-writable context.
+HINT:  Rewrite the query to only include one writable clause.
+DROP TABLE t_mixed_test;
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
index b211e3eceed..0b3da6cd98c 100644
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -1594,3 +1594,52 @@ EXPLAIN(COSTS OFF, VERBOSE) WITH ins AS (
 )
 SELECT sum(a) INTO t_w_cte_relp_1 FROM ins;
 DROP TABLE t_w_cte_relp;
+
+--
+-- readable CTE with SELECT INTO clause.
+--
+WITH sel AS (
+  SELECT 1 as a
+)
+SELECT a INTO t_r_cte FROM sel;
+SELECT * FROM t_r_cte;
+DROP TABLE t_r_cte;
+
+--
+-- Multiple readable CTEs with SELECT INTO
+--
+WITH cte1 AS (SELECT 1 as a),
+     cte2 AS (SELECT 2 as b)
+SELECT a, b INTO t_multi_r_cte FROM cte1, cte2;
+SELECT * FROM t_multi_r_cte;
+DROP TABLE t_multi_r_cte;
+
+--
+-- Nested SELECT in CTE with SELECT INTO
+--
+WITH nested_cte AS (
+  SELECT * FROM (SELECT 100 as val, 'test'::text as name) subq
+)
+SELECT val, name INTO t_nested_r_cte FROM nested_cte;
+SELECT * FROM t_nested_r_cte;
+DROP TABLE t_nested_r_cte;
+
+--
+-- CTE with JOIN and SELECT INTO
+--
+WITH cte1 AS (SELECT 1 as id, 'foo' as val),
+     cte2 AS (SELECT 1 as id, 'bar' as val)
+SELECT cte1.id, cte1.val as val1, cte2.val as val2
+INTO t_join_r_cte
+FROM cte1 JOIN cte2 ON cte1.id = cte2.id;
+SELECT * FROM t_join_r_cte;
+DROP TABLE t_join_r_cte;
+
+--
+-- Verify mixed readable and writable CTEs blocked (negative test)
+--
+CREATE TABLE t_mixed_test (a int);
+WITH read_cte AS (SELECT 1 as x),
+     write_cte AS (INSERT INTO t_mixed_test VALUES (1) RETURNING *)
+SELECT x INTO t_should_fail2 FROM read_cte;  -- should fail
+DROP TABLE t_mixed_test;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to