postgresql/src/backend/parser/parse_cte.c

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1198 lines
37 KiB
C
Raw Normal View History

/*-------------------------------------------------------------------------
*
* parse_cte.c
* handle CTEs (common table expressions) in parser
*
* Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
2010-09-20 22:08:53 +02:00
* src/backend/parser/parse_cte.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_type.h"
#include "nodes/nodeFuncs.h"
#include "parser/analyze.h"
#include "parser/parse_coerce.h"
#include "parser/parse_collate.h"
#include "parser/parse_cte.h"
#include "parser/parse_expr.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/typcache.h"
/* Enumeration of contexts in which a self-reference is disallowed */
typedef enum
{
RECURSION_OK,
RECURSION_NONRECURSIVETERM, /* inside the left-hand term */
RECURSION_SUBLINK, /* inside a sublink */
RECURSION_OUTERJOIN, /* inside nullable side of an outer join */
RECURSION_INTERSECT, /* underneath INTERSECT (ALL) */
RECURSION_EXCEPT /* underneath EXCEPT (ALL) */
} RecursionContext;
/* Associated error messages --- each must have one %s for CTE name */
static const char *const recursion_errormsgs[] = {
/* RECURSION_OK */
NULL,
/* RECURSION_NONRECURSIVETERM */
gettext_noop("recursive reference to query \"%s\" must not appear within its non-recursive term"),
/* RECURSION_SUBLINK */
gettext_noop("recursive reference to query \"%s\" must not appear within a subquery"),
/* RECURSION_OUTERJOIN */
gettext_noop("recursive reference to query \"%s\" must not appear within an outer join"),
/* RECURSION_INTERSECT */
gettext_noop("recursive reference to query \"%s\" must not appear within INTERSECT"),
/* RECURSION_EXCEPT */
gettext_noop("recursive reference to query \"%s\" must not appear within EXCEPT")
};
/*
* For WITH RECURSIVE, we have to find an ordering of the clause members
* with no forward references, and determine which members are recursive
* (i.e., self-referential). It is convenient to do this with an array
* of CteItems instead of a list of CommonTableExprs.
*/
typedef struct CteItem
{
CommonTableExpr *cte; /* One CTE to examine */
int id; /* Its ID number for dependencies */
Bitmapset *depends_on; /* CTEs depended on (not including self) */
} CteItem;
/* CteState is what we need to pass around in the tree walkers */
typedef struct CteState
{
/* global state: */
ParseState *pstate; /* global parse state */
CteItem *items; /* array of CTEs and extra data */
int numitems; /* number of CTEs */
/* working state during a tree walk: */
int curitem; /* index of item currently being examined */
List *innerwiths; /* list of lists of CommonTableExpr */
/* working state for checkWellFormedRecursion walk only: */
int selfrefcount; /* number of self-references detected */
RecursionContext context; /* context to allow or disallow self-ref */
} CteState;
static void analyzeCTE(ParseState *pstate, CommonTableExpr *cte);
/* Dependency processing functions */
static void makeDependencyGraph(CteState *cstate);
static bool makeDependencyGraphWalker(Node *node, CteState *cstate);
static void TopologicalSort(ParseState *pstate, CteItem *items, int numitems);
/* Recursion validity checker functions */
static void checkWellFormedRecursion(CteState *cstate);
static bool checkWellFormedRecursionWalker(Node *node, CteState *cstate);
static void checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate);
/*
* transformWithClause -
* Transform the list of WITH clause "common table expressions" into
* Query nodes.
*
* The result is the list of transformed CTEs to be put into the output
* Query. (This is in fact the same as the ending value of p_ctenamespace,
* but it seems cleaner to not expose that in the function's API.)
*/
List *
transformWithClause(ParseState *pstate, WithClause *withClause)
{
ListCell *lc;
/* Only one WITH clause per query level */
Assert(pstate->p_ctenamespace == NIL);
Assert(pstate->p_future_ctes == NIL);
/*
* For either type of WITH, there must not be duplicate CTE names in the
* list. Check this right away so we needn't worry later.
*
* Also, tentatively mark each CTE as non-recursive, and initialize its
* reference count to zero, and set pstate->p_hasModifyingCTE if needed.
*/
foreach(lc, withClause->ctes)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
ListCell *rest;
/* MERGE is allowed by parser, but unimplemented. Reject for now */
if (IsA(cte->ctequery, MergeStmt))
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("MERGE not supported in WITH query"),
parser_errposition(pstate, cte->location));
Represent Lists as expansible arrays, not chains of cons-cells. Originally, Postgres Lists were a more or less exact reimplementation of Lisp lists, which consist of chains of separately-allocated cons cells, each having a value and a next-cell link. We'd hacked that once before (commit d0b4399d8) to add a separate List header, but the data was still in cons cells. That makes some operations -- notably list_nth() -- O(N), and it's bulky because of the next-cell pointers and per-cell palloc overhead, and it's very cache-unfriendly if the cons cells end up scattered around rather than being adjacent. In this rewrite, we still have List headers, but the data is in a resizable array of values, with no next-cell links. Now we need at most two palloc's per List, and often only one, since we can allocate some values in the same palloc call as the List header. (Of course, extending an existing List may require repalloc's to enlarge the array. But this involves just O(log N) allocations not O(N).) Of course this is not without downsides. The key difficulty is that addition or deletion of a list entry may now cause other entries to move, which it did not before. For example, that breaks foreach() and sister macros, which historically used a pointer to the current cons-cell as loop state. We can repair those macros transparently by making their actual loop state be an integer list index; the exposed "ListCell *" pointer is no longer state carried across loop iterations, but is just a derived value. (In practice, modern compilers can optimize things back to having just one loop state value, at least for simple cases with inline loop bodies.) In principle, this is a semantics change for cases where the loop body inserts or deletes list entries ahead of the current loop index; but I found no such cases in the Postgres code. The change is not at all transparent for code that doesn't use foreach() but chases lists "by hand" using lnext(). The largest share of such code in the backend is in loops that were maintaining "prev" and "next" variables in addition to the current-cell pointer, in order to delete list cells efficiently using list_delete_cell(). However, we no longer need a previous-cell pointer to delete a list cell efficiently. Keeping a next-cell pointer doesn't work, as explained above, but we can improve matters by changing such code to use a regular foreach() loop and then using the new macro foreach_delete_current() to delete the current cell. (This macro knows how to update the associated foreach loop's state so that no cells will be missed in the traversal.) There remains a nontrivial risk of code assuming that a ListCell * pointer will remain good over an operation that could now move the list contents. To help catch such errors, list.c can be compiled with a new define symbol DEBUG_LIST_MEMORY_USAGE that forcibly moves list contents whenever that could possibly happen. This makes list operations significantly more expensive so it's not normally turned on (though it is on by default if USE_VALGRIND is on). There are two notable API differences from the previous code: * lnext() now requires the List's header pointer in addition to the current cell's address. * list_delete_cell() no longer requires a previous-cell argument. These changes are somewhat unfortunate, but on the other hand code using either function needs inspection to see if it is assuming anything it shouldn't, so it's not all bad. Programmers should be aware of these significant performance changes: * list_nth() and related functions are now O(1); so there's no major access-speed difference between a list and an array. * Inserting or deleting a list element now takes time proportional to the distance to the end of the list, due to moving the array elements. (However, it typically *doesn't* require palloc or pfree, so except in long lists it's probably still faster than before.) Notably, lcons() used to be about the same cost as lappend(), but that's no longer true if the list is long. Code that uses lcons() and list_delete_first() to maintain a stack might usefully be rewritten to push and pop at the end of the list rather than the beginning. * There are now list_insert_nth...() and list_delete_nth...() functions that add or remove a list cell identified by index. These have the data-movement penalty explained above, but there's no search penalty. * list_concat() and variants now copy the second list's data into storage belonging to the first list, so there is no longer any sharing of cells between the input lists. The second argument is now declared "const List *" to reflect that it isn't changed. This patch just does the minimum needed to get the new implementation in place and fix bugs exposed by the regression tests. As suggested by the foregoing, there's a fair amount of followup work remaining to do. Also, the ENABLE_LIST_COMPAT macros are finally removed in this commit. Code using those should have been gone a dozen years ago. Patch by me; thanks to David Rowley, Jesper Pedersen, and others for review. Discussion: https://postgr.es/m/11587.1550975080@sss.pgh.pa.us
2019-07-15 19:41:58 +02:00
for_each_cell(rest, withClause->ctes, lnext(withClause->ctes, lc))
{
CommonTableExpr *cte2 = (CommonTableExpr *) lfirst(rest);
if (strcmp(cte->ctename, cte2->ctename) == 0)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_ALIAS),
errmsg("WITH query name \"%s\" specified more than once",
cte2->ctename),
parser_errposition(pstate, cte2->location)));
}
cte->cterecursive = false;
cte->cterefcount = 0;
if (!IsA(cte->ctequery, SelectStmt))
{
/* must be a data-modifying statement */
Assert(IsA(cte->ctequery, InsertStmt) ||
IsA(cte->ctequery, UpdateStmt) ||
IsA(cte->ctequery, DeleteStmt));
pstate->p_hasModifyingCTE = true;
}
}
if (withClause->recursive)
{
/*
* For WITH RECURSIVE, we rearrange the list elements if needed to
* eliminate forward references. First, build a work array and set up
* the data structure needed by the tree walkers.
*/
CteState cstate;
int i;
cstate.pstate = pstate;
cstate.numitems = list_length(withClause->ctes);
cstate.items = (CteItem *) palloc0(cstate.numitems * sizeof(CteItem));
i = 0;
foreach(lc, withClause->ctes)
{
cstate.items[i].cte = (CommonTableExpr *) lfirst(lc);
cstate.items[i].id = i;
i++;
}
/*
* Find all the dependencies and sort the CteItems into a safe
* processing order. Also, mark CTEs that contain self-references.
*/
makeDependencyGraph(&cstate);
/*
* Check that recursive queries are well-formed.
*/
checkWellFormedRecursion(&cstate);
/*
* Set up the ctenamespace for parse analysis. Per spec, all the WITH
* items are visible to all others, so stuff them all in before parse
* analysis. We build the list in safe processing order so that the
* planner can process the queries in sequence.
*/
for (i = 0; i < cstate.numitems; i++)
{
CommonTableExpr *cte = cstate.items[i].cte;
pstate->p_ctenamespace = lappend(pstate->p_ctenamespace, cte);
}
/*
* Do parse analysis in the order determined by the topological sort.
*/
for (i = 0; i < cstate.numitems; i++)
{
CommonTableExpr *cte = cstate.items[i].cte;
analyzeCTE(pstate, cte);
}
}
else
{
/*
* For non-recursive WITH, just analyze each CTE in sequence and then
* add it to the ctenamespace. This corresponds to the spec's
* definition of the scope of each WITH name. However, to allow error
* reports to be aware of the possibility of an erroneous reference,
* we maintain a list in p_future_ctes of the not-yet-visible CTEs.
*/
pstate->p_future_ctes = list_copy(withClause->ctes);
foreach(lc, withClause->ctes)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
analyzeCTE(pstate, cte);
pstate->p_ctenamespace = lappend(pstate->p_ctenamespace, cte);
pstate->p_future_ctes = list_delete_first(pstate->p_future_ctes);
}
}
return pstate->p_ctenamespace;
}
/*
* Perform the actual parse analysis transformation of one CTE. All
* CTEs it depends on have already been loaded into pstate->p_ctenamespace,
* and have been marked with the correct output column names/types.
*/
static void
analyzeCTE(ParseState *pstate, CommonTableExpr *cte)
{
Query *query;
CTESearchClause *search_clause = cte->search_clause;
CTECycleClause *cycle_clause = cte->cycle_clause;
/* Analysis not done already */
Assert(!IsA(cte->ctequery, Query));
/*
* Before analyzing the CTE's query, we'd better identify the data type of
* the cycle mark column if any, since the query could refer to that.
* Other validity checks on the cycle clause will be done afterwards.
*/
if (cycle_clause)
{
TypeCacheEntry *typentry;
Oid op;
cycle_clause->cycle_mark_value =
transformExpr(pstate, cycle_clause->cycle_mark_value,
EXPR_KIND_CYCLE_MARK);
cycle_clause->cycle_mark_default =
transformExpr(pstate, cycle_clause->cycle_mark_default,
EXPR_KIND_CYCLE_MARK);
cycle_clause->cycle_mark_type =
select_common_type(pstate,
list_make2(cycle_clause->cycle_mark_value,
cycle_clause->cycle_mark_default),
"CYCLE", NULL);
cycle_clause->cycle_mark_value =
coerce_to_common_type(pstate,
cycle_clause->cycle_mark_value,
cycle_clause->cycle_mark_type,
"CYCLE/SET/TO");
cycle_clause->cycle_mark_default =
coerce_to_common_type(pstate,
cycle_clause->cycle_mark_default,
cycle_clause->cycle_mark_type,
"CYCLE/SET/DEFAULT");
cycle_clause->cycle_mark_typmod =
select_common_typmod(pstate,
list_make2(cycle_clause->cycle_mark_value,
cycle_clause->cycle_mark_default),
cycle_clause->cycle_mark_type);
cycle_clause->cycle_mark_collation =
select_common_collation(pstate,
list_make2(cycle_clause->cycle_mark_value,
cycle_clause->cycle_mark_default),
true);
/* Might as well look up the relevant <> operator while we are at it */
typentry = lookup_type_cache(cycle_clause->cycle_mark_type,
TYPECACHE_EQ_OPR);
if (!OidIsValid(typentry->eq_opr))
ereport(ERROR,
errcode(ERRCODE_UNDEFINED_FUNCTION),
errmsg("could not identify an equality operator for type %s",
format_type_be(cycle_clause->cycle_mark_type)));
op = get_negator(typentry->eq_opr);
if (!OidIsValid(op))
ereport(ERROR,
errcode(ERRCODE_UNDEFINED_FUNCTION),
errmsg("could not identify an inequality operator for type %s",
format_type_be(cycle_clause->cycle_mark_type)));
cycle_clause->cycle_mark_neop = op;
}
/* Now we can get on with analyzing the CTE's query */
Change unknown-type literals to type text in SELECT and RETURNING lists. Previously, we left such literals alone if the query or subquery had no properties forcing a type decision to be made (such as an ORDER BY or DISTINCT clause using that output column). This meant that "unknown" could be an exposed output column type, which has never been a great idea because it could result in strange failures later on. For example, an outer query that tried to do any operations on an unknown-type subquery output would generally fail with some weird error like "failed to find conversion function from unknown to text" or "could not determine which collation to use for string comparison". Also, if the case occurred in a CREATE VIEW's query then the view would have an unknown-type column, causing similar failures in queries trying to use the view. To fix, at the tail end of parse analysis of a query, forcibly convert any remaining "unknown" literals in its SELECT or RETURNING list to type text. However, provide a switch to suppress that, and use it in the cases of SELECT inside a set operation or INSERT command. In those cases we already had type resolution rules that make use of context information from outside the subquery proper, and we don't want to change that behavior. Also, change creation of an unknown-type column in a relation from a warning to a hard error. The error should be unreachable now in CREATE VIEW or CREATE MATVIEW, but it's still possible to explicitly say "unknown" in CREATE TABLE or CREATE (composite) TYPE. We want to forbid that because it's nothing but a foot-gun. This change creates a pg_upgrade failure case: a matview that contains an unknown-type column can't be pg_upgraded, because reparsing the matview's defining query will now decide that the column is of type text, which doesn't match the cstring-like storage that the old materialized column would actually have. Add a checking pass to detect that. While at it, we can detect tables or composite types that would fail, essentially for free. Those would fail safely anyway later on, but we might as well fail earlier. This patch is by me, but it owes something to previous investigations by Rahila Syed. Also thanks to Ashutosh Bapat and Michael Paquier for review. Discussion: https://postgr.es/m/CAH2L28uwwbL9HUM-WR=hromW1Cvamkn7O-g8fPY2m=_7muJ0oA@mail.gmail.com
2017-01-25 15:17:18 +01:00
query = parse_sub_analyze(cte->ctequery, pstate, cte, false, true);
cte->ctequery = (Node *) query;
/*
* Check that we got something reasonable. These first two cases should
* be prevented by the grammar.
*/
if (!IsA(query, Query))
elog(ERROR, "unexpected non-Query statement in WITH");
if (query->utilityStmt != NULL)
elog(ERROR, "unexpected utility statement in WITH");
/*
* We disallow data-modifying WITH except at the top level of a query,
* because it's not clear when such a modification should be executed.
*/
if (query->commandType != CMD_SELECT &&
pstate->parentParseState != NULL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("WITH clause containing a data-modifying statement must be at the top level"),
parser_errposition(pstate, cte->location)));
/*
* CTE queries are always marked not canSetTag. (Currently this only
* matters for data-modifying statements, for which the flag will be
* propagated to the ModifyTable plan node.)
*/
query->canSetTag = false;
if (!cte->cterecursive)
{
/* Compute the output column names/types if not done yet */
analyzeCTETargetList(pstate, cte, GetCTETargetList(cte));
}
else
{
/*
* Verify that the previously determined output column types and
* collations match what the query really produced. We have to check
* this because the recursive term could have overridden the
* non-recursive term, and we don't have any easy way to fix that.
*/
ListCell *lctlist,
*lctyp,
*lctypmod,
*lccoll;
int varattno;
lctyp = list_head(cte->ctecoltypes);
lctypmod = list_head(cte->ctecoltypmods);
lccoll = list_head(cte->ctecolcollations);
varattno = 0;
foreach(lctlist, GetCTETargetList(cte))
{
TargetEntry *te = (TargetEntry *) lfirst(lctlist);
Node *texpr;
if (te->resjunk)
continue;
varattno++;
Assert(varattno == te->resno);
if (lctyp == NULL || lctypmod == NULL || lccoll == NULL) /* shouldn't happen */
elog(ERROR, "wrong number of output columns in WITH");
texpr = (Node *) te->expr;
if (exprType(texpr) != lfirst_oid(lctyp) ||
exprTypmod(texpr) != lfirst_int(lctypmod))
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("recursive query \"%s\" column %d has type %s in non-recursive term but type %s overall",
cte->ctename, varattno,
format_type_with_typemod(lfirst_oid(lctyp),
lfirst_int(lctypmod)),
format_type_with_typemod(exprType(texpr),
exprTypmod(texpr))),
errhint("Cast the output of the non-recursive term to the correct type."),
parser_errposition(pstate, exprLocation(texpr))));
if (exprCollation(texpr) != lfirst_oid(lccoll))
ereport(ERROR,
(errcode(ERRCODE_COLLATION_MISMATCH),
errmsg("recursive query \"%s\" column %d has collation \"%s\" in non-recursive term but collation \"%s\" overall",
cte->ctename, varattno,
get_collation_name(lfirst_oid(lccoll)),
get_collation_name(exprCollation(texpr))),
errhint("Use the COLLATE clause to set the collation of the non-recursive term."),
parser_errposition(pstate, exprLocation(texpr))));
Represent Lists as expansible arrays, not chains of cons-cells. Originally, Postgres Lists were a more or less exact reimplementation of Lisp lists, which consist of chains of separately-allocated cons cells, each having a value and a next-cell link. We'd hacked that once before (commit d0b4399d8) to add a separate List header, but the data was still in cons cells. That makes some operations -- notably list_nth() -- O(N), and it's bulky because of the next-cell pointers and per-cell palloc overhead, and it's very cache-unfriendly if the cons cells end up scattered around rather than being adjacent. In this rewrite, we still have List headers, but the data is in a resizable array of values, with no next-cell links. Now we need at most two palloc's per List, and often only one, since we can allocate some values in the same palloc call as the List header. (Of course, extending an existing List may require repalloc's to enlarge the array. But this involves just O(log N) allocations not O(N).) Of course this is not without downsides. The key difficulty is that addition or deletion of a list entry may now cause other entries to move, which it did not before. For example, that breaks foreach() and sister macros, which historically used a pointer to the current cons-cell as loop state. We can repair those macros transparently by making their actual loop state be an integer list index; the exposed "ListCell *" pointer is no longer state carried across loop iterations, but is just a derived value. (In practice, modern compilers can optimize things back to having just one loop state value, at least for simple cases with inline loop bodies.) In principle, this is a semantics change for cases where the loop body inserts or deletes list entries ahead of the current loop index; but I found no such cases in the Postgres code. The change is not at all transparent for code that doesn't use foreach() but chases lists "by hand" using lnext(). The largest share of such code in the backend is in loops that were maintaining "prev" and "next" variables in addition to the current-cell pointer, in order to delete list cells efficiently using list_delete_cell(). However, we no longer need a previous-cell pointer to delete a list cell efficiently. Keeping a next-cell pointer doesn't work, as explained above, but we can improve matters by changing such code to use a regular foreach() loop and then using the new macro foreach_delete_current() to delete the current cell. (This macro knows how to update the associated foreach loop's state so that no cells will be missed in the traversal.) There remains a nontrivial risk of code assuming that a ListCell * pointer will remain good over an operation that could now move the list contents. To help catch such errors, list.c can be compiled with a new define symbol DEBUG_LIST_MEMORY_USAGE that forcibly moves list contents whenever that could possibly happen. This makes list operations significantly more expensive so it's not normally turned on (though it is on by default if USE_VALGRIND is on). There are two notable API differences from the previous code: * lnext() now requires the List's header pointer in addition to the current cell's address. * list_delete_cell() no longer requires a previous-cell argument. These changes are somewhat unfortunate, but on the other hand code using either function needs inspection to see if it is assuming anything it shouldn't, so it's not all bad. Programmers should be aware of these significant performance changes: * list_nth() and related functions are now O(1); so there's no major access-speed difference between a list and an array. * Inserting or deleting a list element now takes time proportional to the distance to the end of the list, due to moving the array elements. (However, it typically *doesn't* require palloc or pfree, so except in long lists it's probably still faster than before.) Notably, lcons() used to be about the same cost as lappend(), but that's no longer true if the list is long. Code that uses lcons() and list_delete_first() to maintain a stack might usefully be rewritten to push and pop at the end of the list rather than the beginning. * There are now list_insert_nth...() and list_delete_nth...() functions that add or remove a list cell identified by index. These have the data-movement penalty explained above, but there's no search penalty. * list_concat() and variants now copy the second list's data into storage belonging to the first list, so there is no longer any sharing of cells between the input lists. The second argument is now declared "const List *" to reflect that it isn't changed. This patch just does the minimum needed to get the new implementation in place and fix bugs exposed by the regression tests. As suggested by the foregoing, there's a fair amount of followup work remaining to do. Also, the ENABLE_LIST_COMPAT macros are finally removed in this commit. Code using those should have been gone a dozen years ago. Patch by me; thanks to David Rowley, Jesper Pedersen, and others for review. Discussion: https://postgr.es/m/11587.1550975080@sss.pgh.pa.us
2019-07-15 19:41:58 +02:00
lctyp = lnext(cte->ctecoltypes, lctyp);
lctypmod = lnext(cte->ctecoltypmods, lctypmod);
lccoll = lnext(cte->ctecolcollations, lccoll);
}
if (lctyp != NULL || lctypmod != NULL || lccoll != NULL) /* shouldn't happen */
elog(ERROR, "wrong number of output columns in WITH");
}
/*
* Now make validity checks on the SEARCH and CYCLE clauses, if present.
*/
if (search_clause || cycle_clause)
{
Query *ctequery;
SetOperationStmt *sos;
if (!cte->cterecursive)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("WITH query is not recursive"),
parser_errposition(pstate, cte->location)));
/*
* SQL requires a WITH list element (CTE) to be "expandable" in order
* to allow a search or cycle clause. That is a stronger requirement
* than just being recursive. It basically means the query expression
* looks like
*
* non-recursive query UNION [ALL] recursive query
*
* and that the recursive query is not itself a set operation.
*
* As of this writing, most of these criteria are already satisfied by
* all recursive CTEs allowed by PostgreSQL. In the future, if
* further variants recursive CTEs are accepted, there might be
* further checks required here to determine what is "expandable".
*/
ctequery = castNode(Query, cte->ctequery);
Assert(ctequery->setOperations);
sos = castNode(SetOperationStmt, ctequery->setOperations);
/*
* This left side check is not required for expandability, but
* rewriteSearchAndCycle() doesn't currently have support for it, so
* we catch it here.
*/
if (!IsA(sos->larg, RangeTblRef))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("with a SEARCH or CYCLE clause, the left side of the UNION must be a SELECT")));
if (!IsA(sos->rarg, RangeTblRef))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("with a SEARCH or CYCLE clause, the right side of the UNION must be a SELECT")));
}
if (search_clause)
{
ListCell *lc;
List *seen = NIL;
foreach(lc, search_clause->search_col_list)
{
String *colname = lfirst_node(String, lc);
if (!list_member(cte->ctecolnames, colname))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("search column \"%s\" not in WITH query column list",
strVal(colname)),
parser_errposition(pstate, search_clause->location)));
if (list_member(seen, colname))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_COLUMN),
errmsg("search column \"%s\" specified more than once",
strVal(colname)),
parser_errposition(pstate, search_clause->location)));
seen = lappend(seen, colname);
}
if (list_member(cte->ctecolnames, makeString(search_clause->search_seq_column)))
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("search sequence column name \"%s\" already used in WITH query column list",
search_clause->search_seq_column),
parser_errposition(pstate, search_clause->location));
}
if (cycle_clause)
{
ListCell *lc;
List *seen = NIL;
foreach(lc, cycle_clause->cycle_col_list)
{
String *colname = lfirst_node(String, lc);
if (!list_member(cte->ctecolnames, colname))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cycle column \"%s\" not in WITH query column list",
strVal(colname)),
parser_errposition(pstate, cycle_clause->location)));
if (list_member(seen, colname))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_COLUMN),
errmsg("cycle column \"%s\" specified more than once",
strVal(colname)),
parser_errposition(pstate, cycle_clause->location)));
seen = lappend(seen, colname);
}
if (list_member(cte->ctecolnames, makeString(cycle_clause->cycle_mark_column)))
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cycle mark column name \"%s\" already used in WITH query column list",
cycle_clause->cycle_mark_column),
parser_errposition(pstate, cycle_clause->location));
if (list_member(cte->ctecolnames, makeString(cycle_clause->cycle_path_column)))
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cycle path column name \"%s\" already used in WITH query column list",
cycle_clause->cycle_path_column),
parser_errposition(pstate, cycle_clause->location));
if (strcmp(cycle_clause->cycle_mark_column,
cycle_clause->cycle_path_column) == 0)
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cycle mark column name and cycle path column name are the same"),
parser_errposition(pstate, cycle_clause->location));
}
if (search_clause && cycle_clause)
{
if (strcmp(search_clause->search_seq_column,
cycle_clause->cycle_mark_column) == 0)
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("search sequence column name and cycle mark column name are the same"),
parser_errposition(pstate, search_clause->location));
if (strcmp(search_clause->search_seq_column,
cycle_clause->cycle_path_column) == 0)
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
2021-05-11 09:06:49 +02:00
errmsg("search sequence column name and cycle path column name are the same"),
parser_errposition(pstate, search_clause->location));
}
}
/*
* Compute derived fields of a CTE, given the transformed output targetlist
*
* For a nonrecursive CTE, this is called after transforming the CTE's query.
* For a recursive CTE, we call it after transforming the non-recursive term,
* and pass the targetlist emitted by the non-recursive term only.
*
* Note: in the recursive case, the passed pstate is actually the one being
* used to analyze the CTE's query, so it is one level lower down than in
* the nonrecursive case. This doesn't matter since we only use it for
* error message context anyway.
*/
void
analyzeCTETargetList(ParseState *pstate, CommonTableExpr *cte, List *tlist)
{
int numaliases;
int varattno;
ListCell *tlistitem;
/* Not done already ... */
Assert(cte->ctecolnames == NIL);
/*
* We need to determine column names, types, and collations. The alias
* column names override anything coming from the query itself. (Note:
* the SQL spec says that the alias list must be empty or exactly as long
* as the output column set; but we allow it to be shorter for consistency
* with Alias handling.)
*/
cte->ctecolnames = copyObject(cte->aliascolnames);
cte->ctecoltypes = cte->ctecoltypmods = cte->ctecolcollations = NIL;
numaliases = list_length(cte->aliascolnames);
varattno = 0;
foreach(tlistitem, tlist)
{
TargetEntry *te = (TargetEntry *) lfirst(tlistitem);
Oid coltype;
int32 coltypmod;
Oid colcoll;
if (te->resjunk)
continue;
varattno++;
Assert(varattno == te->resno);
if (varattno > numaliases)
{
char *attrname;
attrname = pstrdup(te->resname);
cte->ctecolnames = lappend(cte->ctecolnames, makeString(attrname));
}
coltype = exprType((Node *) te->expr);
coltypmod = exprTypmod((Node *) te->expr);
colcoll = exprCollation((Node *) te->expr);
/*
* If the CTE is recursive, force the exposed column type of any
Change unknown-type literals to type text in SELECT and RETURNING lists. Previously, we left such literals alone if the query or subquery had no properties forcing a type decision to be made (such as an ORDER BY or DISTINCT clause using that output column). This meant that "unknown" could be an exposed output column type, which has never been a great idea because it could result in strange failures later on. For example, an outer query that tried to do any operations on an unknown-type subquery output would generally fail with some weird error like "failed to find conversion function from unknown to text" or "could not determine which collation to use for string comparison". Also, if the case occurred in a CREATE VIEW's query then the view would have an unknown-type column, causing similar failures in queries trying to use the view. To fix, at the tail end of parse analysis of a query, forcibly convert any remaining "unknown" literals in its SELECT or RETURNING list to type text. However, provide a switch to suppress that, and use it in the cases of SELECT inside a set operation or INSERT command. In those cases we already had type resolution rules that make use of context information from outside the subquery proper, and we don't want to change that behavior. Also, change creation of an unknown-type column in a relation from a warning to a hard error. The error should be unreachable now in CREATE VIEW or CREATE MATVIEW, but it's still possible to explicitly say "unknown" in CREATE TABLE or CREATE (composite) TYPE. We want to forbid that because it's nothing but a foot-gun. This change creates a pg_upgrade failure case: a matview that contains an unknown-type column can't be pg_upgraded, because reparsing the matview's defining query will now decide that the column is of type text, which doesn't match the cstring-like storage that the old materialized column would actually have. Add a checking pass to detect that. While at it, we can detect tables or composite types that would fail, essentially for free. Those would fail safely anyway later on, but we might as well fail earlier. This patch is by me, but it owes something to previous investigations by Rahila Syed. Also thanks to Ashutosh Bapat and Michael Paquier for review. Discussion: https://postgr.es/m/CAH2L28uwwbL9HUM-WR=hromW1Cvamkn7O-g8fPY2m=_7muJ0oA@mail.gmail.com
2017-01-25 15:17:18 +01:00
* "unknown" column to "text". We must deal with this here because
* we're called on the non-recursive term before there's been any
* attempt to force unknown output columns to some other type. We
* have to resolve unknowns before looking at the recursive term.
*
* The column might contain 'foo' COLLATE "bar", so don't override
* collation if it's already set.
*/
if (cte->cterecursive && coltype == UNKNOWNOID)
{
coltype = TEXTOID;
coltypmod = -1; /* should be -1 already, but be sure */
if (!OidIsValid(colcoll))
colcoll = DEFAULT_COLLATION_OID;
}
cte->ctecoltypes = lappend_oid(cte->ctecoltypes, coltype);
cte->ctecoltypmods = lappend_int(cte->ctecoltypmods, coltypmod);
cte->ctecolcollations = lappend_oid(cte->ctecolcollations, colcoll);
}
if (varattno < numaliases)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("WITH query \"%s\" has %d columns available but %d columns specified",
cte->ctename, varattno, numaliases),
parser_errposition(pstate, cte->location)));
}
/*
* Identify the cross-references of a list of WITH RECURSIVE items,
* and sort into an order that has no forward references.
*/
static void
makeDependencyGraph(CteState *cstate)
{
int i;
for (i = 0; i < cstate->numitems; i++)
{
CommonTableExpr *cte = cstate->items[i].cte;
cstate->curitem = i;
cstate->innerwiths = NIL;
makeDependencyGraphWalker((Node *) cte->ctequery, cstate);
Assert(cstate->innerwiths == NIL);
}
TopologicalSort(cstate->pstate, cstate->items, cstate->numitems);
}
/*
* Tree walker function to detect cross-references and self-references of the
* CTEs in a WITH RECURSIVE list.
*/
static bool
makeDependencyGraphWalker(Node *node, CteState *cstate)
{
if (node == NULL)
return false;
if (IsA(node, RangeVar))
{
RangeVar *rv = (RangeVar *) node;
/* If unqualified name, might be a CTE reference */
if (!rv->schemaname)
{
ListCell *lc;
int i;
/* ... but first see if it's captured by an inner WITH */
foreach(lc, cstate->innerwiths)
{
List *withlist = (List *) lfirst(lc);
ListCell *lc2;
foreach(lc2, withlist)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc2);
if (strcmp(rv->relname, cte->ctename) == 0)
return false; /* yes, so bail out */
}
}
/* No, could be a reference to the query level we are working on */
for (i = 0; i < cstate->numitems; i++)
{
CommonTableExpr *cte = cstate->items[i].cte;
if (strcmp(rv->relname, cte->ctename) == 0)
{
int myindex = cstate->curitem;
if (i != myindex)
{
/* Add cross-item dependency */
cstate->items[myindex].depends_on =
bms_add_member(cstate->items[myindex].depends_on,
cstate->items[i].id);
}
else
{
/* Found out this one is self-referential */
cte->cterecursive = true;
}
break;
}
}
}
return false;
}
if (IsA(node, SelectStmt))
{
SelectStmt *stmt = (SelectStmt *) node;
ListCell *lc;
if (stmt->withClause)
{
if (stmt->withClause->recursive)
{
/*
* In the RECURSIVE case, all query names of the WITH are
* visible to all WITH items as well as the main query. So
* push them all on, process, pop them all off.
*/
cstate->innerwiths = lcons(stmt->withClause->ctes,
cstate->innerwiths);
foreach(lc, stmt->withClause->ctes)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
(void) makeDependencyGraphWalker(cte->ctequery, cstate);
}
(void) raw_expression_tree_walker(node,
makeDependencyGraphWalker,
(void *) cstate);
cstate->innerwiths = list_delete_first(cstate->innerwiths);
}
else
{
/*
* In the non-RECURSIVE case, query names are visible to the
* WITH items after them and to the main query.
*/
cstate->innerwiths = lcons(NIL, cstate->innerwiths);
foreach(lc, stmt->withClause->ctes)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
ListCell *cell1;
(void) makeDependencyGraphWalker(cte->ctequery, cstate);
/* note that recursion could mutate innerwiths list */
cell1 = list_head(cstate->innerwiths);
lfirst(cell1) = lappend((List *) lfirst(cell1), cte);
}
(void) raw_expression_tree_walker(node,
makeDependencyGraphWalker,
(void *) cstate);
cstate->innerwiths = list_delete_first(cstate->innerwiths);
}
/* We're done examining the SelectStmt */
return false;
}
/* if no WITH clause, just fall through for normal processing */
}
if (IsA(node, WithClause))
{
/*
* Prevent raw_expression_tree_walker from recursing directly into a
* WITH clause. We need that to happen only under the control of the
* code above.
*/
return false;
}
return raw_expression_tree_walker(node,
makeDependencyGraphWalker,
(void *) cstate);
}
/*
* Sort by dependencies, using a standard topological sort operation
*/
static void
TopologicalSort(ParseState *pstate, CteItem *items, int numitems)
{
int i,
j;
/* for each position in sequence ... */
for (i = 0; i < numitems; i++)
{
/* ... scan the remaining items to find one that has no dependencies */
for (j = i; j < numitems; j++)
{
if (bms_is_empty(items[j].depends_on))
break;
}
/* if we didn't find one, the dependency graph has a cycle */
if (j >= numitems)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("mutual recursion between WITH items is not implemented"),
parser_errposition(pstate, items[i].cte->location)));
/*
* Found one. Move it to front and remove it from every other item's
* dependencies.
*/
if (i != j)
{
CteItem tmp;
tmp = items[i];
items[i] = items[j];
items[j] = tmp;
}
/*
* Items up through i are known to have no dependencies left, so we
* can skip them in this loop.
*/
for (j = i + 1; j < numitems; j++)
{
items[j].depends_on = bms_del_member(items[j].depends_on,
items[i].id);
}
}
}
/*
* Check that recursive queries are well-formed.
*/
static void
checkWellFormedRecursion(CteState *cstate)
{
int i;
for (i = 0; i < cstate->numitems; i++)
{
CommonTableExpr *cte = cstate->items[i].cte;
SelectStmt *stmt = (SelectStmt *) cte->ctequery;
Assert(!IsA(stmt, Query)); /* not analyzed yet */
/* Ignore items that weren't found to be recursive */
if (!cte->cterecursive)
continue;
/* Must be a SELECT statement */
if (!IsA(stmt, SelectStmt))
ereport(ERROR,
(errcode(ERRCODE_INVALID_RECURSION),
errmsg("recursive query \"%s\" must not contain data-modifying statements",
cte->ctename),
parser_errposition(cstate->pstate, cte->location)));
/* Must have top-level UNION */
if (stmt->op != SETOP_UNION)
ereport(ERROR,
(errcode(ERRCODE_INVALID_RECURSION),
errmsg("recursive query \"%s\" does not have the form non-recursive-term UNION [ALL] recursive-term",
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 */
if (stmt->withClause)
{
cstate->curitem = i;
cstate->innerwiths = NIL;
cstate->selfrefcount = 0;
cstate->context = RECURSION_SUBLINK;
checkWellFormedRecursionWalker((Node *) stmt->withClause->ctes,
cstate);
Assert(cstate->innerwiths == NIL);
}
/*
* Disallow ORDER BY and similar decoration atop the UNION. These
* 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.)
*/
if (stmt->sortClause)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("ORDER BY in a recursive query is not implemented"),
parser_errposition(cstate->pstate,
exprLocation((Node *) stmt->sortClause))));
if (stmt->limitOffset)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("OFFSET in a recursive query is not implemented"),
parser_errposition(cstate->pstate,
exprLocation(stmt->limitOffset))));
if (stmt->limitCount)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("LIMIT in a recursive query is not implemented"),
parser_errposition(cstate->pstate,
exprLocation(stmt->limitCount))));
if (stmt->lockingClause)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("FOR UPDATE/SHARE in a recursive query is not implemented"),
parser_errposition(cstate->pstate,
exprLocation((Node *) stmt->lockingClause))));
}
}
/*
* Tree walker function to detect invalid self-references in a recursive query.
*/
static bool
checkWellFormedRecursionWalker(Node *node, CteState *cstate)
{
RecursionContext save_context = cstate->context;
if (node == NULL)
return false;
if (IsA(node, RangeVar))
{
RangeVar *rv = (RangeVar *) node;
/* If unqualified name, might be a CTE reference */
if (!rv->schemaname)
{
ListCell *lc;
CommonTableExpr *mycte;
/* ... but first see if it's captured by an inner WITH */
foreach(lc, cstate->innerwiths)
{
List *withlist = (List *) lfirst(lc);
ListCell *lc2;
foreach(lc2, withlist)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc2);
if (strcmp(rv->relname, cte->ctename) == 0)
return false; /* yes, so bail out */
}
}
/* No, could be a reference to the query level we are working on */
mycte = cstate->items[cstate->curitem].cte;
if (strcmp(rv->relname, mycte->ctename) == 0)
{
/* Found a recursive reference to the active query */
if (cstate->context != RECURSION_OK)
ereport(ERROR,
(errcode(ERRCODE_INVALID_RECURSION),
errmsg(recursion_errormsgs[cstate->context],
mycte->ctename),
parser_errposition(cstate->pstate,
rv->location)));
/* Count references */
if (++(cstate->selfrefcount) > 1)
ereport(ERROR,
(errcode(ERRCODE_INVALID_RECURSION),
errmsg("recursive reference to query \"%s\" must not appear more than once",
mycte->ctename),
parser_errposition(cstate->pstate,
rv->location)));
}
}
return false;
}
if (IsA(node, SelectStmt))
{
SelectStmt *stmt = (SelectStmt *) node;
ListCell *lc;
if (stmt->withClause)
{
if (stmt->withClause->recursive)
{
/*
* In the RECURSIVE case, all query names of the WITH are
* visible to all WITH items as well as the main query. So
* push them all on, process, pop them all off.
*/
cstate->innerwiths = lcons(stmt->withClause->ctes,
cstate->innerwiths);
foreach(lc, stmt->withClause->ctes)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
(void) checkWellFormedRecursionWalker(cte->ctequery, cstate);
}
checkWellFormedSelectStmt(stmt, cstate);
cstate->innerwiths = list_delete_first(cstate->innerwiths);
}
else
{
/*
* In the non-RECURSIVE case, query names are visible to the
* WITH items after them and to the main query.
*/
cstate->innerwiths = lcons(NIL, cstate->innerwiths);
foreach(lc, stmt->withClause->ctes)
{
CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
ListCell *cell1;
(void) checkWellFormedRecursionWalker(cte->ctequery, cstate);
/* note that recursion could mutate innerwiths list */
cell1 = list_head(cstate->innerwiths);
lfirst(cell1) = lappend((List *) lfirst(cell1), cte);
}
checkWellFormedSelectStmt(stmt, cstate);
cstate->innerwiths = list_delete_first(cstate->innerwiths);
}
}
else
checkWellFormedSelectStmt(stmt, cstate);
/* We're done examining the SelectStmt */
return false;
}
if (IsA(node, WithClause))
{
/*
* Prevent raw_expression_tree_walker from recursing directly into a
* WITH clause. We need that to happen only under the control of the
* code above.
*/
return false;
}
if (IsA(node, JoinExpr))
{
JoinExpr *j = (JoinExpr *) node;
switch (j->jointype)
{
case JOIN_INNER:
checkWellFormedRecursionWalker(j->larg, cstate);
checkWellFormedRecursionWalker(j->rarg, cstate);
checkWellFormedRecursionWalker(j->quals, cstate);
break;
case JOIN_LEFT:
checkWellFormedRecursionWalker(j->larg, cstate);
if (save_context == RECURSION_OK)
cstate->context = RECURSION_OUTERJOIN;
checkWellFormedRecursionWalker(j->rarg, cstate);
cstate->context = save_context;
checkWellFormedRecursionWalker(j->quals, cstate);
break;
case JOIN_FULL:
if (save_context == RECURSION_OK)
cstate->context = RECURSION_OUTERJOIN;
checkWellFormedRecursionWalker(j->larg, cstate);
checkWellFormedRecursionWalker(j->rarg, cstate);
cstate->context = save_context;
checkWellFormedRecursionWalker(j->quals, cstate);
break;
case JOIN_RIGHT:
if (save_context == RECURSION_OK)
cstate->context = RECURSION_OUTERJOIN;
checkWellFormedRecursionWalker(j->larg, cstate);
cstate->context = save_context;
checkWellFormedRecursionWalker(j->rarg, cstate);
checkWellFormedRecursionWalker(j->quals, cstate);
break;
default:
elog(ERROR, "unrecognized join type: %d",
(int) j->jointype);
}
return false;
}
if (IsA(node, SubLink))
{
SubLink *sl = (SubLink *) node;
/*
* we intentionally override outer context, since subquery is
* independent
*/
cstate->context = RECURSION_SUBLINK;
checkWellFormedRecursionWalker(sl->subselect, cstate);
cstate->context = save_context;
checkWellFormedRecursionWalker(sl->testexpr, cstate);
return false;
}
return raw_expression_tree_walker(node,
checkWellFormedRecursionWalker,
(void *) cstate);
}
/*
* subroutine for checkWellFormedRecursionWalker: process a SelectStmt
* without worrying about its WITH clause
*/
static void
checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
{
RecursionContext save_context = cstate->context;
if (save_context != RECURSION_OK)
{
/* just recurse without changing state */
raw_expression_tree_walker((Node *) stmt,
checkWellFormedRecursionWalker,
(void *) cstate);
}
else
{
switch (stmt->op)
{
case SETOP_NONE:
case SETOP_UNION:
raw_expression_tree_walker((Node *) stmt,
checkWellFormedRecursionWalker,
(void *) cstate);
break;
case SETOP_INTERSECT:
if (stmt->all)
cstate->context = RECURSION_INTERSECT;
checkWellFormedRecursionWalker((Node *) stmt->larg,
cstate);
checkWellFormedRecursionWalker((Node *) stmt->rarg,
cstate);
cstate->context = save_context;
checkWellFormedRecursionWalker((Node *) stmt->sortClause,
cstate);
checkWellFormedRecursionWalker((Node *) stmt->limitOffset,
cstate);
checkWellFormedRecursionWalker((Node *) stmt->limitCount,
cstate);
checkWellFormedRecursionWalker((Node *) stmt->lockingClause,
cstate);
/* stmt->withClause is intentionally ignored here */
break;
case SETOP_EXCEPT:
if (stmt->all)
cstate->context = RECURSION_EXCEPT;
checkWellFormedRecursionWalker((Node *) stmt->larg,
cstate);
cstate->context = RECURSION_EXCEPT;
checkWellFormedRecursionWalker((Node *) stmt->rarg,
cstate);
cstate->context = save_context;
checkWellFormedRecursionWalker((Node *) stmt->sortClause,
cstate);
checkWellFormedRecursionWalker((Node *) stmt->limitOffset,
cstate);
checkWellFormedRecursionWalker((Node *) stmt->limitCount,
cstate);
checkWellFormedRecursionWalker((Node *) stmt->lockingClause,
cstate);
/* stmt->withClause is intentionally ignored here */
break;
default:
elog(ERROR, "unrecognized set op: %d",
(int) stmt->op);
}
}
}