Phantom Command IDs - Mailing list pgsql-patches
From | Heikki Linnakangas |
---|---|
Subject | Phantom Command IDs |
Date | |
Msg-id | 451BA5D0.8080509@enterprisedb.com Whole thread Raw |
List | pgsql-patches |
Here's a patch that implements the Phantom Command ID idea that has been discussed. I didn't do anything about the system columns. We need to before applying the patch, because the HeapTupleHeaderGetCmin/max functions don't work properly if called outside the inserting/deleting transaction. In fact, SELECT cmin,cmax FROM foo will cause assertion failures if there's rows with phantom cids in the table. I used the last free bit in t_infomask. At first I thought it wouldn't be necessary, because you could detect that a row has a phantom command id if both xmin and xmax are part of the current top-level transaction (TransactionIdIsCurrentTransactionId). But that doesn't work because we don't consider aborted subtransactions as current. Using the infomask bit seems more robust anyway. -- Heikki Linnakangas EnterpriseDB http://www.enterprisedb.com Index: src/backend/access/heap/heapam.c =================================================================== RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/backend/access/heap/heapam.c,v retrieving revision 1.219 diff -c -r1.219 heapam.c *** src/backend/access/heap/heapam.c 18 Aug 2006 16:09:08 -0000 1.219 --- src/backend/access/heap/heapam.c 26 Sep 2006 11:44:22 -0000 *************** *** 1405,1412 **** tup->t_data->t_infomask |= HEAP_XMAX_INVALID; HeapTupleHeaderSetXmin(tup->t_data, xid); HeapTupleHeaderSetCmin(tup->t_data, cid); ! HeapTupleHeaderSetXmax(tup->t_data, 0); /* zero out Datum fields */ ! HeapTupleHeaderSetCmax(tup->t_data, 0); /* for cleanliness */ tup->t_tableOid = RelationGetRelid(relation); /* --- 1405,1411 ---- tup->t_data->t_infomask |= HEAP_XMAX_INVALID; HeapTupleHeaderSetXmin(tup->t_data, xid); HeapTupleHeaderSetCmin(tup->t_data, cid); ! HeapTupleHeaderSetXmax(tup->t_data, 0); tup->t_tableOid = RelationGetRelid(relation); /* *************** *** 2045,2052 **** newtup->t_data->t_infomask |= (HEAP_XMAX_INVALID | HEAP_UPDATED); HeapTupleHeaderSetXmin(newtup->t_data, xid); HeapTupleHeaderSetCmin(newtup->t_data, cid); ! HeapTupleHeaderSetXmax(newtup->t_data, 0); /* zero out Datum fields */ ! HeapTupleHeaderSetCmax(newtup->t_data, 0); /* for cleanliness */ /* * If the toaster needs to be activated, OR if the new tuple will not fit --- 2044,2050 ---- newtup->t_data->t_infomask |= (HEAP_XMAX_INVALID | HEAP_UPDATED); HeapTupleHeaderSetXmin(newtup->t_data, xid); HeapTupleHeaderSetCmin(newtup->t_data, cid); ! HeapTupleHeaderSetXmax(newtup->t_data, 0); /* * If the toaster needs to be activated, OR if the new tuple will not fit Index: src/backend/access/transam/xact.c =================================================================== RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/backend/access/transam/xact.c,v retrieving revision 1.226 diff -c -r1.226 xact.c *** src/backend/access/transam/xact.c 27 Aug 2006 19:11:46 -0000 1.226 --- src/backend/access/transam/xact.c 27 Sep 2006 11:03:25 -0000 *************** *** 43,48 **** --- 43,49 ---- #include "utils/memutils.h" #include "utils/relcache.h" #include "utils/guc.h" + #include "utils/phantomcid.h" /* *************** *** 1595,1600 **** --- 1596,1602 ---- AtEOXact_Namespace(true); /* smgrcommit already done */ AtEOXact_Files(); + AtEOXact_PhantomCid(); pgstat_count_xact_commit(); CurrentResourceOwner = NULL; *************** *** 1810,1815 **** --- 1812,1818 ---- AtEOXact_Namespace(true); /* smgrcommit already done */ AtEOXact_Files(); + AtEOXact_PhantomCid(); CurrentResourceOwner = NULL; ResourceOwnerDelete(TopTransactionResourceOwner); *************** *** 1961,1966 **** --- 1964,1970 ---- AtEOXact_Namespace(false); smgrabort(); AtEOXact_Files(); + AtEOXact_PhantomCid(); pgstat_count_xact_rollback(); /* Index: src/backend/utils/time/Makefile =================================================================== RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/backend/utils/time/Makefile,v retrieving revision 1.10 diff -c -r1.10 Makefile *** src/backend/utils/time/Makefile 29 Nov 2003 19:52:04 -0000 1.10 --- src/backend/utils/time/Makefile 20 Sep 2006 10:05:19 -0000 *************** *** 12,18 **** top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global ! OBJS = tqual.o all: SUBSYS.o --- 12,18 ---- top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global ! OBJS = tqual.o phantomcid.o all: SUBSYS.o Index: src/backend/utils/time/phantomcid.c =================================================================== RCS file: src/backend/utils/time/phantomcid.c diff -N src/backend/utils/time/phantomcid.c *** /dev/null 1 Jan 1970 00:00:00 -0000 --- src/backend/utils/time/phantomcid.c 28 Sep 2006 09:43:13 -0000 *************** *** 0 **** --- 1,274 ---- + /*------------------------------------------------------------------------- + * + * phantomcid.c + * Phantom command id support routines + * + * Before version 8.3, HeapTupleHeaderData had separate fields for cmin + * and cmax. To reduce the header size, cmin and cmax are now overlayed + * in the same field in the header. That usually works because you rarely + * insert and delete a tuple in the transaction. To make it work when you + * do, we create a phantom command id and store that in the tuple header + * instead of cmin and cmax. The phantom command id maps to the real cmin + * and cmax in a backend-private array. Other backends don't need them, + * because cmin and cmax are only interesting to the inserting/deleting + * transaction. + * + * To allow reusing existing phantom cids, we also keep a hash table that + * maps cmin,cmax pairs to phantom cids. + * + * The array and hash table are kept in TopTransactionContext, and are + * destroyed at the end of transaction. + * + * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * $PostgreSQL$ + * + *------------------------------------------------------------------------- + */ + + #include "postgres.h" + + #include "access/htup.h" + #include "access/xact.h" + #include "utils/memutils.h" + #include "utils/hsearch.h" + + #define PHANTOMCID_DEBUG + + + /* Hash table to lookup phantom cids by cmin and cmax */ + static HTAB *phantomHash = NULL; + + /* Key and entry structures for the hash table */ + typedef struct { + CommandId cmin; + CommandId cmax; + } PhantomCidKeyData; + + typedef struct { + PhantomCidKeyData key; + CommandId phantomcid; + } PhantomCidEntryData; + + typedef PhantomCidKeyData *PhantomCidKey; + typedef PhantomCidEntryData *PhantomCidEntry; + + #define PCID_HASH_SIZE 100 + + + /* An array of cmin,cmax pairs, indexed by phantom command id. + * To convert a phantom cid to cmin and cmax, you do a simple array + * lookup. */ + static PhantomCidKey phantomCids = NULL; + static int usedPhantomCids = 0; /* number of elements in phantomCids */ + static int sizePhantomCids = 0; /* size of phantomCids array */ + + /* Initial size of the array. It will be grown if it fills up */ + #define PCID_ARRAY_INITIAL_SIZE 100 + + + /* prototypes for internal functions */ + static CommandId GetPhantomCommandId(CommandId cmin, CommandId cmax); + static CommandId GetRealCmin(CommandId phantomcid); + static CommandId GetRealCmax(CommandId phantomcid); + + /**** External API ****/ + + /* All these functions rely on the caller to not call functions that don't make + * sense. You should only call cmin related functions in the inserting + * transaction and cmax related functions in the deleting transaction. + */ + + CommandId + HeapTupleHeaderGetCmin(HeapTupleHeader tup) + { + CommandId cid = tup->t_choice.t_heap.t_field4.t_commandid; + + Assert(!(tup->t_infomask & HEAP_MOVED)); + + if (tup->t_infomask & HEAP_PHANTOMCID) + return GetRealCmin(cid); + else + return cid; + } + + void + HeapTupleHeaderSetCmin(HeapTupleHeader tup, CommandId cmin) + { + /* We never need to create a phantom cid for a new tuple. */ + tup->t_choice.t_heap.t_field4.t_commandid = cmin; + } + + CommandId + HeapTupleHeaderGetCmax(HeapTupleHeader tup) + { + CommandId cid = tup->t_choice.t_heap.t_field4.t_commandid; + + Assert(!(tup->t_infomask & HEAP_MOVED)); + + if (tup->t_infomask & HEAP_PHANTOMCID) + return GetRealCmax(cid); + else + return cid; + } + + void + HeapTupleHeaderSetCmax(HeapTupleHeader tup, CommandId cmax) + { + HeapTupleFields *t_heap = &tup->t_choice.t_heap; + + if (!(tup->t_infomask & HEAP_XMIN_COMMITTED) + && TransactionIdIsCurrentTransactionId(t_heap->t_xmin)) + { + /* This row was inserted by our transaction, and now we're + * deleting it. Need to use phantom cid. */ + CommandId cmin = t_heap->t_field4.t_commandid; + t_heap->t_field4.t_commandid = GetPhantomCommandId(cmin, cmax); + tup->t_infomask |= HEAP_PHANTOMCID; + } + else { + t_heap->t_field4.t_commandid = cmax; + tup->t_infomask &= ~HEAP_PHANTOMCID; + } + } + /* + * Phantom command IDs are only interesting to the inserting and deleting + * transaction, so we can forget about them at the end of transaction. + */ + void + AtEOXact_PhantomCid() + { + usedPhantomCids = 0; + sizePhantomCids = 0; + /* Don't bother to pfree. These are allocated in TopTransactionContext, + * so they're going to be freed at the end of transaction anyway. + */ + phantomCids = NULL; + phantomHash = NULL; + } + + + /**** Internal routines ****/ + + /* + * Get a phantom command id that maps to cmin and cmax. + * + * We try to reuse old phantom command ids when possible. + */ + static + CommandId GetPhantomCommandId(CommandId cmin, CommandId cmax) + { + CommandId phantomcid; + PhantomCidKeyData key; + PhantomCidEntry entry = NULL; + bool found; + + #ifdef PHANTOMCID_DEBUG + elog(LOG, "GetPhantomCommandId cmin: %d cmax: %d", cmin, cmax); + #endif + + /* Create the array and hash table the first time we need to use + * phantom cids in the transaction. + */ + if(phantomCids == NULL) + { + HASHCTL hash_ctl; + MemoryContext oldcontext; + + oldcontext = MemoryContextSwitchTo(TopTransactionContext); + + phantomCids = + palloc(sizeof(PhantomCidKeyData) * PCID_ARRAY_INITIAL_SIZE); + sizePhantomCids = PCID_ARRAY_INITIAL_SIZE; + + memset(&hash_ctl, 0, sizeof(hash_ctl)); + hash_ctl.keysize = sizeof(PhantomCidKeyData); + hash_ctl.entrysize = sizeof(PhantomCidEntryData); + hash_ctl.hash = tag_hash; + hash_ctl.hcxt = TopTransactionContext; + + phantomHash = + hash_create("Phantom command id hash table", + PCID_HASH_SIZE, &hash_ctl, + HASH_ELEM | HASH_FUNCTION | HASH_CONTEXT); + + MemoryContextSwitchTo(TopTransactionContext); + } + + /* Try to find an old phantom cid with the same cmin and cmax for reuse */ + + key.cmin = cmin; + key.cmax = cmax; + entry = (PhantomCidEntry) + hash_search(phantomHash, (void *) &key, HASH_ENTER, &found); + + if (found) + { + #ifdef PHANTOMCID_DEBUG + elog(LOG, "result: %d (reused)", entry->phantomcid); + #endif + return entry->phantomcid; + } + else + { + /* We have to create a new phantom cid. Check that there's room + * for it in the array, and grow it if there isn't */ + if (usedPhantomCids >= sizePhantomCids) + { + /* We need to grow the array */ + + MemoryContext oldcontext; + oldcontext = MemoryContextSwitchTo(TopTransactionContext); + + /* XXX: Should we create a bigger hash table too? */ + sizePhantomCids *= 2; + phantomCids = + repalloc(phantomCids, + sizeof(PhantomCidKeyData) * sizePhantomCids); + + MemoryContextSwitchTo(oldcontext); + } + + phantomcid = usedPhantomCids; + entry->phantomcid = phantomcid; + + phantomCids[phantomcid].cmin = cmin; + phantomCids[phantomcid].cmax = cmax; + + usedPhantomCids++; + + #ifdef PHANTOMCID_DEBUG + elog(LOG, "result: %d", phantomcid); + #endif + } + + return phantomcid; + } + + static CommandId + GetRealCmin(CommandId phantomcid) + { + Assert(phantomcid < usedPhantomCids); + + #ifdef PHANTOMCID_DEBUG + elog(LOG, "GetRealCmin phantomcid: %d -> %d", phantomcid, phantomCids[phantomcid].cmin); + #endif + + + return phantomCids[phantomcid].cmin; + } + + static CommandId + GetRealCmax(CommandId phantomcid) + { + Assert(phantomcid < usedPhantomCids); + + #ifdef PHANTOMCID_DEBUG + elog(LOG, "GetRealCmax phantomcid: %d -> %d", phantomcid, phantomCids[phantomcid].cmax); + #endif + + return phantomCids[phantomcid].cmax; + } + Index: src/include/access/htup.h =================================================================== RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/include/access/htup.h,v retrieving revision 1.85 diff -c -r1.85 htup.h *** src/include/access/htup.h 13 Jul 2006 17:47:01 -0000 1.85 --- src/include/access/htup.h 28 Sep 2006 09:23:03 -0000 *************** *** 65,77 **** * object ID (if HEAP_HASOID is set in t_infomask) * user data fields * ! * We store five "virtual" fields Xmin, Cmin, Xmax, Cmax, and Xvac in four ! * physical fields. Xmin, Cmin and Xmax are always really stored, but ! * Cmax and Xvac share a field. This works because we know that there are ! * only a limited number of states that a tuple can be in, and that Cmax ! * is only interesting for the lifetime of the deleting transaction. ! * This assumes that VACUUM FULL never tries to move a tuple whose Cmax ! * is still interesting (ie, delete-in-progress). * * Note that in 7.3 and 7.4 a similar idea was applied to Xmax and Cmin. * However, with the advent of subtransactions, a tuple may need both Xmax --- 65,81 ---- * object ID (if HEAP_HASOID is set in t_infomask) * user data fields * ! * We store five "virtual" fields Xmin, Cmin, Xmax, Cmax, and Xvac in three ! * physical fields. Xmin and Xmax are always really stored, but Cmin, Cmax ! * and Xvac share a field. This works because we know that there are only ! * a limited number of states that a tuple can be in, and that Cmax and Cmin ! * are only interesting for the lifetime of the deleting or inserting ! * transaction. If a tuple is inserted and deleted in the same transaction, ! * we use a phantom command id that maps to the real cmin and cmax. The ! * mapping is local to the backend. See phantomcid.c for more details. ! * ! * This assumes that VACUUM FULL never tries to move a tuple whose Cmax or ! * Cmin is still interesting (ie, delete- or insert-in-progress). * * Note that in 7.3 and 7.4 a similar idea was applied to Xmax and Cmin. * However, with the advent of subtransactions, a tuple may need both Xmax *************** *** 103,114 **** typedef struct HeapTupleFields { TransactionId t_xmin; /* inserting xact ID */ - CommandId t_cmin; /* inserting command ID */ TransactionId t_xmax; /* deleting or locking xact ID */ union { ! CommandId t_cmax; /* deleting or locking command ID */ TransactionId t_xvac; /* VACUUM FULL xact ID */ } t_field4; } HeapTupleFields; --- 107,121 ---- typedef struct HeapTupleFields { TransactionId t_xmin; /* inserting xact ID */ TransactionId t_xmax; /* deleting or locking xact ID */ union { ! /* t_commandid is the inserting command ID (cmin), the deleting ! * command ID (cmax), or a phantom cid that maps to cmin and cmax if ! * the tuple was inserted and deleted in the same transaction ! */ ! CommandId t_commandid; TransactionId t_xvac; /* VACUUM FULL xact ID */ } t_field4; } HeapTupleFields; *************** *** 163,169 **** #define HEAP_HASCOMPRESSED 0x0008 /* has compressed stored attribute(s) */ #define HEAP_HASEXTENDED 0x000C /* the two above combined */ #define HEAP_HASOID 0x0010 /* has an object-id field */ ! /* 0x0020 is presently unused */ #define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker */ #define HEAP_XMAX_SHARED_LOCK 0x0080 /* xmax is shared locker */ /* if either LOCK bit is set, xmax hasn't deleted the tuple, only locked it */ --- 170,176 ---- #define HEAP_HASCOMPRESSED 0x0008 /* has compressed stored attribute(s) */ #define HEAP_HASEXTENDED 0x000C /* the two above combined */ #define HEAP_HASOID 0x0010 /* has an object-id field */ ! #define HEAP_PHANTOMCID 0x0020 /* t_commandid is a phantom cid */ #define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker */ #define HEAP_XMAX_SHARED_LOCK 0x0080 /* xmax is shared locker */ /* if either LOCK bit is set, xmax hasn't deleted the tuple, only locked it */ *************** *** 210,243 **** TransactionIdStore((xid), &(tup)->t_choice.t_heap.t_xmax) \ ) ! #define HeapTupleHeaderGetCmin(tup) \ ! ( \ ! (tup)->t_choice.t_heap.t_cmin \ ! ) ! ! #define HeapTupleHeaderSetCmin(tup, cid) \ ! ( \ ! (tup)->t_choice.t_heap.t_cmin = (cid) \ ! ) ! ! /* ! * Note: GetCmax will produce wrong answers after SetXvac has been executed ! * by a transaction other than the inserting one. We could check ! * HEAP_XMAX_INVALID and return FirstCommandId if it's clear, but since that ! * bit will be set again if the deleting transaction aborts, there'd be no ! * real gain in safety from the extra test. So, just rely on the caller not ! * to trust the value unless it's meaningful. */ - #define HeapTupleHeaderGetCmax(tup) \ - ( \ - (tup)->t_choice.t_heap.t_field4.t_cmax \ - ) - - #define HeapTupleHeaderSetCmax(tup, cid) \ - do { \ - Assert(!((tup)->t_infomask & HEAP_MOVED)); \ - (tup)->t_choice.t_heap.t_field4.t_cmax = (cid); \ - } while (0) #define HeapTupleHeaderGetXvac(tup) \ ( \ --- 217,225 ---- TransactionIdStore((xid), &(tup)->t_choice.t_heap.t_xmax) \ ) ! /* HeapTupleHeaderGetCmin, HeapTupleHeaderGetCmax, and ..SetCmin and ..SetCmax ! * are defined in phantomcid.h */ #define HeapTupleHeaderGetXvac(tup) \ ( \ *************** *** 613,616 **** --- 595,605 ---- #define SizeOfHeapInplace (offsetof(xl_heap_inplace, target) + SizeOfHeapTid) + /* prototypes of HeapTupleHeader* functions implemented in + * utils/time/phantomcid.c */ + extern CommandId HeapTupleHeaderGetCmin(HeapTupleHeader tup); + extern void HeapTupleHeaderSetCmin(HeapTupleHeader tup, CommandId cmin); + extern CommandId HeapTupleHeaderGetCmax(HeapTupleHeader tup); + extern void HeapTupleHeaderSetCmax(HeapTupleHeader tup, CommandId cmax); + #endif /* HTUP_H */ Index: src/include/utils/phantomcid.h =================================================================== RCS file: src/include/utils/phantomcid.h diff -N src/include/utils/phantomcid.h *** /dev/null 1 Jan 1970 00:00:00 -0000 --- src/include/utils/phantomcid.h 28 Sep 2006 09:12:11 -0000 *************** *** 0 **** --- 1,25 ---- + /*------------------------------------------------------------------------- + * + * phantomcid.h + * Phantom cid function definitions + * + * + * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * $PostgreSQL$ + * + *------------------------------------------------------------------------- + */ + #ifndef PHANTOMCID_H + #define PHANTOMCID_H + + /* HeapTupleHeaderGetCmin, *SetCmin, *GetCmax and *SetCmax + * function prototypes are in access/htup.h, because that's + * where the macro definitions that the functions replaced + * used to be. + */ + + extern void AtEOXact_PhantomCid(void); + + #endif /* PHANTOMCID_H */ Index: src/test/regress/parallel_schedule =================================================================== RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/test/regress/parallel_schedule,v retrieving revision 1.35 diff -c -r1.35 parallel_schedule *** src/test/regress/parallel_schedule 30 Aug 2006 23:34:22 -0000 1.35 --- src/test/regress/parallel_schedule 27 Sep 2006 12:53:04 -0000 *************** *** 61,67 **** # ---------- # The fourth group of parallel test # ---------- ! test: select_into select_distinct select_distinct_on select_implicit select_having subselect union case join aggregatestransactions random portals arrays btree_index hash_index update namespace prepared_xacts delete test: privileges test: misc --- 61,67 ---- # ---------- # The fourth group of parallel test # ---------- ! test: select_into select_distinct select_distinct_on select_implicit select_having subselect union case join aggregatestransactions random portals arrays btree_index hash_index update namespace prepared_xacts delete phantomcid test: privileges test: misc Index: src/test/regress/serial_schedule =================================================================== RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/test/regress/serial_schedule,v retrieving revision 1.33 diff -c -r1.33 serial_schedule *** src/test/regress/serial_schedule 30 Aug 2006 23:34:22 -0000 1.33 --- src/test/regress/serial_schedule 27 Sep 2006 12:52:34 -0000 *************** *** 104,106 **** --- 104,107 ---- test: returning test: stats test: tablespace + test: phantomcid Index: src/test/regress/expected/phantomcid.out =================================================================== RCS file: src/test/regress/expected/phantomcid.out diff -N src/test/regress/expected/phantomcid.out *** /dev/null 1 Jan 1970 00:00:00 -0000 --- src/test/regress/expected/phantomcid.out 28 Sep 2006 08:51:09 -0000 *************** *** 0 **** --- 1,28 ---- + CREATE TEMP TABLE phantomcidtest (foobar int); + BEGIN; + INSERT INTO phantomcidtest VALUES (1); + SELECT * FROM phantomcidtest; + foobar + -------- + 1 + (1 row) + + DELETE FROM phantomcidtest; + SELECT * FROM phantomcidtest; + foobar + -------- + (0 rows) + + COMMIT; + /* Test phantom cids with portals */ + BEGIN; + INSERT INTO phantomcidtest VALUES (1); + DECLARE c CURSOR FOR SELECT * FROM phantomcidtest; + DELETE FROM phantomcidtest; + FETCH ALL FROM c; + foobar + -------- + 1 + (1 row) + + COMMIT; Index: src/test/regress/sql/phantomcid.sql =================================================================== RCS file: src/test/regress/sql/phantomcid.sql diff -N src/test/regress/sql/phantomcid.sql *** /dev/null 1 Jan 1970 00:00:00 -0000 --- src/test/regress/sql/phantomcid.sql 27 Sep 2006 12:54:18 -0000 *************** *** 0 **** --- 1,27 ---- + CREATE TEMP TABLE phantomcidtest (foobar int); + + + BEGIN; + + INSERT INTO phantomcidtest VALUES (1); + + SELECT * FROM phantomcidtest; + + DELETE FROM phantomcidtest; + + SELECT * FROM phantomcidtest; + + COMMIT; + + /* Test phantom cids with portals */ + BEGIN; + + INSERT INTO phantomcidtest VALUES (1); + + DECLARE c CURSOR FOR SELECT * FROM phantomcidtest; + + DELETE FROM phantomcidtest; + + FETCH ALL FROM c; + + COMMIT;
pgsql-patches by date: