diff --git a/contrib/pg_dirtyread/.gitignore b/contrib/pg_dirtyread/.gitignore new file mode 100644 index 0000000..fc4b6d4 --- /dev/null +++ b/contrib/pg_dirtyread/.gitignore @@ -0,0 +1,5 @@ +regression.diffs +regression.out +results/ +*.o +*.so diff --git a/contrib/pg_dirtyread/.travis.yml b/contrib/pg_dirtyread/.travis.yml new file mode 100644 index 0000000..f11b08b --- /dev/null +++ b/contrib/pg_dirtyread/.travis.yml @@ -0,0 +1,48 @@ +# run the testsuite on travis-ci.com +--- +# versions to run on +env: + - PG_SUPPORTED_VERSIONS=9.2 + - PG_SUPPORTED_VERSIONS=9.3 + - PG_SUPPORTED_VERSIONS=9.4 + - PG_SUPPORTED_VERSIONS=9.5 + - PG_SUPPORTED_VERSIONS=9.6 + - PG_SUPPORTED_VERSIONS=10 + +language: C +dist: trusty +sudo: required + +before_install: + # apt.postgresql.org is already configured, we just need to add devel + - | + DIST=trusty-pgdg + if [ "$PG_SUPPORTED_VERSIONS" = "10" ]; then + # update pgdg-source.list + sudo sed -i -e "s/pgdg.*/pgdg-testing main $PG_SUPPORTED_VERSIONS/" /etc/apt/sources.list.d/pgdg*.list + DIST=trusty-pgdg-testing + fi + - sudo apt-get -qq update + +install: + - export DEBIAN_FRONTEND=noninteractive # suppress warnings about deprecated PostgreSQL versions + # trusty's pg_buildext doesn't cope with PG version numbers >= 10, so upgrade that to -pgdg + - sudo apt-get install debhelper devscripts fakeroot postgresql-server-dev-$PG_SUPPORTED_VERSIONS postgresql-server-dev-all/$DIST + # install PostgreSQL $PG_SUPPORTED_VERSIONS if not there yet + - | + if [ ! -x /usr/lib/postgresql/$PG_SUPPORTED_VERSIONS/bin/postgres ]; then + sudo apt-get install postgresql-common # upgrade pg-common first ... + sudo /etc/init.d/postgresql stop # ... so we can stop postgresql again before installing the server + sudo apt-get install postgresql-$PG_SUPPORTED_VERSIONS + fi + # stop the travis-provided cluster + - sudo /etc/init.d/postgresql stop + - pg_lsclusters + - dpkg -l postgresql\* | cat + +script: + - pg_buildext updatecontrol + - dpkg-buildpackage -us -uc -rfakeroot + - for deb in ../*.deb; do echo "$deb:"; dpkg-deb --info $deb; dpkg-deb --contents $deb; done + - sudo debi + - pg_buildext -i '--locale=C.UTF-8' installcheck diff --git a/contrib/pg_dirtyread/LICENSE b/contrib/pg_dirtyread/LICENSE new file mode 100644 index 0000000..a052b2b --- /dev/null +++ b/contrib/pg_dirtyread/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 1996-2017, PostgreSQL Global Development Group +Copyright (c) 2012, OmniTI Computer Consulting, Inc. +Portions Copyright (c) 1994, The Regents of the University of California + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. +* Neither the name OmniTI Computer Consulting, Inc. nor the names + of its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contrib/pg_dirtyread/Makefile b/contrib/pg_dirtyread/Makefile index afbffc4..ac6f1f9 100644 --- a/contrib/pg_dirtyread/Makefile +++ b/contrib/pg_dirtyread/Makefile @@ -1,8 +1,13 @@ -MODULES = pg_dirtyread +MODULE_big = pg_dirtyread +OBJS = pg_dirtyread.o dirtyread_tupconvert.o EXTENSION = pg_dirtyread DATA = pg_dirtyread--1.0.sql +REGRESS = extension dirtyread + PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) + +pg_dirtyread.o dirtyread_tupconvert.o: dirtyread_tupconvert.h diff --git a/contrib/pg_dirtyread/README.md b/contrib/pg_dirtyread/README.md index c6f283d..5bc1725 100644 --- a/contrib/pg_dirtyread/README.md +++ b/contrib/pg_dirtyread/README.md @@ -1,5 +1,5 @@ -pg_dirtyread 1.0 -================ +pg_dirtyread +============ The pg_dirtyread extension provides the ability to read dead but unvacuumed rows from a relation. @@ -23,18 +23,103 @@ to find it: env PG_CONFIG=/path/to/pg_config make && make install -Loading +Loading and Using ------- Once pg_dirtyread is built and installed, you can add it to a database. Loading pg_dirtyread is as simple as connecting to a database as a super user and running: + ```sql CREATE EXTENSION pg_dirtyread; + SELECT * FROM pg_dirtyread('tablename'::regclass) AS t(col1 type1, col2 type2, ...); + ``` -Using ------ +The `pg_dirtyread()` function returns RECORD, therefore it is necessary to +attach a table alias clause that describes the table schema. Columns are +matched by name, so it is possible to omit some columns in the alias, or +rearrange columns. + +Example: + + ```sql + CREATE EXTENSION pg_dirtyread; + + -- Create table and disable autovacuum + CREATE TABLE foo (bar bigint, baz text); + ALTER TABLE foo SET ( + autovacuum_enabled = false, toast.autovacuum_enabled = false + ); + + INSERT INTO foo VALUES (1, 'Test'), (2, 'New Test'); + DELETE FROM foo WHERE bar = 1; SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); + ``` Where the schema of `foo` is `(bar bigint, baz text)`. + +System Columns +-------------- + +System columns such as `xmax` and `ctid` can be retrieved by including them in +the table alias attached to the `pg_dirtyread()` call. A special column `dead` of +type boolean is available to report dead rows (as by `HeapTupleIsSurelyDead`). +The `dead` column is not usable during recovery, i.e. most notably not on +standby servers. + + ```sql + SELECT * FROM pg_dirtyread('foo'::regclass) + AS t(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean, + oid oid, bar bigint, baz text); + tableoid │ ctid │ xmin │ xmax │ cmin │ cmax │ dead │ oid │ bar │ baz + ──────────┼───────┼──────┼──────┼──────┼──────┼──────┼─────┼─────┼─────────────────── + 41823 │ (0,1) │ 1484 │ 1485 │ 0 │ 0 │ t │ 0 │ 1 │ Delete + 41823 │ (0,2) │ 1484 │ 0 │ 0 │ 0 │ f │ 0 │ 2 │ Insert + 41823 │ (0,3) │ 1484 │ 1486 │ 0 │ 0 │ t │ 0 │ 3 │ Update + 41823 │ (0,4) │ 1484 │ 1488 │ 0 │ 0 │ f │ 0 │ 4 │ Not deleted + 41823 │ (0,5) │ 1484 │ 1489 │ 1 │ 1 │ f │ 0 │ 5 │ Not updated + 41823 │ (0,6) │ 1486 │ 0 │ 0 │ 0 │ f │ 0 │ 3 │ Updated + 41823 │ (0,7) │ 1489 │ 0 │ 1 │ 1 │ t │ 0 │ 5 │ Not quite updated + 41823 │ (0,8) │ 1490 │ 0 │ 2 │ 2 │ t │ 0 │ 6 │ Not inserted + ``` + +License +------- + +Original author: Phil Sorber + +Copyright (c) 1996-2017, PostgreSQL Global Development Group + +Copyright (c) 2012, OmniTI Computer Consulting, Inc. + +Portions Copyright (c) 1994, The Regents of the University of California + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. +* Neither the name OmniTI Computer Consulting, Inc. nor the names + of its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contrib/pg_dirtyread/dirtyread_tupconvert.c b/contrib/pg_dirtyread/dirtyread_tupconvert.c new file mode 100644 index 0000000..5c51fd9 --- /dev/null +++ b/contrib/pg_dirtyread/dirtyread_tupconvert.c @@ -0,0 +1,296 @@ +/*------------------------------------------------------------------------- + * + * tupconvert.c + * Tuple conversion support. + * + * These functions provide conversion between rowtypes that are logically + * equivalent but might have columns in a different order or different sets + * of dropped columns. There is some overlap of functionality with the + * executor's "junkfilter" routines, but these functions work on bare + * HeapTuples rather than TupleTableSlots. + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/access/common/tupconvert.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#if PG_VERSION_NUM >= 90300 +#include "access/htup_details.h" +#endif +#include "access/tupconvert.h" +#include "access/sysattr.h" +#include "access/xlog.h" /* RecoveryInProgress */ +#include "catalog/pg_type.h" /* *OID */ +#include "utils/builtins.h" +#include "utils/tqual.h" /* HeapTupleIsSurelyDead */ + +#include "dirtyread_tupconvert.h" + +/* + * The conversion setup routines have the following common API: + * + * The setup routine checks whether the given source and destination tuple + * descriptors are logically compatible. If not, it throws an error. + * If so, it returns NULL if they are physically compatible (ie, no conversion + * is needed), else a TupleConversionMap that can be used by do_convert_tuple + * to perform the conversion. + * + * The TupleConversionMap, if needed, is palloc'd in the caller's memory + * context. Also, the given tuple descriptors are referenced by the map, + * so they must survive as long as the map is needed. + * + * The caller must supply a suitable primary error message to be used if + * a compatibility error is thrown. Recommended coding practice is to use + * gettext_noop() on this string, so that it is translatable but won't + * actually be translated unless the error gets thrown. + * + * + * Implementation notes: + * + * The key component of a TupleConversionMap is an attrMap[] array with + * one entry per output column. This entry contains the 1-based index of + * the corresponding input column, or zero to force a NULL value (for + * a dropped output column). The TupleConversionMap also contains workspace + * arrays. + */ + + +/* + * Set up for tuple conversion, matching input and output columns by name. + * (Dropped columns are ignored in both input and output.) This is intended + * for use when the rowtypes are related by inheritance, so we expect an exact + * match of both type and typmod. The error messages will be a bit unhelpful + * unless both rowtypes are named composite types. + */ +TupleConversionMap * +dirtyread_convert_tuples_by_name(TupleDesc indesc, + TupleDesc outdesc, + const char *msg) +{ + TupleConversionMap *map; + AttrNumber *attrMap; + int n = outdesc->natts; + int i; + bool same; + + /* Verify compatibility and prepare attribute-number map */ + attrMap = dirtyread_convert_tuples_by_name_map(indesc, outdesc, msg); + + /* + * Check to see if the map is one-to-one, in which case we need not do a + * tuple conversion. We must also insist that both tupdescs either + * specify or don't specify an OID column, else we need a conversion to + * add/remove space for that. (For some callers, presence or absence of + * an OID column perhaps would not really matter, but let's be safe.) + */ + if (indesc->natts == outdesc->natts && + indesc->tdhasoid == outdesc->tdhasoid) + { + same = true; + for (i = 0; i < n; i++) + { + if (attrMap[i] == (i + 1)) + continue; + + /* + * If it's a dropped column and the corresponding input column is + * also dropped, we needn't convert. However, attlen and attalign + * must agree. + */ + if (attrMap[i] == 0 && + indesc->attrs[i]->attisdropped && + indesc->attrs[i]->attlen == outdesc->attrs[i]->attlen && + indesc->attrs[i]->attalign == outdesc->attrs[i]->attalign) + continue; + + same = false; + break; + } + } + else + same = false; + + if (same) + { + /* Runtime conversion is not needed */ + elog(DEBUG1, "tuple conversion is not needed"); + pfree(attrMap); + return NULL; + } + + /* Prepare the map structure */ + map = (TupleConversionMap *) palloc(sizeof(TupleConversionMap)); + map->indesc = indesc; + map->outdesc = outdesc; + map->attrMap = attrMap; + /* preallocate workspace for Datum arrays */ + map->outvalues = (Datum *) palloc(n * sizeof(Datum)); + map->outisnull = (bool *) palloc(n * sizeof(bool)); + n = indesc->natts + 1; /* +1 for NULL */ + map->invalues = (Datum *) palloc(n * sizeof(Datum)); + map->inisnull = (bool *) palloc(n * sizeof(bool)); + map->invalues[0] = (Datum) 0; /* set up the NULL entry */ + map->inisnull[0] = true; + + return map; +} + +static const struct system_columns_t { + char *attname; + Oid atttypid; + int32 atttypmod; + int attnum; +} system_columns[] = { + { "ctid", TIDOID, -1, SelfItemPointerAttributeNumber }, + { "oid", OIDOID, -1, ObjectIdAttributeNumber }, + { "xmin", XIDOID, -1, MinTransactionIdAttributeNumber }, + { "cmin", CIDOID, -1, MinCommandIdAttributeNumber }, + { "xmax", XIDOID, -1, MaxTransactionIdAttributeNumber }, + { "cmax", CIDOID, -1, MaxCommandIdAttributeNumber }, + { "tableoid", OIDOID, -1, TableOidAttributeNumber }, + { "dead", BOOLOID, -1, DeadFakeAttributeNumber }, /* fake column to return HeapTupleIsSurelyDead */ + { 0 }, +}; + +/* + * Return a palloc'd bare attribute map for tuple conversion, matching input + * and output columns by name. (Dropped columns are ignored in both input and + * output.) This is normally a subroutine for convert_tuples_by_name, but can + * be used standalone. + */ +AttrNumber * +dirtyread_convert_tuples_by_name_map(TupleDesc indesc, + TupleDesc outdesc, + const char *msg) +{ + AttrNumber *attrMap; + int n; + int i; + + n = outdesc->natts; + attrMap = (AttrNumber *) palloc0(n * sizeof(AttrNumber)); + for (i = 0; i < n; i++) + { + Form_pg_attribute att = outdesc->attrs[i]; + char *attname; + Oid atttypid; + int32 atttypmod; + int j; + + if (att->attisdropped) + continue; /* attrMap[i] is already 0 */ + attname = NameStr(att->attname); + atttypid = att->atttypid; + atttypmod = att->atttypmod; + for (j = 0; j < indesc->natts; j++) + { + att = indesc->attrs[j]; + if (att->attisdropped) + continue; + if (strcmp(attname, NameStr(att->attname)) == 0) + { + /* Found it, check type */ + if (atttypid != att->atttypid || atttypmod != att->atttypmod) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg_internal("%s", _(msg)), + errdetail("Attribute \"%s\" has type %s in corresponding attribute of type %s.", + attname, + format_type_with_typemod(att->atttypid, att->atttypmod), + format_type_be(indesc->tdtypeid)))); + attrMap[i] = (AttrNumber) (j + 1); + break; + } + } + /* Check system columns */ + if (attrMap[i] == 0) + for (j = 0; system_columns[j].attname; j++) + if (strcmp(attname, system_columns[j].attname) == 0) + { + /* Found it, check type */ + if (atttypid != system_columns[j].atttypid || atttypmod != system_columns[j].atttypmod) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg_internal("%s", _(msg)), + errdetail("Attribute \"%s\" has type %s in corresponding attribute of type %s.", + attname, + format_type_be(system_columns[j].atttypid), + format_type_be(indesc->tdtypeid)))); + /* GetOldestXmin() is not available during recovery */ + if (system_columns[j].attnum == DeadFakeAttributeNumber && + RecoveryInProgress()) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Cannot use \"dead\" column during recovery"))); + attrMap[i] = system_columns[j].attnum; + break; + } + if (attrMap[i] == 0) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg_internal("%s", _(msg)), + errdetail("Attribute \"%s\" does not exist in type %s.", + attname, + format_type_be(indesc->tdtypeid)))); + } + + return attrMap; +} + +/* + * Perform conversion of a tuple according to the map. + */ +HeapTuple +dirtyread_do_convert_tuple(HeapTuple tuple, TupleConversionMap *map, TransactionId oldest_xmin) +{ + AttrNumber *attrMap = map->attrMap; + Datum *invalues = map->invalues; + bool *inisnull = map->inisnull; + Datum *outvalues = map->outvalues; + bool *outisnull = map->outisnull; + int outnatts = map->outdesc->natts; + int i; + + /* + * Extract all the values of the old tuple, offsetting the arrays so that + * invalues[0] is left NULL and invalues[1] is the first source attribute; + * this exactly matches the numbering convention in attrMap. + */ + heap_deform_tuple(tuple, map->indesc, invalues + 1, inisnull + 1); + + /* + * Transpose into proper fields of the new tuple. + */ + for (i = 0; i < outnatts; i++) + { + int j = attrMap[i]; + + if (j == DeadFakeAttributeNumber) + { + outvalues[i] = HeapTupleIsSurelyDead(tuple +#if PG_VERSION_NUM < 90400 + ->t_data +#endif + , oldest_xmin); + outisnull[i] = false; + } + else if (j < 0) + outvalues[i] = heap_getsysattr(tuple, j, map->indesc, &outisnull[i]); + else + { + outvalues[i] = invalues[j]; + outisnull[i] = inisnull[j]; + } + } + + /* + * Now form the new tuple. + */ + return heap_form_tuple(map->outdesc, outvalues, outisnull); +} diff --git a/contrib/pg_dirtyread/dirtyread_tupconvert.h b/contrib/pg_dirtyread/dirtyread_tupconvert.h new file mode 100644 index 0000000..a1f28cb --- /dev/null +++ b/contrib/pg_dirtyread/dirtyread_tupconvert.h @@ -0,0 +1,31 @@ +/*------------------------------------------------------------------------- + * + * tupconvert.h + * Tuple conversion support. + * + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/access/tupconvert.h + * + *------------------------------------------------------------------------- + */ +#ifndef DIRTYREAD_TUPCONVERT_H +#define DIRTYREAD_TUPCONVERT_H + +#include "access/tupconvert.h" + +extern TupleConversionMap *dirtyread_convert_tuples_by_name(TupleDesc indesc, + TupleDesc outdesc, + const char *msg); + +extern AttrNumber *dirtyread_convert_tuples_by_name_map(TupleDesc indesc, + TupleDesc outdesc, + const char *msg); + +extern HeapTuple dirtyread_do_convert_tuple(HeapTuple tuple, TupleConversionMap *map, TransactionId oldest_xmin); + +#define DeadFakeAttributeNumber FirstLowInvalidHeapAttributeNumber + +#endif /* DIRTYREAD_TUPCONVERT_H */ diff --git a/contrib/pg_dirtyread/expected/dirtyread.out b/contrib/pg_dirtyread/expected/dirtyread.out new file mode 100644 index 0000000..09e4175 --- /dev/null +++ b/contrib/pg_dirtyread/expected/dirtyread.out @@ -0,0 +1,110 @@ +-- Create table and disable autovacuum +CREATE TABLE foo (bar bigint, baz text); +ALTER TABLE foo SET ( + autovacuum_enabled = false, toast.autovacuum_enabled = false +); +-- single row +INSERT INTO foo VALUES (1, 'Hello world'); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); + bar | baz +-----+------------- + 1 | Hello world +(1 row) + +DELETE FROM foo; +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); + bar | baz +-----+------------- + 1 | Hello world +(1 row) + +VACUUM foo; +-- multiple rows +INSERT INTO foo VALUES (1, 'Delete'), (2, 'Insert'), (3, 'Update'), (4, 'Not deleted'), (5, 'Not updated'); +DELETE FROM foo WHERE bar = 1; +UPDATE foo SET baz = 'Updated' WHERE bar = 3; +BEGIN; + DELETE FROM foo WHERE bar = 4; + UPDATE foo SET baz = 'Not quite updated' where bar = 5; + INSERT INTO foo VALUES (6, 'Not inserted'); +ROLLBACK; +SELECT * FROM foo; + bar | baz +-----+------------- + 2 | Insert + 4 | Not deleted + 5 | Not updated + 3 | Updated +(4 rows) + +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); + bar | baz +-----+------------------- + 1 | Delete + 2 | Insert + 3 | Update + 4 | Not deleted + 5 | Not updated + 3 | Updated + 5 | Not quite updated + 6 | Not inserted +(8 rows) + +-- system columns (don't show tableoid and xmin, but make sure they are numbers) +SELECT CASE WHEN tableoid >= 0 THEN 0 END AS tableoid, + ctid, + CASE WHEN xmin::text::int >= 0 THEN 0 END AS xmin, + CASE WHEN xmax::text <> '0' THEN xmax::text::int - xmin::text::int END AS xmax, + cmin, cmax, dead, oid, bar, baz + FROM pg_dirtyread('foo'::regclass) + AS t(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean, oid oid, bar bigint, baz text); + tableoid | ctid | xmin | xmax | cmin | cmax | dead | oid | bar | baz +----------+-------+------+------+------+------+------+-----+-----+------------------- + 0 | (0,1) | 0 | 1 | 0 | 0 | t | 0 | 1 | Delete + 0 | (0,2) | 0 | | 0 | 0 | f | 0 | 2 | Insert + 0 | (0,3) | 0 | 2 | 0 | 0 | t | 0 | 3 | Update + 0 | (0,4) | 0 | 3 | 0 | 0 | f | 0 | 4 | Not deleted + 0 | (0,5) | 0 | 3 | 1 | 1 | f | 0 | 5 | Not updated + 0 | (0,6) | 0 | | 0 | 0 | f | 0 | 3 | Updated + 0 | (0,7) | 0 | | 1 | 1 | t | 0 | 5 | Not quite updated + 0 | (0,8) | 0 | | 2 | 2 | t | 0 | 6 | Not inserted +(8 rows) + +-- error cases +SELECT pg_dirtyread('foo'::regclass); +ERROR: function returning record called in context that cannot accept type record +SELECT * FROM pg_dirtyread(0) as t(bar bigint, baz text); +ERROR: invalid relation oid "0" +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar int, baz text); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "bar" has type bigint in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(moo bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "moo" does not exist in type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(tableoid bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "tableoid" has type oid in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(ctid bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "ctid" has type tid in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(xmin bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "xmin" has type xid in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(xmax bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "xmax" has type xid in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(cmin bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "cmin" has type cid in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(cmax bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "cmax" has type cid in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(dead bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "dead" has type boolean in corresponding attribute of type foo. +SELECT * FROM pg_dirtyread('foo'::regclass) as t(oid bigint); +ERROR: Error converting tuple descriptors! +DETAIL: Attribute "oid" has type oid in corresponding attribute of type foo. +SET ROLE luser; +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); +ERROR: must be superuser to use pg_dirtyread diff --git a/contrib/pg_dirtyread/expected/extension.out b/contrib/pg_dirtyread/expected/extension.out new file mode 100644 index 0000000..110de4c --- /dev/null +++ b/contrib/pg_dirtyread/expected/extension.out @@ -0,0 +1,5 @@ +CREATE EXTENSION pg_dirtyread; +-- create a non-superuser role, ignoring any output/errors, it might already exist +SET client_min_messages = fatal; +\set QUIET on +CREATE ROLE luser; diff --git a/contrib/pg_dirtyread/pg_dirtyread.c b/contrib/pg_dirtyread/pg_dirtyread.c index 4db3217..423a36e 100644 --- a/contrib/pg_dirtyread/pg_dirtyread.c +++ b/contrib/pg_dirtyread/pg_dirtyread.c @@ -1,5 +1,7 @@ /* + * Copyright (c) 1996-2017, PostgreSQL Global Development Group * Copyright (c) 2012, OmniTI Computer Consulting, Inc. + * Portions Copyright (c) 1994, The Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,13 +39,22 @@ #include "utils/rel.h" #include "catalog/pg_type.h" #include "access/tupconvert.h" +#if PG_VERSION_NUM >= 90300 +#include "access/htup_details.h" +#endif +#include "access/xlog.h" /* RecoveryInProgress */ +#include "miscadmin.h" /* superuser */ +#include "storage/procarray.h" /* GetOldestXmin */ + +#include "dirtyread_tupconvert.h" typedef struct { Relation rel; - HeapScanDesc scan; TupleDesc reltupdesc; TupleConversionMap *map; + HeapScanDesc scan; + TransactionId oldest_xmin; } pg_dirtyread_ctx; PG_MODULE_MAGIC; @@ -55,30 +66,49 @@ Datum pg_dirtyread(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; - MemoryContext oldcontext; pg_dirtyread_ctx *usr_ctx; - Oid relid; - HeapTuple tuplein, tupleout; - TupleDesc tupdesc; + HeapTuple tuplein; if (SRF_IS_FIRSTCALL()) { + MemoryContext oldcontext; + Oid relid; + TupleDesc tupdesc; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use pg_dirtyread"))); + relid = PG_GETARG_OID(0); + if (!OidIsValid(relid)) + elog(ERROR, "invalid relation oid \"%d\"", relid); - if (OidIsValid(relid)) - { - funcctx = SRF_FIRSTCALL_INIT(); - oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - usr_ctx = (pg_dirtyread_ctx *) palloc(sizeof(pg_dirtyread_ctx)); - usr_ctx->rel = heap_open(relid, AccessShareLock); - usr_ctx->reltupdesc = RelationGetDescr(usr_ctx->rel); - get_call_result_type(fcinfo, NULL, &tupdesc); - funcctx->tuple_desc = BlessTupleDesc(tupdesc); - usr_ctx->map = convert_tuples_by_position(usr_ctx->reltupdesc, funcctx->tuple_desc, "Error converting tuple descriptors!"); - usr_ctx->scan = heap_beginscan(usr_ctx->rel, SnapshotAny, 0, NULL); - funcctx->user_fctx = (void *) usr_ctx; - MemoryContextSwitchTo(oldcontext); - } + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + usr_ctx = (pg_dirtyread_ctx *) palloc(sizeof(pg_dirtyread_ctx)); + usr_ctx->rel = heap_open(relid, AccessShareLock); + usr_ctx->reltupdesc = RelationGetDescr(usr_ctx->rel); + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function returning record called in context " + "that cannot accept type record"))); + funcctx->tuple_desc = BlessTupleDesc(tupdesc); + usr_ctx->map = dirtyread_convert_tuples_by_name(usr_ctx->reltupdesc, + funcctx->tuple_desc, "Error converting tuple descriptors!"); + usr_ctx->scan = heap_beginscan(usr_ctx->rel, SnapshotAny, 0, NULL); + /* only call GetOldestXmin while not in recovery */ + if (!RecoveryInProgress()) + usr_ctx->oldest_xmin = GetOldestXmin( +#if PG_VERSION_NUM >= 90400 + usr_ctx->rel +#else + false /* allDbs */ +#endif + , 0); + funcctx->user_fctx = (void *) usr_ctx; + MemoryContextSwitchTo(oldcontext); } funcctx = SRF_PERCALL_SETUP(); @@ -86,8 +116,13 @@ pg_dirtyread(PG_FUNCTION_ARGS) if ((tuplein = heap_getnext(usr_ctx->scan, ForwardScanDirection)) != NULL) { - tupleout = do_convert_tuple(tuplein, usr_ctx->map); - SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tupleout)); + if (usr_ctx->map != NULL) + { + tuplein = dirtyread_do_convert_tuple(tuplein, usr_ctx->map, usr_ctx->oldest_xmin); + SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuplein)); + } + else + SRF_RETURN_NEXT(funcctx, heap_copy_tuple_as_datum(tuplein, usr_ctx->reltupdesc)); } else { @@ -96,3 +131,6 @@ pg_dirtyread(PG_FUNCTION_ARGS) SRF_RETURN_DONE(funcctx); } } + +/* vim:et + */ diff --git a/contrib/pg_dirtyread/sql/dirtyread.sql b/contrib/pg_dirtyread/sql/dirtyread.sql new file mode 100644 index 0000000..66a7486 --- /dev/null +++ b/contrib/pg_dirtyread/sql/dirtyread.sql @@ -0,0 +1,51 @@ +-- Create table and disable autovacuum +CREATE TABLE foo (bar bigint, baz text); +ALTER TABLE foo SET ( + autovacuum_enabled = false, toast.autovacuum_enabled = false +); + +-- single row +INSERT INTO foo VALUES (1, 'Hello world'); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); +DELETE FROM foo; +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); + +VACUUM foo; + +-- multiple rows +INSERT INTO foo VALUES (1, 'Delete'), (2, 'Insert'), (3, 'Update'), (4, 'Not deleted'), (5, 'Not updated'); +DELETE FROM foo WHERE bar = 1; +UPDATE foo SET baz = 'Updated' WHERE bar = 3; +BEGIN; + DELETE FROM foo WHERE bar = 4; + UPDATE foo SET baz = 'Not quite updated' where bar = 5; + INSERT INTO foo VALUES (6, 'Not inserted'); +ROLLBACK; +SELECT * FROM foo; +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); + +-- system columns (don't show tableoid and xmin, but make sure they are numbers) +SELECT CASE WHEN tableoid >= 0 THEN 0 END AS tableoid, + ctid, + CASE WHEN xmin::text::int >= 0 THEN 0 END AS xmin, + CASE WHEN xmax::text <> '0' THEN xmax::text::int - xmin::text::int END AS xmax, + cmin, cmax, dead, oid, bar, baz + FROM pg_dirtyread('foo'::regclass) + AS t(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean, oid oid, bar bigint, baz text); + +-- error cases +SELECT pg_dirtyread('foo'::regclass); +SELECT * FROM pg_dirtyread(0) as t(bar bigint, baz text); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar int, baz text); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(moo bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(tableoid bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(ctid bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(xmin bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(xmax bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(cmin bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(cmax bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(dead bigint); +SELECT * FROM pg_dirtyread('foo'::regclass) as t(oid bigint); + +SET ROLE luser; +SELECT * FROM pg_dirtyread('foo'::regclass) as t(bar bigint, baz text); diff --git a/contrib/pg_dirtyread/sql/extension.sql b/contrib/pg_dirtyread/sql/extension.sql new file mode 100644 index 0000000..eac8818 --- /dev/null +++ b/contrib/pg_dirtyread/sql/extension.sql @@ -0,0 +1,6 @@ +CREATE EXTENSION pg_dirtyread; + +-- create a non-superuser role, ignoring any output/errors, it might already exist +SET client_min_messages = fatal; +\set QUIET on +CREATE ROLE luser; diff --git a/contrib/pg_dirtyread/tupconvert.c.upstream b/contrib/pg_dirtyread/tupconvert.c.upstream new file mode 100644 index 0000000..392a49b --- /dev/null +++ b/contrib/pg_dirtyread/tupconvert.c.upstream @@ -0,0 +1,390 @@ +/*------------------------------------------------------------------------- + * + * tupconvert.c + * Tuple conversion support. + * + * These functions provide conversion between rowtypes that are logically + * equivalent but might have columns in a different order or different sets + * of dropped columns. There is some overlap of functionality with the + * executor's "junkfilter" routines, but these functions work on bare + * HeapTuples rather than TupleTableSlots. + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/access/common/tupconvert.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/tupconvert.h" +#include "utils/builtins.h" + + +/* + * The conversion setup routines have the following common API: + * + * The setup routine checks whether the given source and destination tuple + * descriptors are logically compatible. If not, it throws an error. + * If so, it returns NULL if they are physically compatible (ie, no conversion + * is needed), else a TupleConversionMap that can be used by do_convert_tuple + * to perform the conversion. + * + * The TupleConversionMap, if needed, is palloc'd in the caller's memory + * context. Also, the given tuple descriptors are referenced by the map, + * so they must survive as long as the map is needed. + * + * The caller must supply a suitable primary error message to be used if + * a compatibility error is thrown. Recommended coding practice is to use + * gettext_noop() on this string, so that it is translatable but won't + * actually be translated unless the error gets thrown. + * + * + * Implementation notes: + * + * The key component of a TupleConversionMap is an attrMap[] array with + * one entry per output column. This entry contains the 1-based index of + * the corresponding input column, or zero to force a NULL value (for + * a dropped output column). The TupleConversionMap also contains workspace + * arrays. + */ + + +/* + * Set up for tuple conversion, matching input and output columns by + * position. (Dropped columns are ignored in both input and output.) + * + * Note: the errdetail messages speak of indesc as the "returned" rowtype, + * outdesc as the "expected" rowtype. This is okay for current uses but + * might need generalization in future. + */ +TupleConversionMap * +convert_tuples_by_position(TupleDesc indesc, + TupleDesc outdesc, + const char *msg) +{ + TupleConversionMap *map; + AttrNumber *attrMap; + int nincols; + int noutcols; + int n; + int i; + int j; + bool same; + + /* Verify compatibility and prepare attribute-number map */ + n = outdesc->natts; + attrMap = (AttrNumber *) palloc0(n * sizeof(AttrNumber)); + j = 0; /* j is next physical input attribute */ + nincols = noutcols = 0; /* these count non-dropped attributes */ + same = true; + for (i = 0; i < n; i++) + { + Form_pg_attribute att = outdesc->attrs[i]; + Oid atttypid; + int32 atttypmod; + + if (att->attisdropped) + continue; /* attrMap[i] is already 0 */ + noutcols++; + atttypid = att->atttypid; + atttypmod = att->atttypmod; + for (; j < indesc->natts; j++) + { + att = indesc->attrs[j]; + if (att->attisdropped) + continue; + nincols++; + /* Found matching column, check type */ + if (atttypid != att->atttypid || + (atttypmod != att->atttypmod && atttypmod >= 0)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg_internal("%s", _(msg)), + errdetail("Returned type %s does not match expected type %s in column %d.", + format_type_with_typemod(att->atttypid, + att->atttypmod), + format_type_with_typemod(atttypid, + atttypmod), + noutcols))); + attrMap[i] = (AttrNumber) (j + 1); + j++; + break; + } + if (attrMap[i] == 0) + same = false; /* we'll complain below */ + } + + /* Check for unused input columns */ + for (; j < indesc->natts; j++) + { + if (indesc->attrs[j]->attisdropped) + continue; + nincols++; + same = false; /* we'll complain below */ + } + + /* Report column count mismatch using the non-dropped-column counts */ + if (!same) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg_internal("%s", _(msg)), + errdetail("Number of returned columns (%d) does not match " + "expected column count (%d).", + nincols, noutcols))); + + /* + * Check to see if the map is one-to-one, in which case we need not do a + * tuple conversion. We must also insist that both tupdescs either + * specify or don't specify an OID column, else we need a conversion to + * add/remove space for that. (For some callers, presence or absence of + * an OID column perhaps would not really matter, but let's be safe.) + */ + if (indesc->natts == outdesc->natts && + indesc->tdhasoid == outdesc->tdhasoid) + { + for (i = 0; i < n; i++) + { + if (attrMap[i] == (i + 1)) + continue; + + /* + * If it's a dropped column and the corresponding input column is + * also dropped, we needn't convert. However, attlen and attalign + * must agree. + */ + if (attrMap[i] == 0 && + indesc->attrs[i]->attisdropped && + indesc->attrs[i]->attlen == outdesc->attrs[i]->attlen && + indesc->attrs[i]->attalign == outdesc->attrs[i]->attalign) + continue; + + same = false; + break; + } + } + else + same = false; + + if (same) + { + /* Runtime conversion is not needed */ + pfree(attrMap); + return NULL; + } + + /* Prepare the map structure */ + map = (TupleConversionMap *) palloc(sizeof(TupleConversionMap)); + map->indesc = indesc; + map->outdesc = outdesc; + map->attrMap = attrMap; + /* preallocate workspace for Datum arrays */ + map->outvalues = (Datum *) palloc(n * sizeof(Datum)); + map->outisnull = (bool *) palloc(n * sizeof(bool)); + n = indesc->natts + 1; /* +1 for NULL */ + map->invalues = (Datum *) palloc(n * sizeof(Datum)); + map->inisnull = (bool *) palloc(n * sizeof(bool)); + map->invalues[0] = (Datum) 0; /* set up the NULL entry */ + map->inisnull[0] = true; + + return map; +} + +/* + * Set up for tuple conversion, matching input and output columns by name. + * (Dropped columns are ignored in both input and output.) This is intended + * for use when the rowtypes are related by inheritance, so we expect an exact + * match of both type and typmod. The error messages will be a bit unhelpful + * unless both rowtypes are named composite types. + */ +TupleConversionMap * +convert_tuples_by_name(TupleDesc indesc, + TupleDesc outdesc, + const char *msg) +{ + TupleConversionMap *map; + AttrNumber *attrMap; + int n = outdesc->natts; + int i; + bool same; + + /* Verify compatibility and prepare attribute-number map */ + attrMap = convert_tuples_by_name_map(indesc, outdesc, msg); + + /* + * Check to see if the map is one-to-one, in which case we need not do a + * tuple conversion. We must also insist that both tupdescs either + * specify or don't specify an OID column, else we need a conversion to + * add/remove space for that. (For some callers, presence or absence of + * an OID column perhaps would not really matter, but let's be safe.) + */ + if (indesc->natts == outdesc->natts && + indesc->tdhasoid == outdesc->tdhasoid) + { + same = true; + for (i = 0; i < n; i++) + { + if (attrMap[i] == (i + 1)) + continue; + + /* + * If it's a dropped column and the corresponding input column is + * also dropped, we needn't convert. However, attlen and attalign + * must agree. + */ + if (attrMap[i] == 0 && + indesc->attrs[i]->attisdropped && + indesc->attrs[i]->attlen == outdesc->attrs[i]->attlen && + indesc->attrs[i]->attalign == outdesc->attrs[i]->attalign) + continue; + + same = false; + break; + } + } + else + same = false; + + if (same) + { + /* Runtime conversion is not needed */ + pfree(attrMap); + return NULL; + } + + /* Prepare the map structure */ + map = (TupleConversionMap *) palloc(sizeof(TupleConversionMap)); + map->indesc = indesc; + map->outdesc = outdesc; + map->attrMap = attrMap; + /* preallocate workspace for Datum arrays */ + map->outvalues = (Datum *) palloc(n * sizeof(Datum)); + map->outisnull = (bool *) palloc(n * sizeof(bool)); + n = indesc->natts + 1; /* +1 for NULL */ + map->invalues = (Datum *) palloc(n * sizeof(Datum)); + map->inisnull = (bool *) palloc(n * sizeof(bool)); + map->invalues[0] = (Datum) 0; /* set up the NULL entry */ + map->inisnull[0] = true; + + return map; +} + +/* + * Return a palloc'd bare attribute map for tuple conversion, matching input + * and output columns by name. (Dropped columns are ignored in both input and + * output.) This is normally a subroutine for convert_tuples_by_name, but can + * be used standalone. + */ +AttrNumber * +convert_tuples_by_name_map(TupleDesc indesc, + TupleDesc outdesc, + const char *msg) +{ + AttrNumber *attrMap; + int n; + int i; + + n = outdesc->natts; + attrMap = (AttrNumber *) palloc0(n * sizeof(AttrNumber)); + for (i = 0; i < n; i++) + { + Form_pg_attribute att = outdesc->attrs[i]; + char *attname; + Oid atttypid; + int32 atttypmod; + int j; + + if (att->attisdropped) + continue; /* attrMap[i] is already 0 */ + attname = NameStr(att->attname); + atttypid = att->atttypid; + atttypmod = att->atttypmod; + for (j = 0; j < indesc->natts; j++) + { + att = indesc->attrs[j]; + if (att->attisdropped) + continue; + if (strcmp(attname, NameStr(att->attname)) == 0) + { + /* Found it, check type */ + if (atttypid != att->atttypid || atttypmod != att->atttypmod) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg_internal("%s", _(msg)), + errdetail("Attribute \"%s\" of type %s does not match corresponding attribute of type %s.", + attname, + format_type_be(outdesc->tdtypeid), + format_type_be(indesc->tdtypeid)))); + attrMap[i] = (AttrNumber) (j + 1); + break; + } + } + if (attrMap[i] == 0) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg_internal("%s", _(msg)), + errdetail("Attribute \"%s\" of type %s does not exist in type %s.", + attname, + format_type_be(outdesc->tdtypeid), + format_type_be(indesc->tdtypeid)))); + } + + return attrMap; +} + +/* + * Perform conversion of a tuple according to the map. + */ +HeapTuple +do_convert_tuple(HeapTuple tuple, TupleConversionMap *map) +{ + AttrNumber *attrMap = map->attrMap; + Datum *invalues = map->invalues; + bool *inisnull = map->inisnull; + Datum *outvalues = map->outvalues; + bool *outisnull = map->outisnull; + int outnatts = map->outdesc->natts; + int i; + + /* + * Extract all the values of the old tuple, offsetting the arrays so that + * invalues[0] is left NULL and invalues[1] is the first source attribute; + * this exactly matches the numbering convention in attrMap. + */ + heap_deform_tuple(tuple, map->indesc, invalues + 1, inisnull + 1); + + /* + * Transpose into proper fields of the new tuple. + */ + for (i = 0; i < outnatts; i++) + { + int j = attrMap[i]; + + outvalues[i] = invalues[j]; + outisnull[i] = inisnull[j]; + } + + /* + * Now form the new tuple. + */ + return heap_form_tuple(map->outdesc, outvalues, outisnull); +} + +/* + * Free a TupleConversionMap structure. + */ +void +free_conversion_map(TupleConversionMap *map) +{ + /* indesc and outdesc are not ours to free */ + pfree(map->attrMap); + pfree(map->invalues); + pfree(map->inisnull); + pfree(map->outvalues); + pfree(map->outisnull); + pfree(map); +} diff --git a/contrib/pg_dirtyread/tupconvert.h.upstream b/contrib/pg_dirtyread/tupconvert.h.upstream new file mode 100644 index 0000000..e86cfd5 --- /dev/null +++ b/contrib/pg_dirtyread/tupconvert.h.upstream @@ -0,0 +1,49 @@ +/*------------------------------------------------------------------------- + * + * tupconvert.h + * Tuple conversion support. + * + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/access/tupconvert.h + * + *------------------------------------------------------------------------- + */ +#ifndef TUPCONVERT_H +#define TUPCONVERT_H + +#include "access/htup.h" +#include "access/tupdesc.h" + + +typedef struct TupleConversionMap +{ + TupleDesc indesc; /* tupdesc for source rowtype */ + TupleDesc outdesc; /* tupdesc for result rowtype */ + AttrNumber *attrMap; /* indexes of input fields, or 0 for null */ + Datum *invalues; /* workspace for deconstructing source */ + bool *inisnull; + Datum *outvalues; /* workspace for constructing result */ + bool *outisnull; +} TupleConversionMap; + + +extern TupleConversionMap *convert_tuples_by_position(TupleDesc indesc, + TupleDesc outdesc, + const char *msg); + +extern TupleConversionMap *convert_tuples_by_name(TupleDesc indesc, + TupleDesc outdesc, + const char *msg); + +extern AttrNumber *convert_tuples_by_name_map(TupleDesc indesc, + TupleDesc outdesc, + const char *msg); + +extern HeapTuple do_convert_tuple(HeapTuple tuple, TupleConversionMap *map); + +extern void free_conversion_map(TupleConversionMap *map); + +#endif /* TUPCONVERT_H */