pax_global_header00006660000000000000000000000064146323434150014517gustar00rootroot0000000000000052 comment=3e48547c79278efc238651e8b1e52c0e16f35912 pg_dirtyread-2.7/000077500000000000000000000000001463234341500140445ustar00rootroot00000000000000pg_dirtyread-2.7/.github/000077500000000000000000000000001463234341500154045ustar00rootroot00000000000000pg_dirtyread-2.7/.github/workflows/000077500000000000000000000000001463234341500174415ustar00rootroot00000000000000pg_dirtyread-2.7/.github/workflows/regression.yml000066400000000000000000000017071463234341500223510ustar00rootroot00000000000000name: Build on: push: pull_request: schedule: - cron: '42 10 10 * *' # Monthly jobs: build: runs-on: ubuntu-latest defaults: run: shell: sh strategy: matrix: pgversion: - 9.2 - 9.3 - 9.4 - 9.5 - 9.6 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 env: PGVERSION: ${{ matrix.pgversion }} steps: - name: checkout uses: actions/checkout@v2 - name: install pg run: | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -v $PGVERSION -p -i sudo -u postgres createuser -s "$USER" - name: build run: | make PROFILE="-Werror" sudo -E make install - name: test run: | make installcheck - name: show regression diffs if: ${{ failure() }} run: | cat regression.diffs pg_dirtyread-2.7/.gitignore000066400000000000000000000000671463234341500160370ustar00rootroot00000000000000regression.diffs regression.out results/ *.bc *.o *.so pg_dirtyread-2.7/.vimrc000066400000000000000000000000161463234341500151620ustar00rootroot00000000000000set ts=4 sw=4 pg_dirtyread-2.7/LICENSE000066400000000000000000000031671463234341500150600ustar00rootroot00000000000000Copyright (c) 1996-2024, 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. pg_dirtyread-2.7/Makefile000066400000000000000000000007271463234341500155120ustar00rootroot00000000000000MODULE_big = pg_dirtyread OBJS = pg_dirtyread.o dirtyread_tupconvert.o EXTENSION = pg_dirtyread DATA = pg_dirtyread--1.0.sql \ pg_dirtyread--1.0--2.sql pg_dirtyread--2.sql REGRESS = extension dirtyread oid index PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) # toast test works on PG14+ only ifneq ($(filter-out 9.% 10 11 12 13,$(MAJORVERSION)),) REGRESS += toast endif pg_dirtyread.o dirtyread_tupconvert.o: dirtyread_tupconvert.h pg_dirtyread-2.7/README.md000066400000000000000000000137361463234341500153350ustar00rootroot00000000000000pg_dirtyread ============ The pg_dirtyread extension provides the ability to read dead but unvacuumed rows from a relation. Supports PostgreSQL 9.2 and later. (On 9.2, at least 9.2.9 is required.) Building -------- To build pg_dirtyread, just do this: make make install If you encounter an error such as: make: pg_config: Command not found Be sure that you have `pg_config` installed and in your path. If you used a package management system such as RPM to install PostgreSQL, be sure that the `-devel` package is also installed. If necessary tell the build process where to find it: make PG_CONFIG=/path/to/pg_config make install PG_CONFIG=/path/to/pg_config 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') AS t(col1 type1, col2 type2, ...); ``` 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') as t(bar bigint, baz text); bar │ baz ─────┼────────── 1 │ Test 2 │ New Test ``` Dropped Columns --------------- The content of dropped columns can be retrieved as long as the table has not been rewritten (e.g. via `VACUUM FULL` or `CLUSTER`). Use `dropped_N` to access the Nth column, counting from 1. PostgreSQL deletes the type information of the original column, so only a few sanity checks can be done if the correct type was specified in the table alias; checked are type length, type alignment, type modifier, and pass-by-value. ```sql CREATE TABLE ab(a text, b text); INSERT INTO ab VALUES ('Hello', 'World'); ALTER TABLE ab DROP COLUMN b; DELETE FROM ab; SELECT * FROM pg_dirtyread('ab') ab(a text, dropped_2 text); a │ dropped_2 ───────┼─────────── Hello │ World ``` 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. The `oid` column is only available in PostgreSQL version 11 and earlier. ```sql SELECT * FROM pg_dirtyread('foo') AS t(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean, bar bigint, baz text); tableoid │ ctid │ xmin │ xmax │ cmin │ cmax │ dead │ bar │ baz ──────────┼───────┼──────┼──────┼──────┼──────┼──────┼─────┼─────────────────── 41823 │ (0,1) │ 1484 │ 1485 │ 0 │ 0 │ t │ 1 │ Delete 41823 │ (0,2) │ 1484 │ 0 │ 0 │ 0 │ f │ 2 │ Insert 41823 │ (0,3) │ 1484 │ 1486 │ 0 │ 0 │ t │ 3 │ Update 41823 │ (0,4) │ 1484 │ 1488 │ 0 │ 0 │ f │ 4 │ Not deleted 41823 │ (0,5) │ 1484 │ 1489 │ 1 │ 1 │ f │ 5 │ Not updated 41823 │ (0,6) │ 1486 │ 0 │ 0 │ 0 │ f │ 3 │ Updated 41823 │ (0,7) │ 1489 │ 0 │ 1 │ 1 │ t │ 5 │ Not quite updated 41823 │ (0,8) │ 1490 │ 0 │ 2 │ 2 │ t │ 6 │ Not inserted ``` Authors ------- pg_dirtyread 1.0 was written by Phil Sorber in 2012. Christoph Berg added the ability to retrieve system columns in version 1.1, released 2017, and took over further maintenance. License ------- Copyright (c) 1996-2024, 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. pg_dirtyread-2.7/contrib/000077500000000000000000000000001463234341500155045ustar00rootroot00000000000000pg_dirtyread-2.7/contrib/bad_tuples.sql000066400000000000000000000025761463234341500203610ustar00rootroot00000000000000create or replace function bad_tuples(relname regclass) returns table (page int, ctid tid, sqlstate text, sqlerrm text) as $$ declare pages int; page int; ctid tid; begin select pg_relation_size(relname) / current_setting('block_size')::int into pages; for page in 0 .. pages-1 loop if page % 10000 = 0 then raise notice '%: page % of %', relname, page, pages; end if; begin for ctid in select t_ctid from heap_page_items(get_raw_page(relname::text, page)) loop begin execute format('SELECT length(t::text) FROM %s t WHERE ctid=%L', relname, ctid); exception -- bad tuple when others then bad_tuples.page := page; bad_tuples.ctid := ctid; bad_tuples.sqlstate := sqlstate; bad_tuples.sqlerrm := sqlerrm; return next; end; end loop; exception -- bad page when undefined_function then raise exception undefined_function using message = SQLERRM, hint = 'Use CREATE EXTENSION pageinspect; to create it'; when others then bad_tuples.page := page; bad_tuples.sqlstate := sqlstate; bad_tuples.sqlerrm := sqlerrm; return next; end; end loop; end; $$ language plpgsql; comment on function bad_tuples(regclass) is 'return ctids of all tuples in a table that trigger an error'; pg_dirtyread-2.7/contrib/foreign_key.sql000066400000000000000000000037701463234341500205350ustar00rootroot00000000000000create or replace function check_foreign_key(con_oid oid, max_missing bigint default 100, missing out text) returns setof text language plpgsql as $$-- Author: Christoph Berg declare con_name text; def text; rel text; relcols text; -- referencing table frel text; frelcols text; -- referenced table fkpred text; query text; missing_count bigint default 0; begin select into strict con_name, def, rel, relcols, frel, frelcols, fkpred conname, pg_get_constraintdef(oid, true), conrelid::regclass, relcolumns, confrelid::regclass, frelcolumns, fkpredicate from pg_constraint, lateral (select string_agg(format('%I', a1.attname), ', ') relcolumns, string_agg(format('%I', a2.attname), ', ') frelcolumns, string_agg(format('rel.%I = frel.%I', a1.attname, a2.attname), ' and ') fkpredicate from generate_subscripts(conkey, 1) u, lateral (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[u]) a1, lateral (select attname from pg_attribute where attrelid = confrelid and attnum = confkey[u]) a2 ) consub where oid = con_oid; raise notice 'FK % on %: %', con_name, rel, def; query := format('select (%s) from %s rel where (%s) is not null and not exists (select from %s frel where %s)', relcols, rel, relcols, frel, fkpred); --raise notice '%', query; for missing in execute query loop missing_count := missing_count + 1; if max_missing is not null and missing_count > max_missing then raise notice 'Stopping after % missing keys', missing_count; exit; end if; return next; end loop; if missing_count > 0 then raise warning 'Found % rows in table % (%) missing in table % (%)', missing_count, rel, relcols, frel, frelcols; end if; return; end$$; comment on function check_foreign_key is 'Check FOREIGN KEY for missing rows'; /* Suggested usage: \x \t select format('select * from check_foreign_key(%s)', oid) from pg_constraint where contype = 'f' \gexec */ pg_dirtyread-2.7/contrib/index_duplicates.sql000066400000000000000000000032471463234341500215570ustar00rootroot00000000000000create or replace function check_unique_index(relid regclass, max_dupes bigint default 100, dupe out text, count out text) returns setof record language plpgsql set enable_indexscan = off set enable_indexonlyscan = off set enable_bitmapscan = off as $$-- Author: Christoph Berg declare tbl text; isunique boolean; key text; query text; dupe_count bigint default 0; begin select into strict tbl, isunique indrelid::regclass, indisunique from pg_index where indexrelid = relid; select into strict key string_agg(quote_ident(attname), ', ') from pg_index i, unnest(indkey) u(indcolumn), lateral (select attrelid, attnum, attname from pg_attribute) a where i.indrelid=a.attrelid and u.indcolumn = a.attnum and indexrelid = relid; raise notice 'Checking index % on % (%)', relid, tbl, key; if not isunique then raise warning 'Index % is not UNIQUE', relid; end if; query := format('select (%s) dupe, count(*) from %s where (%s) is not null group by %s having count(*) > 1', key, tbl, key, key); --raise notice '%', query; for dupe, count in execute query loop dupe_count := dupe_count + 1; if max_dupes is not null and dupe_count > max_dupes then raise notice 'Stopping after % duplicate keys', dupe_count; exit; end if; return next; end loop; if dupe_count > 0 then raise warning 'Found % duplicates in table %, index % on (%)', dupe_count, tbl, relid, key; end if; return; end$$; comment on function check_unique_index is 'Check UNIQUE index for duplicates'; /* Suggested usage: \x \t select format('select * from check_unique_index(%L)', indexrelid::regclass) from pg_index where indisunique \gexec */ pg_dirtyread-2.7/contrib/infomask.sql000066400000000000000000000061561463234341500200440ustar00rootroot00000000000000CREATE FUNCTION t_infomask(i IN integer, HASNULL OUT boolean, HASVARWIDTH OUT boolean, HASEXTERNAL OUT boolean, HASOID_OLD OUT boolean, XMAX_KEYSHR_LOCK OUT boolean, COMBOCID OUT boolean, XMAX_EXCL_LOCK OUT boolean, XMAX_LOCK_ONLY OUT boolean, XMIN_COMMITTED OUT boolean, XMIN_INVALID OUT boolean, XMAX_COMMITTED OUT boolean, XMAX_INVALID OUT boolean, XMAX_IS_MULTI OUT boolean, UPDATED OUT boolean, MOVED_OFF OUT boolean, MOVED_IN OUT boolean) LANGUAGE SQL AS $$SELECT /* HASNULL */ i & x'0001'::int <> 0, /* has null attribute(s) */ /* HASVARWIDTH */ i & x'0002'::int <> 0, /* has variable-width attribute(s) */ /* HASEXTERNAL */ i & x'0004'::int <> 0, /* has external stored attribute(s) */ /* HASOID_OLD */ i & x'0008'::int <> 0, /* has an object-id field */ /* XMAX_KEYSHR_LOCK */ i & x'0010'::int <> 0, /* xmax is a key-shared locker */ /* COMBOCID */ i & x'0020'::int <> 0, /* t_cid is a combo cid */ /* XMAX_EXCL_LOCK */ i & x'0040'::int <> 0, /* xmax is exclusive locker */ /* XMAX_LOCK_ONLY */ i & x'0080'::int <> 0, /* xmax, if valid, is only a locker */ /* XMIN_COMMITTED */ i & x'0100'::int <> 0, /* t_xmin committed */ /* XMIN_INVALID */ i & x'0200'::int <> 0, /* t_xmin invalid/aborted */ /* XMAX_COMMITTED */ i & x'0400'::int <> 0, /* t_xmax committed */ /* XMAX_INVALID */ i & x'0800'::int <> 0, /* t_xmax invalid/aborted */ /* XMAX_IS_MULTI */ i & x'1000'::int <> 0, /* t_xmax is a MultiXactId */ /* UPDATED */ i & x'2000'::int <> 0, /* this is UPDATEd version of row */ /* MOVED_OFF */ i & x'4000'::int <> 0, /* moved to another place by pre-9.0 */ /* MOVED_IN */ i & x'8000'::int <> 0 /* moved from another place by pre-9.0 */ $$; CREATE FUNCTION t_infomask2(i2 IN integer, NATTS OUT integer, KEYS_UPDATED OUT boolean, HOT_UPDATED OUT boolean, ONLY_TUPLE OUT boolean) LANGUAGE SQL AS $$SELECT /* NATTS_MASK */ i2 & x'07FF'::int, /* 11 bits for number of attributes */ /* bits 0x1800 are available */ /* KEYS_UPDATED */ i2 & x'2000'::int <> 0, /* tuple was updated and key cols modified, or tuple deleted */ /* HOT_UPDATED */ i2 & x'4000'::int <> 0, /* tuple was HOT-updated */ /* ONLY_TUPLE */ i2 & x'8000'::int <> 0 /* this is heap-only tuple */ $$; CREATE FUNCTION t_infomask(i IN integer, i2 IN integer, HASNULL OUT boolean, HASVARWIDTH OUT boolean, HASEXTERNAL OUT boolean, HASOID_OLD OUT boolean, XMAX_KEYSHR_LOCK OUT boolean, COMBOCID OUT boolean, XMAX_EXCL_LOCK OUT boolean, XMAX_LOCK_ONLY OUT boolean, XMIN_COMMITTED OUT boolean, XMIN_INVALID OUT boolean, XMAX_COMMITTED OUT boolean, XMAX_INVALID OUT boolean, XMAX_IS_MULTI OUT boolean, UPDATED OUT boolean, MOVED_OFF OUT boolean, MOVED_IN OUT boolean, NATTS OUT integer, KEYS_UPDATED OUT boolean, HOT_UPDATED OUT boolean, ONLY_TUPLE OUT boolean) LANGUAGE SQL AS $$SELECT * FROM t_infomask(i), t_infomask2(i2)$$; pg_dirtyread-2.7/contrib/pg_xact.sql000066400000000000000000000022611463234341500176530ustar00rootroot00000000000000/* src/include/access/clog.h #define TRANSACTION_STATUS_IN_PROGRESS 0x00 #define TRANSACTION_STATUS_COMMITTED 0x01 #define TRANSACTION_STATUS_ABORTED 0x02 #define TRANSACTION_STATUS_SUB_COMMITTED 0x03 */ /* SLRU_PAGES_PER_SEGMENT*BLCKSZ*CLOG_XACTS_PER_BYTE = 1M transactions per file */ CREATE OR REPLACE FUNCTION pg_xact(start bigint, stop bigint, file text DEFAULT 'pg_xact/0000') RETURNS TABLE(xid bigint, status text) LANGUAGE SQL AS $$WITH xact(xact) AS (SELECT pg_read_binary_file(file)) SELECT i, CASE 2*get_bit(xact, 2*i::int+1) + get_bit(xact, 2*i::int) WHEN 0 THEN 'in progress' WHEN 1 THEN 'committed' WHEN 2 THEN 'aborted' WHEN 3 THEN 'subtransaction commited' END FROM xact, generate_series(start, stop) g(i) $$; CREATE OR REPLACE FUNCTION pg_xact(xid bigint) RETURNS text LANGUAGE SQL AS $$WITH xact(xact, off) AS (SELECT pg_read_binary_file('pg_xact/' || repeat('0', 4-length(to_hex(xid >> 20))) || to_hex(xid >> 20)), 2 * (xid % (1<<20))::int) SELECT CASE 2 * get_bit(xact, off + 1)::int + get_bit(xact, off)::int WHEN 0 THEN 'in progress' WHEN 1 THEN 'committed' WHEN 2 THEN 'aborted' WHEN 3 THEN 'subtransaction commited' END FROM xact$$; pg_dirtyread-2.7/contrib/read_table.sql000066400000000000000000000026721463234341500203160ustar00rootroot00000000000000create or replace function read_table(relname regclass) returns setof record as $$ declare pages int; page int; ctid tid; r record; sql_state text; error text; begin select pg_relation_size(relname) / current_setting('block_size')::int into pages; for page in 0 .. pages-1 loop begin for ctid in select t_ctid from heap_page_items(get_raw_page(relname::text, page)) loop begin execute format('SELECT * FROM %s WHERE ctid=%L', relname, ctid) into r; if r is not null then return next r; end if; exception -- bad tuple when others then get stacked diagnostics sql_state := RETURNED_SQLSTATE; get stacked diagnostics error := MESSAGE_TEXT; raise notice 'Skipping ctid %: %: %', ctid, sql_state, error; end; end loop; exception -- bad page when undefined_function then raise exception undefined_function using message = SQLERRM, hint = 'Use CREATE EXTENSION pageinspect; to create it'; when others then get stacked diagnostics sql_state := RETURNED_SQLSTATE; get stacked diagnostics error := MESSAGE_TEXT; raise notice 'Skipping page %: %: %', page, sql_state, error; end; end loop; end; $$ language plpgsql; comment on function read_table(regclass) is 'read all good tuples from a table, skipping over all tuples that trigger an error'; pg_dirtyread-2.7/contrib/rescue_table.sql000066400000000000000000000044751463234341500206740ustar00rootroot00000000000000create or replace function rescue_table(relname regclass, savename name default null, "create" boolean default true, start_block int default 0, end_block int default null) returns text as $$ declare pages int; page int; ctid tid; row_count bigint; good_tuples bigint := 0; bad_pages bigint := 0; bad_tuples bigint := 0; sql_state text; error text; begin if savename is null then savename := relname || '_rescue'; end if; if rescue_table.create then execute format('CREATE TABLE %s (LIKE %s)', savename, relname); end if; select pg_relation_size(relname) / current_setting('block_size')::int into pages; if end_block is null or end_block > pages-1 then end_block := pages-1; end if; for page in start_block .. end_block loop if page % 10000 = 0 then raise notice '%: page % of %', relname, page, pages; end if; begin for ctid in select t_ctid from heap_page_items(get_raw_page(relname::text, page)) loop begin execute format('INSERT INTO %s SELECT * FROM %s WHERE ctid=%L', savename, relname, ctid); get diagnostics row_count = ROW_COUNT; good_tuples := good_tuples + row_count; exception -- bad tuple when others then get stacked diagnostics sql_state := RETURNED_SQLSTATE; get stacked diagnostics error := MESSAGE_TEXT; raise notice 'Skipping ctid %: %: %', ctid, sql_state, error; bad_tuples := bad_tuples + 1; end; end loop; exception -- bad page when undefined_function then raise exception undefined_function using message = SQLERRM, hint = 'Use CREATE EXTENSION pageinspect; to create it'; when others then get stacked diagnostics sql_state := RETURNED_SQLSTATE; get stacked diagnostics error := MESSAGE_TEXT; raise notice 'Skipping page %: %: %', page, sql_state, error; bad_pages := bad_pages + 1; end; end loop; error := format('rescue_table %s into %s: %s of %s pages are bad, %s bad tuples, %s tuples copied', relname, savename, bad_pages, pages, bad_tuples, good_tuples); raise log '%', error; return error; end; $$ language plpgsql; comment on function rescue_table(regclass, name, boolean, int, int) is 'copy all good tuples from a table to another one'; pg_dirtyread-2.7/debian/000077500000000000000000000000001463234341500152665ustar00rootroot00000000000000pg_dirtyread-2.7/debian/.gitignore000066400000000000000000000000541463234341500172550ustar00rootroot00000000000000*debhelper* files postgresql-*/ *.substvars pg_dirtyread-2.7/debian/changelog000066400000000000000000000065021463234341500171430ustar00rootroot00000000000000pg-dirtyread (2.7-1) unstable; urgency=medium * Set SO_TYPE_SEQSCAN in heap_beginscan() to support PG17. * Exercise toast in tests. -- Christoph Berg Wed, 12 Jun 2024 18:03:05 +0200 pg-dirtyread (2.6-2) unstable; urgency=medium * Upload for PostgreSQL 16. * Use ${postgresql:Depends}. -- Christoph Berg Sun, 17 Sep 2023 19:01:39 +0200 pg-dirtyread (2.6-1) unstable; urgency=medium * Update test output for PG16. -- Christoph Berg Mon, 03 Jul 2023 20:06:54 +0200 pg-dirtyread (2.5-1) unstable; urgency=medium * Upload for PostgreSQL 15. * New upstream version. * debian/watch: Look at GitHub tags instead of releases. -- Christoph Berg Fri, 21 Oct 2022 10:36:40 +0200 pg-dirtyread (2.4-1) unstable; urgency=medium * New upstream version with support for PostgreSQL 14. * Update package URLs. -- Christoph Berg Tue, 16 Nov 2021 19:07:15 +0100 pg-dirtyread (2.3-2) unstable; urgency=medium * Upload for PostgreSQL 13. * Use dh --with pgxs. * R³: no. * DH 13. * debian/tests: Use 'make' instead of postgresql-server-dev-all. -- Christoph Berg Mon, 19 Oct 2020 11:11:52 +0200 pg-dirtyread (2.3-1) unstable; urgency=medium * Support PostgreSQL 13. -- Christoph Berg Thu, 21 May 2020 23:06:52 +0200 pg-dirtyread (2.2-1) unstable; urgency=medium * New upstream version, upload for PostgreSQL 12. + regress: Try reading from an index. + Contrib: Add function to read all good tuples from a table, skipping over all tuples that trigger an error, add function to return ctids of all tuples in a table that trigger an error. + README: Fix instructions on setting PG_CONFIG. -- Christoph Berg Fri, 25 Oct 2019 13:18:47 +0200 pg-dirtyread (2.1-1) unstable; urgency=medium * Support PostgreSQL 12. -- Christoph Berg Fri, 14 Jun 2019 15:43:03 +0200 pg-dirtyread (2.0-3) unstable; urgency=medium * Update PostgreSQL Maintainers address. -- Christoph Berg Thu, 07 Feb 2019 11:26:25 +0100 pg-dirtyread (2.0-2) unstable; urgency=medium * Upload for PostgreSQL 11. * Update watch file to ignore debian/ tags. -- Christoph Berg Fri, 12 Oct 2018 12:54:36 +0200 pg-dirtyread (2.0-1) unstable; urgency=medium * Change pg_dirtyread to take regclass as argument. * Add watch file, change source format to 3.0 (quilt). -- Christoph Berg Mon, 23 Jul 2018 22:44:04 +0200 pg-dirtyread (1.3) unstable; urgency=medium * Upload for PostgreSQL 10. * Use TupleDescAttr to access tuple descriptor attributes. -- Christoph Berg Sat, 23 Sep 2017 22:59:08 +0200 pg-dirtyread (1.2) unstable; urgency=medium * Refuse to return the "dead" pseudo column during recovery, because GetOldestXmin() asserts !RecoveryInProgress(). Spotted by Andreas Seltenreich, thanks! -- Christoph Berg Sun, 06 Aug 2017 16:57:41 +0200 pg-dirtyread (1.1) unstable; urgency=medium * Initial release. * Changes from 1.0: + Fix some crashes. + Add ability to retrieve system columns such as xmax and ctid. + Add "dead" column to allow identification of removed rows -- Christoph Berg Sun, 23 Jul 2017 12:47:01 +0200 pg_dirtyread-2.7/debian/control000066400000000000000000000012731463234341500166740ustar00rootroot00000000000000Source: pg-dirtyread Section: database Priority: optional Maintainer: Debian PostgreSQL Maintainers Uploaders: Christoph Berg Build-Depends: debhelper-compat (= 13), postgresql-all (>= 217~) Standards-Version: 4.6.2 Rules-Requires-Root: no Vcs-Browser: https://github.com/df7cb/pg_dirtyread Vcs-Git: https://github.com/df7cb/pg_dirtyread.git Package: postgresql-16-dirtyread Architecture: any Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends} Description: Read dead but unvacuumed tuples from a PostgreSQL relation The pg_dirtyread extension provides the ability to read dead but unvacuumed rows from a PostgreSQL relation. pg_dirtyread-2.7/debian/control.in000066400000000000000000000013021463234341500172720ustar00rootroot00000000000000Source: pg-dirtyread Section: database Priority: optional Maintainer: Debian PostgreSQL Maintainers Uploaders: Christoph Berg Build-Depends: debhelper-compat (= 13), postgresql-all (>= 217~) Standards-Version: 4.6.2 Rules-Requires-Root: no Vcs-Browser: https://github.com/df7cb/pg_dirtyread Vcs-Git: https://github.com/df7cb/pg_dirtyread.git Package: postgresql-PGVERSION-dirtyread Architecture: any Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends} Description: Read dead but unvacuumed tuples from a PostgreSQL relation The pg_dirtyread extension provides the ability to read dead but unvacuumed rows from a PostgreSQL relation. pg_dirtyread-2.7/debian/copyright000066400000000000000000000035361463234341500172300ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: pg_dirtyread Source: https://github.com/df7cb/pg_dirtyread Files: * Copyright: 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 License: BSD-like 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. pg_dirtyread-2.7/debian/pgversions000066400000000000000000000000431463234341500174050ustar00rootroot00000000000000# needs HeapTupleIsSurelyDead 9.2+ pg_dirtyread-2.7/debian/rules000077500000000000000000000001431463234341500163440ustar00rootroot00000000000000#!/usr/bin/make -f override_dh_installdocs: dh_installdocs --all README.* %: dh $@ --with pgxs pg_dirtyread-2.7/debian/source/000077500000000000000000000000001463234341500165665ustar00rootroot00000000000000pg_dirtyread-2.7/debian/source/format000066400000000000000000000000141463234341500177740ustar00rootroot000000000000003.0 (quilt) pg_dirtyread-2.7/debian/tests/000077500000000000000000000000001463234341500164305ustar00rootroot00000000000000pg_dirtyread-2.7/debian/tests/control000066400000000000000000000001001463234341500200220ustar00rootroot00000000000000Depends: @, make Tests: installcheck Restrictions: allow-stderr pg_dirtyread-2.7/debian/tests/installcheck000077500000000000000000000000431463234341500210170ustar00rootroot00000000000000#!/bin/sh pg_buildext installcheck pg_dirtyread-2.7/debian/watch000066400000000000000000000001111463234341500163100ustar00rootroot00000000000000version=4 https://github.com/df7cb/pg_dirtyread/tags .*/([0-9.]*).tar.gz pg_dirtyread-2.7/dirtyread_tupconvert.c000066400000000000000000000301661463234341500204760ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * Copy of PostgreSQL 11's tupconvert.c for use by pg_dirtyread. The difference * is added support for system columns like xmin/xmax/oid. PostgreSQL 14 * refactored it a lot, but the PG 11 version still works, so we stick with it. * * 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-2019, 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" #if PG_VERSION_NUM >= 120000 #include "access/heapam.h" #else #include "utils/tqual.h" /* HeapTupleIsSurelyDead */ #endif #include "dirtyread_tupconvert.h" #if PG_VERSION_NUM < 100000 /* from src/include/access/tupdesc.h, introduced in 2cd708452 */ #define TupleDescAttr(tupdesc, i) ((tupdesc)->attrs[(i)]) #endif /* * 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 #if PG_VERSION_NUM < 120000 && indesc->tdhasoid == outdesc->tdhasoid #endif ) { same = true; for (i = 0; i < n; i++) { Form_pg_attribute inatt; Form_pg_attribute outatt; 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. */ inatt = TupleDescAttr(indesc, i); outatt = TupleDescAttr(outdesc, i); if (attrMap[i] == 0 && inatt->attisdropped && inatt->attlen == outatt->attlen && inatt->attalign == outatt->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; #if PG_VERSION_NUM >= 130000 /* TupleConversionMap->attrMap changed in PG13; luckily our old data structure is just a member of that */ map->attrMap = (AttrMap *) palloc(sizeof(AttrMap)); map->attrMap->attnums = attrMap; map->attrMap->maplen = n; #else map->attrMap = attrMap; #endif /* 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 }, #if PG_VERSION_NUM < 120000 { "oid", OIDOID, -1, ObjectIdAttributeNumber }, #endif { "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. * * This version from dirtyread_tupconvert.c adds the ability to retrieve dropped * columns by requesting "dropped_N" as output column, where N is the attnum. */ 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 outatt = TupleDescAttr(outdesc, i); char *attname; Oid atttypid; int32 atttypmod; int j; if (outatt->attisdropped) continue; /* attrMap[i] is already 0 */ attname = NameStr(outatt->attname); atttypid = outatt->atttypid; atttypmod = outatt->atttypmod; for (j = 0; j < indesc->natts; j++) { Form_pg_attribute inatt = TupleDescAttr(indesc, j); if (inatt->attisdropped) continue; if (strcmp(attname, NameStr(inatt->attname)) == 0) { /* Found it, check type */ if (atttypid != inatt->atttypid || atttypmod != inatt->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(inatt->atttypid, inatt->atttypmod), format_type_be(indesc->tdtypeid)))); attrMap[i] = (AttrNumber) (j + 1); break; } } /* Check dropped columns */ if (attrMap[i] == 0) if (strncmp(attname, "dropped_", sizeof("dropped_") - 1) == 0) { Form_pg_attribute inatt; j = atoi(attname + sizeof("dropped_") - 1); if (j < 1 || j > indesc->natts) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg_internal("%s", _(msg)), errdetail("Attribute \"%s\" index is out of range 1 .. %d.", attname, indesc->natts))); inatt = TupleDescAttr(indesc, j - 1); if (! inatt->attisdropped) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg_internal("%s", _(msg)), errdetail("Attribute %d is not a dropped column.", j))); if (outatt->attlen != inatt->attlen) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg_internal("%s", _(msg)), errdetail("Type length of dropped column \"%s\" was %d.", attname, inatt->attlen))); if (outatt->attbyval != inatt->attbyval) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg_internal("%s", _(msg)), errdetail("\"By value\" of dropped column \"%s\" does not match.", attname))); if (outatt->attalign != inatt->attalign) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg_internal("%s", _(msg)), errdetail("Alignment of dropped column \"%s\" was %c.", attname, inatt->attalign))); inatt->atttypid = atttypid; if (atttypmod != inatt->atttypmod) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg_internal("%s", _(msg)), errdetail("Type modifier of dropped column \"%s\" was %s.", attname, format_type_with_typemod(inatt->atttypid, inatt->atttypmod)))); attrMap[i] = (AttrNumber) j; } /* 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, OldestXminType oldest_xmin) { AttrNumber *attrMap = #if PG_VERSION_NUM >= 130000 map->attrMap->attnums; #else map->attrMap; #endif 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); } pg_dirtyread-2.7/dirtyread_tupconvert.h000066400000000000000000000021361463234341500204770ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * tupconvert.h * Tuple conversion support. * * * Portions Copyright (c) 1996-2018, 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" #include "utils/snapmgr.h" #if PG_VERSION_NUM >= 140000 #define OldestXminType GlobalVisState * #else #define OldestXminType TransactionId #endif 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, OldestXminType oldest_xmin); #define DeadFakeAttributeNumber FirstLowInvalidHeapAttributeNumber #endif /* TUPCONVERT_H */ pg_dirtyread-2.7/expected/000077500000000000000000000000001463234341500156455ustar00rootroot00000000000000pg_dirtyread-2.7/expected/dirtyread.out000066400000000000000000000222731463234341500203730ustar00rootroot00000000000000-- 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') as t(bar bigint, baz text); bar | baz -----+------------- 1 | Hello world (1 row) DELETE FROM foo; SELECT * FROM pg_dirtyread('foo') 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') 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, bar, baz FROM pg_dirtyread('foo') AS t(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean, bar bigint, baz text); tableoid | ctid | xmin | xmax | cmin | cmax | dead | bar | baz ----------+-------+------+------+------+------+------+-----+------------------- 0 | (0,1) | 0 | 1 | 0 | 0 | t | 1 | Delete 0 | (0,2) | 0 | | 0 | 0 | f | 2 | Insert 0 | (0,3) | 0 | 2 | 0 | 0 | t | 3 | Update 0 | (0,4) | 0 | 3 | 0 | 0 | f | 4 | Not deleted 0 | (0,5) | 0 | 3 | 1 | 1 | f | 5 | Not updated 0 | (0,6) | 0 | | 0 | 0 | f | 3 | Updated 0 | (0,7) | 0 | | 1 | 1 | t | 5 | Not quite updated 0 | (0,8) | 0 | | 2 | 2 | t | 6 | Not inserted (8 rows) -- error cases SELECT pg_dirtyread('foo'); 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(NULL) as t(bar bigint, baz text); ERROR: invalid relation oid "0" SELECT * FROM pg_dirtyread('foo') 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') as t(moo bigint); ERROR: Error converting tuple descriptors! DETAIL: Attribute "moo" does not exist in type foo. SELECT * FROM pg_dirtyread('foo') 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') 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') 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') 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') 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') 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') as t(dead bigint); ERROR: Error converting tuple descriptors! DETAIL: Attribute "dead" has type boolean in corresponding attribute of type foo. SET ROLE luser; SELECT * FROM pg_dirtyread('foo') as t(bar bigint, baz text); ERROR: must be superuser to use pg_dirtyread RESET ROLE; -- reading from dropped columns CREATE TABLE bar ( id int, a int, b bigint, c text, d varchar(10), e boolean, f bigint[], z int ); ALTER TABLE bar SET ( autovacuum_enabled = false, toast.autovacuum_enabled = false ); INSERT INTO bar VALUES (1, 2, 3, '4', '5', true, '{7}', 8); ALTER TABLE bar DROP COLUMN a, DROP COLUMN b, DROP COLUMN c, DROP COLUMN d, DROP COLUMN e, DROP COLUMN f; INSERT INTO bar VALUES (2, 8); SELECT * FROM bar; id | z ----+--- 1 | 8 2 | 8 (2 rows) SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); id | dropped_2 | dropped_3 | dropped_4 | dropped_5 | dropped_6 | dropped_7 | z ----+-----------+-----------+-----------+-----------+-----------+-----------+--- 1 | 2 | 3 | 4 | 5 | t | {7} | 8 2 | | | | | | | 8 (2 rows) -- errors SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_0 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); ERROR: Error converting tuple descriptors! DETAIL: Attribute "dropped_0" index is out of range 1 .. 8. SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_9 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); ERROR: Error converting tuple descriptors! DETAIL: Attribute "dropped_9" index is out of range 1 .. 8. SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 bigint, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); ERROR: Error converting tuple descriptors! DETAIL: Type length of dropped column "dropped_2" was 4. SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 int, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); ERROR: Error converting tuple descriptors! DETAIL: Type length of dropped column "dropped_3" was 8. -- mismatch not catched: SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 timestamptz, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); id | dropped_2 | dropped_3 | dropped_4 | dropped_5 | dropped_6 | dropped_7 | z ----+-----------+-------------------------------------+-----------+-----------+-----------+-----------+--- 1 | 2 | Fri Dec 31 16:00:00.000003 1999 PST | 4 | 5 | t | {7} | 8 2 | | | | | | | 8 (2 rows) SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 int, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); ERROR: Error converting tuple descriptors! DETAIL: Type length of dropped column "dropped_4" was -1. SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(11), dropped_6 boolean, dropped_7 bigint[], z int); ERROR: Error converting tuple descriptors! DETAIL: Type modifier of dropped column "dropped_5" was character varying(10). SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 text, dropped_7 bigint[], z int); ERROR: Error converting tuple descriptors! DETAIL: Type length of dropped column "dropped_6" was 1. SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 int[], z int); ERROR: Error converting tuple descriptors! DETAIL: Alignment of dropped column "dropped_7" was d. -- mismatch not catched: SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 timestamptz[], z int); id | dropped_2 | dropped_3 | dropped_4 | dropped_5 | dropped_6 | dropped_7 | z ----+-----------+-----------+-----------+-----------+-----------+-----------+--- 1 | 2 | 3 | 4 | 5 | t | {7} | 8 2 | | | | | | | 8 (2 rows) -- clean table VACUUM FULL bar; SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); id | dropped_2 | dropped_3 | dropped_4 | dropped_5 | dropped_6 | dropped_7 | z ----+-----------+-----------+-----------+-----------+-----------+-----------+--- 1 | | | | | | | 8 2 | | | | | | | 8 (2 rows) pg_dirtyread-2.7/expected/extension.out000066400000000000000000000003141463234341500204100ustar00rootroot00000000000000CREATE EXTENSION pg_dirtyread; -- create a non-superuser role, ignoring any output/errors, it might already exist DO $$ BEGIN CREATE ROLE luser; EXCEPTION WHEN duplicate_object THEN NULL; END; $$; pg_dirtyread-2.7/expected/index.out000066400000000000000000000003651463234341500175110ustar00rootroot00000000000000select setting::int >= 160000 as is_pg16 from pg_settings where name = 'server_version_num'; is_pg16 --------- f (1 row) CREATE INDEX ON foo(bar); SELECT * FROM pg_dirtyread('foo_bar_idx') as t(bar bigint); ERROR: "foo_bar_idx" is an index pg_dirtyread-2.7/expected/index_1.out000066400000000000000000000004641463234341500177310ustar00rootroot00000000000000select setting::int >= 160000 as is_pg16 from pg_settings where name = 'server_version_num'; is_pg16 --------- t (1 row) CREATE INDEX ON foo(bar); SELECT * FROM pg_dirtyread('foo_bar_idx') as t(bar bigint); ERROR: cannot open relation "foo_bar_idx" DETAIL: This operation is not supported for indexes. pg_dirtyread-2.7/expected/oid.out000066400000000000000000000007731463234341500171600ustar00rootroot00000000000000-- test oid columns (removed in PostgreSQL 12) SELECT setting::int >= 120000 AS is_pg_12 FROM pg_settings WHERE name = 'server_version_num'; is_pg_12 ---------- t (1 row) SELECT * FROM pg_dirtyread('foo') AS t(oid oid, bar bigint, baz text); ERROR: Error converting tuple descriptors! DETAIL: Attribute "oid" does not exist in type foo. -- error cases SELECT * FROM pg_dirtyread('foo') as t(oid bigint); ERROR: Error converting tuple descriptors! DETAIL: Attribute "oid" does not exist in type foo. pg_dirtyread-2.7/expected/oid_1.out000066400000000000000000000012711463234341500173720ustar00rootroot00000000000000-- test oid columns (removed in PostgreSQL 12) SELECT setting::int >= 120000 AS is_pg_12 FROM pg_settings WHERE name = 'server_version_num'; is_pg_12 ---------- f (1 row) SELECT * FROM pg_dirtyread('foo') AS t(oid oid, bar bigint, baz text); oid | bar | baz -----+-----+------------------- 0 | 1 | Delete 0 | 2 | Insert 0 | 3 | Update 0 | 4 | Not deleted 0 | 5 | Not updated 0 | 3 | Updated 0 | 5 | Not quite updated 0 | 6 | Not inserted (8 rows) -- error cases SELECT * FROM pg_dirtyread('foo') as t(oid bigint); ERROR: Error converting tuple descriptors! DETAIL: Attribute "oid" has type oid in corresponding attribute of type foo. pg_dirtyread-2.7/expected/toast.out000066400000000000000000000052531463234341500175350ustar00rootroot00000000000000create table toast ( description text, data text ); insert into toast values ('short inline', 'xxx'); insert into toast values ('long inline uncompressed', repeat('x', 200)); alter table toast alter column data set storage external; insert into toast values ('external uncompressed', repeat('0123456789 8< ', 200)); alter table toast alter column data set storage extended; insert into toast values ('inline compressed pglz', repeat('0123456789 8< ', 200)); insert into toast values ('extended compressed pglz', repeat('0123456789 8< ', 20000)); alter table toast alter column data set compression lz4; insert into toast values ('inline compressed lz4', repeat('0123456789 8< ', 200)); insert into toast values ('extended compressed lz4', repeat('0123456789 8< ', 50000)); select description, pg_column_size(data), substring(data, 1, 50) as data from toast; description | pg_column_size | data --------------------------+----------------+---------------------------------------------------- short inline | 4 | xxx long inline uncompressed | 204 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx external uncompressed | 2800 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 inline compressed pglz | 59 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 extended compressed pglz | 3226 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 inline compressed lz4 | 42 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 extended compressed lz4 | 2772 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 (7 rows) delete from toast; -- toasted values are uncompressed after pg_dirtyread select description, pg_column_size(data), substring(data, 1, 50) as data from pg_dirtyread('toast') as (description text, data text); description | pg_column_size | data --------------------------+----------------+---------------------------------------------------- short inline | 4 | xxx long inline uncompressed | 204 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx external uncompressed | 2804 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 inline compressed pglz | 59 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 extended compressed pglz | 280004 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 inline compressed lz4 | 42 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 extended compressed lz4 | 700004 | 0123456789 8< 0123456789 8< 0123456789 8< 01234567 (7 rows) pg_dirtyread-2.7/pg_dirtyread--1.0--2.sql000066400000000000000000000002021463234341500200210ustar00rootroot00000000000000DROP FUNCTION pg_dirtyread(oid); CREATE FUNCTION pg_dirtyread(regclass) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C; pg_dirtyread-2.7/pg_dirtyread--1.0.sql000066400000000000000000000001301463234341500176050ustar00rootroot00000000000000CREATE FUNCTION pg_dirtyread(oid) RETURNS setof record AS 'MODULE_PATHNAME' LANGUAGE C; pg_dirtyread-2.7/pg_dirtyread--2.sql000066400000000000000000000001401463234341500174510ustar00rootroot00000000000000CREATE FUNCTION pg_dirtyread(regclass) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C; pg_dirtyread-2.7/pg_dirtyread.c000066400000000000000000000127751463234341500167010ustar00rootroot00000000000000/* * Copyright (c) 1996-2024, 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. * */ #include "postgres.h" #include "funcapi.h" #include "access/heapam.h" #if PG_VERSION_NUM >= 120000 #include "access/table.h" #include "utils/snapmgr.h" #else #include "utils/tqual.h" #endif #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; TupleDesc reltupdesc; TupleConversionMap *map; #if PG_VERSION_NUM >= 120000 TableScanDesc scan; #else HeapScanDesc scan; #endif OldestXminType oldest_xmin; } pg_dirtyread_ctx; PG_MODULE_MAGIC; PG_FUNCTION_INFO_V1(pg_dirtyread); PGDLLEXPORT Datum pg_dirtyread(PG_FUNCTION_ARGS); Datum pg_dirtyread(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; pg_dirtyread_ctx *usr_ctx; 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); funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); usr_ctx = (pg_dirtyread_ctx *) palloc(sizeof(pg_dirtyread_ctx)); usr_ctx->rel = #if PG_VERSION_NUM >= 120000 table_open(relid, AccessShareLock); #else heap_open(relid, AccessShareLock); #endif 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 #if PG_VERSION_NUM >= 120000 , NULL, SO_TYPE_SEQSCAN #endif ); #if PG_VERSION_NUM >= 140000 usr_ctx->oldest_xmin = GlobalVisTestFor(usr_ctx->rel); #else /* 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); #endif funcctx->user_fctx = (void *) usr_ctx; MemoryContextSwitchTo(oldcontext); } funcctx = SRF_PERCALL_SETUP(); usr_ctx = (pg_dirtyread_ctx *) funcctx->user_fctx; if ((tuplein = heap_getnext(usr_ctx->scan, ForwardScanDirection)) != NULL) { 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 { heap_endscan(usr_ctx->scan); #if PG_VERSION_NUM >= 120000 table_close(usr_ctx->rel, AccessShareLock); #else heap_close(usr_ctx->rel, AccessShareLock); #endif SRF_RETURN_DONE(funcctx); } } /* vim:et */ pg_dirtyread-2.7/pg_dirtyread.control000066400000000000000000000002261463234341500201230ustar00rootroot00000000000000# pg_dirtyread default_version = '2' comment = 'Read dead but unvacuumed rows from table' module_pathname = '$libdir/pg_dirtyread' relocatable = true pg_dirtyread-2.7/sql/000077500000000000000000000000001463234341500146435ustar00rootroot00000000000000pg_dirtyread-2.7/sql/dirtyread.sql000066400000000000000000000110021463234341500173450ustar00rootroot00000000000000-- 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') as t(bar bigint, baz text); DELETE FROM foo; SELECT * FROM pg_dirtyread('foo') 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') 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, bar, baz FROM pg_dirtyread('foo') AS t(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean, bar bigint, baz text); -- error cases SELECT pg_dirtyread('foo'); SELECT * FROM pg_dirtyread(0) as t(bar bigint, baz text); SELECT * FROM pg_dirtyread(NULL) as t(bar bigint, baz text); SELECT * FROM pg_dirtyread('foo') as t(bar int, baz text); SELECT * FROM pg_dirtyread('foo') as t(moo bigint); SELECT * FROM pg_dirtyread('foo') as t(tableoid bigint); SELECT * FROM pg_dirtyread('foo') as t(ctid bigint); SELECT * FROM pg_dirtyread('foo') as t(xmin bigint); SELECT * FROM pg_dirtyread('foo') as t(xmax bigint); SELECT * FROM pg_dirtyread('foo') as t(cmin bigint); SELECT * FROM pg_dirtyread('foo') as t(cmax bigint); SELECT * FROM pg_dirtyread('foo') as t(dead bigint); SET ROLE luser; SELECT * FROM pg_dirtyread('foo') as t(bar bigint, baz text); RESET ROLE; -- reading from dropped columns CREATE TABLE bar ( id int, a int, b bigint, c text, d varchar(10), e boolean, f bigint[], z int ); ALTER TABLE bar SET ( autovacuum_enabled = false, toast.autovacuum_enabled = false ); INSERT INTO bar VALUES (1, 2, 3, '4', '5', true, '{7}', 8); ALTER TABLE bar DROP COLUMN a, DROP COLUMN b, DROP COLUMN c, DROP COLUMN d, DROP COLUMN e, DROP COLUMN f; INSERT INTO bar VALUES (2, 8); SELECT * FROM bar; SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); -- errors SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_0 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_9 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 bigint, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 int, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); -- mismatch not catched: SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 timestamptz, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 int, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(11), dropped_6 boolean, dropped_7 bigint[], z int); SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 text, dropped_7 bigint[], z int); SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 int[], z int); -- mismatch not catched: SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 timestamptz[], z int); -- clean table VACUUM FULL bar; SELECT * FROM pg_dirtyread('bar') bar(id int, dropped_2 int, dropped_3 bigint, dropped_4 text, dropped_5 varchar(10), dropped_6 boolean, dropped_7 bigint[], z int); pg_dirtyread-2.7/sql/extension.sql000066400000000000000000000003151463234341500173770ustar00rootroot00000000000000CREATE EXTENSION pg_dirtyread; -- create a non-superuser role, ignoring any output/errors, it might already exist DO $$ BEGIN CREATE ROLE luser; EXCEPTION WHEN duplicate_object THEN NULL; END; $$; pg_dirtyread-2.7/sql/index.sql000066400000000000000000000002641463234341500164750ustar00rootroot00000000000000select setting::int >= 160000 as is_pg16 from pg_settings where name = 'server_version_num'; CREATE INDEX ON foo(bar); SELECT * FROM pg_dirtyread('foo_bar_idx') as t(bar bigint); pg_dirtyread-2.7/sql/oid.sql000066400000000000000000000004321463234341500161360ustar00rootroot00000000000000-- test oid columns (removed in PostgreSQL 12) SELECT setting::int >= 120000 AS is_pg_12 FROM pg_settings WHERE name = 'server_version_num'; SELECT * FROM pg_dirtyread('foo') AS t(oid oid, bar bigint, baz text); -- error cases SELECT * FROM pg_dirtyread('foo') as t(oid bigint); pg_dirtyread-2.7/sql/toast.sql000066400000000000000000000020671463234341500165230ustar00rootroot00000000000000create table toast ( description text, data text ); insert into toast values ('short inline', 'xxx'); insert into toast values ('long inline uncompressed', repeat('x', 200)); alter table toast alter column data set storage external; insert into toast values ('external uncompressed', repeat('0123456789 8< ', 200)); alter table toast alter column data set storage extended; insert into toast values ('inline compressed pglz', repeat('0123456789 8< ', 200)); insert into toast values ('extended compressed pglz', repeat('0123456789 8< ', 20000)); alter table toast alter column data set compression lz4; insert into toast values ('inline compressed lz4', repeat('0123456789 8< ', 200)); insert into toast values ('extended compressed lz4', repeat('0123456789 8< ', 50000)); select description, pg_column_size(data), substring(data, 1, 50) as data from toast; delete from toast; -- toasted values are uncompressed after pg_dirtyread select description, pg_column_size(data), substring(data, 1, 50) as data from pg_dirtyread('toast') as (description text, data text); pg_dirtyread-2.7/tupconvert.c.upstream000066400000000000000000000266471463234341500202770ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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-2018, 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 = TupleDescAttr(outdesc, 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 = TupleDescAttr(indesc, 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 (TupleDescAttr(indesc, 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++) { Form_pg_attribute inatt; Form_pg_attribute outatt; 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. */ inatt = TupleDescAttr(indesc, i); outatt = TupleDescAttr(outdesc, i); if (attrMap[i] == 0 && inatt->attisdropped && inatt->attlen == outatt->attlen && inatt->attalign == outatt->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++) { Form_pg_attribute inatt; Form_pg_attribute outatt; 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. */ inatt = TupleDescAttr(indesc, i); outatt = TupleDescAttr(outdesc, i); if (attrMap[i] == 0 && inatt->attisdropped && inatt->attlen == outatt->attlen && inatt->attalign == outatt->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 outatt = TupleDescAttr(outdesc, i); char *attname; Oid atttypid; int32 atttypmod; int j; if (outatt->attisdropped) continue; /* attrMap[i] is already 0 */ attname = NameStr(outatt->attname); atttypid = outatt->atttypid; atttypmod = outatt->atttypmod; for (j = 0; j < indesc->natts; j++) { Form_pg_attribute inatt = TupleDescAttr(indesc, j); if (inatt->attisdropped) continue; if (strcmp(attname, NameStr(inatt->attname)) == 0) { /* Found it, check type */ if (atttypid != inatt->atttypid || atttypmod != inatt->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); } pg_dirtyread-2.7/tupconvert.h.upstream000066400000000000000000000026371463234341500202750ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * tupconvert.h * Tuple conversion support. * * * Portions Copyright (c) 1996-2018, 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 */