Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Avoid unhelpful internal error for incorrect recursive-WITH queries.
authorTom Lane <tgl@sss.pgh.pa.us>
Sun, 14 Jul 2024 17:49:46 +0000 (13:49 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Sun, 14 Jul 2024 17:49:46 +0000 (13:49 -0400)
checkWellFormedRecursion would issue "missing recursive reference"
if a WITH RECURSIVE query contained a single self-reference but
that self-reference was inside a top-level WITH, ORDER BY, LIMIT,
etc, rather than inside the second arm of the UNION as expected.
We already intended to throw more-on-point errors for such cases,
but those error checks must be done before examining the UNION arm
in order to have the desired results.  So this patch need only
move some code (and improve the comments).

Per bug #18536 from Alexander Lakhin.  Back-patch to all supported
branches.

Discussion: https://postgr.es/m/18536-0a342ec07901203e@postgresql.org

src/backend/parser/parse_cte.c
src/test/regress/expected/with.out
src/test/regress/sql/with.sql

index b24989e4170a68861b990f61d0c298f74cdc7467..6a300f816c915b26f29823892b15e0083d5fab31 100644 (file)
@@ -659,25 +659,14 @@ checkWellFormedRecursion(CteState *cstate)
                            cte->ctename),
                     parser_errposition(cstate->pstate, cte->location)));
 
-       /* The left-hand operand mustn't contain self-reference at all */
-       cstate->curitem = i;
-       cstate->innerwiths = NIL;
-       cstate->selfrefcount = 0;
-       cstate->context = RECURSION_NONRECURSIVETERM;
-       checkWellFormedRecursionWalker((Node *) stmt->larg, cstate);
-       Assert(cstate->innerwiths == NIL);
-
-       /* Right-hand operand should contain one reference in a valid place */
-       cstate->curitem = i;
-       cstate->innerwiths = NIL;
-       cstate->selfrefcount = 0;
-       cstate->context = RECURSION_OK;
-       checkWellFormedRecursionWalker((Node *) stmt->rarg, cstate);
-       Assert(cstate->innerwiths == NIL);
-       if (cstate->selfrefcount != 1)  /* shouldn't happen */
-           elog(ERROR, "missing recursive reference");
-
-       /* WITH mustn't contain self-reference, either */
+       /*
+        * Really, we should insist that there not be a top-level WITH, since
+        * syntactically that would enclose the UNION.  However, we've not
+        * done so in the past and it's probably too late to change.  Settle
+        * for insisting that WITH not contain a self-reference.  Test this
+        * before examining the UNION arms, to avoid issuing confusing errors
+        * in such cases.
+        */
        if (stmt->withClause)
        {
            cstate->curitem = i;
@@ -694,7 +683,9 @@ checkWellFormedRecursion(CteState *cstate)
         * don't make sense because it's impossible to figure out what they
         * mean when we have only part of the recursive query's results. (If
         * we did allow them, we'd have to check for recursive references
-        * inside these subtrees.)
+        * inside these subtrees.  As for WITH, we have to do this before
+        * examining the UNION arms, to avoid issuing confusing errors if
+        * there is a recursive reference here.)
         */
        if (stmt->sortClause)
            ereport(ERROR,
@@ -720,6 +711,28 @@ checkWellFormedRecursion(CteState *cstate)
                     errmsg("FOR UPDATE/SHARE in a recursive query is not implemented"),
                     parser_errposition(cstate->pstate,
                                        exprLocation((Node *) stmt->lockingClause))));
+
+       /*
+        * Now we can get on with checking the UNION operands themselves.
+        *
+        * The left-hand operand mustn't contain a self-reference at all.
+        */
+       cstate->curitem = i;
+       cstate->innerwiths = NIL;
+       cstate->selfrefcount = 0;
+       cstate->context = RECURSION_NONRECURSIVETERM;
+       checkWellFormedRecursionWalker((Node *) stmt->larg, cstate);
+       Assert(cstate->innerwiths == NIL);
+
+       /* Right-hand operand should contain one reference in a valid place */
+       cstate->curitem = i;
+       cstate->innerwiths = NIL;
+       cstate->selfrefcount = 0;
+       cstate->context = RECURSION_OK;
+       checkWellFormedRecursionWalker((Node *) stmt->rarg, cstate);
+       Assert(cstate->innerwiths == NIL);
+       if (cstate->selfrefcount != 1)  /* shouldn't happen */
+           elog(ERROR, "missing recursive reference");
    }
 }
 
index 8f7e433f6073be4d3731f962804b3e2bcf985730..2c3e36f5fe1d37e2b429a26551c688afe507445d 100644 (file)
@@ -1054,6 +1054,46 @@ WITH RECURSIVE x(n) AS (SELECT n FROM x UNION ALL SELECT 1)
 ERROR:  recursive reference to query "x" must not appear within its non-recursive term
 LINE 1: WITH RECURSIVE x(n) AS (SELECT n FROM x UNION ALL SELECT 1)
                                               ^
+-- allow this, because we historically have
+WITH RECURSIVE x(n) AS (
+  WITH x1 AS (SELECT 1 AS n)
+    SELECT 0
+    UNION
+    SELECT * FROM x1)
+   SELECT * FROM x;
+ n 
+---
+ 0
+ 1
+(2 rows)
+
+-- but this should be rejected
+WITH RECURSIVE x(n) AS (
+  WITH x1 AS (SELECT 1 FROM x)
+    SELECT 0
+    UNION
+    SELECT * FROM x1)
+   SELECT * FROM x;
+ERROR:  recursive reference to query "x" must not appear within a subquery
+LINE 2:   WITH x1 AS (SELECT 1 FROM x)
+                                    ^
+-- and this too
+WITH RECURSIVE x(n) AS (
+  (WITH x1 AS (SELECT 1 FROM x) SELECT * FROM x1)
+  UNION
+  SELECT 0)
+   SELECT * FROM x;
+ERROR:  recursive reference to query "x" must not appear within its non-recursive term
+LINE 2:   (WITH x1 AS (SELECT 1 FROM x) SELECT * FROM x1)
+                                     ^
+-- and this
+WITH RECURSIVE x(n) AS (
+  SELECT 0 UNION SELECT 1
+  ORDER BY (SELECT n FROM x))
+   SELECT * FROM x;
+ERROR:  ORDER BY in a recursive query is not implemented
+LINE 3:   ORDER BY (SELECT n FROM x))
+                   ^
 CREATE TEMPORARY TABLE y (a INTEGER);
 INSERT INTO y SELECT generate_series(1, 10);
 -- LEFT JOIN
index 5b8993330a487209c3ff503a59a68d38d51f5526..c2d1555fb082b55d4f53c88613b80a5cd0a31125 100644 (file)
@@ -479,6 +479,35 @@ WITH RECURSIVE x(n) AS (SELECT n FROM x)
 WITH RECURSIVE x(n) AS (SELECT n FROM x UNION ALL SELECT 1)
    SELECT * FROM x;
 
+-- allow this, because we historically have
+WITH RECURSIVE x(n) AS (
+  WITH x1 AS (SELECT 1 AS n)
+    SELECT 0
+    UNION
+    SELECT * FROM x1)
+   SELECT * FROM x;
+
+-- but this should be rejected
+WITH RECURSIVE x(n) AS (
+  WITH x1 AS (SELECT 1 FROM x)
+    SELECT 0
+    UNION
+    SELECT * FROM x1)
+   SELECT * FROM x;
+
+-- and this too
+WITH RECURSIVE x(n) AS (
+  (WITH x1 AS (SELECT 1 FROM x) SELECT * FROM x1)
+  UNION
+  SELECT 0)
+   SELECT * FROM x;
+
+-- and this
+WITH RECURSIVE x(n) AS (
+  SELECT 0 UNION SELECT 1
+  ORDER BY (SELECT n FROM x))
+   SELECT * FROM x;
+
 CREATE TEMPORARY TABLE y (a INTEGER);
 INSERT INTO y SELECT generate_series(1, 10);