From 293913568e6a7a86fd1479e1cff8e2ecb58d6568 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 15:44:03 +0200 Subject: Adding upstream version 16.2. Signed-off-by: Daniel Baumann --- doc/src/sgml/trigger.sgml | 1032 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1032 insertions(+) create mode 100644 doc/src/sgml/trigger.sgml (limited to 'doc/src/sgml/trigger.sgml') diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml new file mode 100644 index 0000000..6e1f370 --- /dev/null +++ b/doc/src/sgml/trigger.sgml @@ -0,0 +1,1032 @@ + + + + Triggers + + + trigger + + + + This chapter provides general information about writing trigger functions. + Trigger functions can be written in most of the available procedural + languages, including + PL/pgSQL (), + PL/Tcl (), + PL/Perl (), and + PL/Python (). + After reading this chapter, you should consult the chapter for + your favorite procedural language to find out the language-specific + details of writing a trigger in it. + + + + It is also possible to write a trigger function in C, although + most people find it easier to use one of the procedural languages. + It is not currently possible to write a trigger function in the + plain SQL function language. + + + + Overview of Trigger Behavior + + + A trigger is a specification that the database should automatically + execute a particular function whenever a certain type of operation is + performed. Triggers can be attached to tables (partitioned or not), + views, and foreign tables. + + + + On tables and foreign tables, triggers can be defined to execute either + before or after any INSERT, UPDATE, + or DELETE operation, either once per modified row, + or once per SQL statement. + UPDATE triggers can moreover be set to fire only if + certain columns are mentioned in the SET clause of + the UPDATE statement. Triggers can also fire + for TRUNCATE statements. If a trigger event occurs, + the trigger's function is called at the appropriate time to handle the + event. + + + + On views, triggers can be defined to execute instead of + INSERT, UPDATE, or + DELETE operations. + Such INSTEAD OF triggers + are fired once for each row that needs to be modified in the view. + It is the responsibility of the + trigger's function to perform the necessary modifications to the view's + underlying base table(s) and, where appropriate, return the modified + row as it will appear in the view. Triggers on views can also be defined + to execute once per SQL statement, before or after + INSERT, UPDATE, or + DELETE operations. + However, such triggers are fired only if there is also + an INSTEAD OF trigger on the view. Otherwise, + any statement targeting the view must be rewritten into a statement + affecting its underlying base table(s), and then the triggers + that will be fired are the ones attached to the base table(s). + + + + The trigger function must be defined before the trigger itself can be + created. The trigger function must be declared as a + function taking no arguments and returning type trigger. + (The trigger function receives its input through a specially-passed + TriggerData structure, not in the form of ordinary function + arguments.) + + + + Once a suitable trigger function has been created, the trigger is + established with + . + The same trigger function can be used for multiple triggers. + + + + PostgreSQL offers both per-row + triggers and per-statement triggers. With a per-row + trigger, the trigger function + is invoked once for each row that is affected by the statement + that fired the trigger. In contrast, a per-statement trigger is + invoked only once when an appropriate statement is executed, + regardless of the number of rows affected by that statement. In + particular, a statement that affects zero rows will still result + in the execution of any applicable per-statement triggers. These + two types of triggers are sometimes called row-level + triggers and statement-level triggers, + respectively. Triggers on TRUNCATE may only be + defined at statement level, not per-row. + + + + Triggers are also classified according to whether they fire + before, after, or + instead of the operation. These are referred to + as BEFORE triggers, AFTER triggers, and + INSTEAD OF triggers respectively. + Statement-level BEFORE triggers naturally fire before the + statement starts to do anything, while statement-level AFTER + triggers fire at the very end of the statement. These types of + triggers may be defined on tables, views, or foreign tables. Row-level + BEFORE triggers fire immediately before a particular row is + operated on, while row-level AFTER triggers fire at the end of + the statement (but before any statement-level AFTER triggers). + These types of triggers may only be defined on tables and + foreign tables, not views. + INSTEAD OF triggers may only be + defined on views, and only at row level; they fire immediately as each + row in the view is identified as needing to be operated on. + + + + The execution of an AFTER trigger can be deferred + to the end of the transaction, rather than the end of the statement, + if it was defined as a constraint trigger. + In all cases, a trigger is executed as part of the same transaction as + the statement that triggered it, so if either the statement or the + trigger causes an error, the effects of both will be rolled back. + + + + A statement that targets a parent table in an inheritance or partitioning + hierarchy does not cause the statement-level triggers of affected child + tables to be fired; only the parent table's statement-level triggers are + fired. However, row-level triggers of any affected child tables will be + fired. + + + + If an INSERT contains an ON CONFLICT + DO UPDATE clause, it is possible that the effects of + row-level BEFORE INSERT triggers and + row-level BEFORE UPDATE triggers can + both be applied in a way that is apparent from the final state of + the updated row, if an EXCLUDED column is referenced. + There need not be an EXCLUDED column reference for + both sets of row-level BEFORE triggers to execute, + though. The + possibility of surprising outcomes should be considered when there + are both BEFORE INSERT and + BEFORE UPDATE row-level triggers + that change a row being inserted/updated (this can be + problematic even if the modifications are more or less equivalent, if + they're not also idempotent). Note that statement-level + UPDATE triggers are executed when ON + CONFLICT DO UPDATE is specified, regardless of whether or not + any rows were affected by the UPDATE (and + regardless of whether the alternative UPDATE + path was ever taken). An INSERT with an + ON CONFLICT DO UPDATE clause will execute + statement-level BEFORE INSERT + triggers first, then statement-level BEFORE + UPDATE triggers, followed by statement-level + AFTER UPDATE triggers and finally + statement-level AFTER INSERT + triggers. + + + + If an UPDATE on a partitioned table causes a row to move + to another partition, it will be performed as a DELETE + from the original partition followed by an INSERT into + the new partition. In this case, all row-level BEFORE + UPDATE triggers and all row-level + BEFORE DELETE triggers are fired on + the original partition. Then all row-level BEFORE + INSERT triggers are fired on the destination partition. + The possibility of surprising outcomes should be considered when all these + triggers affect the row being moved. As far as AFTER ROW + triggers are concerned, AFTER DELETE + and AFTER INSERT triggers are + applied; but AFTER UPDATE triggers + are not applied because the UPDATE has been converted to + a DELETE and an INSERT. As far as + statement-level triggers are concerned, none of the + DELETE or INSERT triggers are fired, + even if row movement occurs; only the UPDATE triggers + defined on the target table used in the UPDATE statement + will be fired. + + + + No separate triggers are defined for MERGE. Instead, + statement-level or row-level UPDATE, + DELETE, and INSERT triggers are fired + depending on (for statement-level triggers) what actions are specified in + the MERGE query and (for row-level triggers) what + actions are performed. + + + + While running a MERGE command, statement-level + BEFORE and AFTER triggers are + fired for events specified in the actions of the MERGE + command, irrespective of whether or not the action is ultimately performed. + This is the same as an UPDATE statement that updates + no rows, yet statement-level triggers are fired. + The row-level triggers are fired only when a row is actually updated, + inserted or deleted. So it's perfectly legal that while statement-level + triggers are fired for certain types of action, no row-level triggers + are fired for the same kind of action. + + + + Trigger functions invoked by per-statement triggers should always + return NULL. Trigger functions invoked by per-row + triggers can return a table row (a value of + type HeapTuple) to the calling executor, + if they choose. A row-level trigger fired before an operation has + the following choices: + + + + + It can return NULL to skip the operation for the + current row. This instructs the executor to not perform the + row-level operation that invoked the trigger (the insertion, + modification, or deletion of a particular table row). + + + + + + For row-level INSERT + and UPDATE triggers only, the returned row + becomes the row that will be inserted or will replace the row + being updated. This allows the trigger function to modify the + row being inserted or updated. + + + + + A row-level BEFORE trigger that does not intend to cause + either of these behaviors must be careful to return as its result the same + row that was passed in (that is, the NEW row + for INSERT and UPDATE + triggers, the OLD row for + DELETE triggers). + + + + A row-level INSTEAD OF trigger should either return + NULL to indicate that it did not modify any data from + the view's underlying base tables, or it should return the view + row that was passed in (the NEW row + for INSERT and UPDATE + operations, or the OLD row for + DELETE operations). A nonnull return value is + used to signal that the trigger performed the necessary data + modifications in the view. This will cause the count of the number + of rows affected by the command to be incremented. For + INSERT and UPDATE operations only, the trigger + may modify the NEW row before returning it. This will + change the data returned by + INSERT RETURNING or UPDATE RETURNING, + and is useful when the view will not show exactly the same data + that was provided. + + + + The return value is ignored for row-level triggers fired after an + operation, and so they can return NULL. + + + + Some considerations apply for generated + columns.generated columnin + triggers Stored generated columns are computed after + BEFORE triggers and before AFTER + triggers. Therefore, the generated value can be inspected in + AFTER triggers. In BEFORE triggers, + the OLD row contains the old generated value, as one + would expect, but the NEW row does not yet contain the + new generated value and should not be accessed. In the C language + interface, the content of the column is undefined at this point; a + higher-level programming language should prevent access to a stored + generated column in the NEW row in a + BEFORE trigger. Changes to the value of a generated + column in a BEFORE trigger are ignored and will be + overwritten. + + + + If more than one trigger is defined for the same event on the same + relation, the triggers will be fired in alphabetical order by + trigger name. In the case of BEFORE and + INSTEAD OF triggers, the possibly-modified row returned by + each trigger becomes the input to the next trigger. If any + BEFORE or INSTEAD OF trigger returns + NULL, the operation is abandoned for that row and subsequent + triggers are not fired (for that row). + + + + A trigger definition can also specify a Boolean WHEN + condition, which will be tested to see whether the trigger should + be fired. In row-level triggers the WHEN condition can + examine the old and/or new values of columns of the row. (Statement-level + triggers can also have WHEN conditions, although the feature + is not so useful for them.) In a BEFORE trigger, the + WHEN + condition is evaluated just before the function is or would be executed, + so using WHEN is not materially different from testing the + same condition at the beginning of the trigger function. However, in + an AFTER trigger, the WHEN condition is evaluated + just after the row update occurs, and it determines whether an event is + queued to fire the trigger at the end of statement. So when an + AFTER trigger's + WHEN condition does not return true, it is not necessary + to queue an event nor to re-fetch the row at end of statement. This + can result in significant speedups in statements that modify many + rows, if the trigger only needs to be fired for a few of the rows. + INSTEAD OF triggers do not support + WHEN conditions. + + + + Typically, row-level BEFORE triggers are used for checking or + modifying the data that will be inserted or updated. For example, + a BEFORE trigger might be used to insert the current time into a + timestamp column, or to check that two elements of the row are + consistent. Row-level AFTER triggers are most sensibly + used to propagate the updates to other tables, or make consistency + checks against other tables. The reason for this division of labor is + that an AFTER trigger can be certain it is seeing the final + value of the row, while a BEFORE trigger cannot; there might + be other BEFORE triggers firing after it. If you have no + specific reason to make a trigger BEFORE or + AFTER, the BEFORE case is more efficient, since + the information about + the operation doesn't have to be saved until end of statement. + + + + If a trigger function executes SQL commands then these + commands might fire triggers again. This is known as cascading + triggers. There is no direct limitation on the number of cascade + levels. It is possible for cascades to cause a recursive invocation + of the same trigger; for example, an INSERT + trigger might execute a command that inserts an additional row + into the same table, causing the INSERT trigger + to be fired again. It is the trigger programmer's responsibility + to avoid infinite recursion in such scenarios. + + + + + trigger + arguments for trigger functions + + When a trigger is being defined, arguments can be specified for + it. The purpose of including arguments in the + trigger definition is to allow different triggers with similar + requirements to call the same function. As an example, there + could be a generalized trigger function that takes as its + arguments two column names and puts the current user in one and + the current time stamp in the other. Properly written, this + trigger function would be independent of the specific table it is + triggering on. So the same function could be used for + INSERT events on any table with suitable + columns, to automatically track creation of records in a + transaction table for example. It could also be used to track + last-update events if defined as an UPDATE + trigger. + + + + Each programming language that supports triggers has its own method + for making the trigger input data available to the trigger function. + This input data includes the type of trigger event (e.g., + INSERT or UPDATE) as well as any + arguments that were listed in CREATE TRIGGER. + For a row-level trigger, the input data also includes the + NEW row for INSERT and + UPDATE triggers, and/or the OLD row + for UPDATE and DELETE triggers. + + + + By default, statement-level triggers do not have any way to examine the + individual row(s) modified by the statement. But an AFTER + STATEMENT trigger can request that transition tables + be created to make the sets of affected rows available to the trigger. + AFTER ROW triggers can also request transition tables, so + that they can see the total changes in the table as well as the change in + the individual row they are currently being fired for. The method for + examining the transition tables again depends on the programming language + that is being used, but the typical approach is to make the transition + tables act like read-only temporary tables that can be accessed by SQL + commands issued within the trigger function. + + + + + + Visibility of Data Changes + + + If you execute SQL commands in your trigger function, and these + commands access the table that the trigger is for, then + you need to be aware of the data visibility rules, because they determine + whether these SQL commands will see the data change that the trigger + is fired for. Briefly: + + + + + + Statement-level triggers follow simple visibility rules: none of + the changes made by a statement are visible to statement-level + BEFORE triggers, whereas all + modifications are visible to statement-level AFTER + triggers. + + + + + + The data change (insertion, update, or deletion) causing the + trigger to fire is naturally not visible + to SQL commands executed in a row-level BEFORE trigger, + because it hasn't happened yet. + + + + + + However, SQL commands executed in a row-level BEFORE + trigger will see the effects of data + changes for rows previously processed in the same outer + command. This requires caution, since the ordering of these + change events is not in general predictable; an SQL command that + affects multiple rows can visit the rows in any order. + + + + + + Similarly, a row-level INSTEAD OF trigger will see the + effects of data changes made by previous firings of INSTEAD + OF triggers in the same outer command. + + + + + + When a row-level AFTER trigger is fired, all data + changes made + by the outer command are already complete, and are visible to + the invoked trigger function. + + + + + + + If your trigger function is written in any of the standard procedural + languages, then the above statements apply only if the function is + declared VOLATILE. Functions that are declared + STABLE or IMMUTABLE will not see changes made by + the calling command in any case. + + + + Further information about data visibility rules can be found in + . The example in contains a demonstration of these rules. + + + + + Writing Trigger Functions in C + + + trigger + in C + + + + transition tables + referencing from C trigger + + + + This section describes the low-level details of the interface to a + trigger function. This information is only needed when writing + trigger functions in C. If you are using a higher-level language then + these details are handled for you. In most cases you should consider + using a procedural language before writing your triggers in C. The + documentation of each procedural language explains how to write a + trigger in that language. + + + + Trigger functions must use the version 1 function manager + interface. + + + + When a function is called by the trigger manager, it is not passed + any normal arguments, but it is passed a context + pointer pointing to a TriggerData structure. C + functions can check whether they were called from the trigger + manager or not by executing the macro: + +CALLED_AS_TRIGGER(fcinfo) + + which expands to: + +((fcinfo)->context != NULL && IsA((fcinfo)->context, TriggerData)) + + If this returns true, then it is safe to cast + fcinfo->context to type TriggerData + * and make use of the pointed-to + TriggerData structure. The function must + not alter the TriggerData + structure or any of the data it points to. + + + + struct TriggerData is defined in + commands/trigger.h: + + +typedef struct TriggerData +{ + NodeTag type; + TriggerEvent tg_event; + Relation tg_relation; + HeapTuple tg_trigtuple; + HeapTuple tg_newtuple; + Trigger *tg_trigger; + TupleTableSlot *tg_trigslot; + TupleTableSlot *tg_newslot; + Tuplestorestate *tg_oldtable; + Tuplestorestate *tg_newtable; + const Bitmapset *tg_updatedcols; +} TriggerData; + + + where the members are defined as follows: + + + + type + + + Always T_TriggerData. + + + + + + tg_event + + + Describes the event for which the function is called. You can use the + following macros to examine tg_event: + + + + TRIGGER_FIRED_BEFORE(tg_event) + + + Returns true if the trigger fired before the operation. + + + + + + TRIGGER_FIRED_AFTER(tg_event) + + + Returns true if the trigger fired after the operation. + + + + + + TRIGGER_FIRED_INSTEAD(tg_event) + + + Returns true if the trigger fired instead of the operation. + + + + + + TRIGGER_FIRED_FOR_ROW(tg_event) + + + Returns true if the trigger fired for a row-level event. + + + + + + TRIGGER_FIRED_FOR_STATEMENT(tg_event) + + + Returns true if the trigger fired for a statement-level event. + + + + + + TRIGGER_FIRED_BY_INSERT(tg_event) + + + Returns true if the trigger was fired by an INSERT command. + + + + + + TRIGGER_FIRED_BY_UPDATE(tg_event) + + + Returns true if the trigger was fired by an UPDATE command. + + + + + + TRIGGER_FIRED_BY_DELETE(tg_event) + + + Returns true if the trigger was fired by a DELETE command. + + + + + + TRIGGER_FIRED_BY_TRUNCATE(tg_event) + + + Returns true if the trigger was fired by a TRUNCATE command. + + + + + + + + + + tg_relation + + + A pointer to a structure describing the relation that the trigger fired for. + Look at utils/rel.h for details about + this structure. The most interesting things are + tg_relation->rd_att (descriptor of the relation + tuples) and tg_relation->rd_rel->relname + (relation name; the type is not char* but + NameData; use + SPI_getrelname(tg_relation) to get a char* if you + need a copy of the name). + + + + + + tg_trigtuple + + + A pointer to the row for which the trigger was fired. This is + the row being inserted, updated, or deleted. If this trigger + was fired for an INSERT or + DELETE then this is what you should return + from the function if you don't want to replace the row with + a different one (in the case of INSERT) or + skip the operation. For triggers on foreign tables, values of system + columns herein are unspecified. + + + + + + tg_newtuple + + + A pointer to the new version of the row, if the trigger was + fired for an UPDATE, and NULL if + it is for an INSERT or a + DELETE. This is what you have to return + from the function if the event is an UPDATE + and you don't want to replace this row by a different one or + skip the operation. For triggers on foreign tables, values of system + columns herein are unspecified. + + + + + + tg_trigger + + + A pointer to a structure of type Trigger, + defined in utils/reltrigger.h: + + +typedef struct Trigger +{ + Oid tgoid; + char *tgname; + Oid tgfoid; + int16 tgtype; + char tgenabled; + bool tgisinternal; + bool tgisclone; + Oid tgconstrrelid; + Oid tgconstrindid; + Oid tgconstraint; + bool tgdeferrable; + bool tginitdeferred; + int16 tgnargs; + int16 tgnattr; + int16 *tgattr; + char **tgargs; + char *tgqual; + char *tgoldtable; + char *tgnewtable; +} Trigger; + + + where tgname is the trigger's name, + tgnargs is the number of arguments in + tgargs, and tgargs is an array of + pointers to the arguments specified in the CREATE + TRIGGER statement. The other members are for internal use + only. + + + + + + tg_trigslot + + + The slot containing tg_trigtuple, + or a NULL pointer if there is no such tuple. + + + + + + tg_newslot + + + The slot containing tg_newtuple, + or a NULL pointer if there is no such tuple. + + + + + + tg_oldtable + + + A pointer to a structure of type Tuplestorestate + containing zero or more rows in the format specified by + tg_relation, or a NULL pointer + if there is no OLD TABLE transition relation. + + + + + + tg_newtable + + + A pointer to a structure of type Tuplestorestate + containing zero or more rows in the format specified by + tg_relation, or a NULL pointer + if there is no NEW TABLE transition relation. + + + + + + tg_updatedcols + + + For UPDATE triggers, a bitmap set indicating the + columns that were updated by the triggering command. Generic trigger + functions can use this to optimize actions by not having to deal with + columns that were not changed. + + + + As an example, to determine whether a column with attribute number + attnum (1-based) is a member of this bitmap set, + call bms_is_member(attnum - + FirstLowInvalidHeapAttributeNumber, + trigdata->tg_updatedcols)). + + + + For triggers other than UPDATE triggers, this will + be NULL. + + + + + + + + To allow queries issued through SPI to reference transition tables, see + . + + + + A trigger function must return either a + HeapTuple pointer or a NULL pointer + (not an SQL null value, that is, do not set isNull true). + Be careful to return either + tg_trigtuple or tg_newtuple, + as appropriate, if you don't want to modify the row being operated on. + + + + + A Complete Trigger Example + + + Here is a very simple example of a trigger function written in C. + (Examples of triggers written in procedural languages can be found + in the documentation of the procedural languages.) + + + + The function trigf reports the number of rows in the + table ttest and skips the actual operation if the + command attempts to insert a null value into the column + x. (So the trigger acts as a not-null constraint but + doesn't abort the transaction.) + + + + First, the table definition: + +CREATE TABLE ttest ( + x integer +); + + + + + This is the source code of the trigger function: +context; + TupleDesc tupdesc; + HeapTuple rettuple; + char *when; + bool checknull = false; + bool isnull; + int ret, i; + + /* make sure it's called as a trigger at all */ + if (!CALLED_AS_TRIGGER(fcinfo)) + elog(ERROR, "trigf: not called by trigger manager"); + + /* tuple to return to executor */ + if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) + rettuple = trigdata->tg_newtuple; + else + rettuple = trigdata->tg_trigtuple; + + /* check for null values */ + if (!TRIGGER_FIRED_BY_DELETE(trigdata->tg_event) + && TRIGGER_FIRED_BEFORE(trigdata->tg_event)) + checknull = true; + + if (TRIGGER_FIRED_BEFORE(trigdata->tg_event)) + when = "before"; + else + when = "after "; + + tupdesc = trigdata->tg_relation->rd_att; + + /* connect to SPI manager */ + if ((ret = SPI_connect()) < 0) + elog(ERROR, "trigf (fired %s): SPI_connect returned %d", when, ret); + + /* get number of rows in table */ + ret = SPI_exec("SELECT count(*) FROM ttest", 0); + + if (ret < 0) + elog(ERROR, "trigf (fired %s): SPI_exec returned %d", when, ret); + + /* count(*) returns int8, so be careful to convert */ + i = DatumGetInt64(SPI_getbinval(SPI_tuptable->vals[0], + SPI_tuptable->tupdesc, + 1, + &isnull)); + + elog (INFO, "trigf (fired %s): there are %d rows in ttest", when, i); + + SPI_finish(); + + if (checknull) + { + SPI_getbinval(rettuple, tupdesc, 1, &isnull); + if (isnull) + rettuple = NULL; + } + + return PointerGetDatum(rettuple); +} +]]> + + + + + After you have compiled the source code (see ), declare the function and the triggers: + +CREATE FUNCTION trigf() RETURNS trigger + AS 'filename' + LANGUAGE C; + +CREATE TRIGGER tbefore BEFORE INSERT OR UPDATE OR DELETE ON ttest + FOR EACH ROW EXECUTE FUNCTION trigf(); + +CREATE TRIGGER tafter AFTER INSERT OR UPDATE OR DELETE ON ttest + FOR EACH ROW EXECUTE FUNCTION trigf(); + + + + + Now you can test the operation of the trigger: + +=> INSERT INTO ttest VALUES (NULL); +INFO: trigf (fired before): there are 0 rows in ttest +INSERT 0 0 + +-- Insertion skipped and AFTER trigger is not fired + +=> SELECT * FROM ttest; + x +--- +(0 rows) + +=> INSERT INTO ttest VALUES (1); +INFO: trigf (fired before): there are 0 rows in ttest +INFO: trigf (fired after ): there are 1 rows in ttest + ^^^^^^^^ + remember what we said about visibility. +INSERT 167793 1 +vac=> SELECT * FROM ttest; + x +--- + 1 +(1 row) + +=> INSERT INTO ttest SELECT x * 2 FROM ttest; +INFO: trigf (fired before): there are 1 rows in ttest +INFO: trigf (fired after ): there are 2 rows in ttest + ^^^^^^ + remember what we said about visibility. +INSERT 167794 1 +=> SELECT * FROM ttest; + x +--- + 1 + 2 +(2 rows) + +=> UPDATE ttest SET x = NULL WHERE x = 2; +INFO: trigf (fired before): there are 2 rows in ttest +UPDATE 0 +=> UPDATE ttest SET x = 4 WHERE x = 2; +INFO: trigf (fired before): there are 2 rows in ttest +INFO: trigf (fired after ): there are 2 rows in ttest +UPDATE 1 +vac=> SELECT * FROM ttest; + x +--- + 1 + 4 +(2 rows) + +=> DELETE FROM ttest; +INFO: trigf (fired before): there are 2 rows in ttest +INFO: trigf (fired before): there are 1 rows in ttest +INFO: trigf (fired after ): there are 0 rows in ttest +INFO: trigf (fired after ): there are 0 rows in ttest + ^^^^^^ + remember what we said about visibility. +DELETE 2 +=> SELECT * FROM ttest; + x +--- +(0 rows) + + + + + + There are more complex examples in + src/test/regress/regress.c and + in . + + + -- cgit v1.2.3