diff --git a/doc/src/sgml/ref/declare.sgml b/doc/src/sgml/ref/declare.sgml index 4afe2d03b2..f823cf77bb 100644 --- a/doc/src/sgml/ref/declare.sgml +++ b/doc/src/sgml/ref/declare.sgml @@ -1,5 +1,5 @@ @@ -27,7 +27,6 @@ PostgreSQL documentation DECLARE name [ BINARY ] [ INSENSITIVE ] [ [ NO ] SCROLL ] CURSOR [ { WITH | WITHOUT } HOLD ] FOR query - [ FOR { READ ONLY | UPDATE [ OF column [, ...] ] } ] @@ -37,50 +36,10 @@ DECLARE name [ BINARY ] [ INSENSITI DECLARE allows a user to create cursors, which can be used to retrieve - a small number of rows at a time out of a larger query. Cursors can - return data either in text or in binary format using + a small number of rows at a time out of a larger query. + After the cursor is created, rows are fetched from it using . - - - Normal cursors return data in text format, the same as a - SELECT would produce. Since data is stored natively in - binary format, the system must do a conversion to produce the text - format. Once the information comes back in text form, the client - application might need to convert it to a binary format to manipulate - it. In addition, data in the text format is often larger in size - than in the binary format. Binary cursors return the data in a - binary representation that might be more easily manipulated. - Nevertheless, if you intend to display the data as text anyway, - retrieving it in text form will - save you some effort on the client side. - - - - As an example, if a query returns a value of one from an integer column, - you would get a string of 1 with a default cursor - whereas with a binary cursor you would get - a 4-byte field containing the internal representation of the value - (in big-endian byte order). - - - - Binary cursors should be used carefully. Many applications, - including psql, are not prepared to - handle binary cursors and expect data to come back in the text - format. - - - - - When the client application uses the extended query protocol - to issue a FETCH command, the Bind protocol message - specifies whether data is to be retrieved in text or binary format. - This choice overrides the way that the cursor is defined. The concept - of a binary cursor as such is thus obsolete when using extended query - protocol — any cursor can be treated as either text or binary. - - @@ -110,10 +69,10 @@ DECLARE name [ BINARY ] [ INSENSITI Indicates that data retrieved from the cursor should be - unaffected by updates to the tables underlying the cursor while - the cursor exists. In PostgreSQL, - all cursors are insensitive; this key word currently has no - effect and is present for compatibility with the SQL standard. + unaffected by updates to the table(s) underlying the cursor that occur + after the cursor is created. In PostgreSQL, + this is the default behavior; so this key word has no + effect and is only accepted for compatibility with the SQL standard. @@ -163,34 +122,6 @@ DECLARE name [ BINARY ] [ INSENSITI - - - FOR READ ONLY - FOR UPDATE - - - FOR READ ONLY indicates that the cursor will - be used in a read-only mode. FOR UPDATE - indicates that the cursor will be used to update tables. Since - cursor updates are not currently supported in - PostgreSQL, specifying FOR - UPDATE will cause an error message and specifying - FOR READ ONLY has no effect. - - - - - - column - - - Column(s) to be updated by the cursor. Since cursor updates are - not currently supported in - PostgreSQL, the FOR - UPDATE clause provokes an error message. - - - @@ -203,6 +134,38 @@ DECLARE name [ BINARY ] [ INSENSITI Notes + + Normal cursors return data in text format, the same as a + SELECT would produce. The BINARY option + specifies that the cursor should return data in binary format. + This reduces conversion effort for both the server and client, + at the cost of more programmer effort to deal with platform-dependent + binary data formats. + As an example, if a query returns a value of one from an integer column, + you would get a string of 1 with a default cursor, + whereas with a binary cursor you would get + a 4-byte field containing the internal representation of the value + (in big-endian byte order). + + + + Binary cursors should be used carefully. Many applications, + including psql, are not prepared to + handle binary cursors and expect data to come back in the text + format. + + + + + When the client application uses the extended query protocol + to issue a FETCH command, the Bind protocol message + specifies whether data is to be retrieved in text or binary format. + This choice overrides the way that the cursor is defined. The concept + of a binary cursor as such is thus obsolete when using extended query + protocol — any cursor can be treated as either text or binary. + + + Unless WITH HOLD is specified, the cursor created by this command can only be used within the current @@ -232,6 +195,11 @@ DECLARE name [ BINARY ] [ INSENSITI transactions. + + WITH HOLD may not be specified when the query + includes FOR UPDATE or FOR SHARE. + + The SCROLL option should be specified when defining a cursor that will be used to fetch backwards. This is required by @@ -245,6 +213,23 @@ DECLARE name [ BINARY ] [ INSENSITI specified, then backward fetches are disallowed in any case. + + If the cursor's query includes FOR UPDATE or FOR + SHARE, then returned rows are locked at the time they are first + fetched, in the same way as for a regular + command with + these options. + In addition, the returned rows will be the most up-to-date versions; + therefore these options provide the equivalent of what the SQL standard + calls a sensitive cursor. It is often wise to use FOR + UPDATE if the cursor is intended to be used with UPDATE + ... WHERE CURRENT OF or DELETE ... WHERE CURRENT OF, + since this will prevent other sessions from changing the rows between + the time they are fetched and the time they are updated. Without + FOR UPDATE, a subsequent WHERE CURRENT OF command + will have no effect if the row was changed meanwhile. + + The SQL standard only makes provisions for cursors in embedded SQL. The PostgreSQL @@ -280,14 +265,16 @@ DECLARE liahona CURSOR FOR SELECT * FROM films; Compatibility - The SQL standard allows cursors only in embedded - SQL and in modules. PostgreSQL - permits cursors to be used interactively. + The SQL standard specifies that by default, cursors are sensitive to + concurrent updates of the underlying data. In + PostgreSQL, cursors are insensitive by default, + and can be made sensitive by specifying FOR UPDATE. - The SQL standard allows cursors to update table data. All - PostgreSQL cursors are read only. + The SQL standard allows cursors only in embedded + SQL and in modules. PostgreSQL + permits cursors to be used interactively. diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml index f57c56ecf7..b5c7d97f52 100644 --- a/doc/src/sgml/ref/delete.sgml +++ b/doc/src/sgml/ref/delete.sgml @@ -1,5 +1,5 @@ @@ -22,7 +22,7 @@ PostgreSQL documentation DELETE FROM [ ONLY ] table [ [ AS ] alias ] [ USING usinglist ] - [ WHERE condition ] + [ WHERE condition | WHERE CURRENT OF cursor_name ] [ RETURNING * | output_expression [ AS output_name ] [, ...] ] @@ -134,9 +134,23 @@ DELETE FROM [ ONLY ] table [ [ AS ] condition - An expression returning a value of type - boolean, which determines the rows that are to be - deleted. + An expression that returns a value of type boolean. + Only rows for which this expression returns true + will be deleted. + + + + + + cursor_name + + + The name of the cursor to use in a WHERE CURRENT OF + condition. The row to be deleted is the one most recently fetched + from this cursor. The cursor must be a simple (non-join, non-aggregate) + query on the DELETE's target table. + Note that WHERE CURRENT OF cannot be + specified together with a boolean condition. @@ -236,6 +250,14 @@ DELETE FROM films; Delete completed tasks, returning full details of the deleted rows: DELETE FROM tasks WHERE status = 'DONE' RETURNING *; + + + + + Delete the row of tasks on which the cursor + c_tasks is currently positioned: + +DELETE FROM tasks WHERE CURRENT OF c_tasks; diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml index df0bec9a1d..2c6a480c4a 100644 --- a/doc/src/sgml/ref/update.sgml +++ b/doc/src/sgml/ref/update.sgml @@ -1,5 +1,5 @@ @@ -24,7 +24,7 @@ UPDATE [ ONLY ] table [ [ AS ] column = { expression | DEFAULT } | ( column [, ...] ) = ( { expression | DEFAULT } [, ...] ) } [, ...] [ FROM fromlist ] - [ WHERE condition ] + [ WHERE condition | WHERE CURRENT OF cursor_name ] [ RETURNING * | output_expression [ AS output_name ] [, ...] ] @@ -160,6 +160,20 @@ UPDATE [ ONLY ] table [ [ AS ] + + cursor_name + + + The name of the cursor to use in a WHERE CURRENT OF + condition. The row to be updated is the one most recently fetched + from this cursor. The cursor must be a simple (non-join, non-aggregate) + query on the UPDATE's target table. + Note that WHERE CURRENT OF cannot be + specified together with a boolean condition. + + + + output_expression @@ -309,6 +323,15 @@ UPDATE wines SET stock = stock + 24 WHERE winename = 'Chateau Lafite 2003'; COMMIT; + + + Change the kind column of the table + films in the row on which the cursor + c_films is currently positioned: + +UPDATE films SET kind = 'Dramatic' WHERE CURRENT OF c_films; + + diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 55256d9e46..cb4ab9dc31 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -4,7 +4,7 @@ # Makefile for executor # # IDENTIFICATION -# $PostgreSQL: pgsql/src/backend/executor/Makefile,v 1.25 2007/01/20 17:16:11 petere Exp $ +# $PostgreSQL: pgsql/src/backend/executor/Makefile,v 1.26 2007/06/11 01:16:22 tgl Exp $ # #------------------------------------------------------------------------- @@ -12,7 +12,7 @@ subdir = src/backend/executor top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -OBJS = execAmi.o execGrouping.o execJunk.o execMain.o \ +OBJS = execAmi.o execCurrent.o execGrouping.o execJunk.o execMain.o \ execProcnode.o execQual.o execScan.o execTuples.o \ execUtils.o functions.o instrument.o nodeAppend.o nodeAgg.o \ nodeBitmapAnd.o nodeBitmapOr.o \ diff --git a/src/backend/executor/execCurrent.c b/src/backend/executor/execCurrent.c new file mode 100644 index 0000000000..ce95d58b81 --- /dev/null +++ b/src/backend/executor/execCurrent.c @@ -0,0 +1,185 @@ +/*------------------------------------------------------------------------- + * + * execCurrent.c + * executor support for WHERE CURRENT OF cursor + * + * Portions Copyright (c) 1996-2007, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * $PostgreSQL: pgsql/src/backend/executor/execCurrent.c,v 1.1 2007/06/11 01:16:22 tgl Exp $ + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "executor/executor.h" +#include "utils/lsyscache.h" +#include "utils/portal.h" + + +static ScanState *search_plan_tree(PlanState *node, Oid table_oid); + + +/* + * execCurrentOf + * + * Given the name of a cursor and the OID of a table, determine which row + * of the table is currently being scanned by the cursor, and return its + * TID into *current_tid. + * + * Returns TRUE if a row was identified. Returns FALSE if the cursor is valid + * for the table but is not currently scanning a row of the table (this is a + * legal situation in inheritance cases). Raises error if cursor is not a + * valid updatable scan of the specified table. + */ +bool +execCurrentOf(char *cursor_name, Oid table_oid, + ItemPointer current_tid) +{ + char *table_name; + Portal portal; + QueryDesc *queryDesc; + ScanState *scanstate; + HeapTuple tup; + + /* Fetch table name for possible use in error messages */ + table_name = get_rel_name(table_oid); + if (table_name == NULL) + elog(ERROR, "cache lookup failed for relation %u", table_oid); + + /* Find the cursor's portal */ + portal = GetPortalByName(cursor_name); + if (!PortalIsValid(portal)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_CURSOR), + errmsg("cursor \"%s\" does not exist", cursor_name))); + + /* + * We have to watch out for non-SELECT queries as well as held cursors, + * both of which may have null queryDesc. + */ + if (portal->strategy != PORTAL_ONE_SELECT) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_STATE), + errmsg("cursor \"%s\" is not a SELECT query", + cursor_name))); + queryDesc = PortalGetQueryDesc(portal); + if (queryDesc == NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_STATE), + errmsg("cursor \"%s\" is held from a previous transaction", + cursor_name))); + + /* + * Dig through the cursor's plan to find the scan node. Fail if it's + * not there or buried underneath aggregation. + */ + scanstate = search_plan_tree(ExecGetActivePlanTree(queryDesc), + table_oid); + if (!scanstate) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_STATE), + errmsg("cursor \"%s\" is not a simply updatable scan of table \"%s\"", + cursor_name, table_name))); + + /* + * The cursor must have a current result row: per the SQL spec, it's + * an error if not. We test this at the top level, rather than at + * the scan node level, because in inheritance cases any one table + * scan could easily not be on a row. We want to return false, not + * raise error, if the passed-in table OID is for one of the inactive + * scans. + */ + if (portal->atStart || portal->atEnd) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_STATE), + errmsg("cursor \"%s\" is not positioned on a row", + cursor_name))); + + /* Now OK to return false if we found an inactive scan */ + if (TupIsNull(scanstate->ss_ScanTupleSlot)) + return false; + + tup = scanstate->ss_ScanTupleSlot->tts_tuple; + if (tup == NULL) + elog(ERROR, "CURRENT OF applied to non-materialized tuple"); + Assert(tup->t_tableOid == table_oid); + + *current_tid = tup->t_self; + + return true; +} + +/* + * search_plan_tree + * + * Search through a PlanState tree for a scan node on the specified table. + * Return NULL if not found or multiple candidates. + */ +static ScanState * +search_plan_tree(PlanState *node, Oid table_oid) +{ + if (node == NULL) + return NULL; + switch (nodeTag(node)) + { + /* + * scan nodes can all be treated alike + */ + case T_SeqScanState: + case T_IndexScanState: + case T_BitmapHeapScanState: + case T_TidScanState: + { + ScanState *sstate = (ScanState *) node; + + if (RelationGetRelid(sstate->ss_currentRelation) == table_oid) + return sstate; + break; + } + + /* + * For Append, we must look through the members; watch out for + * multiple matches (possible if it was from UNION ALL) + */ + case T_AppendState: + { + AppendState *astate = (AppendState *) node; + ScanState *result = NULL; + int i; + + for (i = 0; i < astate->as_nplans; i++) + { + ScanState *elem = search_plan_tree(astate->appendplans[i], + table_oid); + + if (!elem) + continue; + if (result) + return NULL; /* multiple matches */ + result = elem; + } + return result; + } + + /* + * Result and Limit can be descended through (these are safe + * because they always return their input's current row) + */ + case T_ResultState: + case T_LimitState: + return search_plan_tree(node->lefttree, table_oid); + + /* + * SubqueryScan too, but it keeps the child in a different place + */ + case T_SubqueryScanState: + return search_plan_tree(((SubqueryScanState *) node)->subplan, + table_oid); + + default: + /* Otherwise, assume we can't descend through it */ + break; + } + return NULL; +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 82b17b19f0..dfd1a84ab7 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -26,7 +26,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/execMain.c,v 1.294 2007/06/03 17:07:00 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/executor/execMain.c,v 1.295 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -2368,6 +2368,24 @@ EvalPlanQualStop(evalPlanQual *epq) epq->planstate = NULL; } +/* + * ExecGetActivePlanTree --- get the active PlanState tree from a QueryDesc + * + * Ordinarily this is just the one mentioned in the QueryDesc, but if we + * are looking at a row returned by the EvalPlanQual machinery, we need + * to look at the subsidiary state instead. + */ +PlanState * +ExecGetActivePlanTree(QueryDesc *queryDesc) +{ + EState *estate = queryDesc->estate; + + if (estate && estate->es_useEvalPlan && estate->es_evalPlanQual != NULL) + return estate->es_evalPlanQual->planstate; + else + return queryDesc->planstate; +} + /* * Support for SELECT INTO (a/k/a CREATE TABLE AS) diff --git a/src/backend/executor/execQual.c b/src/backend/executor/execQual.c index fd54b89f0a..5549142e70 100644 --- a/src/backend/executor/execQual.c +++ b/src/backend/executor/execQual.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/execQual.c,v 1.218 2007/06/05 21:31:04 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/executor/execQual.c,v 1.219 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -151,6 +151,8 @@ static Datum ExecEvalCoerceViaIO(CoerceViaIOState *iostate, static Datum ExecEvalArrayCoerceExpr(ArrayCoerceExprState *astate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); +static Datum ExecEvalCurrentOfExpr(ExprState *exprstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone); /* ---------------------------------------------------------------- @@ -3618,6 +3620,41 @@ ExecEvalArrayCoerceExpr(ArrayCoerceExprState *astate, astate->amstate); } +/* ---------------------------------------------------------------- + * ExecEvalCurrentOfExpr + * + * Normally, the planner will convert CURRENT OF into a TidScan qualification, + * but we have plain execQual support in case it doesn't. + * ---------------------------------------------------------------- + */ +static Datum +ExecEvalCurrentOfExpr(ExprState *exprstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) +{ + CurrentOfExpr *cexpr = (CurrentOfExpr *) exprstate->expr; + bool result; + HeapTuple tup; + ItemPointerData cursor_tid; + + if (isDone) + *isDone = ExprSingleResult; + *isNull = false; + + Assert(cexpr->cvarno != INNER); + Assert(cexpr->cvarno != OUTER); + Assert(!TupIsNull(econtext->ecxt_scantuple)); + tup = econtext->ecxt_scantuple->tts_tuple; + if (tup == NULL) + elog(ERROR, "CURRENT OF applied to non-materialized tuple"); + + if (execCurrentOf(cexpr->cursor_name, tup->t_tableOid, &cursor_tid)) + result = ItemPointerEquals(&cursor_tid, &(tup->t_self)); + else + result = false; + + return BoolGetDatum(result); +} + /* * ExecEvalExprSwitchContext @@ -4266,6 +4303,10 @@ ExecInitExpr(Expr *node, PlanState *parent) state = (ExprState *) cstate; } break; + case T_CurrentOfExpr: + state = (ExprState *) makeNode(ExprState); + state->evalfunc = ExecEvalCurrentOfExpr; + break; case T_TargetEntry: { TargetEntry *tle = (TargetEntry *) node; diff --git a/src/backend/executor/nodeTidscan.c b/src/backend/executor/nodeTidscan.c index 9b6724537e..986ff1f3f1 100644 --- a/src/backend/executor/nodeTidscan.c +++ b/src/backend/executor/nodeTidscan.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/nodeTidscan.c,v 1.53 2007/01/05 22:19:28 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/executor/nodeTidscan.c,v 1.54 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -61,8 +61,8 @@ TidListCreate(TidScanState *tidstate) /* * We initialize the array with enough slots for the case that all quals - * are simple OpExprs. If there's any ScalarArrayOpExprs, we may have to - * enlarge the array. + * are simple OpExprs or CurrentOfExprs. If there are any + * ScalarArrayOpExprs, we may have to enlarge the array. */ numAllocTids = list_length(evalList); tidList = (ItemPointerData *) @@ -148,6 +148,25 @@ TidListCreate(TidScanState *tidstate) pfree(ipdatums); pfree(ipnulls); } + else if (expr && IsA(expr, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) expr; + ItemPointerData cursor_tid; + + if (execCurrentOf(cexpr->cursor_name, + RelationGetRelid(tidstate->ss.ss_currentRelation), + &cursor_tid)) + { + if (numTids >= numAllocTids) + { + numAllocTids *= 2; + tidList = (ItemPointerData *) + repalloc(tidList, + numAllocTids * sizeof(ItemPointerData)); + } + tidList[numTids++] = cursor_tid; + } + } else elog(ERROR, "could not identify CTID expression"); } diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 39027b1dbc..c868ff574d 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -15,7 +15,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.377 2007/06/05 21:31:04 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.378 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1299,6 +1299,20 @@ _copySetToDefault(SetToDefault *from) return newnode; } +/* + * _copyCurrentOfExpr + */ +static CurrentOfExpr * +_copyCurrentOfExpr(CurrentOfExpr *from) +{ + CurrentOfExpr *newnode = makeNode(CurrentOfExpr); + + COPY_SCALAR_FIELD(cvarno); + COPY_STRING_FIELD(cursor_name); + + return newnode; +} + /* * _copyTargetEntry */ @@ -3177,6 +3191,9 @@ copyObject(void *from) case T_SetToDefault: retval = _copySetToDefault(from); break; + case T_CurrentOfExpr: + retval = _copyCurrentOfExpr(from); + break; case T_TargetEntry: retval = _copyTargetEntry(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 8a0957c117..04072c7a65 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -18,7 +18,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.308 2007/06/05 21:31:04 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.309 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -598,6 +598,15 @@ _equalSetToDefault(SetToDefault *a, SetToDefault *b) return true; } +static bool +_equalCurrentOfExpr(CurrentOfExpr *a, CurrentOfExpr *b) +{ + COMPARE_SCALAR_FIELD(cvarno); + COMPARE_STRING_FIELD(cursor_name); + + return true; +} + static bool _equalTargetEntry(TargetEntry *a, TargetEntry *b) { @@ -2124,6 +2133,9 @@ equal(void *a, void *b) case T_SetToDefault: retval = _equalSetToDefault(a, b); break; + case T_CurrentOfExpr: + retval = _equalCurrentOfExpr(a, b); + break; case T_TargetEntry: retval = _equalTargetEntry(a, b); break; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 5de540642f..869905f0cc 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/outfuncs.c,v 1.309 2007/06/05 21:31:04 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/outfuncs.c,v 1.310 2007/06/11 01:16:22 tgl Exp $ * * NOTES * Every node type that can appear in stored rules' parsetrees *must* @@ -1058,6 +1058,15 @@ _outSetToDefault(StringInfo str, SetToDefault *node) WRITE_INT_FIELD(typeMod); } +static void +_outCurrentOfExpr(StringInfo str, CurrentOfExpr *node) +{ + WRITE_NODE_TYPE("CURRENTOFEXPR"); + + WRITE_UINT_FIELD(cvarno); + WRITE_STRING_FIELD(cursor_name); +} + static void _outTargetEntry(StringInfo str, TargetEntry *node) { @@ -2229,6 +2238,9 @@ _outNode(StringInfo str, void *obj) case T_SetToDefault: _outSetToDefault(str, obj); break; + case T_CurrentOfExpr: + _outCurrentOfExpr(str, obj); + break; case T_TargetEntry: _outTargetEntry(str, obj); break; diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 86c9e911a7..e91a6e5b50 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/readfuncs.c,v 1.207 2007/06/05 21:31:04 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/readfuncs.c,v 1.208 2007/06/11 01:16:22 tgl Exp $ * * NOTES * Path and Plan nodes do not have any readfuncs support, because we @@ -873,6 +873,20 @@ _readSetToDefault(void) READ_DONE(); } +/* + * _readCurrentOfExpr + */ +static CurrentOfExpr * +_readCurrentOfExpr(void) +{ + READ_LOCALS(CurrentOfExpr); + + READ_UINT_FIELD(cvarno); + READ_STRING_FIELD(cursor_name); + + READ_DONE(); +} + /* * _readTargetEntry */ @@ -1093,6 +1107,8 @@ parseNodeString(void) return_value = _readCoerceToDomainValue(); else if (MATCH("SETTODEFAULT", 12)) return_value = _readSetToDefault(); + else if (MATCH("CURRENTOFEXPR", 13)) + return_value = _readCurrentOfExpr(); else if (MATCH("TARGETENTRY", 11)) return_value = _readTargetEntry(); else if (MATCH("RANGETBLREF", 11)) diff --git a/src/backend/optimizer/path/clausesel.c b/src/backend/optimizer/path/clausesel.c index b8bbc29c50..4b48ae1e26 100644 --- a/src/backend/optimizer/path/clausesel.c +++ b/src/backend/optimizer/path/clausesel.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/path/clausesel.c,v 1.85 2007/04/21 21:01:44 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/path/clausesel.c,v 1.86 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -18,6 +18,7 @@ #include "nodes/makefuncs.h" #include "optimizer/clauses.h" #include "optimizer/cost.h" +#include "optimizer/pathnode.h" #include "optimizer/plancat.h" #include "parser/parsetree.h" #include "utils/fmgroids.h" @@ -712,6 +713,15 @@ clause_selectivity(PlannerInfo *root, varRelid, jointype); } + else if (IsA(clause, CurrentOfExpr)) + { + /* CURRENT OF selects at most one row of its table */ + CurrentOfExpr *cexpr = (CurrentOfExpr *) clause; + RelOptInfo *crel = find_base_rel(root, cexpr->cvarno); + + if (crel->tuples > 0) + s1 = 1.0 / crel->tuples; + } else if (IsA(clause, RelabelType)) { /* Not sure this case is needed, but it can't hurt */ diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index a4d03e9f8f..f76c778998 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -54,7 +54,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/path/costsize.c,v 1.184 2007/06/05 21:31:05 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/path/costsize.c,v 1.185 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -770,6 +770,7 @@ cost_tidscan(Path *path, PlannerInfo *root, Cost startup_cost = 0; Cost run_cost = 0; Cost cpu_per_tuple; + QualCost tid_qual_cost; int ntuples; ListCell *l; @@ -799,12 +800,20 @@ cost_tidscan(Path *path, PlannerInfo *root, } } + /* + * The TID qual expressions will be computed once, any other baserestrict + * quals once per retrived tuple. + */ + cost_qual_eval(&tid_qual_cost, tidquals, root); + /* disk costs --- assume each tuple on a different page */ run_cost += random_page_cost * ntuples; /* CPU costs */ - startup_cost += baserel->baserestrictcost.startup; - cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost.per_tuple; + startup_cost += baserel->baserestrictcost.startup + + tid_qual_cost.per_tuple; + cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost.per_tuple - + tid_qual_cost.per_tuple; run_cost += cpu_per_tuple * ntuples; path->startup_cost = startup_cost; @@ -1991,6 +2000,11 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) cpu_operator_cost; } } + else if (IsA(node, CurrentOfExpr)) + { + /* This is noticeably more expensive than a typical operator */ + context->total.per_tuple += 100 * cpu_operator_cost; + } else if (IsA(node, SubLink)) { /* This routine should not be applied to un-planned expressions */ diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c index 2470493708..84564dde73 100644 --- a/src/backend/optimizer/path/tidpath.c +++ b/src/backend/optimizer/path/tidpath.c @@ -12,6 +12,12 @@ * this allows * WHERE ctid IN (tid1, tid2, ...) * + * We also support "WHERE CURRENT OF cursor" conditions (CurrentOfExpr), + * which amount to "CTID = run-time-determined-TID". These could in + * theory be translated to a simple comparison of CTID to the result of + * a function, but in practice it works better to keep the special node + * representation all the way through to execution. + * * There is currently no special support for joins involving CTID; in * particular nothing corresponding to best_inner_indexscan(). Since it's * not very useful to store TIDs of one table in another table, there @@ -24,7 +30,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/path/tidpath.c,v 1.29 2007/01/05 22:19:31 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/path/tidpath.c,v 1.30 2007/06/11 01:16:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -174,6 +180,12 @@ TidQualFromExpr(Node *expr, int varno) if (IsTidEqualAnyClause((ScalarArrayOpExpr *) expr, varno)) rlst = list_make1(expr); } + else if (expr && IsA(expr, CurrentOfExpr)) + { + /* another base case: check for CURRENT OF on this rel */ + if (((CurrentOfExpr *) expr)->cvarno == varno) + rlst = list_make1(expr); + } else if (and_clause(expr)) { foreach(l, ((BoolExpr *) expr)->args) diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 90a49983ac..055b47beec 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -9,7 +9,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/plan/setrefs.c,v 1.135 2007/04/30 00:16:43 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/plan/setrefs.c,v 1.136 2007/06/11 01:16:23 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -618,6 +618,15 @@ fix_scan_expr_mutator(Node *node, fix_scan_expr_context *context) var->varnoold += context->rtoffset; return (Node *) var; } + if (IsA(node, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node); + + Assert(cexpr->cvarno != INNER); + Assert(cexpr->cvarno != OUTER); + cexpr->cvarno += context->rtoffset; + return (Node *) cexpr; + } /* * Since we update opcode info in-place, this part could possibly * scribble on the planner's input data structures, but it's OK. diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index 2b273f738a..5e80dc1559 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -22,7 +22,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/prep/prepunion.c,v 1.141 2007/04/21 05:56:41 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/prep/prepunion.c,v 1.142 2007/06/11 01:16:23 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1132,6 +1132,14 @@ adjust_appendrel_attrs_mutator(Node *node, AppendRelInfo *context) } return (Node *) var; } + if (IsA(node, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node); + + if (cexpr->cvarno == context->parent_relid) + cexpr->cvarno = context->child_relid; + return (Node *) cexpr; + } if (IsA(node, RangeTblRef)) { RangeTblRef *rtr = (RangeTblRef *) copyObject(node); diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 00f5f6e2c8..2cf0ffd28b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/util/clauses.c,v 1.245 2007/06/05 21:31:05 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/util/clauses.c,v 1.246 2007/06/11 01:16:23 tgl Exp $ * * HISTORY * AUTHOR DATE MAJOR EVENT @@ -3407,6 +3407,7 @@ expression_tree_walker(Node *node, case T_CoerceToDomainValue: case T_CaseTestExpr: case T_SetToDefault: + case T_CurrentOfExpr: case T_RangeTblRef: case T_OuterJoinInfo: /* primitive node types with no expression subnodes */ @@ -3873,6 +3874,7 @@ expression_tree_mutator(Node *node, case T_CoerceToDomainValue: case T_CaseTestExpr: case T_SetToDefault: + case T_CurrentOfExpr: case T_RangeTblRef: case T_OuterJoinInfo: return (Node *) copyObject(node); diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c index 13702b7d46..c501c82792 100644 --- a/src/backend/optimizer/util/var.c +++ b/src/backend/optimizer/util/var.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/util/var.c,v 1.69 2007/01/05 22:19:33 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/util/var.c,v 1.70 2007/06/11 01:16:23 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -111,6 +111,14 @@ pull_varnos_walker(Node *node, pull_varnos_context *context) context->varnos = bms_add_member(context->varnos, var->varno); return false; } + if (IsA(node, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) node; + + if (context->sublevels_up == 0) + context->varnos = bms_add_member(context->varnos, cexpr->cvarno); + return false; + } if (IsA(node, Query)) { /* Recurse into RTE subquery or not-yet-planned sublink subquery */ @@ -217,6 +225,8 @@ contain_var_clause_walker(Node *node, void *context) return true; /* abort the tree traversal and return true */ return false; } + if (IsA(node, CurrentOfExpr)) + return true; return expression_tree_walker(node, contain_var_clause_walker, context); } @@ -249,6 +259,13 @@ contain_vars_of_level_walker(Node *node, int *sublevels_up) { if (((Var *) node)->varlevelsup == *sublevels_up) return true; /* abort tree traversal and return true */ + return false; + } + if (IsA(node, CurrentOfExpr)) + { + if (*sublevels_up == 0) + return true; + return false; } if (IsA(node, Query)) { @@ -376,6 +393,29 @@ find_minimum_var_level_walker(Node *node, } } } + if (IsA(node, CurrentOfExpr)) + { + int varlevelsup = 0; + + /* convert levelsup to frame of reference of original query */ + varlevelsup -= context->sublevels_up; + /* ignore local vars of subqueries */ + if (varlevelsup >= 0) + { + if (context->min_varlevel < 0 || + context->min_varlevel > varlevelsup) + { + context->min_varlevel = varlevelsup; + + /* + * As soon as we find a local variable, we can abort the tree + * traversal, since min_varlevel is then certainly 0. + */ + if (varlevelsup == 0) + return true; + } + } + } /* * An Aggref must be treated like a Var of its level. Normally we'd get diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 68475387be..3ac6f000d8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -20,7 +20,7 @@ * Portions Copyright (c) 1996-2007, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/backend/parser/analyze.c,v 1.363 2007/04/27 22:05:48 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/analyze.c,v 1.364 2007/06/11 01:16:24 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -3178,12 +3178,12 @@ transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt) (errcode(ERRCODE_INVALID_CURSOR_DEFINITION), errmsg("DECLARE CURSOR cannot specify INTO"))); - /* Implementation restriction (might go away someday) */ - if (result->rowMarks != NIL) + /* FOR UPDATE and WITH HOLD are not compatible */ + if (result->rowMarks != NIL && (stmt->options & CURSOR_OPT_HOLD)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DECLARE CURSOR ... FOR UPDATE/SHARE is not supported"), - errdetail("Cursors must be READ ONLY."))); + errmsg("DECLARE CURSOR WITH HOLD ... FOR UPDATE/SHARE is not supported"), + errdetail("Holdable cursors must be READ ONLY."))); /* We won't need the raw querytree any more */ stmt->query = NULL; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8884da2289..b50be6bd73 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -11,7 +11,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.591 2007/04/27 22:05:48 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.592 2007/06/11 01:16:25 tgl Exp $ * * HISTORY * AUTHOR DATE MAJOR EVENT @@ -296,7 +296,7 @@ static Node *makeXmlExpr(XmlExprOp op, char *name, List *named_args, List *args) %type TableElement ConstraintElem TableFuncElement %type columnDef %type def_elem old_aggr_elem -%type def_arg columnElem where_clause +%type def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr func_expr AexprConst indirection_el columnref in_expr having_clause func_table array_expr %type row type_list array_expr_list @@ -377,8 +377,8 @@ static Node *makeXmlExpr(XmlExprOp op, char *name, List *named_args, List *args) CLUSTER COALESCE COLLATE COLUMN COMMENT COMMIT COMMITTED CONCURRENTLY CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONVERSION_P CONVERT COPY COST CREATE CREATEDB - CREATEROLE CREATEUSER CROSS CSV CURRENT_DATE CURRENT_ROLE CURRENT_TIME - CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE + CREATEROLE CREATEUSER CROSS CSV CURRENT_P CURRENT_DATE CURRENT_ROLE + CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS @@ -5715,7 +5715,7 @@ returning_clause: *****************************************************************************/ DeleteStmt: DELETE_P FROM relation_expr_opt_alias - using_clause where_clause returning_clause + using_clause where_or_current_clause returning_clause { DeleteStmt *n = makeNode(DeleteStmt); n->relation = $3; @@ -5771,7 +5771,7 @@ opt_nowait: NOWAIT { $$ = TRUE; } UpdateStmt: UPDATE relation_expr_opt_alias SET set_clause_list from_clause - where_clause + where_or_current_clause returning_clause { UpdateStmt *n = makeNode(UpdateStmt); @@ -6562,6 +6562,18 @@ where_clause: | /*EMPTY*/ { $$ = NULL; } ; +/* variant for UPDATE and DELETE */ +where_or_current_clause: + WHERE a_expr { $$ = $2; } + | WHERE CURRENT_P OF name + { + CurrentOfExpr *n = makeNode(CurrentOfExpr); + n->cursor_name = $4; + $$ = (Node *) n; + } + | /*EMPTY*/ { $$ = NULL; } + ; + TableFuncElementList: TableFuncElement @@ -8818,6 +8830,7 @@ unreserved_keyword: | CREATEROLE | CREATEUSER | CSV + | CURRENT_P | CURSOR | CYCLE | DATABASE diff --git a/src/backend/parser/keywords.c b/src/backend/parser/keywords.c index 5c8ef10a21..b48a0c7958 100644 --- a/src/backend/parser/keywords.c +++ b/src/backend/parser/keywords.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/parser/keywords.c,v 1.187 2007/04/26 16:13:12 neilc Exp $ + * $PostgreSQL: pgsql/src/backend/parser/keywords.c,v 1.188 2007/06/11 01:16:25 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -101,6 +101,7 @@ static const ScanKeyword ScanKeywords[] = { {"createuser", CREATEUSER}, {"cross", CROSS}, {"csv", CSV}, + {"current", CURRENT_P}, {"current_date", CURRENT_DATE}, {"current_role", CURRENT_ROLE}, {"current_time", CURRENT_TIME}, diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 45107e43ac..6601bfe40e 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/parser/parse_expr.c,v 1.218 2007/06/05 21:31:05 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/parse_expr.c,v 1.219 2007/06/11 01:16:25 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -243,6 +243,21 @@ transformExpr(ParseState *pstate, Node *expr) result = transformBooleanTest(pstate, (BooleanTest *) expr); break; + case T_CurrentOfExpr: + { + CurrentOfExpr *c = (CurrentOfExpr *) expr; + int sublevels_up; + + /* CURRENT OF can only appear at top level of UPDATE/DELETE */ + Assert(pstate->p_target_rangetblentry != NULL); + c->cvarno = RTERangeTablePosn(pstate, + pstate->p_target_rangetblentry, + &sublevels_up); + Assert(sublevels_up == 0); + result = expr; + break; + } + /********************************************* * Quietly accept node types that may be presented when we are * called on an already-transformed tree. @@ -1863,6 +1878,9 @@ exprType(Node *expr) case T_SetToDefault: type = ((SetToDefault *) expr)->typeId; break; + case T_CurrentOfExpr: + type = BOOLOID; + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); type = InvalidOid; /* keep compiler quiet */ diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c index 8ea9ac103c..19b5663890 100644 --- a/src/backend/rewrite/rewriteManip.c +++ b/src/backend/rewrite/rewriteManip.c @@ -7,7 +7,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/rewrite/rewriteManip.c,v 1.103 2007/01/05 22:19:36 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/rewrite/rewriteManip.c,v 1.104 2007/06/11 01:16:25 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -151,6 +151,14 @@ OffsetVarNodes_walker(Node *node, OffsetVarNodes_context *context) } return false; } + if (IsA(node, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) node; + + if (context->sublevels_up == 0) + cexpr->cvarno += context->offset; + return false; + } if (IsA(node, RangeTblRef)) { RangeTblRef *rtr = (RangeTblRef *) node; @@ -302,6 +310,15 @@ ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context) } return false; } + if (IsA(node, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) node; + + if (context->sublevels_up == 0 && + cexpr->cvarno == context->rt_index) + cexpr->cvarno = context->new_index; + return false; + } if (IsA(node, RangeTblRef)) { RangeTblRef *rtr = (RangeTblRef *) node; @@ -466,6 +483,13 @@ IncrementVarSublevelsUp_walker(Node *node, var->varlevelsup += context->delta_sublevels_up; return false; /* done here */ } + if (IsA(node, CurrentOfExpr)) + { + /* this should not happen */ + if (context->min_sublevels_up == 0) + elog(ERROR, "cannot push down CurrentOfExpr"); + return false; + } if (IsA(node, Aggref)) { Aggref *agg = (Aggref *) node; @@ -536,6 +560,15 @@ rangeTableEntry_used_walker(Node *node, return true; return false; } + if (IsA(node, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) node; + + if (context->sublevels_up == 0 && + cexpr->cvarno == context->rt_index) + return true; + return false; + } if (IsA(node, RangeTblRef)) { RangeTblRef *rtr = (RangeTblRef *) node; @@ -932,8 +965,27 @@ ResolveNew_mutator(Node *node, ResolveNew_context *context) } /* otherwise fall through to copy the var normally */ } + else if (IsA(node, CurrentOfExpr)) + { + CurrentOfExpr *cexpr = (CurrentOfExpr *) node; + int this_varno = (int) cexpr->cvarno; - if (IsA(node, Query)) + if (this_varno == context->target_varno && + context->sublevels_up == 0) + { + /* + * We get here if a WHERE CURRENT OF expression turns out to + * apply to a view. Someday we might be able to translate + * the expression to apply to an underlying table of the view, + * but right now it's not implemented. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WHERE CURRENT OF on a view is not implemented"))); + } + /* otherwise fall through to copy the expr normally */ + } + else if (IsA(node, Query)) { /* Recurse into RTE subquery or not-yet-planned sublink subquery */ Query *newnode; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 78be35ef36..c6f6b88248 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -9,7 +9,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.259 2007/06/05 21:31:06 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.260 2007/06/11 01:16:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -3086,6 +3086,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_Param: case T_CoerceToDomainValue: case T_SetToDefault: + case T_CurrentOfExpr: /* single words: always simple */ return true; @@ -4134,6 +4135,11 @@ get_rule_expr(Node *node, deparse_context *context, appendStringInfo(buf, "DEFAULT"); break; + case T_CurrentOfExpr: + appendStringInfo(buf, "CURRENT OF %s", + quote_identifier(((CurrentOfExpr *) node)->cursor_name)); + break; + case T_List: { char *sep; diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index f9d8d107b3..408519c1e3 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2007, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/executor/executor.h,v 1.139 2007/02/27 01:11:25 tgl Exp $ + * $PostgreSQL: pgsql/src/include/executor/executor.h,v 1.140 2007/06/11 01:16:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -70,6 +70,12 @@ extern bool ExecSupportsMarkRestore(NodeTag plantype); extern bool ExecSupportsBackwardScan(Plan *node); extern bool ExecMayReturnRawTuples(PlanState *node); +/* + * prototypes from functions in execCurrent.c + */ +extern bool execCurrentOf(char *cursor_name, Oid table_oid, + ItemPointer current_tid); + /* * prototypes from functions in execGrouping.c */ @@ -135,6 +141,7 @@ extern void ExecConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate); extern TupleTableSlot *EvalPlanQual(EState *estate, Index rti, ItemPointer tid, TransactionId priorXmax, CommandId curCid); +extern PlanState *ExecGetActivePlanTree(QueryDesc *queryDesc); extern DestReceiver *CreateIntoRelDestReceiver(void); /* diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 31186930d2..c54a1a4522 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2007, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/nodes/nodes.h,v 1.200 2007/06/05 21:31:08 tgl Exp $ + * $PostgreSQL: pgsql/src/include/nodes/nodes.h,v 1.201 2007/06/11 01:16:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -139,6 +139,7 @@ typedef enum NodeTag T_CoerceToDomain, T_CoerceToDomainValue, T_SetToDefault, + T_CurrentOfExpr, T_TargetEntry, T_RangeTblRef, T_JoinExpr, diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index a567a8e26d..9a3e09b77e 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -10,7 +10,7 @@ * Portions Copyright (c) 1996-2007, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/nodes/primnodes.h,v 1.130 2007/06/05 21:31:08 tgl Exp $ + * $PostgreSQL: pgsql/src/include/nodes/primnodes.h,v 1.131 2007/06/11 01:16:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -915,6 +915,21 @@ typedef struct SetToDefault int32 typeMod; /* typemod for substituted value */ } SetToDefault; +/* + * Node representing [WHERE] CURRENT OF cursor_name + * + * CURRENT OF is a bit like a Var, in that it carries the rangetable index + * of the target relation being constrained; this aids placing the expression + * correctly during planning. We can assume however that its "levelsup" is + * always zero, due to the syntactic constraints on where it can appear. + */ +typedef struct CurrentOfExpr +{ + Expr xpr; + Index cvarno; /* RT index of target relation */ + char *cursor_name; /* name of referenced cursor */ +} CurrentOfExpr; + /*-------------------- * TargetEntry - * a target entry (used in query target lists) diff --git a/src/test/regress/expected/portals.out b/src/test/regress/expected/portals.out index 9e618dbc5f..3638664b1b 100644 --- a/src/test/regress/expected/portals.out +++ b/src/test/regress/expected/portals.out @@ -899,3 +899,176 @@ SELECT name FROM pg_cursors ORDER BY 1; (0 rows) COMMIT; +-- +-- Tests for updatable cursors +-- +CREATE TEMP TABLE uctest(f1 int, f2 text); +INSERT INTO uctest VALUES (1, 'one'), (2, 'two'), (3, 'three'); +SELECT * FROM uctest; + f1 | f2 +----+------- + 1 | one + 2 | two + 3 | three +(3 rows) + +-- Check DELETE WHERE CURRENT +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest; +FETCH 2 FROM c1; + f1 | f2 +----+----- + 1 | one + 2 | two +(2 rows) + +DELETE FROM uctest WHERE CURRENT OF c1; +-- should show deletion +SELECT * FROM uctest; + f1 | f2 +----+------- + 1 | one + 3 | three +(2 rows) + +-- cursor did not move +FETCH ALL FROM c1; + f1 | f2 +----+------- + 3 | three +(1 row) + +-- cursor is insensitive +MOVE BACKWARD ALL IN c1; +FETCH ALL FROM c1; + f1 | f2 +----+------- + 1 | one + 2 | two + 3 | three +(3 rows) + +COMMIT; +-- should still see deletion +SELECT * FROM uctest; + f1 | f2 +----+------- + 1 | one + 3 | three +(2 rows) + +-- Check UPDATE WHERE CURRENT; this time use FOR UPDATE +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest FOR UPDATE; +FETCH c1; + f1 | f2 +----+----- + 1 | one +(1 row) + +UPDATE uctest SET f1 = 8 WHERE CURRENT OF c1; +SELECT * FROM uctest; + f1 | f2 +----+------- + 3 | three + 8 | one +(2 rows) + +COMMIT; +SELECT * FROM uctest; + f1 | f2 +----+------- + 3 | three + 8 | one +(2 rows) + +-- Check inheritance cases +CREATE TEMP TABLE ucchild () inherits (uctest); +INSERT INTO ucchild values(100, 'hundred'); +SELECT * FROM uctest; + f1 | f2 +-----+--------- + 3 | three + 8 | one + 100 | hundred +(3 rows) + +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest; +FETCH 1 FROM c1; + f1 | f2 +----+------- + 3 | three +(1 row) + +UPDATE uctest SET f1 = f1 + 10 WHERE CURRENT OF c1; +FETCH 1 FROM c1; + f1 | f2 +----+----- + 8 | one +(1 row) + +UPDATE uctest SET f1 = f1 + 10 WHERE CURRENT OF c1; +FETCH 1 FROM c1; + f1 | f2 +-----+--------- + 100 | hundred +(1 row) + +UPDATE uctest SET f1 = f1 + 10 WHERE CURRENT OF c1; +FETCH 1 FROM c1; + f1 | f2 +----+---- +(0 rows) + +COMMIT; +SELECT * FROM uctest; + f1 | f2 +-----+--------- + 13 | three + 18 | one + 110 | hundred +(3 rows) + +-- Check various error cases +DELETE FROM uctest WHERE CURRENT OF c1; -- fail, no such cursor +ERROR: cursor "c1" does not exist +DECLARE cx CURSOR WITH HOLD FOR SELECT * FROM uctest; +DELETE FROM uctest WHERE CURRENT OF cx; -- fail, can't use held cursor +ERROR: cursor "cx" is held from a previous transaction +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM tenk2; +DELETE FROM uctest WHERE CURRENT OF c; -- fail, cursor on wrong table +ERROR: cursor "c" is not a simply updatable scan of table "uctest" +ROLLBACK; +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM tenk1 JOIN tenk2 USING (unique1); +DELETE FROM tenk1 WHERE CURRENT OF c; -- fail, cursor is on a join +ERROR: cursor "c" is not a simply updatable scan of table "tenk1" +ROLLBACK; +BEGIN; +DECLARE c CURSOR FOR SELECT f1,count(*) FROM uctest GROUP BY f1; +DELETE FROM uctest WHERE CURRENT OF c; -- fail, cursor is on aggregation +ERROR: cursor "c" is not a simply updatable scan of table "uctest" +ROLLBACK; +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest; +DELETE FROM uctest WHERE CURRENT OF c1; -- fail, no current row +ERROR: cursor "c1" is not positioned on a row +ROLLBACK; +-- WHERE CURRENT OF may someday work with views, but today is not that day. +-- For now, just make sure it errors out cleanly. +CREATE TEMP VIEW ucview AS SELECT * FROM uctest; +CREATE RULE ucrule AS ON DELETE TO ucview DO INSTEAD + DELETE FROM uctest WHERE f1 = OLD.f1; +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM ucview; +FETCH FROM c1; + f1 | f2 +----+------- + 13 | three +(1 row) + +DELETE FROM ucview WHERE CURRENT OF c1; -- fail, views not supported +ERROR: WHERE CURRENT OF on a view is not implemented +ROLLBACK; diff --git a/src/test/regress/sql/portals.sql b/src/test/regress/sql/portals.sql index 18f803b439..382a28c4e3 100644 --- a/src/test/regress/sql/portals.sql +++ b/src/test/regress/sql/portals.sql @@ -316,5 +316,85 @@ CLOSE ALL; SELECT name FROM pg_cursors ORDER BY 1; COMMIT; +-- +-- Tests for updatable cursors +-- +CREATE TEMP TABLE uctest(f1 int, f2 text); +INSERT INTO uctest VALUES (1, 'one'), (2, 'two'), (3, 'three'); +SELECT * FROM uctest; +-- Check DELETE WHERE CURRENT +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest; +FETCH 2 FROM c1; +DELETE FROM uctest WHERE CURRENT OF c1; +-- should show deletion +SELECT * FROM uctest; +-- cursor did not move +FETCH ALL FROM c1; +-- cursor is insensitive +MOVE BACKWARD ALL IN c1; +FETCH ALL FROM c1; +COMMIT; +-- should still see deletion +SELECT * FROM uctest; + +-- Check UPDATE WHERE CURRENT; this time use FOR UPDATE +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest FOR UPDATE; +FETCH c1; +UPDATE uctest SET f1 = 8 WHERE CURRENT OF c1; +SELECT * FROM uctest; +COMMIT; +SELECT * FROM uctest; + +-- Check inheritance cases +CREATE TEMP TABLE ucchild () inherits (uctest); +INSERT INTO ucchild values(100, 'hundred'); +SELECT * FROM uctest; + +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest; +FETCH 1 FROM c1; +UPDATE uctest SET f1 = f1 + 10 WHERE CURRENT OF c1; +FETCH 1 FROM c1; +UPDATE uctest SET f1 = f1 + 10 WHERE CURRENT OF c1; +FETCH 1 FROM c1; +UPDATE uctest SET f1 = f1 + 10 WHERE CURRENT OF c1; +FETCH 1 FROM c1; +COMMIT; +SELECT * FROM uctest; + +-- Check various error cases + +DELETE FROM uctest WHERE CURRENT OF c1; -- fail, no such cursor +DECLARE cx CURSOR WITH HOLD FOR SELECT * FROM uctest; +DELETE FROM uctest WHERE CURRENT OF cx; -- fail, can't use held cursor +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM tenk2; +DELETE FROM uctest WHERE CURRENT OF c; -- fail, cursor on wrong table +ROLLBACK; +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM tenk1 JOIN tenk2 USING (unique1); +DELETE FROM tenk1 WHERE CURRENT OF c; -- fail, cursor is on a join +ROLLBACK; +BEGIN; +DECLARE c CURSOR FOR SELECT f1,count(*) FROM uctest GROUP BY f1; +DELETE FROM uctest WHERE CURRENT OF c; -- fail, cursor is on aggregation +ROLLBACK; +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM uctest; +DELETE FROM uctest WHERE CURRENT OF c1; -- fail, no current row +ROLLBACK; + +-- WHERE CURRENT OF may someday work with views, but today is not that day. +-- For now, just make sure it errors out cleanly. +CREATE TEMP VIEW ucview AS SELECT * FROM uctest; +CREATE RULE ucrule AS ON DELETE TO ucview DO INSTEAD + DELETE FROM uctest WHERE f1 = OLD.f1; +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM ucview; +FETCH FROM c1; +DELETE FROM ucview WHERE CURRENT OF c1; -- fail, views not supported +ROLLBACK;