Index: doc/src/sgml/ref/create_trigger.sgml
===================================================================
RCS file: /usr/local/cvsroot/pgsql/doc/src/sgml/ref/create_trigger.sgml,v
retrieving revision 1.43
diff -c -p -r1.43 create_trigger.sgml
*** doc/src/sgml/ref/create_trigger.sgml 9 Dec 2005 19:39:41 -0000 1.43
--- doc/src/sgml/ref/create_trigger.sgml 7 Jul 2006 21:58:04 -0000
*************** PostgreSQL documentation
*** 18,27 ****
--- 18,32 ----
CREATE TRIGGER
+
+ WHEN
+
+
CREATE TRIGGER name { BEFORE | AFTER } { event [ OR ... ] }
ON table [ FOR [ EACH ] { ROW | STATEMENT } ]
+ [ WHEN ( expr> ) ]
EXECUTE PROCEDURE funcname ( arguments )
*************** CREATE TRIGGER expr
+
+
+ An SQL expression which returns a boolean result.
+
+
+
+ INSERT triggers may refer only to the
+ NEW table. DELETE triggers
+ may only refer to the OLD table.
+ UPDATE triggers may refer to both. The
+ expression may not refer to any other tables.
+
+
+
+ This feature is not supported on FOR STATEMENT> triggers.
+
+
+
+
+
funcname
Index: src/backend/commands/trigger.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/commands/trigger.c,v
retrieving revision 1.203
diff -c -p -r1.203 trigger.c
*** src/backend/commands/trigger.c 16 Jun 2006 20:23:44 -0000 1.203
--- src/backend/commands/trigger.c 8 Jul 2006 10:12:46 -0000
***************
*** 31,37 ****
--- 31,39 ----
#include "executor/instrument.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
#include "parser/parse_func.h"
+ #include "rewrite/rewriteManip.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
*************** static HeapTuple ExecCallTriggerFunc(Tri
*** 55,60 ****
--- 57,66 ----
MemoryContext per_tuple_context);
static void AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event,
bool row_trigger, HeapTuple oldtup, HeapTuple newtup);
+ static void setup_trigger_quals(ResultRelInfo *ri, EState *estate,
+ bool before, int event);
+ static bool test_trig_qual(EState *estate, Relation rel, HeapTuple oldtuple,
+ HeapTuple newtuple, List *qual, int event);
/*
*************** CreateTrigger(CreateTrigStmt *stmt, bool
*** 182,187 ****
--- 188,203 ----
}
/*
+ * SQL:2003 doesn't specifically prohibit WHEN clauses for statement-level
+ * triggers, but Oracle doesn't allow them, and it's not clear they'd be
+ * useful anyway. Therefore, disallow them for now.
+ */
+ if (!stmt->row && stmt->when != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("WHEN clause cannot be specified for statement-level triggers")));
+
+ /*
* Generate the trigger's OID now, so that we can use it in the name if
* needed.
*/
*************** CreateTrigger(CreateTrigStmt *stmt, bool
*** 315,320 ****
--- 331,338 ----
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgdeferrable - 1] = BoolGetDatum(stmt->deferrable);
values[Anum_pg_trigger_tginitdeferred - 1] = BoolGetDatum(stmt->initdeferred);
+ values[Anum_pg_trigger_tgqual - 1] = DirectFunctionCall1(textin,
+ CStringGetDatum(nodeToString(stmt->when)));
if (stmt->args)
{
*************** CreateTrigger(CreateTrigStmt *stmt, bool
*** 442,447 ****
--- 460,473 ----
}
}
+ if (stmt->when)
+ {
+ ChangeVarNodes(stmt->when, TRIG_OLD_VARNO, 1, 0);
+ ChangeVarNodes(stmt->when, TRIG_NEW_VARNO, 2, 0);
+ recordDependencyOnExpr(&myself, stmt->when, stmt->rtable,
+ DEPENDENCY_NORMAL);
+ }
+
/* Keep lock on target rel until end of xact */
heap_close(rel, NoLock);
*************** RelationBuildTriggers(Relation relation)
*** 875,880 ****
--- 901,909 ----
{
Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(htup);
Trigger *build;
+ Datum tmp;
+ bool isnull;
+ char *when_str;
if (found >= ntrigs)
elog(ERROR, "too many trigger records found for relation \"%s\"",
*************** RelationBuildTriggers(Relation relation)
*** 927,932 ****
--- 956,979 ----
else
build->tgargs = NULL;
+ /* get the trigger's WHEN clause, if any */
+ tmp = heap_getattr(htup, Anum_pg_trigger_tgqual,
+ RelationGetDescr(tgrel), &isnull);
+ Assert(!isnull);
+
+ when_str = DatumGetCString(DirectFunctionCall1(textout,
+ PointerGetDatum(tmp)));
+
+ /*
+ * XXX: we leak the node here because FreeTriggerDesc() has no
+ * ability to do a deep free of a Node.
+ *
+ * Ideally, the Node would be created in its own context which
+ * we could just reset. Since we create the node in the
+ * CacheMemoryContext the effect of this leak will be long lived
+ */
+
+ build->when = (Node *) stringToNode(when_str);
found++;
}
*************** CopyTriggerDesc(TriggerDesc *trigdesc)
*** 1074,1079 ****
--- 1121,1127 ----
newargs[j] = pstrdup(trigger->tgargs[j]);
trigger->tgargs = newargs;
}
+ trigger->when = copyObject(trigdesc->triggers[i].when);
trigger++;
}
*************** FreeTriggerDesc(TriggerDesc *trigdesc)
*** 1175,1180 ****
--- 1223,1229 ----
pfree(trigger->tgargs[trigger->tgnargs]);
pfree(trigger->tgargs);
}
+ /* XXX: leaks trigger->when */
trigger++;
}
pfree(trigdesc->triggers);
*************** equalTriggerDescs(TriggerDesc *trigdesc1
*** 1232,1237 ****
--- 1281,1288 ----
return false;
if (trig1->tgnattr != trig2->tgnattr)
return false;
+ if (!equal(trig1->when, trig2->when))
+ return false;
if (trig1->tgnattr > 0 &&
memcmp(trig1->tgattr, trig2->tgattr,
trig1->tgnattr * sizeof(int2)) != 0)
*************** ExecBRInsertTriggers(EState *estate, Res
*** 1389,1399 ****
--- 1440,1454 ----
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
int ntrigs = trigdesc->n_before_row[TRIGGER_EVENT_INSERT];
int *tgindx = trigdesc->tg_before_row[TRIGGER_EVENT_INSERT];
+ TrigQualState *qual_state;
HeapTuple newtuple = trigtuple;
HeapTuple oldtuple;
TriggerData LocTriggerData;
int i;
+ setup_trigger_quals(relinfo, estate, true, TRIGGER_EVENT_INSERT);
+ qual_state = relinfo->ri_TrigQuals;
+
LocTriggerData.type = T_TriggerData;
LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
TRIGGER_EVENT_ROW |
*************** ExecBRInsertTriggers(EState *estate, Res
*** 1401,1412 ****
--- 1456,1481 ----
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
LocTriggerData.tg_newtuple = NULL;
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+
for (i = 0; i < ntrigs; i++)
{
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
if (!trigger->tgenabled)
continue;
+
+ /* Check the trigger's WHEN clause, if any */
+ if (trigger->when)
+ {
+ bool res;
+
+ res = test_trig_qual(estate, relinfo->ri_RelationDesc,
+ NULL, trigtuple, qual_state->quals[tgindx[i]],
+ TRIGGER_EVENT_INSERT);
+ if (!res)
+ continue;
+ }
+
LocTriggerData.tg_trigtuple = oldtuple = newtuple;
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
LocTriggerData.tg_trigger = trigger;
*************** ExecBRDeleteTriggers(EState *estate, Res
*** 1501,1506 ****
--- 1570,1576 ----
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
int ntrigs = trigdesc->n_before_row[TRIGGER_EVENT_DELETE];
int *tgindx = trigdesc->tg_before_row[TRIGGER_EVENT_DELETE];
+ TrigQualState *qual_state;
bool result = true;
TriggerData LocTriggerData;
HeapTuple trigtuple;
*************** ExecBRDeleteTriggers(EState *estate, Res
*** 1512,1517 ****
--- 1582,1590 ----
if (trigtuple == NULL)
return false;
+ setup_trigger_quals(relinfo, estate, true, TRIGGER_EVENT_DELETE);
+ qual_state = relinfo->ri_TrigQuals;
+
LocTriggerData.type = T_TriggerData;
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
TRIGGER_EVENT_ROW |
*************** ExecBRDeleteTriggers(EState *estate, Res
*** 1525,1530 ****
--- 1598,1615 ----
if (!trigger->tgenabled)
continue;
+
+ /* Check the trigger's WHEN clause, if any */
+ if (trigger->when)
+ {
+ bool res;
+
+ res = test_trig_qual(estate, relinfo->ri_RelationDesc,
+ trigtuple, NULL, qual_state->quals[tgindx[i]],
+ TRIGGER_EVENT_DELETE);
+ if (!res)
+ continue;
+ }
LocTriggerData.tg_trigtuple = trigtuple;
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
LocTriggerData.tg_trigger = trigger;
*************** ExecBRUpdateTriggers(EState *estate, Res
*** 1632,1637 ****
--- 1717,1723 ----
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
int ntrigs = trigdesc->n_before_row[TRIGGER_EVENT_UPDATE];
int *tgindx = trigdesc->tg_before_row[TRIGGER_EVENT_UPDATE];
+ TrigQualState *qual_state;
TriggerData LocTriggerData;
HeapTuple trigtuple;
HeapTuple oldtuple;
*************** ExecBRUpdateTriggers(EState *estate, Res
*** 1643,1648 ****
--- 1729,1737 ----
if (trigtuple == NULL)
return NULL;
+ setup_trigger_quals(relinfo, estate, true, TRIGGER_EVENT_UPDATE);
+ qual_state = relinfo->ri_TrigQuals;
+
/*
* In READ COMMITTED isolation level it's possible that newtuple was
* changed due to concurrent update.
*************** ExecBRUpdateTriggers(EState *estate, Res
*** 1661,1666 ****
--- 1750,1768 ----
if (!trigger->tgenabled)
continue;
+
+ /* Check the trigger's WHEN clause, if any */
+ if (trigger->when)
+ {
+ bool res;
+
+ res = test_trig_qual(estate, relinfo->ri_RelationDesc,
+ trigtuple, newtuple, qual_state->quals[tgindx[i]],
+ TRIGGER_EVENT_UPDATE);
+ if (!res)
+ continue;
+ }
+
LocTriggerData.tg_trigtuple = trigtuple;
LocTriggerData.tg_newtuple = oldtuple = newtuple;
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
*************** AfterTriggerSaveEvent(ResultRelInfo *rel
*** 3262,3264 ****
--- 3364,3457 ----
afterTriggerAddEvent(new_event);
}
}
+
+ /*
+ * There is some inefficiency here. Subsequent calls to setup_trigger_quals()
+ * during the same command will end up iterating through the array of
+ * triggers. Ideally, this redundant work should be avoided.
+ */
+
+ static void
+ setup_trigger_quals(ResultRelInfo *ri, EState *estate, bool before, int event)
+ {
+ TriggerDesc *trigdesc = ri->ri_TrigDesc;
+ MemoryContext old_cxt;
+ TrigQualState *qual_state;
+ int i;
+ int ntrigs;
+ int *tgindx;
+
+ old_cxt = MemoryContextSwitchTo(estate->es_query_cxt);
+ if (ri->ri_TrigQuals == NULL)
+ {
+ ri->ri_TrigQuals = palloc(sizeof(TrigQualState));
+ ri->ri_TrigQuals->quals = palloc0(trigdesc->numtriggers * sizeof(List *));
+ }
+
+ qual_state = ri->ri_TrigQuals;
+
+ if (before)
+ {
+ ntrigs = trigdesc->n_before_row[event];
+ tgindx = trigdesc->tg_before_row[event];
+ }
+ else
+ {
+ ntrigs = trigdesc->n_after_row[event];
+ tgindx = trigdesc->tg_after_row[event];
+ }
+
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger;
+ int trig_idx;
+ List *qual;
+
+ trig_idx = tgindx[i];
+ trigger = &trigdesc->triggers[trig_idx];
+
+ if (!trigger->when)
+ continue;
+
+ if (qual_state->quals[trig_idx])
+ continue;
+
+ qual = make_ands_implicit((Expr *) trigger->when);
+ qual_state->quals[trig_idx] = (List *) ExecPrepareExpr((Expr *) qual,
+ estate);
+ }
+
+ MemoryContextSwitchTo(old_cxt);
+ }
+
+ /*
+ * Test if a trigger qualification evaluates true for the
+ * input tuple(s)
+ */
+ static bool
+ test_trig_qual(EState *estate, Relation rel, HeapTuple oldtuple,
+ HeapTuple newtuple, List *qual, int event)
+ {
+ ExprContext *econtext = GetPerTupleExprContext(estate);
+ TupleDesc tupdesc = RelationGetDescr(rel);
+
+ if (event == TRIGGER_EVENT_INSERT ||
+ event == TRIGGER_EVENT_UPDATE)
+ {
+ if (econtext->ecxt_newtuple == NULL)
+ econtext->ecxt_newtuple = MakeSingleTupleTableSlot(tupdesc);
+ ExecClearTuple(econtext->ecxt_newtuple);
+ ExecStoreTuple(newtuple, econtext->ecxt_newtuple,
+ InvalidBuffer, false);
+ }
+ if (event == TRIGGER_EVENT_UPDATE ||
+ event == TRIGGER_EVENT_DELETE)
+ {
+ if (econtext->ecxt_oldtuple == NULL)
+ econtext->ecxt_oldtuple = MakeSingleTupleTableSlot(tupdesc);
+ ExecClearTuple(econtext->ecxt_oldtuple);
+ ExecStoreTuple(oldtuple, econtext->ecxt_oldtuple, InvalidBuffer, false);
+ }
+
+ return ExecQual(qual, econtext, false);
+ }
Index: src/backend/executor/execMain.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/executor/execMain.c,v
retrieving revision 1.273
diff -c -p -r1.273 execMain.c
*** src/backend/executor/execMain.c 3 Jul 2006 22:45:38 -0000 1.273
--- src/backend/executor/execMain.c 7 Jul 2006 18:55:35 -0000
*************** initResultRelInfo(ResultRelInfo *resultR
*** 914,919 ****
--- 914,920 ----
resultRelInfo->ri_TrigFunctions = NULL;
resultRelInfo->ri_TrigInstrument = NULL;
}
+ resultRelInfo->ri_TrigQuals = NULL;
resultRelInfo->ri_ConstraintExprs = NULL;
resultRelInfo->ri_junkFilter = NULL;
Index: src/backend/executor/execQual.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/executor/execQual.c,v
retrieving revision 1.191
diff -c -p -r1.191 execQual.c
*** src/backend/executor/execQual.c 16 Jun 2006 18:42:21 -0000 1.191
--- src/backend/executor/execQual.c 7 Jul 2006 19:32:34 -0000
*************** ExecEvalVar(ExprState *exprstate, ExprCo
*** 459,464 ****
--- 459,472 ----
Assert(attnum > 0);
break;
+ case TRIG_OLD_VARNO: /* old tuple in trigger context */
+ slot = econtext->ecxt_oldtuple;
+ break;
+
+ case TRIG_NEW_VARNO: /* new tuple in trigger context */
+ slot = econtext->ecxt_newtuple;
+ break;
+
default: /* get the tuple from the relation being
* scanned */
slot = econtext->ecxt_scantuple;
Index: src/backend/executor/execUtils.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/executor/execUtils.c,v
retrieving revision 1.135
diff -c -p -r1.135 execUtils.c
*** src/backend/executor/execUtils.c 16 Jun 2006 18:42:22 -0000 1.135
--- src/backend/executor/execUtils.c 7 Jul 2006 13:04:01 -0000
*************** CreateExprContext(EState *estate)
*** 296,301 ****
--- 296,303 ----
econtext->ecxt_scantuple = NULL;
econtext->ecxt_innertuple = NULL;
econtext->ecxt_outertuple = NULL;
+ econtext->ecxt_oldtuple = NULL;
+ econtext->ecxt_newtuple = NULL;
econtext->ecxt_per_query_memory = estate->es_query_cxt;
*************** FreeExprContext(ExprContext *econtext)
*** 356,361 ****
--- 358,371 ----
/* Call any registered callbacks */
ShutdownExprContext(econtext);
+
+ /* Clean up special slots for NEW and OLD trigger tuples */
+ if (econtext->ecxt_newtuple)
+ ExecDropSingleTupleTableSlot(econtext->ecxt_newtuple);
+
+ if (econtext->ecxt_oldtuple)
+ ExecDropSingleTupleTableSlot(econtext->ecxt_oldtuple);
+
/* And clean up the memory used */
MemoryContextDelete(econtext->ecxt_per_tuple_memory);
/* Unlink self from owning EState */
Index: src/backend/nodes/copyfuncs.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/nodes/copyfuncs.c,v
retrieving revision 1.342
diff -c -p -r1.342 copyfuncs.c
*** src/backend/nodes/copyfuncs.c 3 Jul 2006 22:45:38 -0000 1.342
--- src/backend/nodes/copyfuncs.c 7 Jul 2006 13:04:02 -0000
*************** _copyCreateTrigStmt(CreateTrigStmt *from
*** 2441,2446 ****
--- 2441,2448 ----
COPY_SCALAR_FIELD(deferrable);
COPY_SCALAR_FIELD(initdeferred);
COPY_NODE_FIELD(constrrel);
+ COPY_NODE_FIELD(when);
+ COPY_NODE_FIELD(rtable);
return newnode;
}
Index: src/backend/nodes/equalfuncs.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/nodes/equalfuncs.c,v
retrieving revision 1.276
diff -c -p -r1.276 equalfuncs.c
*** src/backend/nodes/equalfuncs.c 3 Jul 2006 22:45:38 -0000 1.276
--- src/backend/nodes/equalfuncs.c 7 Jul 2006 13:04:03 -0000
*************** _equalCreateTrigStmt(CreateTrigStmt *a,
*** 1306,1311 ****
--- 1306,1313 ----
COMPARE_SCALAR_FIELD(deferrable);
COMPARE_SCALAR_FIELD(initdeferred);
COMPARE_NODE_FIELD(constrrel);
+ COMPARE_NODE_FIELD(when);
+ COMPARE_NODE_FIELD(rtable);
return true;
}
Index: src/backend/parser/analyze.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/parser/analyze.c,v
retrieving revision 1.338
diff -c -p -r1.338 analyze.c
*** src/backend/parser/analyze.c 3 Jul 2006 22:45:39 -0000 1.338
--- src/backend/parser/analyze.c 7 Jul 2006 19:45:06 -0000
*************** static Query *transformInsertStmt(ParseS
*** 106,111 ****
--- 106,112 ----
static Query *transformIndexStmt(ParseState *pstate, IndexStmt *stmt);
static Query *transformRuleStmt(ParseState *query, RuleStmt *stmt,
List **extras_before, List **extras_after);
+ static Query *transformCreateTrigStmt(ParseState *pstate, CreateTrigStmt *stmt);
static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt);
static Query *transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt);
static Node *transformSetOperationTree(ParseState *pstate, SelectStmt *stmt);
*************** transformStmt(ParseState *pstate, Node *
*** 324,329 ****
--- 325,335 ----
extras_before, extras_after);
break;
+ case T_CreateTrigStmt:
+ result = transformCreateTrigStmt(pstate,
+ (CreateTrigStmt *) parseTree);
+ break;
+
case T_ViewStmt:
result = transformViewStmt(pstate, (ViewStmt *) parseTree,
extras_before, extras_after);
*************** transformRuleStmt(ParseState *pstate, Ru
*** 1852,1857 ****
--- 1858,1965 ----
return qry;
}
+ /*
+ * transformCreateTrigStmt -
+ * transform a CREATE TRIGGER statement. Most of the work we do is
+ * transforming the statement's WHEN clause, if any.
+ */
+ static Query *
+ transformCreateTrigStmt(ParseState *pstate, CreateTrigStmt *stmt)
+ {
+ Query *qry;
+ Relation rel;
+ RangeTblEntry *oldrte;
+ RangeTblEntry *newrte;
+ int i;
+
+ qry = makeNode(Query);
+ qry->commandType = CMD_UTILITY;
+ qry->utilityStmt = (Node *) stmt;
+
+ /* If there's no WHEN clause, we're done. */
+ if (!stmt->when)
+ return qry;
+
+ /*
+ * Note that we acquire and keep an exclusive lock on the target table
+ * only if there's a WHEN clause; if there's no WHEN, we acquire the same
+ * lock in CreateTrigger(), so the effect should be the same.
+ */
+ rel = heap_openrv(stmt->relation, AccessExclusiveLock);
+
+ /*
+ * Setup RTEs for the NEW and OLD relations in the main pstate, for use in
+ * parsing the trigger qualification. We initially add "OLD" with RT index
+ * 1, and "NEW" with RT index 2, and then change them to use the correct
+ * varnos below.
+ */
+ Assert(pstate->p_rtable == NIL);
+ oldrte = addRangeTableEntryForRelation(pstate, rel,
+ makeAlias("*OLD*", NIL),
+ false, false);
+ newrte = addRangeTableEntryForRelation(pstate, rel,
+ makeAlias("*NEW*", NIL),
+ false, false);
+
+ for (i = 0; stmt->actions[i] != '\0'; i++)
+ {
+ if (stmt->actions[i] == 'd' || stmt->actions[i] == 'u')
+ {
+ addRTEtoQuery(pstate, oldrte, false, true, true);
+ break;
+ }
+ }
+
+ for (i = 0; stmt->actions[i] != '\0'; i++)
+ {
+ if (stmt->actions[i] == 'i' || stmt->actions[i] == 'u')
+ {
+ addRTEtoQuery(pstate, newrte, false, true, true);
+ break;
+ }
+ }
+
+ /* process WHEN clause as though it was a WHERE clause */
+ stmt->when = transformWhereClause(pstate, stmt->when, "WHEN");
+
+ if (list_length(pstate->p_rtable) != 2)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("trigger WHEN condition may not contain "
+ "references to other relations")));
+
+ stmt->rtable = list_copy(pstate->p_rtable);
+
+ /* aggregates not allowed */
+ if (pstate->p_hasAggs)
+ ereport(ERROR,
+ (errcode(ERRCODE_GROUPING_ERROR),
+ errmsg("trigger WHEN condition may not contain aggregate functions")));
+
+ /* subselects are not allowed either, at least for now */
+ if (pstate->p_hasSubLinks)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("trigger WHEN condition may not contain subqueries")));
+
+ /*
+ * Rewrite the WHEN expression to give the right varno to the NEW and OLD
+ * relations, so that these relations can be treated specially by the
+ * executor.
+ *
+ * We could avoid doing this by having the executor code do it for us when
+ * we initialise the expressions out of pg_trigger. This seems like a
+ * better place to do it, especially if you consider very complex
+ * expressions.
+ */
+ ChangeVarNodes(stmt->when, 1, TRIG_OLD_VARNO, 0);
+ ChangeVarNodes(stmt->when, 2, TRIG_NEW_VARNO, 0);
+
+ /* Close relation, but keep the exclusive lock */
+ heap_close(rel, NoLock);
+
+ return qry;
+ }
/*
* transformSelectStmt -
Index: src/backend/parser/gram.y
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/parser/gram.y,v
retrieving revision 2.551
diff -c -p -r2.551 gram.y
*** src/backend/parser/gram.y 3 Jul 2006 22:45:39 -0000 2.551
--- src/backend/parser/gram.y 7 Jul 2006 20:20:40 -0000
***************
*** 79,85 ****
extern List *parsetree; /* final parse result is delivered here */
! static bool QueryIsRule = FALSE;
/*
* If you need access to certain yacc-generated variables and find that
--- 79,85 ----
extern List *parsetree; /* final parse result is delivered here */
! static bool QueryIsRuleOrTrigger = FALSE;
/*
* If you need access to certain yacc-generated variables and find that
*************** static void doNegateFloat(Value *v);
*** 169,175 ****
UnlistenStmt UpdateStmt VacuumStmt
VariableResetStmt VariableSetStmt VariableShowStmt
ViewStmt CheckPointStmt CreateConversionStmt
! DeallocateStmt PrepareStmt ExecuteStmt
DropOwnedStmt ReassignOwnedStmt
%type select_no_parens select_with_parens select_clause
--- 169,175 ----
UnlistenStmt UpdateStmt VacuumStmt
VariableResetStmt VariableSetStmt VariableShowStmt
ViewStmt CheckPointStmt CreateConversionStmt
! DeallocateStmt PrepareStmt ExecuteStmt TriggerWhen
DropOwnedStmt ReassignOwnedStmt
%type select_no_parens select_with_parens select_clause
*************** DropTableSpaceStmt: DROP TABLESPACE name
*** 2519,2540 ****
*****************************************************************************/
CreateTrigStmt:
! CREATE TRIGGER name TriggerActionTime TriggerEvents ON
! qualified_name TriggerForSpec EXECUTE PROCEDURE
func_name '(' TriggerFuncArgs ')'
{
CreateTrigStmt *n = makeNode(CreateTrigStmt);
n->trigname = $3;
! n->relation = $7;
! n->funcname = $11;
! n->args = $13;
! n->before = $4;
! n->row = $8;
! memcpy(n->actions, $5, 4);
n->isconstraint = FALSE;
n->deferrable = FALSE;
n->initdeferred = FALSE;
n->constrrel = NULL;
$$ = (Node *)n;
}
| CREATE CONSTRAINT TRIGGER name AFTER TriggerEvents ON
--- 2519,2545 ----
*****************************************************************************/
CreateTrigStmt:
! CREATE TRIGGER name
! { QueryIsRuleOrTrigger=TRUE; }
! TriggerActionTime TriggerEvents ON
! qualified_name TriggerForSpec TriggerWhen EXECUTE PROCEDURE
func_name '(' TriggerFuncArgs ')'
{
CreateTrigStmt *n = makeNode(CreateTrigStmt);
n->trigname = $3;
! n->relation = $8;
! n->funcname = $13;
! n->args = $15;
! n->before = $5;
! n->row = $9;
! memcpy(n->actions, $6, 4);
n->isconstraint = FALSE;
n->deferrable = FALSE;
n->initdeferred = FALSE;
n->constrrel = NULL;
+ n->when = $10;
+ n->rtable = NIL;
+ QueryIsRuleOrTrigger = FALSE;
$$ = (Node *)n;
}
| CREATE CONSTRAINT TRIGGER name AFTER TriggerEvents ON
*************** CreateTrigStmt:
*** 2554,2560 ****
n->isconstraint = TRUE;
n->deferrable = ($10 & 1) != 0;
n->initdeferred = ($10 & 2) != 0;
!
n->constrrel = $9;
$$ = (Node *)n;
}
--- 2559,2566 ----
n->isconstraint = TRUE;
n->deferrable = ($10 & 1) != 0;
n->initdeferred = ($10 & 2) != 0;
! n->when = NULL;
! n->rtable = NIL;
n->constrrel = $9;
$$ = (Node *)n;
}
*************** TriggerForType:
*** 2617,2622 ****
--- 2623,2633 ----
| STATEMENT { $$ = FALSE; }
;
+ TriggerWhen:
+ WHEN '(' a_expr ')' { $$ = $3; }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
TriggerFuncArgs:
TriggerFuncArg { $$ = list_make1($1); }
| TriggerFuncArgs ',' TriggerFuncArg { $$ = lappend($1, $3); }
*************** AlterOwnerStmt: ALTER AGGREGATE func_nam
*** 4480,4486 ****
*****************************************************************************/
RuleStmt: CREATE opt_or_replace RULE name AS
! { QueryIsRule=TRUE; }
ON event TO qualified_name where_clause
DO opt_instead RuleActionList
{
--- 4491,4497 ----
*****************************************************************************/
RuleStmt: CREATE opt_or_replace RULE name AS
! { QueryIsRuleOrTrigger=TRUE; }
ON event TO qualified_name where_clause
DO opt_instead RuleActionList
{
*************** RuleStmt: CREATE opt_or_replace RULE nam
*** 4493,4499 ****
n->instead = $13;
n->actions = $14;
$$ = (Node *)n;
! QueryIsRule=FALSE;
}
;
--- 4504,4510 ----
n->instead = $13;
n->actions = $14;
$$ = (Node *)n;
! QueryIsRuleOrTrigger=FALSE;
}
;
*************** reserved_keyword:
*** 8851,8857 ****
SpecialRuleRelation:
OLD
{
! if (QueryIsRule)
$$ = "*OLD*";
else
ereport(ERROR,
--- 8862,8868 ----
SpecialRuleRelation:
OLD
{
! if (QueryIsRuleOrTrigger)
$$ = "*OLD*";
else
ereport(ERROR,
*************** SpecialRuleRelation:
*** 8860,8866 ****
}
| NEW
{
! if (QueryIsRule)
$$ = "*NEW*";
else
ereport(ERROR,
--- 8871,8877 ----
}
| NEW
{
! if (QueryIsRuleOrTrigger)
$$ = "*NEW*";
else
ereport(ERROR,
*************** SystemTypeName(char *name)
*** 9238,9244 ****
void
parser_init(void)
{
! QueryIsRule = FALSE;
}
/* exprIsNullConstant()
--- 9249,9255 ----
void
parser_init(void)
{
! QueryIsRuleOrTrigger = FALSE;
}
/* exprIsNullConstant()
Index: src/backend/utils/adt/ruleutils.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/backend/utils/adt/ruleutils.c,v
retrieving revision 1.227
diff -c -p -r1.227 ruleutils.c
*** src/backend/utils/adt/ruleutils.c 4 Jul 2006 04:35:49 -0000 1.227
--- src/backend/utils/adt/ruleutils.c 7 Jul 2006 13:04:13 -0000
***************
*** 38,43 ****
--- 38,44 ----
#include "parser/parse_oper.h"
#include "parser/parse_type.h"
#include "parser/parsetree.h"
+ #include "parser/parse_relation.h"
#include "rewrite/rewriteHandler.h"
#include "rewrite/rewriteManip.h"
#include "rewrite/rewriteSupport.h"
*************** pg_get_triggerdef(PG_FUNCTION_ARGS)
*** 435,440 ****
--- 436,445 ----
SysScanDesc tgscan;
int findx = 0;
char *tgname;
+ Datum when_text;
+ char *when_str;
+ Node *node;
+ bool isnull;
/*
* Fetch the pg_trigger tuple by the Oid of the trigger
*************** pg_get_triggerdef(PG_FUNCTION_ARGS)
*** 514,519 ****
--- 519,570 ----
else
appendStringInfo(&buf, "FOR EACH STATEMENT ");
+ /* handle WHEN clause */
+ when_text = heap_getattr(ht_trig, Anum_pg_trigger_tgqual,
+ RelationGetDescr(tgrel), &isnull);
+ Assert(!isnull);
+
+ when_str = DatumGetCString(DirectFunctionCall1(textout, when_text));
+ node = (Node *) stringToNode(when_str);
+
+ if (node)
+ {
+ Relation rel;
+ RangeTblEntry *oldrte;
+ RangeTblEntry *newrte;
+ deparse_context context;
+ deparse_namespace dpns;
+
+ rel = heap_open(trigrec->tgrelid, AccessShareLock);
+
+ appendStringInfo(&buf, "WHEN ");
+
+ oldrte = addRangeTableEntryForRelation(NULL, rel,
+ makeAlias("*OLD*", NIL),
+ false, false);
+
+ newrte = addRangeTableEntryForRelation(NULL, rel,
+ makeAlias("*NEW*", NIL),
+ false, false);
+
+ ChangeVarNodes(node, TRIG_OLD_VARNO, 1, 0);
+ ChangeVarNodes(node, TRIG_NEW_VARNO, 2, 0);
+
+ context.buf = &buf;
+ context.namespaces = list_make1(&dpns);
+ context.varprefix = true;
+ context.prettyFlags = 0;
+ context.indentLevel = PRETTYINDENT_STD;
+
+ dpns.rtable = list_make2(oldrte, newrte);
+ dpns.outer_varno = dpns.inner_varno = 0;
+ dpns.outer_rte = dpns.inner_rte = NULL;
+
+ get_rule_expr(node, &context, false);
+ appendStringInfo(&buf, " ");
+ heap_close(rel, AccessShareLock);
+ }
+
appendStringInfo(&buf, "EXECUTE PROCEDURE %s(",
generate_function_name(trigrec->tgfoid, 0, NULL));
Index: src/bin/pg_dump/pg_dump.c
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/bin/pg_dump/pg_dump.c,v
retrieving revision 1.439
diff -c -p -r1.439 pg_dump.c
*** src/bin/pg_dump/pg_dump.c 2 Jul 2006 02:23:21 -0000 1.439
--- src/bin/pg_dump/pg_dump.c 7 Jul 2006 13:04:21 -0000
*************** dumpSequence(Archive *fout, TableInfo *t
*** 8048,8077 ****
}
static void
! dumpTrigger(Archive *fout, TriggerInfo *tginfo)
{
TableInfo *tbinfo = tginfo->tgtable;
- PQExpBuffer query;
- PQExpBuffer delqry;
const char *p;
int findx;
- if (dataOnly)
- return;
-
- query = createPQExpBuffer();
- delqry = createPQExpBuffer();
-
- /*
- * DROP must be fully qualified in case same name appears in pg_catalog
- */
- appendPQExpBuffer(delqry, "DROP TRIGGER %s ",
- fmtId(tginfo->dobj.name));
- appendPQExpBuffer(delqry, "ON %s.",
- fmtId(tbinfo->dobj.namespace->dobj.name));
- appendPQExpBuffer(delqry, "%s;\n",
- fmtId(tbinfo->dobj.name));
-
if (tginfo->tgisconstraint)
{
appendPQExpBuffer(query, "CREATE CONSTRAINT TRIGGER ");
--- 8048,8059 ----
}
static void
! dumpTriggerManually(Archive *fout, TriggerInfo *tginfo, PQExpBuffer query)
{
TableInfo *tbinfo = tginfo->tgtable;
const char *p;
int findx;
if (tginfo->tgisconstraint)
{
appendPQExpBuffer(query, "CREATE CONSTRAINT TRIGGER ");
*************** dumpTrigger(Archive *fout, TriggerInfo *
*** 8194,8199 ****
--- 8176,8234 ----
p = p + 4;
}
appendPQExpBuffer(query, ");\n");
+ }
+
+ static void
+ dumpTrigger(Archive *fout, TriggerInfo *tginfo)
+ {
+ TableInfo *tbinfo = tginfo->tgtable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+
+ if (dataOnly)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+
+ /*
+ * DROP must be fully qualified in case same name appears in pg_catalog
+ */
+ appendPQExpBuffer(delqry, "DROP TRIGGER %s ",
+ fmtId(tginfo->dobj.name));
+ appendPQExpBuffer(delqry, "ON %s.",
+ fmtId(tbinfo->dobj.namespace->dobj.name));
+ appendPQExpBuffer(delqry, "%s;\n",
+ fmtId(tbinfo->dobj.name));
+
+ /*
+ * If pg_get_triggerdef() is available, use it. Otherwise, reconstruct the
+ * CREATE TRIGGER command manually.
+ */
+ if (g_fout->remoteVersion >= 70300)
+ {
+ PQExpBuffer def_query;
+ PGresult *res;
+
+ def_query = createPQExpBuffer();
+ appendPQExpBuffer(def_query,
+ "SELECT pg_catalog.pg_get_triggerdef('%u'::pg_catalog.oid) AS definition",
+ tginfo->dobj.catId.oid);
+ res = PQexec(g_conn, def_query->data);
+ check_sql_result(res, g_conn, def_query->data, PGRES_TUPLES_OK);
+
+ if (PQntuples(res) != 1)
+ {
+ write_msg(NULL, "query to get trigger \"%s\" for table \"%s\" failed: wrong number of rows returned",
+ tginfo->dobj.name, tbinfo->dobj.name);
+ exit_nicely();
+ }
+
+ appendPQExpBuffer(query, "%s;\n", PQgetvalue(res, 0, 0));
+ destroyPQExpBuffer(def_query);
+ }
+ else
+ dumpTriggerManually(fout, tginfo, query);
if (!tginfo->tgenabled)
{
Index: src/include/catalog/pg_trigger.h
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/include/catalog/pg_trigger.h,v
retrieving revision 1.25
diff -c -p -r1.25 pg_trigger.h
*** src/include/catalog/pg_trigger.h 11 Mar 2006 04:38:38 -0000 1.25
--- src/include/catalog/pg_trigger.h 5 Jul 2006 17:37:22 -0000
*************** CATALOG(pg_trigger,2620)
*** 48,53 ****
--- 48,54 ----
/* VARIABLE LENGTH FIELDS: */
int2vector tgattr; /* reserved for column-specific triggers */
bytea tgargs; /* first\000second\000tgnargs\000 */
+ text tgqual; /* string form of qualification clause */
} FormData_pg_trigger;
/* ----------------
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 61,67 ****
* compiler constants for pg_trigger
* ----------------
*/
! #define Natts_pg_trigger 13
#define Anum_pg_trigger_tgrelid 1
#define Anum_pg_trigger_tgname 2
#define Anum_pg_trigger_tgfoid 3
--- 62,68 ----
* compiler constants for pg_trigger
* ----------------
*/
! #define Natts_pg_trigger 14
#define Anum_pg_trigger_tgrelid 1
#define Anum_pg_trigger_tgname 2
#define Anum_pg_trigger_tgfoid 3
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 75,80 ****
--- 76,82 ----
#define Anum_pg_trigger_tgnargs 11
#define Anum_pg_trigger_tgattr 12
#define Anum_pg_trigger_tgargs 13
+ #define Anum_pg_trigger_tgqual 14
#define TRIGGER_TYPE_ROW (1 << 0)
#define TRIGGER_TYPE_BEFORE (1 << 1)
Index: src/include/nodes/execnodes.h
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/include/nodes/execnodes.h,v
retrieving revision 1.152
diff -c -p -r1.152 execnodes.h
*** src/include/nodes/execnodes.h 28 Jun 2006 19:40:52 -0000 1.152
--- src/include/nodes/execnodes.h 7 Jul 2006 19:03:02 -0000
*************** typedef struct ExprContext
*** 100,105 ****
--- 100,107 ----
TupleTableSlot *ecxt_scantuple;
TupleTableSlot *ecxt_innertuple;
TupleTableSlot *ecxt_outertuple;
+ TupleTableSlot *ecxt_oldtuple; /* OLD tuple in trigger context */
+ TupleTableSlot *ecxt_newtuple; /* NEW tuple in trigger context */
/* Memory contexts for expression evaluation --- see notes above */
MemoryContext ecxt_per_query_memory;
*************** typedef struct JunkFilter
*** 247,252 ****
--- 249,269 ----
TupleTableSlot *jf_resultSlot;
} JunkFilter;
+ /*
+ * State information for trigger WHEN clauses. Since ExecPrepareExpr() could
+ * be expensive, we want to initialise each expression once and
+ * reuse it again and again
+ *
+ * "quals" is an array of lists, with one array element for each trigger (in
+ * the same order as the array of Triggers kept in the TriggerDesc). Each
+ * List represents an Expr, in "ANDs implicit" format.
+ */
+ typedef struct TrigQualState
+ {
+ List **quals;
+ } TrigQualState;
+
+
/* ----------------
* ResultRelInfo information
*
*************** typedef struct JunkFilter
*** 262,267 ****
--- 279,285 ----
* IndexRelationInfo array of key/attr info for indices
* TrigDesc triggers to be fired, if any
* TrigFunctions cached lookup info for trigger functions
+ * TrigQuals state for trigger WHEN clauses
* TrigInstrument optional runtime measurements for triggers
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
*************** typedef struct ResultRelInfo
*** 277,282 ****
--- 295,301 ----
IndexInfo **ri_IndexRelationInfo;
TriggerDesc *ri_TrigDesc;
FmgrInfo *ri_TrigFunctions;
+ TrigQualState *ri_TrigQuals;
struct Instrumentation *ri_TrigInstrument;
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
Index: src/include/nodes/parsenodes.h
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/include/nodes/parsenodes.h,v
retrieving revision 1.315
diff -c -p -r1.315 parsenodes.h
*** src/include/nodes/parsenodes.h 3 Jul 2006 22:45:40 -0000 1.315
--- src/include/nodes/parsenodes.h 7 Jul 2006 13:04:27 -0000
*************** typedef struct CreateTrigStmt
*** 1162,1167 ****
--- 1162,1169 ----
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation */
+ Node *when; /* WHEN clause qual */
+ List *rtable; /* range table for interpreting WHEN expr */
} CreateTrigStmt;
/* ----------------------
Index: src/include/nodes/primnodes.h
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/include/nodes/primnodes.h,v
retrieving revision 1.113
diff -c -p -r1.113 primnodes.h
*** src/include/nodes/primnodes.h 22 Apr 2006 01:26:01 -0000 1.113
--- src/include/nodes/primnodes.h 7 Jul 2006 19:05:51 -0000
*************** typedef struct Expr
*** 101,108 ****
* The code doesn't really need varnoold/varoattno, but they are very useful
* for debugging and interpreting completed plans, so we keep them around.
*/
! #define INNER 65000
! #define OUTER 65001
#define PRS2_OLD_VARNO 1
#define PRS2_NEW_VARNO 2
--- 101,110 ----
* The code doesn't really need varnoold/varoattno, but they are very useful
* for debugging and interpreting completed plans, so we keep them around.
*/
! #define INNER 65000
! #define OUTER 65001
! #define TRIG_OLD_VARNO 65002
! #define TRIG_NEW_VARNO 65003
#define PRS2_OLD_VARNO 1
#define PRS2_NEW_VARNO 2
Index: src/include/utils/rel.h
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/include/utils/rel.h,v
retrieving revision 1.91
diff -c -p -r1.91 rel.h
*** src/include/utils/rel.h 3 Jul 2006 22:45:41 -0000 1.91
--- src/include/utils/rel.h 7 Jul 2006 13:04:30 -0000
*************** typedef struct Trigger
*** 62,67 ****
--- 62,68 ----
int16 tgnattr;
int16 *tgattr;
char **tgargs;
+ Node *when;
} Trigger;
typedef struct TriggerDesc
Index: src/test/regress/expected/triggers.out
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/test/regress/expected/triggers.out,v
retrieving revision 1.23
diff -c -p -r1.23 triggers.out
*** src/test/regress/expected/triggers.out 26 Jun 2006 17:24:41 -0000 1.23
--- src/test/regress/expected/triggers.out 7 Jul 2006 20:48:15 -0000
*************** NOTICE: row 1 not changed
*** 537,539 ****
--- 537,562 ----
NOTICE: row 2 not changed
DROP TABLE trigger_test;
DROP FUNCTION mytrigger();
+ -- test the WHEN clause for trigger definitions
+ CREATE OR REPLACE FUNCTION notify_trig() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ raise notice '%s invoked: new.a = %, new.b = %', tg_name, new.a, new.b;
+ return new;
+ end$$;
+ CREATE TABLE when_test (a int, b int);
+ CREATE TRIGGER t1 BEFORE INSERT ON when_test FOR EACH ROW
+ WHEN (new.a > 5)
+ EXECUTE PROCEDURE notify_trig();
+ INSERT INTO when_test VALUES (NULL, 0); -- shouldn't fire
+ INSERT INTO when_test VALUES (10, 100); -- should fire
+ NOTICE: t1s invoked: new.a = 10, new.b = 100
+ CREATE TRIGGER t2 BEFORE UPDATE ON when_test FOR EACH ROW
+ WHEN (new.b > 50)
+ EXECUTE PROCEDURE notify_trig();
+ UPDATE when_test SET b = b + 50; -- should fire once
+ NOTICE: t2s invoked: new.a = 10, new.b = 150
+ UPDATE when_test SET b = b + 1; -- should fire twice
+ NOTICE: t2s invoked: new.a = , new.b = 51
+ NOTICE: t2s invoked: new.a = 10, new.b = 151
+ DROP TABLE when_test;
+ DROP FUNCTION notify_trig();
Index: src/test/regress/sql/triggers.sql
===================================================================
RCS file: /usr/local/cvsroot/pgsql/src/test/regress/sql/triggers.sql,v
retrieving revision 1.13
diff -c -p -r1.13 triggers.sql
*** src/test/regress/sql/triggers.sql 26 Jun 2006 17:24:41 -0000 1.13
--- src/test/regress/sql/triggers.sql 7 Jul 2006 13:15:22 -0000
*************** UPDATE trigger_test SET f3 = NULL;
*** 415,417 ****
--- 415,444 ----
DROP TABLE trigger_test;
DROP FUNCTION mytrigger();
+
+ -- test the WHEN clause for trigger definitions
+ CREATE OR REPLACE FUNCTION notify_trig() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ raise notice '%s invoked: new.a = %, new.b = %', tg_name, new.a, new.b;
+ return new;
+ end$$;
+
+ CREATE TABLE when_test (a int, b int);
+
+ CREATE TRIGGER t1 BEFORE INSERT ON when_test FOR EACH ROW
+ WHEN (new.a > 5)
+ EXECUTE PROCEDURE notify_trig();
+
+ INSERT INTO when_test VALUES (NULL, 0); -- shouldn't fire
+ INSERT INTO when_test VALUES (10, 100); -- should fire
+
+ CREATE TRIGGER t2 BEFORE UPDATE ON when_test FOR EACH ROW
+ WHEN (new.b > 50)
+ EXECUTE PROCEDURE notify_trig();
+
+ UPDATE when_test SET b = b + 50; -- should fire once
+ UPDATE when_test SET b = b + 1; -- should fire twice
+
+ DROP TABLE when_test;
+ DROP FUNCTION notify_trig();
+