mirror of
https://github.com/postgres/postgres.git
synced 2025-11-09 06:21:09 +03:00
Implement sharable row-level locks, and use them for foreign key references
to eliminate unnecessary deadlocks. This commit adds SELECT ... FOR SHARE paralleling SELECT ... FOR UPDATE. The implementation uses a new SLRU data structure (managed much like pg_subtrans) to represent multiple- transaction-ID sets. When more than one transaction is holding a shared lock on a particular row, we create a MultiXactId representing that set of transactions and store its ID in the row's XMAX. This scheme allows an effectively unlimited number of row locks, just as we did before, while not costing any extra overhead except when a shared lock actually has to be shared. Still TODO: use the regular lock manager to control the grant order when multiple backends are waiting for a row lock. Alvaro Herrera and Tom Lane.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
|
||||
* Portions Copyright (c) 1994, Regents of the University of California
|
||||
*
|
||||
* $PostgreSQL: pgsql/src/backend/parser/analyze.c,v 1.320 2005/04/14 20:03:24 tgl Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/parser/analyze.c,v 1.321 2005/04/28 21:47:14 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@@ -134,7 +134,7 @@ static void transformFKConstraints(ParseState *pstate,
|
||||
bool isAddConstraint);
|
||||
static void applyColumnNames(List *dst, List *src);
|
||||
static List *getSetColTypes(ParseState *pstate, Node *node);
|
||||
static void transformForUpdate(Query *qry, List *forUpdate);
|
||||
static void transformLocking(Query *qry, List *lockedRels, bool forUpdate);
|
||||
static void transformConstraintAttrs(List *constraintList);
|
||||
static void transformColumnType(ParseState *pstate, ColumnDef *column);
|
||||
static void release_pstate_resources(ParseState *pstate);
|
||||
@@ -1810,8 +1810,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
|
||||
|
||||
qry->commandType = CMD_SELECT;
|
||||
|
||||
/* make FOR UPDATE clause available to addRangeTableEntry */
|
||||
pstate->p_forUpdate = stmt->forUpdate;
|
||||
/* make FOR UPDATE/FOR SHARE list available to addRangeTableEntry */
|
||||
pstate->p_lockedRels = stmt->lockedRels;
|
||||
|
||||
/* process the FROM clause */
|
||||
transformFromClause(pstate, stmt->fromClause);
|
||||
@@ -1870,8 +1870,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
|
||||
if (pstate->p_hasAggs || qry->groupClause || qry->havingQual)
|
||||
parseCheckAggregates(pstate, qry);
|
||||
|
||||
if (stmt->forUpdate != NIL)
|
||||
transformForUpdate(qry, stmt->forUpdate);
|
||||
if (stmt->lockedRels != NIL)
|
||||
transformLocking(qry, stmt->lockedRels, stmt->forUpdate);
|
||||
|
||||
return qry;
|
||||
}
|
||||
@@ -1899,7 +1899,8 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
|
||||
List *sortClause;
|
||||
Node *limitOffset;
|
||||
Node *limitCount;
|
||||
List *forUpdate;
|
||||
List *lockedRels;
|
||||
bool forUpdate;
|
||||
Node *node;
|
||||
ListCell *left_tlist,
|
||||
*dtlist;
|
||||
@@ -1937,18 +1938,19 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
|
||||
sortClause = stmt->sortClause;
|
||||
limitOffset = stmt->limitOffset;
|
||||
limitCount = stmt->limitCount;
|
||||
lockedRels = stmt->lockedRels;
|
||||
forUpdate = stmt->forUpdate;
|
||||
|
||||
stmt->sortClause = NIL;
|
||||
stmt->limitOffset = NULL;
|
||||
stmt->limitCount = NULL;
|
||||
stmt->forUpdate = NIL;
|
||||
stmt->lockedRels = NIL;
|
||||
|
||||
/* We don't support forUpdate with set ops at the moment. */
|
||||
if (forUpdate)
|
||||
/* We don't support FOR UPDATE/SHARE with set ops at the moment. */
|
||||
if (lockedRels)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE is not allowed with UNION/INTERSECT/EXCEPT")));
|
||||
errmsg("SELECT FOR UPDATE/SHARE is not allowed with UNION/INTERSECT/EXCEPT")));
|
||||
|
||||
/*
|
||||
* Recursively transform the components of the tree.
|
||||
@@ -2083,8 +2085,8 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
|
||||
if (pstate->p_hasAggs || qry->groupClause || qry->havingQual)
|
||||
parseCheckAggregates(pstate, qry);
|
||||
|
||||
if (forUpdate != NIL)
|
||||
transformForUpdate(qry, forUpdate);
|
||||
if (lockedRels != NIL)
|
||||
transformLocking(qry, lockedRels, forUpdate);
|
||||
|
||||
return qry;
|
||||
}
|
||||
@@ -2107,11 +2109,11 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("INTO is only allowed on first SELECT of UNION/INTERSECT/EXCEPT")));
|
||||
/* We don't support forUpdate with set ops at the moment. */
|
||||
if (stmt->forUpdate)
|
||||
/* We don't support FOR UPDATE/SHARE with set ops at the moment. */
|
||||
if (stmt->lockedRels)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE is not allowed with UNION/INTERSECT/EXCEPT")));
|
||||
errmsg("SELECT FOR UPDATE/SHARE is not allowed with UNION/INTERSECT/EXCEPT")));
|
||||
|
||||
/*
|
||||
* If an internal node of a set-op tree has ORDER BY, UPDATE, or LIMIT
|
||||
@@ -2128,7 +2130,7 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt)
|
||||
{
|
||||
Assert(stmt->larg != NULL && stmt->rarg != NULL);
|
||||
if (stmt->sortClause || stmt->limitOffset || stmt->limitCount ||
|
||||
stmt->forUpdate)
|
||||
stmt->lockedRels)
|
||||
isLeaf = true;
|
||||
else
|
||||
isLeaf = false;
|
||||
@@ -2711,47 +2713,67 @@ transformExecuteStmt(ParseState *pstate, ExecuteStmt *stmt)
|
||||
|
||||
/* exported so planner can check again after rewriting, query pullup, etc */
|
||||
void
|
||||
CheckSelectForUpdate(Query *qry)
|
||||
CheckSelectLocking(Query *qry, bool forUpdate)
|
||||
{
|
||||
const char *operation;
|
||||
|
||||
if (forUpdate)
|
||||
operation = "SELECT FOR UPDATE";
|
||||
else
|
||||
operation = "SELECT FOR SHARE";
|
||||
|
||||
if (qry->setOperations)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE is not allowed with UNION/INTERSECT/EXCEPT")));
|
||||
/* translator: %s is a SQL command, like SELECT FOR UPDATE */
|
||||
errmsg("%s is not allowed with UNION/INTERSECT/EXCEPT", operation)));
|
||||
if (qry->distinctClause != NIL)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE is not allowed with DISTINCT clause")));
|
||||
/* translator: %s is a SQL command, like SELECT FOR UPDATE */
|
||||
errmsg("%s is not allowed with DISTINCT clause", operation)));
|
||||
if (qry->groupClause != NIL)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE is not allowed with GROUP BY clause")));
|
||||
/* translator: %s is a SQL command, like SELECT FOR UPDATE */
|
||||
errmsg("%s is not allowed with GROUP BY clause", operation)));
|
||||
if (qry->havingQual != NULL)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE is not allowed with HAVING clause")));
|
||||
/* translator: %s is a SQL command, like SELECT FOR UPDATE */
|
||||
errmsg("%s is not allowed with HAVING clause", operation)));
|
||||
if (qry->hasAggs)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE is not allowed with aggregate functions")));
|
||||
/* translator: %s is a SQL command, like SELECT FOR UPDATE */
|
||||
errmsg("%s is not allowed with aggregate functions", operation)));
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert FOR UPDATE name list into rowMarks list of integer relids
|
||||
* Convert FOR UPDATE/SHARE name list into rowMarks list of integer relids
|
||||
*
|
||||
* NB: if you need to change this, see also markQueryForUpdate()
|
||||
* NB: if you need to change this, see also markQueryForLocking()
|
||||
* in rewriteHandler.c.
|
||||
*/
|
||||
static void
|
||||
transformForUpdate(Query *qry, List *forUpdate)
|
||||
transformLocking(Query *qry, List *lockedRels, bool forUpdate)
|
||||
{
|
||||
List *rowMarks = qry->rowMarks;
|
||||
List *rowMarks;
|
||||
ListCell *l;
|
||||
ListCell *rt;
|
||||
Index i;
|
||||
|
||||
CheckSelectForUpdate(qry);
|
||||
if (qry->rowMarks && forUpdate != qry->forUpdate)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("cannot use both FOR UPDATE and FOR SHARE in one query")));
|
||||
qry->forUpdate = forUpdate;
|
||||
|
||||
if (linitial(forUpdate) == NULL)
|
||||
CheckSelectLocking(qry, forUpdate);
|
||||
|
||||
rowMarks = qry->rowMarks;
|
||||
|
||||
if (linitial(lockedRels) == NULL)
|
||||
{
|
||||
/* all regular tables used in query */
|
||||
i = 0;
|
||||
@@ -2770,10 +2792,11 @@ transformForUpdate(Query *qry, List *forUpdate)
|
||||
case RTE_SUBQUERY:
|
||||
|
||||
/*
|
||||
* FOR UPDATE of subquery is propagated to subquery's
|
||||
* rels
|
||||
* FOR UPDATE/SHARE of subquery is propagated to all
|
||||
* of subquery's rels
|
||||
*/
|
||||
transformForUpdate(rte->subquery, list_make1(NULL));
|
||||
transformLocking(rte->subquery, list_make1(NULL),
|
||||
forUpdate);
|
||||
break;
|
||||
default:
|
||||
/* ignore JOIN, SPECIAL, FUNCTION RTEs */
|
||||
@@ -2784,7 +2807,7 @@ transformForUpdate(Query *qry, List *forUpdate)
|
||||
else
|
||||
{
|
||||
/* just the named tables */
|
||||
foreach(l, forUpdate)
|
||||
foreach(l, lockedRels)
|
||||
{
|
||||
char *relname = strVal(lfirst(l));
|
||||
|
||||
@@ -2806,25 +2829,26 @@ transformForUpdate(Query *qry, List *forUpdate)
|
||||
case RTE_SUBQUERY:
|
||||
|
||||
/*
|
||||
* FOR UPDATE of subquery is propagated to
|
||||
* subquery's rels
|
||||
* FOR UPDATE/SHARE of subquery is propagated to
|
||||
* all of subquery's rels
|
||||
*/
|
||||
transformForUpdate(rte->subquery, list_make1(NULL));
|
||||
transformLocking(rte->subquery, list_make1(NULL),
|
||||
forUpdate);
|
||||
break;
|
||||
case RTE_JOIN:
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE cannot be applied to a join")));
|
||||
errmsg("SELECT FOR UPDATE/SHARE cannot be applied to a join")));
|
||||
break;
|
||||
case RTE_SPECIAL:
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE cannot be applied to NEW or OLD")));
|
||||
errmsg("SELECT FOR UPDATE/SHARE cannot be applied to NEW or OLD")));
|
||||
break;
|
||||
case RTE_FUNCTION:
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SELECT FOR UPDATE cannot be applied to a function")));
|
||||
errmsg("SELECT FOR UPDATE/SHARE cannot be applied to a function")));
|
||||
break;
|
||||
default:
|
||||
elog(ERROR, "unrecognized RTE type: %d",
|
||||
@@ -2837,7 +2861,7 @@ transformForUpdate(Query *qry, List *forUpdate)
|
||||
if (rt == NULL)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_UNDEFINED_TABLE),
|
||||
errmsg("relation \"%s\" in FOR UPDATE clause not found in FROM clause",
|
||||
errmsg("relation \"%s\" in FOR UPDATE/SHARE clause not found in FROM clause",
|
||||
relname)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.488 2005/04/23 17:22:16 tgl Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.489 2005/04/28 21:47:14 tgl Exp $
|
||||
*
|
||||
* HISTORY
|
||||
* AUTHOR DATE MAJOR EVENT
|
||||
@@ -87,7 +87,7 @@ static List *check_func_name(List *names);
|
||||
static List *extractArgTypes(List *parameters);
|
||||
static SelectStmt *findLeftmostSelect(SelectStmt *node);
|
||||
static void insertSelectOptions(SelectStmt *stmt,
|
||||
List *sortClause, List *forUpdate,
|
||||
List *sortClause, List *lockingClause,
|
||||
Node *limitOffset, Node *limitCount);
|
||||
static Node *makeSetOp(SetOperation op, bool all, Node *larg, Node *rarg);
|
||||
static Node *doNegate(Node *n);
|
||||
@@ -242,7 +242,8 @@ static void doNegateFloat(Value *v);
|
||||
%type <oncommit> OnCommitOption
|
||||
%type <withoids> OptWithOids WithOidsAs
|
||||
|
||||
%type <list> for_update_clause opt_for_update_clause update_list
|
||||
%type <list> for_locking_clause opt_for_locking_clause
|
||||
update_list
|
||||
%type <boolean> opt_all
|
||||
|
||||
%type <node> join_outer join_qual
|
||||
@@ -4886,9 +4887,9 @@ select_with_parens:
|
||||
;
|
||||
|
||||
/*
|
||||
* FOR UPDATE may be before or after LIMIT/OFFSET.
|
||||
* FOR UPDATE/SHARE may be before or after LIMIT/OFFSET.
|
||||
* In <=7.2.X, LIMIT/OFFSET had to be after FOR UPDATE
|
||||
* We now support both orderings, but prefer LIMIT/OFFSET before FOR UPDATE
|
||||
* We now support both orderings, but prefer LIMIT/OFFSET before FOR UPDATE/SHARE
|
||||
* 2002-08-28 bjm
|
||||
*/
|
||||
select_no_parens:
|
||||
@@ -4899,13 +4900,13 @@ select_no_parens:
|
||||
NULL, NULL);
|
||||
$$ = $1;
|
||||
}
|
||||
| select_clause opt_sort_clause for_update_clause opt_select_limit
|
||||
| select_clause opt_sort_clause for_locking_clause opt_select_limit
|
||||
{
|
||||
insertSelectOptions((SelectStmt *) $1, $2, $3,
|
||||
list_nth($4, 0), list_nth($4, 1));
|
||||
$$ = $1;
|
||||
}
|
||||
| select_clause opt_sort_clause select_limit opt_for_update_clause
|
||||
| select_clause opt_sort_clause select_limit opt_for_locking_clause
|
||||
{
|
||||
insertSelectOptions((SelectStmt *) $1, $2, $4,
|
||||
list_nth($3, 0), list_nth($3, 1));
|
||||
@@ -5146,13 +5147,14 @@ having_clause:
|
||||
| /*EMPTY*/ { $$ = NULL; }
|
||||
;
|
||||
|
||||
for_update_clause:
|
||||
FOR UPDATE update_list { $$ = $3; }
|
||||
for_locking_clause:
|
||||
FOR UPDATE update_list { $$ = lcons(makeString("for_update"), $3); }
|
||||
| FOR SHARE update_list { $$ = lcons(makeString("for_share"), $3); }
|
||||
| FOR READ ONLY { $$ = NULL; }
|
||||
;
|
||||
|
||||
opt_for_update_clause:
|
||||
for_update_clause { $$ = $1; }
|
||||
opt_for_locking_clause:
|
||||
for_locking_clause { $$ = $1; }
|
||||
| /* EMPTY */ { $$ = NULL; }
|
||||
;
|
||||
|
||||
@@ -8379,7 +8381,7 @@ findLeftmostSelect(SelectStmt *node)
|
||||
*/
|
||||
static void
|
||||
insertSelectOptions(SelectStmt *stmt,
|
||||
List *sortClause, List *forUpdate,
|
||||
List *sortClause, List *lockingClause,
|
||||
Node *limitOffset, Node *limitCount)
|
||||
{
|
||||
/*
|
||||
@@ -8394,13 +8396,27 @@ insertSelectOptions(SelectStmt *stmt,
|
||||
errmsg("multiple ORDER BY clauses not allowed")));
|
||||
stmt->sortClause = sortClause;
|
||||
}
|
||||
if (forUpdate)
|
||||
if (lockingClause)
|
||||
{
|
||||
if (stmt->forUpdate)
|
||||
Value *type;
|
||||
|
||||
if (stmt->lockedRels)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("multiple FOR UPDATE clauses not allowed")));
|
||||
stmt->forUpdate = forUpdate;
|
||||
errmsg("multiple FOR UPDATE/FOR SHARE clauses not allowed")));
|
||||
|
||||
Assert(list_length(lockingClause) > 1);
|
||||
/* 1st is Value node containing "for_update" or "for_share" */
|
||||
type = (Value *) linitial(lockingClause);
|
||||
Assert(IsA(type, String));
|
||||
if (strcmp(strVal(type), "for_update") == 0)
|
||||
stmt->forUpdate = true;
|
||||
else if (strcmp(strVal(type), "for_share") == 0)
|
||||
stmt->forUpdate = false;
|
||||
else
|
||||
elog(ERROR, "invalid first node in locking clause");
|
||||
|
||||
stmt->lockedRels = list_delete_first(lockingClause);
|
||||
}
|
||||
if (limitOffset)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/parser/parse_relation.c,v 1.106 2005/04/13 16:50:55 tgl Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/parser/parse_relation.c,v 1.107 2005/04/28 21:47:14 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@@ -40,7 +40,7 @@ static Node *scanNameSpaceForRelid(ParseState *pstate, Node *nsnode,
|
||||
Oid relid);
|
||||
static void scanNameSpaceForConflict(ParseState *pstate, Node *nsnode,
|
||||
RangeTblEntry *rte1, const char *aliasname1);
|
||||
static bool isForUpdate(ParseState *pstate, char *refname);
|
||||
static bool isLockedRel(ParseState *pstate, char *refname);
|
||||
static void expandRelation(Oid relid, Alias *eref,
|
||||
int rtindex, int sublevels_up,
|
||||
bool include_dropped,
|
||||
@@ -759,9 +759,9 @@ addRangeTableEntry(ParseState *pstate,
|
||||
* Get the rel's OID. This access also ensures that we have an
|
||||
* up-to-date relcache entry for the rel. Since this is typically the
|
||||
* first access to a rel in a statement, be careful to get the right
|
||||
* access level depending on whether we're doing SELECT FOR UPDATE.
|
||||
* access level depending on whether we're doing SELECT FOR UPDATE/SHARE.
|
||||
*/
|
||||
lockmode = isForUpdate(pstate, refname) ? RowShareLock : AccessShareLock;
|
||||
lockmode = isLockedRel(pstate, refname) ? RowShareLock : AccessShareLock;
|
||||
rel = heap_openrv(relation, lockmode);
|
||||
rte->relid = RelationGetRelid(rel);
|
||||
|
||||
@@ -1121,17 +1121,17 @@ addRangeTableEntryForJoin(ParseState *pstate,
|
||||
}
|
||||
|
||||
/*
|
||||
* Has the specified refname been selected FOR UPDATE?
|
||||
* Has the specified refname been selected FOR UPDATE/FOR SHARE?
|
||||
*/
|
||||
static bool
|
||||
isForUpdate(ParseState *pstate, char *refname)
|
||||
isLockedRel(ParseState *pstate, char *refname)
|
||||
{
|
||||
/* Outer loop to check parent query levels as well as this one */
|
||||
while (pstate != NULL)
|
||||
{
|
||||
if (pstate->p_forUpdate != NIL)
|
||||
if (pstate->p_lockedRels != NIL)
|
||||
{
|
||||
if (linitial(pstate->p_forUpdate) == NULL)
|
||||
if (linitial(pstate->p_lockedRels) == NULL)
|
||||
{
|
||||
/* all tables used in query */
|
||||
return true;
|
||||
@@ -1141,7 +1141,7 @@ isForUpdate(ParseState *pstate, char *refname)
|
||||
/* just the named tables */
|
||||
ListCell *l;
|
||||
|
||||
foreach(l, pstate->p_forUpdate)
|
||||
foreach(l, pstate->p_lockedRels)
|
||||
{
|
||||
char *rname = strVal(lfirst(l));
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/parser/parse_type.c,v 1.73 2004/12/31 22:00:27 pgsql Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/parser/parse_type.c,v 1.74 2005/04/28 21:47:14 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@@ -432,7 +432,7 @@ parseTypeString(const char *str, Oid *type_id, int32 *typmod)
|
||||
stmt->sortClause != NIL ||
|
||||
stmt->limitOffset != NULL ||
|
||||
stmt->limitCount != NULL ||
|
||||
stmt->forUpdate != NIL ||
|
||||
stmt->lockedRels != NIL ||
|
||||
stmt->op != SETOP_NONE)
|
||||
goto fail;
|
||||
if (list_length(stmt->targetList) != 1)
|
||||
|
||||
Reference in New Issue
Block a user