diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:17:33 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:17:33 +0000 |
commit | 5e45211a64149b3c659b90ff2de6fa982a5a93ed (patch) | |
tree | 739caf8c461053357daa9f162bef34516c7bf452 /src/test/modules | |
parent | Initial commit. (diff) | |
download | postgresql-15-5e45211a64149b3c659b90ff2de6fa982a5a93ed.tar.xz postgresql-15-5e45211a64149b3c659b90ff2de6fa982a5a93ed.zip |
Adding upstream version 15.5.upstream/15.5
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/test/modules')
288 files changed, 28479 insertions, 0 deletions
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile new file mode 100644 index 0000000..9090226 --- /dev/null +++ b/src/test/modules/Makefile @@ -0,0 +1,41 @@ +# src/test/modules/Makefile + +subdir = src/test/modules +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +SUBDIRS = \ + brin \ + commit_ts \ + delay_execution \ + dummy_index_am \ + dummy_seclabel \ + libpq_pipeline \ + plsample \ + snapshot_too_old \ + spgist_name_ops \ + test_bloomfilter \ + test_ddl_deparse \ + test_extensions \ + test_ginpostinglist \ + test_integerset \ + test_misc \ + test_oat_hooks \ + test_parser \ + test_pg_dump \ + test_predtest \ + test_rbtree \ + test_regex \ + test_rls_hooks \ + test_shm_mq \ + unsafe_tests \ + worker_spi + +ifeq ($(with_ssl),openssl) +SUBDIRS += ssl_passphrase_callback +else +ALWAYS_SUBDIRS += ssl_passphrase_callback +endif + +$(recurse) +$(recurse_always) diff --git a/src/test/modules/README b/src/test/modules/README new file mode 100644 index 0000000..025ecac --- /dev/null +++ b/src/test/modules/README @@ -0,0 +1,20 @@ +Test extensions and libraries +============================= + +src/test/modules contains PostgreSQL extensions that are primarily or entirely +intended for testing PostgreSQL and/or to serve as example code. The extensions +here aren't intended to be installed in a production server and aren't suitable +for "real work". + +Furthermore, while you can do "make install" and "make installcheck" in +this directory or its children, it is NOT ADVISABLE to do so with a server +containing valuable data. Some of these tests may have undesirable +side-effects on roles or other global objects within the tested server. +"make installcheck-world" at the top level does not recurse into this +directory. + +Most extensions have their own pg_regress tests or isolationtester specs. Some +are also used by tests elsewhere in the tree. + +If you're adding new hooks or other functionality exposed as C-level API this +is where to add the tests for it. diff --git a/src/test/modules/brin/.gitignore b/src/test/modules/brin/.gitignore new file mode 100644 index 0000000..d6d1aaa --- /dev/null +++ b/src/test/modules/brin/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/output_iso/ +/tmp_check/ +/tmp_check_iso/ diff --git a/src/test/modules/brin/Makefile b/src/test/modules/brin/Makefile new file mode 100644 index 0000000..e74af89 --- /dev/null +++ b/src/test/modules/brin/Makefile @@ -0,0 +1,17 @@ +# src/test/modules/brin/Makefile + +EXTRA_INSTALL = contrib/pageinspect contrib/pg_walinspect + +ISOLATION = summarization-and-inprogress-insertion +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/brin +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/brin/expected/summarization-and-inprogress-insertion.out b/src/test/modules/brin/expected/summarization-and-inprogress-insertion.out new file mode 100644 index 0000000..584ac26 --- /dev/null +++ b/src/test/modules/brin/expected/summarization-and-inprogress-insertion.out @@ -0,0 +1,51 @@ +Parsed test spec with 2 sessions + +starting permutation: s2check s1b s2b s1i s2summ s1c s2c s2check +step s2check: SELECT * FROM brin_page_items(get_raw_page('brinidx', 2), 'brinidx'::regclass); +itemoffset|blknum|attnum|allnulls|hasnulls|placeholder|value +----------+------+------+--------+--------+-----------+-------- + 1| 0| 1|f |t |f |{1 .. 1} +(1 row) + +step s1b: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2b: BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT 1; +?column? +-------- + 1 +(1 row) + +step s1i: INSERT INTO brin_iso VALUES (1000); +step s2summ: SELECT brin_summarize_new_values('brinidx'::regclass); +brin_summarize_new_values +------------------------- + 1 +(1 row) + +step s1c: COMMIT; +step s2c: COMMIT; +step s2check: SELECT * FROM brin_page_items(get_raw_page('brinidx', 2), 'brinidx'::regclass); +itemoffset|blknum|attnum|allnulls|hasnulls|placeholder|value +----------+------+------+--------+--------+-----------+----------- + 1| 0| 1|f |t |f |{1 .. 1} + 2| 1| 1|f |f |f |{1 .. 1000} +(2 rows) + + +starting permutation: s2check s1b s1i s2vacuum s1c s2check +step s2check: SELECT * FROM brin_page_items(get_raw_page('brinidx', 2), 'brinidx'::regclass); +itemoffset|blknum|attnum|allnulls|hasnulls|placeholder|value +----------+------+------+--------+--------+-----------+-------- + 1| 0| 1|f |t |f |{1 .. 1} +(1 row) + +step s1b: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1i: INSERT INTO brin_iso VALUES (1000); +step s2vacuum: VACUUM brin_iso; +step s1c: COMMIT; +step s2check: SELECT * FROM brin_page_items(get_raw_page('brinidx', 2), 'brinidx'::regclass); +itemoffset|blknum|attnum|allnulls|hasnulls|placeholder|value +----------+------+------+--------+--------+-----------+----------- + 1| 0| 1|f |t |f |{1 .. 1} + 2| 1| 1|f |f |f |{1 .. 1000} +(2 rows) + diff --git a/src/test/modules/brin/specs/summarization-and-inprogress-insertion.spec b/src/test/modules/brin/specs/summarization-and-inprogress-insertion.spec new file mode 100644 index 0000000..18ba92b --- /dev/null +++ b/src/test/modules/brin/specs/summarization-and-inprogress-insertion.spec @@ -0,0 +1,45 @@ +# This test verifies that values inserted in transactions still in progress +# are considered during concurrent range summarization (either using the +# brin_summarize_new_values function or regular VACUUM). + +setup +{ + CREATE TABLE brin_iso ( + value int + ) WITH (fillfactor=10); + CREATE INDEX brinidx ON brin_iso USING brin (value) WITH (pages_per_range=1); + -- this fills the first page + INSERT INTO brin_iso VALUES (NULL); + DO $$ + DECLARE curtid tid; + BEGIN + LOOP + INSERT INTO brin_iso VALUES (1) RETURNING ctid INTO curtid; + EXIT WHEN curtid > tid '(1, 0)'; + END LOOP; + END; + $$; + CREATE EXTENSION IF NOT EXISTS pageinspect; +} + +teardown +{ + DROP TABLE brin_iso; +} + +session "s1" +step "s1b" { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step "s1i" { INSERT INTO brin_iso VALUES (1000); } +step "s1c" { COMMIT; } + +session "s2" +step "s2b" { BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT 1; } +step "s2summ" { SELECT brin_summarize_new_values('brinidx'::regclass); } +step "s2c" { COMMIT; } + +step "s2vacuum" { VACUUM brin_iso; } + +step "s2check" { SELECT * FROM brin_page_items(get_raw_page('brinidx', 2), 'brinidx'::regclass); } + +permutation "s2check" "s1b" "s2b" "s1i" "s2summ" "s1c" "s2c" "s2check" +permutation "s2check" "s1b" "s1i" "s2vacuum" "s1c" "s2check" diff --git a/src/test/modules/brin/t/01_workitems.pl b/src/test/modules/brin/t/01_workitems.pl new file mode 100644 index 0000000..3108c02 --- /dev/null +++ b/src/test/modules/brin/t/01_workitems.pl @@ -0,0 +1,46 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Verify that work items work correctly + +use strict; +use warnings; + +use PostgreSQL::Test::Utils; +use Test::More; +use PostgreSQL::Test::Cluster; + +my $node = PostgreSQL::Test::Cluster->new('tango'); +$node->init; +$node->append_conf('postgresql.conf', 'autovacuum_naptime=1s'); +$node->start; + +$node->safe_psql('postgres', 'create extension pageinspect'); + +# Create a table with an autosummarizing BRIN index +$node->safe_psql( + 'postgres', + 'create table brin_wi (a int) with (fillfactor = 10); + create index brin_wi_idx on brin_wi using brin (a) with (pages_per_range=1, autosummarize=on); + ' +); +my $count = $node->safe_psql('postgres', + "select count(*) from brin_page_items(get_raw_page('brin_wi_idx', 2), 'brin_wi_idx'::regclass)" +); +is($count, '1', "initial index state is correct"); + +$node->safe_psql('postgres', + 'insert into brin_wi select * from generate_series(1, 100)'); + +$node->poll_query_until( + 'postgres', + "select count(*) > 1 from brin_page_items(get_raw_page('brin_wi_idx', 2), 'brin_wi_idx'::regclass)", + 't'); + +$count = $node->safe_psql('postgres', + "select count(*) > 1 from brin_page_items(get_raw_page('brin_wi_idx', 2), 'brin_wi_idx'::regclass)" +); +is($count, 't', "index got summarized"); +$node->stop; + +done_testing(); diff --git a/src/test/modules/brin/t/02_wal_consistency.pl b/src/test/modules/brin/t/02_wal_consistency.pl new file mode 100644 index 0000000..cbc269b --- /dev/null +++ b/src/test/modules/brin/t/02_wal_consistency.pl @@ -0,0 +1,75 @@ +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Verify WAL consistency + +use strict; +use warnings; + +use PostgreSQL::Test::Utils; +use Test::More; +use PostgreSQL::Test::Cluster; + +# Set up primary +my $whiskey = PostgreSQL::Test::Cluster->new('whiskey'); +$whiskey->init(allows_streaming => 1); +$whiskey->append_conf('postgresql.conf', 'wal_consistency_checking = brin'); +$whiskey->start; +$whiskey->safe_psql('postgres', 'create extension pageinspect'); +$whiskey->safe_psql('postgres', 'create extension pg_walinspect'); +is( $whiskey->psql( + 'postgres', + qq[SELECT pg_create_physical_replication_slot('standby_1');]), + 0, + 'physical slot created on primary'); + +# Take backup +my $backup_name = 'brinbkp'; +$whiskey->backup($backup_name); + +# Create streaming standby linking to primary +my $charlie = PostgreSQL::Test::Cluster->new('charlie'); +$charlie->init_from_backup($whiskey, $backup_name, has_streaming => 1); +$charlie->append_conf('postgresql.conf', 'primary_slot_name = standby_1'); +$charlie->start; + +# Now write some WAL in the primary + +$whiskey->safe_psql( + 'postgres', qq{ +create table tbl_timestamp0 (d1 timestamp(0) without time zone) with (fillfactor=10); +create index on tbl_timestamp0 using brin (d1) with (pages_per_range = 1, autosummarize=false); +}); +my $start_lsn = $whiskey->lsn('insert'); +# Run a loop that will end when the second revmap page is created +$whiskey->safe_psql( + 'postgres', q{ +do +$$ +declare + current timestamp with time zone := '2019-03-27 08:14:01.123456789 UTC'; +begin + loop + insert into tbl_timestamp0 select i from + generate_series(current, current + interval '1 day', '28 seconds') i; + perform brin_summarize_new_values('tbl_timestamp0_d1_idx'); + if (brin_metapage_info(get_raw_page('tbl_timestamp0_d1_idx', 0))).lastrevmappage > 1 then + exit; + end if; + current := current + interval '1 day'; + end loop; +end +$$; +}); +my $end_lsn = $whiskey->lsn('flush'); + +my ($ret, $out, $err) = $whiskey->psql( + 'postgres', qq{ + select count(*) from pg_get_wal_records_info('$start_lsn', '$end_lsn') + where resource_manager = 'BRIN' AND + record_type ILIKE '%revmap%' + }); +cmp_ok($out, '>=', 1); + +$whiskey->wait_for_catchup($charlie, 'replay', $whiskey->lsn('insert')); + +done_testing(); diff --git a/src/test/modules/commit_ts/.gitignore b/src/test/modules/commit_ts/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/commit_ts/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/commit_ts/Makefile b/src/test/modules/commit_ts/Makefile new file mode 100644 index 0000000..113bcfa --- /dev/null +++ b/src/test/modules/commit_ts/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/commit_ts/Makefile + +REGRESS = commit_timestamp +REGRESS_OPTS = --temp-config=$(top_srcdir)/src/test/modules/commit_ts/commit_ts.conf +# Disabled because these tests require "track_commit_timestamp = on", +# which typical installcheck users do not have (e.g. buildfarm clients). +NO_INSTALLCHECK = 1 + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/commit_ts +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/commit_ts/commit_ts.conf b/src/test/modules/commit_ts/commit_ts.conf new file mode 100644 index 0000000..e9d3c35 --- /dev/null +++ b/src/test/modules/commit_ts/commit_ts.conf @@ -0,0 +1 @@ +track_commit_timestamp = on diff --git a/src/test/modules/commit_ts/expected/commit_timestamp.out b/src/test/modules/commit_ts/expected/commit_timestamp.out new file mode 100644 index 0000000..bb2fda2 --- /dev/null +++ b/src/test/modules/commit_ts/expected/commit_timestamp.out @@ -0,0 +1,139 @@ +-- +-- Commit Timestamp +-- +SHOW track_commit_timestamp; + track_commit_timestamp +------------------------ + on +(1 row) + +CREATE TABLE committs_test(id serial, ts timestamptz default now()); +INSERT INTO committs_test DEFAULT VALUES; +INSERT INTO committs_test DEFAULT VALUES; +INSERT INTO committs_test DEFAULT VALUES; +SELECT id, + pg_xact_commit_timestamp(xmin) >= ts, + pg_xact_commit_timestamp(xmin) <= now(), + pg_xact_commit_timestamp(xmin) - ts < '60s' -- 60s should give a lot of reserve +FROM committs_test +ORDER BY id; + id | ?column? | ?column? | ?column? +----+----------+----------+---------- + 1 | t | t | t + 2 | t | t | t + 3 | t | t | t +(3 rows) + +DROP TABLE committs_test; +SELECT pg_xact_commit_timestamp('0'::xid); +ERROR: cannot retrieve commit timestamp for transaction 0 +SELECT pg_xact_commit_timestamp('1'::xid); + pg_xact_commit_timestamp +-------------------------- + +(1 row) + +SELECT pg_xact_commit_timestamp('2'::xid); + pg_xact_commit_timestamp +-------------------------- + +(1 row) + +SELECT x.xid::text::bigint > 0 as xid_valid, + x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_last_committed_xact() x; + xid_valid | ts_low | ts_high | valid_roident +-----------+--------+---------+--------------- + t | t | t | f +(1 row) + +-- Test non-normal transaction ids. +SELECT * FROM pg_xact_commit_timestamp_origin(NULL); -- ok, NULL + timestamp | roident +-----------+--------- + | +(1 row) + +SELECT * FROM pg_xact_commit_timestamp_origin('0'::xid); -- error +ERROR: cannot retrieve commit timestamp for transaction 0 +SELECT * FROM pg_xact_commit_timestamp_origin('1'::xid); -- ok, NULL + timestamp | roident +-----------+--------- + | +(1 row) + +SELECT * FROM pg_xact_commit_timestamp_origin('2'::xid); -- ok, NULL + timestamp | roident +-----------+--------- + | +(1 row) + +-- Test transaction without replication origin +SELECT txid_current() as txid_no_origin \gset +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_last_committed_xact() x; + ts_low | ts_high | valid_roident +--------+---------+--------------- + t | t | f +(1 row) + +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_xact_commit_timestamp_origin(:'txid_no_origin') x; + ts_low | ts_high | valid_roident +--------+---------+--------------- + t | t | f +(1 row) + +-- Test transaction with replication origin +SELECT pg_replication_origin_create('regress_commit_ts: get_origin') != 0 + AS valid_roident; + valid_roident +--------------- + t +(1 row) + +SELECT pg_replication_origin_session_setup('regress_commit_ts: get_origin'); + pg_replication_origin_session_setup +------------------------------------- + +(1 row) + +SELECT txid_current() as txid_with_origin \gset +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + r.roname + FROM pg_last_committed_xact() x, pg_replication_origin r + WHERE r.roident = x.roident; + ts_low | ts_high | roname +--------+---------+------------------------------- + t | t | regress_commit_ts: get_origin +(1 row) + +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + r.roname + FROM pg_xact_commit_timestamp_origin(:'txid_with_origin') x, pg_replication_origin r + WHERE r.roident = x.roident; + ts_low | ts_high | roname +--------+---------+------------------------------- + t | t | regress_commit_ts: get_origin +(1 row) + +SELECT pg_replication_origin_session_reset(); + pg_replication_origin_session_reset +------------------------------------- + +(1 row) + +SELECT pg_replication_origin_drop('regress_commit_ts: get_origin'); + pg_replication_origin_drop +---------------------------- + +(1 row) + diff --git a/src/test/modules/commit_ts/expected/commit_timestamp_1.out b/src/test/modules/commit_ts/expected/commit_timestamp_1.out new file mode 100644 index 0000000..f37e701 --- /dev/null +++ b/src/test/modules/commit_ts/expected/commit_timestamp_1.out @@ -0,0 +1,119 @@ +-- +-- Commit Timestamp +-- +SHOW track_commit_timestamp; + track_commit_timestamp +------------------------ + off +(1 row) + +CREATE TABLE committs_test(id serial, ts timestamptz default now()); +INSERT INTO committs_test DEFAULT VALUES; +INSERT INTO committs_test DEFAULT VALUES; +INSERT INTO committs_test DEFAULT VALUES; +SELECT id, + pg_xact_commit_timestamp(xmin) >= ts, + pg_xact_commit_timestamp(xmin) <= now(), + pg_xact_commit_timestamp(xmin) - ts < '60s' -- 60s should give a lot of reserve +FROM committs_test +ORDER BY id; +ERROR: could not get commit timestamp data +HINT: Make sure the configuration parameter "track_commit_timestamp" is set. +DROP TABLE committs_test; +SELECT pg_xact_commit_timestamp('0'::xid); +ERROR: cannot retrieve commit timestamp for transaction 0 +SELECT pg_xact_commit_timestamp('1'::xid); + pg_xact_commit_timestamp +-------------------------- + +(1 row) + +SELECT pg_xact_commit_timestamp('2'::xid); + pg_xact_commit_timestamp +-------------------------- + +(1 row) + +SELECT x.xid::text::bigint > 0 as xid_valid, + x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_last_committed_xact() x; +ERROR: could not get commit timestamp data +HINT: Make sure the configuration parameter "track_commit_timestamp" is set. +-- Test non-normal transaction ids. +SELECT * FROM pg_xact_commit_timestamp_origin(NULL); -- ok, NULL + timestamp | roident +-----------+--------- + | +(1 row) + +SELECT * FROM pg_xact_commit_timestamp_origin('0'::xid); -- error +ERROR: cannot retrieve commit timestamp for transaction 0 +SELECT * FROM pg_xact_commit_timestamp_origin('1'::xid); -- ok, NULL + timestamp | roident +-----------+--------- + | +(1 row) + +SELECT * FROM pg_xact_commit_timestamp_origin('2'::xid); -- ok, NULL + timestamp | roident +-----------+--------- + | +(1 row) + +-- Test transaction without replication origin +SELECT txid_current() as txid_no_origin \gset +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_last_committed_xact() x; +ERROR: could not get commit timestamp data +HINT: Make sure the configuration parameter "track_commit_timestamp" is set. +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_xact_commit_timestamp_origin(:'txid_no_origin') x; +ERROR: could not get commit timestamp data +HINT: Make sure the configuration parameter "track_commit_timestamp" is set. +-- Test transaction with replication origin +SELECT pg_replication_origin_create('regress_commit_ts: get_origin') != 0 + AS valid_roident; + valid_roident +--------------- + t +(1 row) + +SELECT pg_replication_origin_session_setup('regress_commit_ts: get_origin'); + pg_replication_origin_session_setup +------------------------------------- + +(1 row) + +SELECT txid_current() as txid_with_origin \gset +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + r.roname + FROM pg_last_committed_xact() x, pg_replication_origin r + WHERE r.roident = x.roident; +ERROR: could not get commit timestamp data +HINT: Make sure the configuration parameter "track_commit_timestamp" is set. +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + r.roname + FROM pg_xact_commit_timestamp_origin(:'txid_with_origin') x, pg_replication_origin r + WHERE r.roident = x.roident; +ERROR: could not get commit timestamp data +HINT: Make sure the configuration parameter "track_commit_timestamp" is set. +SELECT pg_replication_origin_session_reset(); + pg_replication_origin_session_reset +------------------------------------- + +(1 row) + +SELECT pg_replication_origin_drop('regress_commit_ts: get_origin'); + pg_replication_origin_drop +---------------------------- + +(1 row) + diff --git a/src/test/modules/commit_ts/sql/commit_timestamp.sql b/src/test/modules/commit_ts/sql/commit_timestamp.sql new file mode 100644 index 0000000..3bb7bb2 --- /dev/null +++ b/src/test/modules/commit_ts/sql/commit_timestamp.sql @@ -0,0 +1,64 @@ +-- +-- Commit Timestamp +-- +SHOW track_commit_timestamp; +CREATE TABLE committs_test(id serial, ts timestamptz default now()); + +INSERT INTO committs_test DEFAULT VALUES; +INSERT INTO committs_test DEFAULT VALUES; +INSERT INTO committs_test DEFAULT VALUES; + +SELECT id, + pg_xact_commit_timestamp(xmin) >= ts, + pg_xact_commit_timestamp(xmin) <= now(), + pg_xact_commit_timestamp(xmin) - ts < '60s' -- 60s should give a lot of reserve +FROM committs_test +ORDER BY id; + +DROP TABLE committs_test; + +SELECT pg_xact_commit_timestamp('0'::xid); +SELECT pg_xact_commit_timestamp('1'::xid); +SELECT pg_xact_commit_timestamp('2'::xid); + +SELECT x.xid::text::bigint > 0 as xid_valid, + x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_last_committed_xact() x; + +-- Test non-normal transaction ids. +SELECT * FROM pg_xact_commit_timestamp_origin(NULL); -- ok, NULL +SELECT * FROM pg_xact_commit_timestamp_origin('0'::xid); -- error +SELECT * FROM pg_xact_commit_timestamp_origin('1'::xid); -- ok, NULL +SELECT * FROM pg_xact_commit_timestamp_origin('2'::xid); -- ok, NULL + +-- Test transaction without replication origin +SELECT txid_current() as txid_no_origin \gset +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_last_committed_xact() x; +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + roident != 0 AS valid_roident + FROM pg_xact_commit_timestamp_origin(:'txid_no_origin') x; + +-- Test transaction with replication origin +SELECT pg_replication_origin_create('regress_commit_ts: get_origin') != 0 + AS valid_roident; +SELECT pg_replication_origin_session_setup('regress_commit_ts: get_origin'); +SELECT txid_current() as txid_with_origin \gset +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + r.roname + FROM pg_last_committed_xact() x, pg_replication_origin r + WHERE r.roident = x.roident; +SELECT x.timestamp > '-infinity'::timestamptz AS ts_low, + x.timestamp <= now() AS ts_high, + r.roname + FROM pg_xact_commit_timestamp_origin(:'txid_with_origin') x, pg_replication_origin r + WHERE r.roident = x.roident; + +SELECT pg_replication_origin_session_reset(); +SELECT pg_replication_origin_drop('regress_commit_ts: get_origin'); diff --git a/src/test/modules/commit_ts/t/001_base.pl b/src/test/modules/commit_ts/t/001_base.pl new file mode 100644 index 0000000..3f0bb9e --- /dev/null +++ b/src/test/modules/commit_ts/t/001_base.pl @@ -0,0 +1,38 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Single-node test: value can be set, and is still present after recovery + +use strict; +use warnings; + +use PostgreSQL::Test::Utils; +use Test::More; +use PostgreSQL::Test::Cluster; + +my $node = PostgreSQL::Test::Cluster->new('foxtrot'); +$node->init; +$node->append_conf('postgresql.conf', 'track_commit_timestamp = on'); +$node->start; + +# Create a table, compare "now()" to the commit TS of its xmin +$node->safe_psql('postgres', + 'create table t as select now from (select now(), pg_sleep(1)) f'); +my $true = $node->safe_psql('postgres', + 'select t.now - ts.* < \'1s\' from t, pg_class c, pg_xact_commit_timestamp(c.xmin) ts where relname = \'t\'' +); +is($true, 't', 'commit TS is set'); +my $ts = $node->safe_psql('postgres', + 'select ts.* from pg_class, pg_xact_commit_timestamp(xmin) ts where relname = \'t\'' +); + +# Verify that we read the same TS after crash recovery +$node->stop('immediate'); +$node->start; + +my $recovered_ts = $node->safe_psql('postgres', + 'select ts.* from pg_class, pg_xact_commit_timestamp(xmin) ts where relname = \'t\'' +); +is($recovered_ts, $ts, 'commit TS remains after crash recovery'); + +done_testing(); diff --git a/src/test/modules/commit_ts/t/002_standby.pl b/src/test/modules/commit_ts/t/002_standby.pl new file mode 100644 index 0000000..ace3140 --- /dev/null +++ b/src/test/modules/commit_ts/t/002_standby.pl @@ -0,0 +1,68 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Test simple scenario involving a standby + +use strict; +use warnings; + +use PostgreSQL::Test::Utils; +use Test::More; +use PostgreSQL::Test::Cluster; + +my $bkplabel = 'backup'; +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); + +$primary->append_conf( + 'postgresql.conf', qq{ + track_commit_timestamp = on + max_wal_senders = 5 + }); +$primary->start; +$primary->backup($bkplabel); + +my $standby = PostgreSQL::Test::Cluster->new('standby'); +$standby->init_from_backup($primary, $bkplabel, has_streaming => 1); +$standby->start; + +for my $i (1 .. 10) +{ + $primary->safe_psql('postgres', "create table t$i()"); +} +my $primary_ts = $primary->safe_psql('postgres', + qq{SELECT ts.* FROM pg_class, pg_xact_commit_timestamp(xmin) AS ts WHERE relname = 't10'} +); +my $primary_lsn = + $primary->safe_psql('postgres', 'select pg_current_wal_lsn()'); +$standby->poll_query_until('postgres', + qq{SELECT '$primary_lsn'::pg_lsn <= pg_last_wal_replay_lsn()}) + or die "standby never caught up"; + +my $standby_ts = $standby->safe_psql('postgres', + qq{select ts.* from pg_class, pg_xact_commit_timestamp(xmin) ts where relname = 't10'} +); +is($primary_ts, $standby_ts, "standby gives same value as primary"); + +$primary->append_conf('postgresql.conf', 'track_commit_timestamp = off'); +$primary->restart; +$primary->safe_psql('postgres', 'checkpoint'); +$primary_lsn = $primary->safe_psql('postgres', 'select pg_current_wal_lsn()'); +$standby->poll_query_until('postgres', + qq{SELECT '$primary_lsn'::pg_lsn <= pg_last_wal_replay_lsn()}) + or die "standby never caught up"; +$standby->safe_psql('postgres', 'checkpoint'); + +# This one should raise an error now +my ($ret, $standby_ts_stdout, $standby_ts_stderr) = $standby->psql('postgres', + 'select ts.* from pg_class, pg_xact_commit_timestamp(xmin) ts where relname = \'t10\'' +); +is($ret, 3, 'standby errors when primary turned feature off'); +is($standby_ts_stdout, '', + "standby gives no value when primary turned feature off"); +like( + $standby_ts_stderr, + qr/could not get commit timestamp data/, + 'expected error when primary turned feature off'); + +done_testing(); diff --git a/src/test/modules/commit_ts/t/003_standby_2.pl b/src/test/modules/commit_ts/t/003_standby_2.pl new file mode 100644 index 0000000..16d5f13 --- /dev/null +++ b/src/test/modules/commit_ts/t/003_standby_2.pl @@ -0,0 +1,69 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Test primary/standby scenario where the track_commit_timestamp GUC is +# repeatedly toggled on and off. +use strict; +use warnings; + +use PostgreSQL::Test::Utils; +use Test::More; +use PostgreSQL::Test::Cluster; + +my $bkplabel = 'backup'; +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); +$primary->append_conf( + 'postgresql.conf', qq{ + track_commit_timestamp = on + max_wal_senders = 5 + }); +$primary->start; +$primary->backup($bkplabel); + +my $standby = PostgreSQL::Test::Cluster->new('standby'); +$standby->init_from_backup($primary, $bkplabel, has_streaming => 1); +$standby->start; + +for my $i (1 .. 10) +{ + $primary->safe_psql('postgres', "create table t$i()"); +} +$primary->append_conf('postgresql.conf', 'track_commit_timestamp = off'); +$primary->restart; +$primary->safe_psql('postgres', 'checkpoint'); +my $primary_lsn = + $primary->safe_psql('postgres', 'select pg_current_wal_lsn()'); +$standby->poll_query_until('postgres', + qq{SELECT '$primary_lsn'::pg_lsn <= pg_last_wal_replay_lsn()}) + or die "standby never caught up"; + +$standby->safe_psql('postgres', 'checkpoint'); +$standby->restart; + +my ($psql_ret, $standby_ts_stdout, $standby_ts_stderr) = $standby->psql( + 'postgres', + qq{SELECT ts.* FROM pg_class, pg_xact_commit_timestamp(xmin) AS ts WHERE relname = 't10'} +); +is($psql_ret, 3, 'expect error when getting commit timestamp after restart'); +is($standby_ts_stdout, '', "standby does not return a value after restart"); +like( + $standby_ts_stderr, + qr/could not get commit timestamp data/, + 'expected err msg after restart'); + +$primary->append_conf('postgresql.conf', 'track_commit_timestamp = on'); +$primary->restart; +$primary->append_conf('postgresql.conf', 'track_commit_timestamp = off'); +$primary->restart; + +system_or_bail('pg_ctl', '-D', $standby->data_dir, 'promote'); + +$standby->safe_psql('postgres', "create table t11()"); +my $standby_ts = $standby->safe_psql('postgres', + qq{SELECT ts.* FROM pg_class, pg_xact_commit_timestamp(xmin) AS ts WHERE relname = 't11'} +); +isnt($standby_ts, '', + "standby gives valid value ($standby_ts) after promotion"); + +done_testing(); diff --git a/src/test/modules/commit_ts/t/004_restart.pl b/src/test/modules/commit_ts/t/004_restart.pl new file mode 100644 index 0000000..808164c --- /dev/null +++ b/src/test/modules/commit_ts/t/004_restart.pl @@ -0,0 +1,154 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Testing of commit timestamps preservation across restarts +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node_primary = PostgreSQL::Test::Cluster->new('primary'); +$node_primary->init(allows_streaming => 1); +$node_primary->append_conf('postgresql.conf', 'track_commit_timestamp = on'); +$node_primary->start; + +my ($ret, $stdout, $stderr); + +($ret, $stdout, $stderr) = + $node_primary->psql('postgres', qq[SELECT pg_xact_commit_timestamp('0');]); +is($ret, 3, 'getting ts of InvalidTransactionId reports error'); +like( + $stderr, + qr/cannot retrieve commit timestamp for transaction/, + 'expected error from InvalidTransactionId'); + +($ret, $stdout, $stderr) = + $node_primary->psql('postgres', qq[SELECT pg_xact_commit_timestamp('1');]); +is($ret, 0, 'getting ts of BootstrapTransactionId succeeds'); +is($stdout, '', 'timestamp of BootstrapTransactionId is null'); + +($ret, $stdout, $stderr) = + $node_primary->psql('postgres', qq[SELECT pg_xact_commit_timestamp('2');]); +is($ret, 0, 'getting ts of FrozenTransactionId succeeds'); +is($stdout, '', 'timestamp of FrozenTransactionId is null'); + +# Since FirstNormalTransactionId will've occurred during initdb, long before we +# enabled commit timestamps, it'll be null since we have no cts data for it but +# cts are enabled. +is( $node_primary->safe_psql( + 'postgres', qq[SELECT pg_xact_commit_timestamp('3');]), + '', + 'committs for FirstNormalTransactionId is null'); + +$node_primary->safe_psql('postgres', + qq[CREATE TABLE committs_test(x integer, y timestamp with time zone);]); + +my $xid = $node_primary->safe_psql( + 'postgres', qq[ + BEGIN; + INSERT INTO committs_test(x, y) VALUES (1, current_timestamp); + SELECT pg_current_xact_id()::xid; + COMMIT; +]); + +my $before_restart_ts = $node_primary->safe_psql('postgres', + qq[SELECT pg_xact_commit_timestamp('$xid');]); +ok($before_restart_ts ne '' && $before_restart_ts ne 'null', + 'commit timestamp recorded'); + +$node_primary->stop('immediate'); +$node_primary->start; + +my $after_crash_ts = $node_primary->safe_psql('postgres', + qq[SELECT pg_xact_commit_timestamp('$xid');]); +is($after_crash_ts, $before_restart_ts, + 'timestamps before and after crash are equal'); + +$node_primary->stop('fast'); +$node_primary->start; + +my $after_restart_ts = $node_primary->safe_psql('postgres', + qq[SELECT pg_xact_commit_timestamp('$xid');]); +is($after_restart_ts, $before_restart_ts, + 'timestamps before and after restart are equal'); + +# Now disable commit timestamps +$node_primary->append_conf('postgresql.conf', 'track_commit_timestamp = off'); +$node_primary->stop('fast'); + +# Start the server, which generates a XLOG_PARAMETER_CHANGE record where +# the parameter change is registered. +$node_primary->start; + +# Now restart again the server so as no XLOG_PARAMETER_CHANGE record are +# replayed with the follow-up immediate shutdown. +$node_primary->restart; + +# Move commit timestamps across page boundaries. Things should still +# be able to work across restarts with those transactions committed while +# track_commit_timestamp is disabled. +$node_primary->safe_psql( + 'postgres', + qq(CREATE PROCEDURE consume_xid(cnt int) +AS \$\$ +DECLARE + i int; + BEGIN + FOR i in 1..cnt LOOP + EXECUTE 'SELECT pg_current_xact_id()'; + COMMIT; + END LOOP; + END; +\$\$ +LANGUAGE plpgsql; +)); +$node_primary->safe_psql('postgres', 'CALL consume_xid(2000)'); + +($ret, $stdout, $stderr) = $node_primary->psql('postgres', + qq[SELECT pg_xact_commit_timestamp('$xid');]); +is($ret, 3, 'no commit timestamp from enable tx when cts disabled'); +like( + $stderr, + qr/could not get commit timestamp data/, + 'expected error from enabled tx when committs disabled'); + +# Do a tx while cts disabled +my $xid_disabled = $node_primary->safe_psql( + 'postgres', qq[ + BEGIN; + INSERT INTO committs_test(x, y) VALUES (2, current_timestamp); + SELECT pg_current_xact_id(); + COMMIT; +]); + +# Should be inaccessible +($ret, $stdout, $stderr) = $node_primary->psql('postgres', + qq[SELECT pg_xact_commit_timestamp('$xid_disabled');]); +is($ret, 3, 'no commit timestamp when disabled'); +like( + $stderr, + qr/could not get commit timestamp data/, + 'expected error from disabled tx when committs disabled'); + +# Re-enable, restart and ensure we can still get the old timestamps +$node_primary->append_conf('postgresql.conf', 'track_commit_timestamp = on'); + +# An immediate shutdown is used here. At next startup recovery will +# replay transactions which committed when track_commit_timestamp was +# disabled, and the facility should be able to work properly. +$node_primary->stop('immediate'); +$node_primary->start; + +my $after_enable_ts = $node_primary->safe_psql('postgres', + qq[SELECT pg_xact_commit_timestamp('$xid');]); +is($after_enable_ts, '', 'timestamp of enabled tx null after re-enable'); + +my $after_enable_disabled_ts = $node_primary->safe_psql('postgres', + qq[SELECT pg_xact_commit_timestamp('$xid_disabled');]); +is($after_enable_disabled_ts, '', + 'timestamp of disabled tx null after re-enable'); + +$node_primary->stop; + +done_testing(); diff --git a/src/test/modules/delay_execution/.gitignore b/src/test/modules/delay_execution/.gitignore new file mode 100644 index 0000000..ba2160b --- /dev/null +++ b/src/test/modules/delay_execution/.gitignore @@ -0,0 +1,3 @@ +# Generated subdirectories +/output_iso/ +/tmp_check_iso/ diff --git a/src/test/modules/delay_execution/Makefile b/src/test/modules/delay_execution/Makefile new file mode 100644 index 0000000..70f24e8 --- /dev/null +++ b/src/test/modules/delay_execution/Makefile @@ -0,0 +1,22 @@ +# src/test/modules/delay_execution/Makefile + +PGFILEDESC = "delay_execution - allow delay between parsing and execution" + +MODULE_big = delay_execution +OBJS = \ + $(WIN32RES) \ + delay_execution.o + +ISOLATION = partition-addition \ + partition-removal-1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/delay_execution +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/delay_execution/delay_execution.c b/src/test/modules/delay_execution/delay_execution.c new file mode 100644 index 0000000..407ebc0 --- /dev/null +++ b/src/test/modules/delay_execution/delay_execution.c @@ -0,0 +1,98 @@ +/*------------------------------------------------------------------------- + * + * delay_execution.c + * Test module to allow delay between parsing and execution of a query. + * + * The delay is implemented by taking and immediately releasing a specified + * advisory lock. If another process has previously taken that lock, the + * current process will be blocked until the lock is released; otherwise, + * there's no effect. This allows an isolationtester script to reliably + * test behaviors where some specified action happens in another backend + * between parsing and execution of any desired query. + * + * Copyright (c) 2020-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/delay_execution/delay_execution.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include <limits.h> + +#include "optimizer/planner.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/inval.h" + + +PG_MODULE_MAGIC; + +/* GUC: advisory lock ID to use. Zero disables the feature. */ +static int post_planning_lock_id = 0; + +/* Save previous planner hook user to be a good citizen */ +static planner_hook_type prev_planner_hook = NULL; + +/* Module load function */ +void _PG_init(void); + + +/* planner_hook function to provide the desired delay */ +static PlannedStmt * +delay_execution_planner(Query *parse, const char *query_string, + int cursorOptions, ParamListInfo boundParams) +{ + PlannedStmt *result; + + /* Invoke the planner, possibly via a previous hook user */ + if (prev_planner_hook) + result = prev_planner_hook(parse, query_string, cursorOptions, + boundParams); + else + result = standard_planner(parse, query_string, cursorOptions, + boundParams); + + /* If enabled, delay by taking and releasing the specified lock */ + if (post_planning_lock_id != 0) + { + DirectFunctionCall1(pg_advisory_lock_int8, + Int64GetDatum((int64) post_planning_lock_id)); + DirectFunctionCall1(pg_advisory_unlock_int8, + Int64GetDatum((int64) post_planning_lock_id)); + + /* + * Ensure that we notice any pending invalidations, since the advisory + * lock functions don't do this. + */ + AcceptInvalidationMessages(); + } + + return result; +} + +/* Module load function */ +void +_PG_init(void) +{ + /* Set up the GUC to control which lock is used */ + DefineCustomIntVariable("delay_execution.post_planning_lock_id", + "Sets the advisory lock ID to be locked/unlocked after planning.", + "Zero disables the delay.", + &post_planning_lock_id, + 0, + 0, INT_MAX, + PGC_USERSET, + 0, + NULL, + NULL, + NULL); + + MarkGUCPrefixReserved("delay_execution"); + + /* Install our hook */ + prev_planner_hook = planner_hook; + planner_hook = delay_execution_planner; +} diff --git a/src/test/modules/delay_execution/expected/partition-addition.out b/src/test/modules/delay_execution/expected/partition-addition.out new file mode 100644 index 0000000..7d6572b --- /dev/null +++ b/src/test/modules/delay_execution/expected/partition-addition.out @@ -0,0 +1,27 @@ +Parsed test spec with 2 sessions + +starting permutation: s2lock s1exec s2addp s2unlock +step s2lock: SELECT pg_advisory_lock(12345); +pg_advisory_lock +---------------- + +(1 row) + +step s1exec: LOAD 'delay_execution'; + SET delay_execution.post_planning_lock_id = 12345; + SELECT * FROM foo WHERE a <> 1 AND a <> (SELECT 3); <waiting ...> +step s2addp: CREATE TABLE foo2 (LIKE foo); + ALTER TABLE foo ATTACH PARTITION foo2 FOR VALUES IN (2); + INSERT INTO foo VALUES (2, 'ADD2'); +step s2unlock: SELECT pg_advisory_unlock(12345); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1exec: <... completed> +a|b +-+--- +4|GHI +(1 row) + diff --git a/src/test/modules/delay_execution/expected/partition-removal-1.out b/src/test/modules/delay_execution/expected/partition-removal-1.out new file mode 100644 index 0000000..b81b999 --- /dev/null +++ b/src/test/modules/delay_execution/expected/partition-removal-1.out @@ -0,0 +1,233 @@ +Parsed test spec with 3 sessions + +starting permutation: s3lock s1b s1exec s2remp s3check s3unlock s3check s1c +step s3lock: SELECT pg_advisory_lock(12543); +pg_advisory_lock +---------------- + +(1 row) + +step s1b: BEGIN; +step s1exec: SELECT * FROM partrem WHERE a <> 1 AND a <> (SELECT 3); <waiting ...> +step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...> +step s3check: SELECT * FROM partrem; +a|b +-+--- +1|ABC +3|DEF +(2 rows) + +step s3unlock: SELECT pg_advisory_unlock(12543); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1exec: <... completed> +a|b +-+--- +2|JKL +(1 row) + +step s3check: SELECT * FROM partrem; +a|b +-+--- +1|ABC +3|DEF +(2 rows) + +step s1c: COMMIT; +step s2remp: <... completed> + +starting permutation: s3lock s1brr s1exec s2remp s3check s3unlock s3check s1c +step s3lock: SELECT pg_advisory_lock(12543); +pg_advisory_lock +---------------- + +(1 row) + +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1exec: SELECT * FROM partrem WHERE a <> 1 AND a <> (SELECT 3); <waiting ...> +step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...> +step s3check: SELECT * FROM partrem; +a|b +-+--- +1|ABC +3|DEF +(2 rows) + +step s3unlock: SELECT pg_advisory_unlock(12543); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1exec: <... completed> +a|b +-+--- +2|JKL +(1 row) + +step s3check: SELECT * FROM partrem; +a|b +-+--- +1|ABC +3|DEF +(2 rows) + +step s1c: COMMIT; +step s2remp: <... completed> + +starting permutation: s3lock s1b s1exec2 s2remp s3unlock s1c +step s3lock: SELECT pg_advisory_lock(12543); +pg_advisory_lock +---------------- + +(1 row) + +step s1b: BEGIN; +step s1exec2: SELECT * FROM partrem WHERE a <> (SELECT 2) AND a <> 1; <waiting ...> +step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...> +step s3unlock: SELECT pg_advisory_unlock(12543); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1exec2: <... completed> +a|b +-+--- +3|DEF +(1 row) + +step s1c: COMMIT; +step s2remp: <... completed> + +starting permutation: s3lock s1brr s1exec2 s2remp s3unlock s1c +step s3lock: SELECT pg_advisory_lock(12543); +pg_advisory_lock +---------------- + +(1 row) + +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1exec2: SELECT * FROM partrem WHERE a <> (SELECT 2) AND a <> 1; <waiting ...> +step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...> +step s3unlock: SELECT pg_advisory_unlock(12543); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1exec2: <... completed> +a|b +-+--- +3|DEF +(1 row) + +step s1c: COMMIT; +step s2remp: <... completed> + +starting permutation: s3lock s1brr s1prepare s2remp s1execprep s3unlock s1check s1c s1check s1dealloc +step s3lock: SELECT pg_advisory_lock(12543); +pg_advisory_lock +---------------- + +(1 row) + +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1prepare: PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI'); +step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...> +step s1execprep: EXECUTE ins(2); <waiting ...> +step s3unlock: SELECT pg_advisory_unlock(12543); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1execprep: <... completed> +step s1check: SELECT * FROM partrem WHERE b = 'GHI'; +a|b +-+--- +2|GHI +(1 row) + +step s1c: COMMIT; +step s2remp: <... completed> +step s1check: SELECT * FROM partrem WHERE b = 'GHI'; +a|b +-+- +(0 rows) + +step s1dealloc: DEALLOCATE ins; + +starting permutation: s1brr s1prepare s2remp s3lock s1execprep s3unlock s1check s1c s1check s1dealloc +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1prepare: PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI'); +step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...> +step s3lock: SELECT pg_advisory_lock(12543); +pg_advisory_lock +---------------- + +(1 row) + +step s1execprep: EXECUTE ins(2); <waiting ...> +step s3unlock: SELECT pg_advisory_unlock(12543); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1execprep: <... completed> +step s1check: SELECT * FROM partrem WHERE b = 'GHI'; +a|b +-+--- +2|GHI +(1 row) + +step s1c: COMMIT; +step s2remp: <... completed> +step s1check: SELECT * FROM partrem WHERE b = 'GHI'; +a|b +-+- +(0 rows) + +step s1dealloc: DEALLOCATE ins; + +starting permutation: s1brr s1check s3lock s2remp s1prepare s1execprep s3unlock s1check s1c s1check s1dealloc +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1check: SELECT * FROM partrem WHERE b = 'GHI'; +a|b +-+- +(0 rows) + +step s3lock: SELECT pg_advisory_lock(12543); +pg_advisory_lock +---------------- + +(1 row) + +step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...> +step s1prepare: PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI'); +step s1execprep: EXECUTE ins(2); <waiting ...> +step s3unlock: SELECT pg_advisory_unlock(12543); +pg_advisory_unlock +------------------ +t +(1 row) + +step s1execprep: <... completed> +step s1check: SELECT * FROM partrem WHERE b = 'GHI'; +a|b +-+--- +2|GHI +(1 row) + +step s1c: COMMIT; +step s2remp: <... completed> +step s1check: SELECT * FROM partrem WHERE b = 'GHI'; +a|b +-+- +(0 rows) + +step s1dealloc: DEALLOCATE ins; diff --git a/src/test/modules/delay_execution/specs/partition-addition.spec b/src/test/modules/delay_execution/specs/partition-addition.spec new file mode 100644 index 0000000..2a09482 --- /dev/null +++ b/src/test/modules/delay_execution/specs/partition-addition.spec @@ -0,0 +1,38 @@ +# Test addition of a partition with less-than-exclusive locking. + +setup +{ + CREATE TABLE foo (a int, b text) PARTITION BY LIST(a); + CREATE TABLE foo1 PARTITION OF foo FOR VALUES IN (1); + CREATE TABLE foo3 PARTITION OF foo FOR VALUES IN (3); + CREATE TABLE foo4 PARTITION OF foo FOR VALUES IN (4); + INSERT INTO foo VALUES (1, 'ABC'); + INSERT INTO foo VALUES (3, 'DEF'); + INSERT INTO foo VALUES (4, 'GHI'); +} + +teardown +{ + DROP TABLE foo; +} + +# The SELECT will be planned with just the three partitions shown above, +# of which we expect foo1 to be pruned at planning and foo3 at execution. +# Then we'll block, and by the time the query is actually executed, +# partition foo2 will also exist. We expect that not to be scanned. +# This test is specifically designed to check ExecCreatePartitionPruneState's +# code for matching up the partition lists in such cases. + +session "s1" +step "s1exec" { LOAD 'delay_execution'; + SET delay_execution.post_planning_lock_id = 12345; + SELECT * FROM foo WHERE a <> 1 AND a <> (SELECT 3); } + +session "s2" +step "s2lock" { SELECT pg_advisory_lock(12345); } +step "s2unlock" { SELECT pg_advisory_unlock(12345); } +step "s2addp" { CREATE TABLE foo2 (LIKE foo); + ALTER TABLE foo ATTACH PARTITION foo2 FOR VALUES IN (2); + INSERT INTO foo VALUES (2, 'ADD2'); } + +permutation "s2lock" "s1exec" "s2addp" "s2unlock" diff --git a/src/test/modules/delay_execution/specs/partition-removal-1.spec b/src/test/modules/delay_execution/specs/partition-removal-1.spec new file mode 100644 index 0000000..5ee2750 --- /dev/null +++ b/src/test/modules/delay_execution/specs/partition-removal-1.spec @@ -0,0 +1,58 @@ +# Test removal of a partition with less-than-exclusive locking. + +setup +{ + CREATE TABLE partrem (a int, b text) PARTITION BY LIST(a); + CREATE TABLE partrem1 PARTITION OF partrem FOR VALUES IN (1); + CREATE TABLE partrem2 PARTITION OF partrem FOR VALUES IN (2); + CREATE TABLE partrem3 PARTITION OF partrem FOR VALUES IN (3); + INSERT INTO partrem VALUES (1, 'ABC'); + INSERT INTO partrem VALUES (2, 'JKL'); + INSERT INTO partrem VALUES (3, 'DEF'); +} + +teardown +{ + DROP TABLE IF EXISTS partrem, partrem2; +} + +session "s1" +setup { LOAD 'delay_execution'; + SET delay_execution.post_planning_lock_id = 12543; } +step "s1b" { BEGIN; } +step "s1brr" { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step "s1exec" { SELECT * FROM partrem WHERE a <> 1 AND a <> (SELECT 3); } +step "s1exec2" { SELECT * FROM partrem WHERE a <> (SELECT 2) AND a <> 1; } +step "s1prepare" { PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI'); } +step "s1execprep" { EXECUTE ins(2); } +step "s1check" { SELECT * FROM partrem WHERE b = 'GHI'; } +step "s1c" { COMMIT; } +step "s1dealloc" { DEALLOCATE ins; } + +session "s2" +step "s2remp" { ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; } + +session "s3" +step "s3lock" { SELECT pg_advisory_lock(12543); } +step "s3unlock" { SELECT pg_advisory_unlock(12543); } +step "s3check" { SELECT * FROM partrem; } + +# The SELECT will be planned with all three partitions shown above, +# of which we expect partrem1 to be pruned at planning and partrem3 at +# execution. Then we'll block, and by the time the query is actually +# executed, detach of partrem2 is already underway (so its row doesn't +# show up in s3's result), but we expect its row to still appear in the +# result for s1. +permutation "s3lock" "s1b" "s1exec" "s2remp" "s3check" "s3unlock" "s3check" "s1c" +permutation "s3lock" "s1brr" "s1exec" "s2remp" "s3check" "s3unlock" "s3check" "s1c" + +# In this case we're testing that after pruning partrem2 at runtime, the +# query still works correctly. +permutation "s3lock" "s1b" "s1exec2" "s2remp" "s3unlock" "s1c" +permutation "s3lock" "s1brr" "s1exec2" "s2remp" "s3unlock" "s1c" + +# In this case we test that an insert that's prepared in repeatable read +# mode still works after detaching. +permutation "s3lock" "s1brr" "s1prepare" "s2remp" "s1execprep" "s3unlock" "s1check" "s1c" "s1check" "s1dealloc" +permutation "s1brr" "s1prepare" "s2remp" "s3lock" "s1execprep" "s3unlock" "s1check" "s1c" "s1check" "s1dealloc" +permutation "s1brr" "s1check" "s3lock" "s2remp" "s1prepare" "s1execprep" "s3unlock" "s1check" "s1c" "s1check" "s1dealloc" diff --git a/src/test/modules/dummy_index_am/.gitignore b/src/test/modules/dummy_index_am/.gitignore new file mode 100644 index 0000000..44d119c --- /dev/null +++ b/src/test/modules/dummy_index_am/.gitignore @@ -0,0 +1,3 @@ +# Generated subdirectories +/log/ +/results/ diff --git a/src/test/modules/dummy_index_am/Makefile b/src/test/modules/dummy_index_am/Makefile new file mode 100644 index 0000000..aaf544a --- /dev/null +++ b/src/test/modules/dummy_index_am/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/dummy_index_am/Makefile + +MODULES = dummy_index_am + +EXTENSION = dummy_index_am +DATA = dummy_index_am--1.0.sql +PGFILEDESC = "dummy_index_am - index access method template" + +REGRESS = reloptions + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/dummy_index_am +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/dummy_index_am/README b/src/test/modules/dummy_index_am/README new file mode 100644 index 0000000..61510f0 --- /dev/null +++ b/src/test/modules/dummy_index_am/README @@ -0,0 +1,12 @@ +Dummy Index AM +============== + +Dummy index AM is a module for testing any facility usable by an index +access method, whose code is kept a maximum simple. + +This includes tests for all relation option types: +- boolean +- enum +- integer +- real +- strings (with and without NULL as default) diff --git a/src/test/modules/dummy_index_am/dummy_index_am--1.0.sql b/src/test/modules/dummy_index_am/dummy_index_am--1.0.sql new file mode 100644 index 0000000..005863d --- /dev/null +++ b/src/test/modules/dummy_index_am/dummy_index_am--1.0.sql @@ -0,0 +1,19 @@ +/* src/test/modules/dummy_index_am/dummy_index_am--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION dummy_index_am" to load this file. \quit + +CREATE FUNCTION dihandler(internal) +RETURNS index_am_handler +AS 'MODULE_PATHNAME' +LANGUAGE C; + +-- Access method +CREATE ACCESS METHOD dummy_index_am TYPE INDEX HANDLER dihandler; +COMMENT ON ACCESS METHOD dummy_index_am IS 'dummy index access method'; + +-- Operator classes +CREATE OPERATOR CLASS int4_ops +DEFAULT FOR TYPE int4 USING dummy_index_am AS + OPERATOR 1 = (int4, int4), + FUNCTION 1 hashint4(int4); diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c new file mode 100644 index 0000000..a0894ff --- /dev/null +++ b/src/test/modules/dummy_index_am/dummy_index_am.c @@ -0,0 +1,333 @@ +/*------------------------------------------------------------------------- + * + * dummy_index_am.c + * Index AM template main file. + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/test/modules/dummy_index_am/dummy_index_am.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/amapi.h" +#include "access/reloptions.h" +#include "catalog/index.h" +#include "commands/vacuum.h" +#include "nodes/pathnodes.h" +#include "utils/guc.h" +#include "utils/rel.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +/* parse table for fillRelOptions */ +relopt_parse_elt di_relopt_tab[6]; + +/* Kind of relation options for dummy index */ +relopt_kind di_relopt_kind; + +typedef enum DummyAmEnum +{ + DUMMY_AM_ENUM_ONE, + DUMMY_AM_ENUM_TWO +} DummyAmEnum; + +/* Dummy index options */ +typedef struct DummyIndexOptions +{ + int32 vl_len_; /* varlena header (do not touch directly!) */ + int option_int; + double option_real; + bool option_bool; + DummyAmEnum option_enum; + int option_string_val_offset; + int option_string_null_offset; +} DummyIndexOptions; + +relopt_enum_elt_def dummyAmEnumValues[] = +{ + {"one", DUMMY_AM_ENUM_ONE}, + {"two", DUMMY_AM_ENUM_TWO}, + {(const char *) NULL} /* list terminator */ +}; + +/* Handler for index AM */ +PG_FUNCTION_INFO_V1(dihandler); + +/* + * Validation function for string relation options. + */ +static void +validate_string_option(const char *value) +{ + ereport(NOTICE, + (errmsg("new option value for string parameter %s", + value ? value : "NULL"))); +} + +/* + * This function creates a full set of relation option types, + * with various patterns. + */ +static void +create_reloptions_table(void) +{ + di_relopt_kind = add_reloption_kind(); + + add_int_reloption(di_relopt_kind, "option_int", + "Integer option for dummy_index_am", + 10, -10, 100, AccessExclusiveLock); + di_relopt_tab[0].optname = "option_int"; + di_relopt_tab[0].opttype = RELOPT_TYPE_INT; + di_relopt_tab[0].offset = offsetof(DummyIndexOptions, option_int); + + add_real_reloption(di_relopt_kind, "option_real", + "Real option for dummy_index_am", + 3.1415, -10, 100, AccessExclusiveLock); + di_relopt_tab[1].optname = "option_real"; + di_relopt_tab[1].opttype = RELOPT_TYPE_REAL; + di_relopt_tab[1].offset = offsetof(DummyIndexOptions, option_real); + + add_bool_reloption(di_relopt_kind, "option_bool", + "Boolean option for dummy_index_am", + true, AccessExclusiveLock); + di_relopt_tab[2].optname = "option_bool"; + di_relopt_tab[2].opttype = RELOPT_TYPE_BOOL; + di_relopt_tab[2].offset = offsetof(DummyIndexOptions, option_bool); + + add_enum_reloption(di_relopt_kind, "option_enum", + "Enum option for dummy_index_am", + dummyAmEnumValues, + DUMMY_AM_ENUM_ONE, + "Valid values are \"one\" and \"two\".", + AccessExclusiveLock); + di_relopt_tab[3].optname = "option_enum"; + di_relopt_tab[3].opttype = RELOPT_TYPE_ENUM; + di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_enum); + + add_string_reloption(di_relopt_kind, "option_string_val", + "String option for dummy_index_am with non-NULL default", + "DefaultValue", &validate_string_option, + AccessExclusiveLock); + di_relopt_tab[4].optname = "option_string_val"; + di_relopt_tab[4].opttype = RELOPT_TYPE_STRING; + di_relopt_tab[4].offset = offsetof(DummyIndexOptions, + option_string_val_offset); + + /* + * String option for dummy_index_am with NULL default, and without + * description. + */ + add_string_reloption(di_relopt_kind, "option_string_null", + NULL, /* description */ + NULL, &validate_string_option, + AccessExclusiveLock); + di_relopt_tab[5].optname = "option_string_null"; + di_relopt_tab[5].opttype = RELOPT_TYPE_STRING; + di_relopt_tab[5].offset = offsetof(DummyIndexOptions, + option_string_null_offset); +} + + +/* + * Build a new index. + */ +static IndexBuildResult * +dibuild(Relation heap, Relation index, IndexInfo *indexInfo) +{ + IndexBuildResult *result; + + result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); + + /* let's pretend that no tuples were scanned */ + result->heap_tuples = 0; + /* and no index tuples were created (that is true) */ + result->index_tuples = 0; + + return result; +} + +/* + * Build an empty index for the initialization fork. + */ +static void +dibuildempty(Relation index) +{ + /* No need to build an init fork for a dummy index */ +} + +/* + * Insert new tuple to index AM. + */ +static bool +diinsert(Relation index, Datum *values, bool *isnull, + ItemPointer ht_ctid, Relation heapRel, + IndexUniqueCheck checkUnique, + bool indexUnchanged, + IndexInfo *indexInfo) +{ + /* nothing to do */ + return false; +} + +/* + * Bulk deletion of all index entries pointing to a set of table tuples. + */ +static IndexBulkDeleteResult * +dibulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, + IndexBulkDeleteCallback callback, void *callback_state) +{ + /* + * There is nothing to delete. Return NULL as there is nothing to pass to + * amvacuumcleanup. + */ + return NULL; +} + +/* + * Post-VACUUM cleanup for index AM. + */ +static IndexBulkDeleteResult * +divacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) +{ + /* Index has not been modified, so returning NULL is fine */ + return NULL; +} + +/* + * Estimate cost of index AM. + */ +static void +dicostestimate(PlannerInfo *root, IndexPath *path, double loop_count, + Cost *indexStartupCost, Cost *indexTotalCost, + Selectivity *indexSelectivity, double *indexCorrelation, + double *indexPages) +{ + /* Tell planner to never use this index! */ + *indexStartupCost = 1.0e10; + *indexTotalCost = 1.0e10; + + /* Do not care about the rest */ + *indexSelectivity = 1; + *indexCorrelation = 0; + *indexPages = 1; +} + +/* + * Parse relation options for index AM, returning a DummyIndexOptions + * structure filled with option values. + */ +static bytea * +dioptions(Datum reloptions, bool validate) +{ + return (bytea *) build_reloptions(reloptions, validate, + di_relopt_kind, + sizeof(DummyIndexOptions), + di_relopt_tab, lengthof(di_relopt_tab)); +} + +/* + * Validator for index AM. + */ +static bool +divalidate(Oid opclassoid) +{ + /* Index is dummy so we are happy with any opclass */ + return true; +} + +/* + * Begin scan of index AM. + */ +static IndexScanDesc +dibeginscan(Relation r, int nkeys, int norderbys) +{ + IndexScanDesc scan; + + /* Let's pretend we are doing something */ + scan = RelationGetIndexScan(r, nkeys, norderbys); + return scan; +} + +/* + * Rescan of index AM. + */ +static void +direscan(IndexScanDesc scan, ScanKey scankey, int nscankeys, + ScanKey orderbys, int norderbys) +{ + /* nothing to do */ +} + +/* + * End scan of index AM. + */ +static void +diendscan(IndexScanDesc scan) +{ + /* nothing to do */ +} + +/* + * Index AM handler function: returns IndexAmRoutine with access method + * parameters and callbacks. + */ +Datum +dihandler(PG_FUNCTION_ARGS) +{ + IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); + + amroutine->amstrategies = 0; + amroutine->amsupport = 1; + amroutine->amcanorder = false; + amroutine->amcanorderbyop = false; + amroutine->amcanbackward = false; + amroutine->amcanunique = false; + amroutine->amcanmulticol = false; + amroutine->amoptionalkey = false; + amroutine->amsearcharray = false; + amroutine->amsearchnulls = false; + amroutine->amstorage = false; + amroutine->amclusterable = false; + amroutine->ampredlocks = false; + amroutine->amcanparallel = false; + amroutine->amcaninclude = false; + amroutine->amusemaintenanceworkmem = false; + amroutine->amparallelvacuumoptions = VACUUM_OPTION_NO_PARALLEL; + amroutine->amkeytype = InvalidOid; + + amroutine->ambuild = dibuild; + amroutine->ambuildempty = dibuildempty; + amroutine->aminsert = diinsert; + amroutine->ambulkdelete = dibulkdelete; + amroutine->amvacuumcleanup = divacuumcleanup; + amroutine->amcanreturn = NULL; + amroutine->amcostestimate = dicostestimate; + amroutine->amoptions = dioptions; + amroutine->amproperty = NULL; + amroutine->ambuildphasename = NULL; + amroutine->amvalidate = divalidate; + amroutine->ambeginscan = dibeginscan; + amroutine->amrescan = direscan; + amroutine->amgettuple = NULL; + amroutine->amgetbitmap = NULL; + amroutine->amendscan = diendscan; + amroutine->ammarkpos = NULL; + amroutine->amrestrpos = NULL; + amroutine->amestimateparallelscan = NULL; + amroutine->aminitparallelscan = NULL; + amroutine->amparallelrescan = NULL; + + PG_RETURN_POINTER(amroutine); +} + +void +_PG_init(void) +{ + create_reloptions_table(); +} diff --git a/src/test/modules/dummy_index_am/dummy_index_am.control b/src/test/modules/dummy_index_am/dummy_index_am.control new file mode 100644 index 0000000..77bdea0 --- /dev/null +++ b/src/test/modules/dummy_index_am/dummy_index_am.control @@ -0,0 +1,5 @@ +# dummy_index_am extension +comment = 'dummy_index_am - index access method template' +default_version = '1.0' +module_pathname = '$libdir/dummy_index_am' +relocatable = true diff --git a/src/test/modules/dummy_index_am/expected/reloptions.out b/src/test/modules/dummy_index_am/expected/reloptions.out new file mode 100644 index 0000000..c873a80 --- /dev/null +++ b/src/test/modules/dummy_index_am/expected/reloptions.out @@ -0,0 +1,145 @@ +-- Tests for relation options +CREATE EXTENSION dummy_index_am; +CREATE TABLE dummy_test_tab (i int4); +-- Silence validation checks for strings +SET client_min_messages TO 'warning'; +-- Test with default values. +CREATE INDEX dummy_test_idx ON dummy_test_tab + USING dummy_index_am (i); +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +-------- +(0 rows) + +DROP INDEX dummy_test_idx; +-- Test with full set of options. +-- Allow validation checks for strings, just for the index creation +SET client_min_messages TO 'notice'; +CREATE INDEX dummy_test_idx ON dummy_test_tab + USING dummy_index_am (i) WITH ( + option_bool = false, + option_int = 5, + option_real = 3.1, + option_enum = 'two', + option_string_val = NULL, + option_string_null = 'val'); +NOTICE: new option value for string parameter null +NOTICE: new option value for string parameter val +-- Silence again validation checks for strings until the end of the test. +SET client_min_messages TO 'warning'; +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +------------------------ + option_bool=false + option_int=5 + option_real=3.1 + option_enum=two + option_string_val=null + option_string_null=val +(6 rows) + +-- ALTER INDEX .. SET +ALTER INDEX dummy_test_idx SET (option_int = 10); +ALTER INDEX dummy_test_idx SET (option_bool = true); +ALTER INDEX dummy_test_idx SET (option_real = 3.2); +ALTER INDEX dummy_test_idx SET (option_string_val = 'val2'); +ALTER INDEX dummy_test_idx SET (option_string_null = NULL); +ALTER INDEX dummy_test_idx SET (option_enum = 'one'); +ALTER INDEX dummy_test_idx SET (option_enum = 'three'); +ERROR: invalid value for enum option "option_enum": three +DETAIL: Valid values are "one" and "two". +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +------------------------- + option_int=10 + option_bool=true + option_real=3.2 + option_string_val=val2 + option_string_null=null + option_enum=one +(6 rows) + +-- ALTER INDEX .. RESET +ALTER INDEX dummy_test_idx RESET (option_int); +ALTER INDEX dummy_test_idx RESET (option_bool); +ALTER INDEX dummy_test_idx RESET (option_real); +ALTER INDEX dummy_test_idx RESET (option_enum); +ALTER INDEX dummy_test_idx RESET (option_string_val); +ALTER INDEX dummy_test_idx RESET (option_string_null); +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +-------- +(0 rows) + +-- Cross-type checks for reloption values +-- Integer +ALTER INDEX dummy_test_idx SET (option_int = 3.3); -- ok +ALTER INDEX dummy_test_idx SET (option_int = true); -- error +ERROR: invalid value for integer option "option_int": true +ALTER INDEX dummy_test_idx SET (option_int = 'val3'); -- error +ERROR: invalid value for integer option "option_int": val3 +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +---------------- + option_int=3.3 +(1 row) + +ALTER INDEX dummy_test_idx RESET (option_int); +-- Boolean +ALTER INDEX dummy_test_idx SET (option_bool = 4); -- error +ERROR: invalid value for boolean option "option_bool": 4 +ALTER INDEX dummy_test_idx SET (option_bool = 1); -- ok, as true +ALTER INDEX dummy_test_idx SET (option_bool = 3.4); -- error +ERROR: invalid value for boolean option "option_bool": 3.4 +ALTER INDEX dummy_test_idx SET (option_bool = 'val4'); -- error +ERROR: invalid value for boolean option "option_bool": val4 +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +--------------- + option_bool=1 +(1 row) + +ALTER INDEX dummy_test_idx RESET (option_bool); +-- Float +ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok +ALTER INDEX dummy_test_idx SET (option_real = true); -- error +ERROR: invalid value for floating point option "option_real": true +ALTER INDEX dummy_test_idx SET (option_real = 'val5'); -- error +ERROR: invalid value for floating point option "option_real": val5 +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +--------------- + option_real=4 +(1 row) + +ALTER INDEX dummy_test_idx RESET (option_real); +-- Enum +ALTER INDEX dummy_test_idx SET (option_enum = 'one'); -- ok +ALTER INDEX dummy_test_idx SET (option_enum = 0); -- error +ERROR: invalid value for enum option "option_enum": 0 +DETAIL: Valid values are "one" and "two". +ALTER INDEX dummy_test_idx SET (option_enum = true); -- error +ERROR: invalid value for enum option "option_enum": true +DETAIL: Valid values are "one" and "two". +ALTER INDEX dummy_test_idx SET (option_enum = 'three'); -- error +ERROR: invalid value for enum option "option_enum": three +DETAIL: Valid values are "one" and "two". +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +----------------- + option_enum=one +(1 row) + +ALTER INDEX dummy_test_idx RESET (option_enum); +-- String +ALTER INDEX dummy_test_idx SET (option_string_val = 4); -- ok +ALTER INDEX dummy_test_idx SET (option_string_val = 3.5); -- ok +ALTER INDEX dummy_test_idx SET (option_string_val = true); -- ok, as "true" +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +------------------------ + option_string_val=true +(1 row) + +ALTER INDEX dummy_test_idx RESET (option_string_val); +DROP INDEX dummy_test_idx; diff --git a/src/test/modules/dummy_index_am/sql/reloptions.sql b/src/test/modules/dummy_index_am/sql/reloptions.sql new file mode 100644 index 0000000..6749d76 --- /dev/null +++ b/src/test/modules/dummy_index_am/sql/reloptions.sql @@ -0,0 +1,83 @@ +-- Tests for relation options +CREATE EXTENSION dummy_index_am; + +CREATE TABLE dummy_test_tab (i int4); + +-- Silence validation checks for strings +SET client_min_messages TO 'warning'; + +-- Test with default values. +CREATE INDEX dummy_test_idx ON dummy_test_tab + USING dummy_index_am (i); +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; +DROP INDEX dummy_test_idx; + +-- Test with full set of options. +-- Allow validation checks for strings, just for the index creation +SET client_min_messages TO 'notice'; +CREATE INDEX dummy_test_idx ON dummy_test_tab + USING dummy_index_am (i) WITH ( + option_bool = false, + option_int = 5, + option_real = 3.1, + option_enum = 'two', + option_string_val = NULL, + option_string_null = 'val'); +-- Silence again validation checks for strings until the end of the test. +SET client_min_messages TO 'warning'; +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + +-- ALTER INDEX .. SET +ALTER INDEX dummy_test_idx SET (option_int = 10); +ALTER INDEX dummy_test_idx SET (option_bool = true); +ALTER INDEX dummy_test_idx SET (option_real = 3.2); +ALTER INDEX dummy_test_idx SET (option_string_val = 'val2'); +ALTER INDEX dummy_test_idx SET (option_string_null = NULL); +ALTER INDEX dummy_test_idx SET (option_enum = 'one'); +ALTER INDEX dummy_test_idx SET (option_enum = 'three'); +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + +-- ALTER INDEX .. RESET +ALTER INDEX dummy_test_idx RESET (option_int); +ALTER INDEX dummy_test_idx RESET (option_bool); +ALTER INDEX dummy_test_idx RESET (option_real); +ALTER INDEX dummy_test_idx RESET (option_enum); +ALTER INDEX dummy_test_idx RESET (option_string_val); +ALTER INDEX dummy_test_idx RESET (option_string_null); +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + +-- Cross-type checks for reloption values +-- Integer +ALTER INDEX dummy_test_idx SET (option_int = 3.3); -- ok +ALTER INDEX dummy_test_idx SET (option_int = true); -- error +ALTER INDEX dummy_test_idx SET (option_int = 'val3'); -- error +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; +ALTER INDEX dummy_test_idx RESET (option_int); +-- Boolean +ALTER INDEX dummy_test_idx SET (option_bool = 4); -- error +ALTER INDEX dummy_test_idx SET (option_bool = 1); -- ok, as true +ALTER INDEX dummy_test_idx SET (option_bool = 3.4); -- error +ALTER INDEX dummy_test_idx SET (option_bool = 'val4'); -- error +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; +ALTER INDEX dummy_test_idx RESET (option_bool); +-- Float +ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok +ALTER INDEX dummy_test_idx SET (option_real = true); -- error +ALTER INDEX dummy_test_idx SET (option_real = 'val5'); -- error +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; +ALTER INDEX dummy_test_idx RESET (option_real); +-- Enum +ALTER INDEX dummy_test_idx SET (option_enum = 'one'); -- ok +ALTER INDEX dummy_test_idx SET (option_enum = 0); -- error +ALTER INDEX dummy_test_idx SET (option_enum = true); -- error +ALTER INDEX dummy_test_idx SET (option_enum = 'three'); -- error +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; +ALTER INDEX dummy_test_idx RESET (option_enum); +-- String +ALTER INDEX dummy_test_idx SET (option_string_val = 4); -- ok +ALTER INDEX dummy_test_idx SET (option_string_val = 3.5); -- ok +ALTER INDEX dummy_test_idx SET (option_string_val = true); -- ok, as "true" +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; +ALTER INDEX dummy_test_idx RESET (option_string_val); + +DROP INDEX dummy_test_idx; diff --git a/src/test/modules/dummy_seclabel/.gitignore b/src/test/modules/dummy_seclabel/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/dummy_seclabel/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/dummy_seclabel/Makefile b/src/test/modules/dummy_seclabel/Makefile new file mode 100644 index 0000000..d93c964 --- /dev/null +++ b/src/test/modules/dummy_seclabel/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/dummy_seclabel/Makefile + +MODULES = dummy_seclabel +PGFILEDESC = "dummy_seclabel - regression testing of the SECURITY LABEL statement" + +EXTENSION = dummy_seclabel +DATA = dummy_seclabel--1.0.sql + +REGRESS = dummy_seclabel + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/dummy_seclabel +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/dummy_seclabel/README b/src/test/modules/dummy_seclabel/README new file mode 100644 index 0000000..a3fcbd7 --- /dev/null +++ b/src/test/modules/dummy_seclabel/README @@ -0,0 +1,41 @@ +The dummy_seclabel module exists only to support regression testing of +the SECURITY LABEL statement. It is not intended to be used in production. + +Rationale +========= + +The SECURITY LABEL statement allows the user to assign security labels to +database objects; however, security labels can only be assigned when +specifically allowed by a loadable module, so this module is provided to +allow proper regression testing. + +Security label providers intended to be used in production will typically be +dependent on a platform-specific feature such as SELinux. This module is +platform-independent, and therefore better-suited to regression testing. + +Usage +===== + +Here's a simple example of usage: + +# postgresql.conf +shared_preload_libraries = 'dummy_seclabel' + +postgres=# CREATE TABLE t (a int, b text); +CREATE TABLE +postgres=# SECURITY LABEL ON TABLE t IS 'classified'; +SECURITY LABEL + +The dummy_seclabel module provides only four hardcoded +labels: unclassified, classified, +secret, and top secret. +It does not allow any other strings as security labels. + +These labels are not used to enforce access controls. They are only used +to check whether the SECURITY LABEL statement works as expected, +or not. + +Author +====== + +KaiGai Kohei <kaigai@ak.jp.nec.com> diff --git a/src/test/modules/dummy_seclabel/dummy_seclabel--1.0.sql b/src/test/modules/dummy_seclabel/dummy_seclabel--1.0.sql new file mode 100644 index 0000000..5f3cb5b --- /dev/null +++ b/src/test/modules/dummy_seclabel/dummy_seclabel--1.0.sql @@ -0,0 +1,8 @@ +/* src/test/modules/dummy_seclabel/dummy_seclabel--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION dummy_seclabel" to load this file. \quit + +CREATE FUNCTION dummy_seclabel_dummy() + RETURNS pg_catalog.void + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/dummy_seclabel/dummy_seclabel.c b/src/test/modules/dummy_seclabel/dummy_seclabel.c new file mode 100644 index 0000000..67658c1 --- /dev/null +++ b/src/test/modules/dummy_seclabel/dummy_seclabel.c @@ -0,0 +1,63 @@ +/* + * dummy_seclabel.c + * + * Dummy security label provider. + * + * This module does not provide anything worthwhile from a security + * perspective, but allows regression testing independent of platform-specific + * features like SELinux. + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + */ +#include "postgres.h" + +#include "commands/seclabel.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "utils/rel.h" + +PG_MODULE_MAGIC; + +/* Entrypoint of the module */ +void _PG_init(void); + +PG_FUNCTION_INFO_V1(dummy_seclabel_dummy); + +static void +dummy_object_relabel(const ObjectAddress *object, const char *seclabel) +{ + if (seclabel == NULL || + strcmp(seclabel, "unclassified") == 0 || + strcmp(seclabel, "classified") == 0) + return; + + if (strcmp(seclabel, "secret") == 0 || + strcmp(seclabel, "top secret") == 0) + { + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("only superuser can set '%s' label", seclabel))); + return; + } + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("'%s' is not a valid security label", seclabel))); +} + +void +_PG_init(void) +{ + register_label_provider("dummy", dummy_object_relabel); +} + +/* + * This function is here just so that the extension is not completely empty + * and the dynamic library is loaded when CREATE EXTENSION runs. + */ +Datum +dummy_seclabel_dummy(PG_FUNCTION_ARGS) +{ + PG_RETURN_VOID(); +} diff --git a/src/test/modules/dummy_seclabel/dummy_seclabel.control b/src/test/modules/dummy_seclabel/dummy_seclabel.control new file mode 100644 index 0000000..8c37272 --- /dev/null +++ b/src/test/modules/dummy_seclabel/dummy_seclabel.control @@ -0,0 +1,4 @@ +comment = 'Test code for SECURITY LABEL feature' +default_version = '1.0' +module_pathname = '$libdir/dummy_seclabel' +relocatable = true diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out new file mode 100644 index 0000000..b2d898a --- /dev/null +++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out @@ -0,0 +1,117 @@ +-- +-- Test for facilities of security label +-- +CREATE EXTENSION dummy_seclabel; +-- initial setups +SET client_min_messages TO 'warning'; +DROP ROLE IF EXISTS regress_dummy_seclabel_user1; +DROP ROLE IF EXISTS regress_dummy_seclabel_user2; +RESET client_min_messages; +CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE; +CREATE USER regress_dummy_seclabel_user2; +CREATE TABLE dummy_seclabel_tbl1 (a int, b text); +CREATE TABLE dummy_seclabel_tbl2 (x int, y text); +CREATE VIEW dummy_seclabel_view1 AS SELECT * FROM dummy_seclabel_tbl2; +CREATE FUNCTION dummy_seclabel_four() RETURNS integer AS $$SELECT 4$$ language sql; +CREATE DOMAIN dummy_seclabel_domain AS text; +ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1; +ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2; +-- +-- Test of SECURITY LABEL statement with a plugin +-- +SET SESSION AUTHORIZATION regress_dummy_seclabel_user1; +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK +SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK +SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail +ERROR: column name must be qualified +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail +ERROR: '...invalid label...' is not a valid security label +SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK +SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail +ERROR: security label provider "unknown_seclabel" is not loaded +SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner) +ERROR: must be owner of table dummy_seclabel_tbl2 +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser) +ERROR: only superuser can set 'secret' label +SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found) +ERROR: relation "dummy_seclabel_tbl3" does not exist +SET SESSION AUTHORIZATION regress_dummy_seclabel_user2; +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- fail +ERROR: must be owner of table dummy_seclabel_tbl1 +SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK +-- +-- Test for shared database object +-- +SET SESSION AUTHORIZATION regress_dummy_seclabel_user1; +SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK +SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail +ERROR: '...invalid label...' is not a valid security label +SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK +SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail +ERROR: security label provider "unknown_seclabel" is not loaded +SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser) +ERROR: only superuser can set 'secret' label +SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found) +ERROR: role "regress_dummy_seclabel_user3" does not exist +SET SESSION AUTHORIZATION regress_dummy_seclabel_user2; +SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged) +ERROR: must have CREATEROLE privilege +RESET SESSION AUTHORIZATION; +-- +-- Test for various types of object +-- +RESET SESSION AUTHORIZATION; +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'top secret'; -- OK +SECURITY LABEL ON VIEW dummy_seclabel_view1 IS 'classified'; -- OK +SECURITY LABEL ON FUNCTION dummy_seclabel_four() IS 'classified'; -- OK +SECURITY LABEL ON DOMAIN dummy_seclabel_domain IS 'classified'; -- OK +CREATE SCHEMA dummy_seclabel_test; +SECURITY LABEL ON SCHEMA dummy_seclabel_test IS 'unclassified'; -- OK +SET client_min_messages = error; +CREATE PUBLICATION dummy_pub; +CREATE SUBSCRIPTION dummy_sub CONNECTION '' PUBLICATION foo WITH (connect = false, slot_name = NONE); +RESET client_min_messages; +SECURITY LABEL ON PUBLICATION dummy_pub IS 'classified'; +SECURITY LABEL ON SUBSCRIPTION dummy_sub IS 'classified'; +SELECT objtype, objname, provider, label FROM pg_seclabels + ORDER BY objtype, objname; + objtype | objname | provider | label +--------------+------------------------------+----------+-------------- + column | dummy_seclabel_tbl1.a | dummy | unclassified + domain | dummy_seclabel_domain | dummy | classified + function | dummy_seclabel_four() | dummy | classified + publication | dummy_pub | dummy | classified + role | regress_dummy_seclabel_user1 | dummy | classified + role | regress_dummy_seclabel_user2 | dummy | unclassified + schema | dummy_seclabel_test | dummy | unclassified + subscription | dummy_sub | dummy | classified + table | dummy_seclabel_tbl1 | dummy | top secret + table | dummy_seclabel_tbl2 | dummy | classified + view | dummy_seclabel_view1 | dummy | classified +(11 rows) + +-- check for event trigger +CREATE FUNCTION event_trigger_test() +RETURNS event_trigger AS $$ + BEGIN RAISE NOTICE 'event %: %', TG_EVENT, TG_TAG; END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER always_start ON ddl_command_start +EXECUTE PROCEDURE event_trigger_test(); +CREATE EVENT TRIGGER always_end ON ddl_command_end +EXECUTE PROCEDURE event_trigger_test(); +CREATE EVENT TRIGGER always_drop ON sql_drop +EXECUTE PROCEDURE event_trigger_test(); +CREATE EVENT TRIGGER always_rewrite ON table_rewrite +EXECUTE PROCEDURE event_trigger_test(); +-- should trigger ddl_command_{start,end} +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; +NOTICE: event ddl_command_start: SECURITY LABEL +NOTICE: event ddl_command_end: SECURITY LABEL +-- clean up +DROP EVENT TRIGGER always_start, always_end, always_drop, always_rewrite; +DROP VIEW dummy_seclabel_view1; +DROP TABLE dummy_seclabel_tbl1, dummy_seclabel_tbl2; +DROP SUBSCRIPTION dummy_sub; +DROP PUBLICATION dummy_pub; +DROP ROLE regress_dummy_seclabel_user1; +DROP ROLE regress_dummy_seclabel_user2; diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql new file mode 100644 index 0000000..8c347b6 --- /dev/null +++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql @@ -0,0 +1,115 @@ +-- +-- Test for facilities of security label +-- +CREATE EXTENSION dummy_seclabel; + +-- initial setups +SET client_min_messages TO 'warning'; + +DROP ROLE IF EXISTS regress_dummy_seclabel_user1; +DROP ROLE IF EXISTS regress_dummy_seclabel_user2; + +RESET client_min_messages; + +CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE; +CREATE USER regress_dummy_seclabel_user2; + +CREATE TABLE dummy_seclabel_tbl1 (a int, b text); +CREATE TABLE dummy_seclabel_tbl2 (x int, y text); +CREATE VIEW dummy_seclabel_view1 AS SELECT * FROM dummy_seclabel_tbl2; +CREATE FUNCTION dummy_seclabel_four() RETURNS integer AS $$SELECT 4$$ language sql; +CREATE DOMAIN dummy_seclabel_domain AS text; + +ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1; +ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2; + +-- +-- Test of SECURITY LABEL statement with a plugin +-- +SET SESSION AUTHORIZATION regress_dummy_seclabel_user1; + +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK +SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK +SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail +SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK +SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail +SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner) +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser) +SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found) + +SET SESSION AUTHORIZATION regress_dummy_seclabel_user2; +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- fail +SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK + +-- +-- Test for shared database object +-- +SET SESSION AUTHORIZATION regress_dummy_seclabel_user1; + +SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK +SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail +SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK +SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail +SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser) +SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found) + +SET SESSION AUTHORIZATION regress_dummy_seclabel_user2; +SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged) + +RESET SESSION AUTHORIZATION; + +-- +-- Test for various types of object +-- +RESET SESSION AUTHORIZATION; + +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'top secret'; -- OK +SECURITY LABEL ON VIEW dummy_seclabel_view1 IS 'classified'; -- OK +SECURITY LABEL ON FUNCTION dummy_seclabel_four() IS 'classified'; -- OK +SECURITY LABEL ON DOMAIN dummy_seclabel_domain IS 'classified'; -- OK +CREATE SCHEMA dummy_seclabel_test; +SECURITY LABEL ON SCHEMA dummy_seclabel_test IS 'unclassified'; -- OK + +SET client_min_messages = error; +CREATE PUBLICATION dummy_pub; +CREATE SUBSCRIPTION dummy_sub CONNECTION '' PUBLICATION foo WITH (connect = false, slot_name = NONE); +RESET client_min_messages; +SECURITY LABEL ON PUBLICATION dummy_pub IS 'classified'; +SECURITY LABEL ON SUBSCRIPTION dummy_sub IS 'classified'; + +SELECT objtype, objname, provider, label FROM pg_seclabels + ORDER BY objtype, objname; + +-- check for event trigger +CREATE FUNCTION event_trigger_test() +RETURNS event_trigger AS $$ + BEGIN RAISE NOTICE 'event %: %', TG_EVENT, TG_TAG; END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER always_start ON ddl_command_start +EXECUTE PROCEDURE event_trigger_test(); + +CREATE EVENT TRIGGER always_end ON ddl_command_end +EXECUTE PROCEDURE event_trigger_test(); + +CREATE EVENT TRIGGER always_drop ON sql_drop +EXECUTE PROCEDURE event_trigger_test(); + +CREATE EVENT TRIGGER always_rewrite ON table_rewrite +EXECUTE PROCEDURE event_trigger_test(); + +-- should trigger ddl_command_{start,end} +SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; + +-- clean up +DROP EVENT TRIGGER always_start, always_end, always_drop, always_rewrite; + +DROP VIEW dummy_seclabel_view1; +DROP TABLE dummy_seclabel_tbl1, dummy_seclabel_tbl2; + +DROP SUBSCRIPTION dummy_sub; +DROP PUBLICATION dummy_pub; + +DROP ROLE regress_dummy_seclabel_user1; +DROP ROLE regress_dummy_seclabel_user2; diff --git a/src/test/modules/libpq_pipeline/.gitignore b/src/test/modules/libpq_pipeline/.gitignore new file mode 100644 index 0000000..3a11e78 --- /dev/null +++ b/src/test/modules/libpq_pipeline/.gitignore @@ -0,0 +1,5 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ +/libpq_pipeline diff --git a/src/test/modules/libpq_pipeline/Makefile b/src/test/modules/libpq_pipeline/Makefile new file mode 100644 index 0000000..65acc3e --- /dev/null +++ b/src/test/modules/libpq_pipeline/Makefile @@ -0,0 +1,25 @@ +# src/test/modules/libpq_pipeline/Makefile + +PGFILEDESC = "libpq_pipeline - test program for pipeline execution" +PGAPPICON = win32 + +PROGRAM = libpq_pipeline +OBJS = $(WIN32RES) libpq_pipeline.o + +NO_INSTALL = 1 + +PG_CPPFLAGS = -I$(libpq_srcdir) +PG_LIBS_INTERNAL += $(libpq_pgport) + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/libpq_pipeline +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/libpq_pipeline/README b/src/test/modules/libpq_pipeline/README new file mode 100644 index 0000000..d8174dd --- /dev/null +++ b/src/test/modules/libpq_pipeline/README @@ -0,0 +1 @@ +Test programs and libraries for libpq diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c new file mode 100644 index 0000000..b30a97d --- /dev/null +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -0,0 +1,1818 @@ +/*------------------------------------------------------------------------- + * + * libpq_pipeline.c + * Verify libpq pipeline execution functionality + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/test/modules/libpq_pipeline/libpq_pipeline.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <sys/time.h> +#ifdef HAVE_SYS_SELECT_H +#include <sys/select.h> +#endif + +#include "catalog/pg_type_d.h" +#include "common/fe_memutils.h" +#include "libpq-fe.h" +#include "pg_getopt.h" +#include "portability/instr_time.h" + + +static void exit_nicely(PGconn *conn); +static void pg_attribute_noreturn() pg_fatal_impl(int line, const char *fmt,...) + pg_attribute_printf(2, 3); +static bool process_result(PGconn *conn, PGresult *res, int results, + int numsent); + +const char *const progname = "libpq_pipeline"; + +/* Options and defaults */ +char *tracefile = NULL; /* path to PQtrace() file */ + + +#ifdef DEBUG_OUTPUT +#define pg_debug(...) do { fprintf(stderr, __VA_ARGS__); } while (0) +#else +#define pg_debug(...) +#endif + +static const char *const drop_table_sql = +"DROP TABLE IF EXISTS pq_pipeline_demo"; +static const char *const create_table_sql = +"CREATE UNLOGGED TABLE pq_pipeline_demo(id serial primary key, itemno integer," +"int8filler int8);"; +static const char *const insert_sql = +"INSERT INTO pq_pipeline_demo(itemno) VALUES ($1)"; +static const char *const insert_sql2 = +"INSERT INTO pq_pipeline_demo(itemno,int8filler) VALUES ($1, $2)"; + +/* max char length of an int32/64, plus sign and null terminator */ +#define MAXINTLEN 12 +#define MAXINT8LEN 20 + +static void +exit_nicely(PGconn *conn) +{ + PQfinish(conn); + exit(1); +} + +/* + * Print an error to stderr and terminate the program. + */ +#define pg_fatal(...) pg_fatal_impl(__LINE__, __VA_ARGS__) +static void +pg_attribute_noreturn() +pg_fatal_impl(int line, const char *fmt,...) +{ + va_list args; + + + fflush(stdout); + + fprintf(stderr, "\n%s:%d: ", progname, line); + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + Assert(fmt[strlen(fmt) - 1] != '\n'); + fprintf(stderr, "\n"); + exit(1); +} + +static void +test_disallowed_in_pipeline(PGconn *conn) +{ + PGresult *res = NULL; + + fprintf(stderr, "test error cases... "); + + if (PQisnonblocking(conn)) + pg_fatal("Expected blocking connection mode"); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("Unable to enter pipeline mode"); + + if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) + pg_fatal("Pipeline mode not activated properly"); + + /* PQexec should fail in pipeline mode */ + res = PQexec(conn, "SELECT 1"); + if (PQresultStatus(res) != PGRES_FATAL_ERROR) + pg_fatal("PQexec should fail in pipeline mode but succeeded"); + if (strcmp(PQerrorMessage(conn), + "synchronous command execution functions are not allowed in pipeline mode\n") != 0) + pg_fatal("did not get expected error message; got: \"%s\"", + PQerrorMessage(conn)); + + /* PQsendQuery should fail in pipeline mode */ + if (PQsendQuery(conn, "SELECT 1") != 0) + pg_fatal("PQsendQuery should fail in pipeline mode but succeeded"); + if (strcmp(PQerrorMessage(conn), + "PQsendQuery not allowed in pipeline mode\n") != 0) + pg_fatal("did not get expected error message; got: \"%s\"", + PQerrorMessage(conn)); + + /* Entering pipeline mode when already in pipeline mode is OK */ + if (PQenterPipelineMode(conn) != 1) + pg_fatal("re-entering pipeline mode should be a no-op but failed"); + + if (PQisBusy(conn) != 0) + pg_fatal("PQisBusy should return 0 when idle in pipeline mode, returned 1"); + + /* ok, back to normal command mode */ + if (PQexitPipelineMode(conn) != 1) + pg_fatal("couldn't exit idle empty pipeline mode"); + + if (PQpipelineStatus(conn) != PQ_PIPELINE_OFF) + pg_fatal("Pipeline mode not terminated properly"); + + /* exiting pipeline mode when not in pipeline mode should be a no-op */ + if (PQexitPipelineMode(conn) != 1) + pg_fatal("pipeline mode exit when not in pipeline mode should succeed but failed"); + + /* can now PQexec again */ + res = PQexec(conn, "SELECT 1"); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("PQexec should succeed after exiting pipeline mode but failed with: %s", + PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +static void +test_multi_pipelines(PGconn *conn) +{ + PGresult *res = NULL; + const char *dummy_params[1] = {"1"}; + Oid dummy_param_oids[1] = {INT4OID}; + + fprintf(stderr, "multi pipeline... "); + + /* + * Queue up a couple of small pipelines and process each without returning + * to command mode first. + */ + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "SELECT $1", 1, dummy_param_oids, + dummy_params, NULL, NULL, 0) != 1) + pg_fatal("dispatching first SELECT failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("Pipeline sync failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "SELECT $1", 1, dummy_param_oids, + dummy_params, NULL, NULL, 0) != 1) + pg_fatal("dispatching second SELECT failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + /* OK, start processing the results */ + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null when there's a pipeline item: %s", + PQerrorMessage(conn)); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("Unexpected result code %s from first pipeline item", + PQresStatus(PQresultStatus(res))); + PQclear(res); + res = NULL; + + if (PQgetResult(conn) != NULL) + pg_fatal("PQgetResult returned something extra after first result"); + + if (PQexitPipelineMode(conn) != 0) + pg_fatal("exiting pipeline mode after query but before sync succeeded incorrectly"); + + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null when sync result expected: %s", + PQerrorMessage(conn)); + + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("Unexpected result code %s instead of sync result, error: %s", + PQresStatus(PQresultStatus(res)), PQerrorMessage(conn)); + PQclear(res); + + /* second pipeline */ + + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null when there's a pipeline item: %s", + PQerrorMessage(conn)); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("Unexpected result code %s from second pipeline item", + PQresStatus(PQresultStatus(res))); + + res = PQgetResult(conn); + if (res != NULL) + pg_fatal("Expected null result, got %s", + PQresStatus(PQresultStatus(res))); + + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null when there's a pipeline item: %s", + PQerrorMessage(conn)); + + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("Unexpected result code %s from second pipeline sync", + PQresStatus(PQresultStatus(res))); + + /* We're still in pipeline mode ... */ + if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) + pg_fatal("Fell out of pipeline mode somehow"); + + /* until we end it, which we can safely do now */ + if (PQexitPipelineMode(conn) != 1) + pg_fatal("attempt to exit pipeline mode failed when it should've succeeded: %s", + PQerrorMessage(conn)); + + if (PQpipelineStatus(conn) != PQ_PIPELINE_OFF) + pg_fatal("exiting pipeline mode didn't seem to work"); + + fprintf(stderr, "ok\n"); +} + +/* + * Test behavior when a pipeline dispatches a number of commands that are + * not flushed by a sync point. + */ +static void +test_nosync(PGconn *conn) +{ + int numqueries = 10; + int results = 0; + int sock = PQsocket(conn); + + fprintf(stderr, "nosync... "); + + if (sock < 0) + pg_fatal("invalid socket"); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("could not enter pipeline mode"); + for (int i = 0; i < numqueries; i++) + { + fd_set input_mask; + struct timeval tv; + + if (PQsendQueryParams(conn, "SELECT repeat('xyzxz', 12)", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("error sending select: %s", PQerrorMessage(conn)); + PQflush(conn); + + /* + * If the server has written anything to us, read (some of) it now. + */ + FD_ZERO(&input_mask); + FD_SET(sock, &input_mask); + tv.tv_sec = 0; + tv.tv_usec = 0; + if (select(sock + 1, &input_mask, NULL, NULL, &tv) < 0) + { + fprintf(stderr, "select() failed: %s\n", strerror(errno)); + exit_nicely(conn); + } + if (FD_ISSET(sock, &input_mask) && PQconsumeInput(conn) != 1) + pg_fatal("failed to read from server: %s", PQerrorMessage(conn)); + } + + /* tell server to flush its output buffer */ + if (PQsendFlushRequest(conn) != 1) + pg_fatal("failed to send flush request"); + PQflush(conn); + + /* Now read all results */ + for (;;) + { + PGresult *res; + + res = PQgetResult(conn); + + /* NULL results are only expected after TUPLES_OK */ + if (res == NULL) + pg_fatal("got unexpected NULL result after %d results", results); + + /* We expect exactly one TUPLES_OK result for each query we sent */ + if (PQresultStatus(res) == PGRES_TUPLES_OK) + { + PGresult *res2; + + /* and one NULL result should follow each */ + res2 = PQgetResult(conn); + if (res2 != NULL) + pg_fatal("expected NULL, got %s", + PQresStatus(PQresultStatus(res2))); + PQclear(res); + results++; + + /* if we're done, we're done */ + if (results == numqueries) + break; + + continue; + } + + /* anything else is unexpected */ + pg_fatal("got unexpected %s\n", PQresStatus(PQresultStatus(res))); + } + + fprintf(stderr, "ok\n"); +} + +/* + * When an operation in a pipeline fails the rest of the pipeline is flushed. We + * still have to get results for each pipeline item, but the item will just be + * a PGRES_PIPELINE_ABORTED code. + * + * This intentionally doesn't use a transaction to wrap the pipeline. You should + * usually use an xact, but in this case we want to observe the effects of each + * statement. + */ +static void +test_pipeline_abort(PGconn *conn) +{ + PGresult *res = NULL; + const char *dummy_params[1] = {"1"}; + Oid dummy_param_oids[1] = {INT4OID}; + int i; + int gotrows; + bool goterror; + + fprintf(stderr, "aborted pipeline... "); + + res = PQexec(conn, drop_table_sql); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("dispatching DROP TABLE failed: %s", PQerrorMessage(conn)); + + res = PQexec(conn, create_table_sql); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("dispatching CREATE TABLE failed: %s", PQerrorMessage(conn)); + + /* + * Queue up a couple of small pipelines and process each without returning + * to command mode first. Make sure the second operation in the first + * pipeline ERRORs. + */ + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + dummy_params[0] = "1"; + if (PQsendQueryParams(conn, insert_sql, 1, dummy_param_oids, + dummy_params, NULL, NULL, 0) != 1) + pg_fatal("dispatching first insert failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "SELECT no_such_function($1)", + 1, dummy_param_oids, dummy_params, + NULL, NULL, 0) != 1) + pg_fatal("dispatching error select failed: %s", PQerrorMessage(conn)); + + dummy_params[0] = "2"; + if (PQsendQueryParams(conn, insert_sql, 1, dummy_param_oids, + dummy_params, NULL, NULL, 0) != 1) + pg_fatal("dispatching second insert failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + dummy_params[0] = "3"; + if (PQsendQueryParams(conn, insert_sql, 1, dummy_param_oids, + dummy_params, NULL, NULL, 0) != 1) + pg_fatal("dispatching second-pipeline insert failed: %s", + PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + /* + * OK, start processing the pipeline results. + * + * We should get a command-ok for the first query, then a fatal error and + * a pipeline aborted message for the second insert, a pipeline-end, then + * a command-ok and a pipeline-ok for the second pipeline operation. + */ + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("Unexpected result status %s: %s", + PQresStatus(PQresultStatus(res)), + PQresultErrorMessage(res)); + PQclear(res); + + /* NULL result to signal end-of-results for this command */ + if ((res = PQgetResult(conn)) != NULL) + pg_fatal("Expected null result, got %s", + PQresStatus(PQresultStatus(res))); + + /* Second query caused error, so we expect an error next */ + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_FATAL_ERROR) + pg_fatal("Unexpected result code -- expected PGRES_FATAL_ERROR, got %s", + PQresStatus(PQresultStatus(res))); + PQclear(res); + + /* NULL result to signal end-of-results for this command */ + if ((res = PQgetResult(conn)) != NULL) + pg_fatal("Expected null result, got %s", + PQresStatus(PQresultStatus(res))); + + /* + * pipeline should now be aborted. + * + * Note that we could still queue more queries at this point if we wanted; + * they'd get added to a new third pipeline since we've already sent a + * second. The aborted flag relates only to the pipeline being received. + */ + if (PQpipelineStatus(conn) != PQ_PIPELINE_ABORTED) + pg_fatal("pipeline should be flagged as aborted but isn't"); + + /* third query in pipeline, the second insert */ + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_PIPELINE_ABORTED) + pg_fatal("Unexpected result code -- expected PGRES_PIPELINE_ABORTED, got %s", + PQresStatus(PQresultStatus(res))); + PQclear(res); + + /* NULL result to signal end-of-results for this command */ + if ((res = PQgetResult(conn)) != NULL) + pg_fatal("Expected null result, got %s", PQresStatus(PQresultStatus(res))); + + if (PQpipelineStatus(conn) != PQ_PIPELINE_ABORTED) + pg_fatal("pipeline should be flagged as aborted but isn't"); + + /* Ensure we're still in pipeline */ + if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) + pg_fatal("Fell out of pipeline mode somehow"); + + /* + * The end of a failed pipeline is a PGRES_PIPELINE_SYNC. + * + * (This is so clients know to start processing results normally again and + * can tell the difference between skipped commands and the sync.) + */ + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("Unexpected result code from first pipeline sync\n" + "Expected PGRES_PIPELINE_SYNC, got %s", + PQresStatus(PQresultStatus(res))); + PQclear(res); + + if (PQpipelineStatus(conn) == PQ_PIPELINE_ABORTED) + pg_fatal("sync should've cleared the aborted flag but didn't"); + + /* We're still in pipeline mode... */ + if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) + pg_fatal("Fell out of pipeline mode somehow"); + + /* the insert from the second pipeline */ + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("Unexpected result code %s from first item in second pipeline", + PQresStatus(PQresultStatus(res))); + PQclear(res); + + /* Read the NULL result at the end of the command */ + if ((res = PQgetResult(conn)) != NULL) + pg_fatal("Expected null result, got %s", PQresStatus(PQresultStatus(res))); + + /* the second pipeline sync */ + if ((res = PQgetResult(conn)) == NULL) + pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("Unexpected result code %s from second pipeline sync", + PQresStatus(PQresultStatus(res))); + PQclear(res); + + if ((res = PQgetResult(conn)) != NULL) + pg_fatal("Expected null result, got %s: %s", + PQresStatus(PQresultStatus(res)), + PQerrorMessage(conn)); + + /* Try to send two queries in one command */ + if (PQsendQueryParams(conn, "SELECT 1; SELECT 2", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", PQerrorMessage(conn)); + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + goterror = false; + while ((res = PQgetResult(conn)) != NULL) + { + switch (PQresultStatus(res)) + { + case PGRES_FATAL_ERROR: + if (strcmp(PQresultErrorField(res, PG_DIAG_SQLSTATE), "42601") != 0) + pg_fatal("expected error about multiple commands, got %s", + PQerrorMessage(conn)); + printf("got expected %s", PQerrorMessage(conn)); + goterror = true; + break; + default: + pg_fatal("got unexpected status %s", PQresStatus(PQresultStatus(res))); + break; + } + } + if (!goterror) + pg_fatal("did not get cannot-insert-multiple-commands error"); + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("got NULL result"); + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("Unexpected result code %s from pipeline sync", + PQresStatus(PQresultStatus(res))); + fprintf(stderr, "ok\n"); + + /* Test single-row mode with an error partways */ + if (PQsendQueryParams(conn, "SELECT 1.0/g FROM generate_series(3, -1, -1) g", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", PQerrorMessage(conn)); + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + PQsetSingleRowMode(conn); + goterror = false; + gotrows = 0; + while ((res = PQgetResult(conn)) != NULL) + { + switch (PQresultStatus(res)) + { + case PGRES_SINGLE_TUPLE: + printf("got row: %s\n", PQgetvalue(res, 0, 0)); + gotrows++; + break; + case PGRES_FATAL_ERROR: + if (strcmp(PQresultErrorField(res, PG_DIAG_SQLSTATE), "22012") != 0) + pg_fatal("expected division-by-zero, got: %s (%s)", + PQerrorMessage(conn), + PQresultErrorField(res, PG_DIAG_SQLSTATE)); + printf("got expected division-by-zero\n"); + goterror = true; + break; + default: + pg_fatal("got unexpected result %s", PQresStatus(PQresultStatus(res))); + } + PQclear(res); + } + if (!goterror) + pg_fatal("did not get division-by-zero error"); + if (gotrows != 3) + pg_fatal("did not get three rows"); + /* the third pipeline sync */ + if ((res = PQgetResult(conn)) == NULL) + pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("Unexpected result code %s from third pipeline sync", + PQresStatus(PQresultStatus(res))); + PQclear(res); + + /* We're still in pipeline mode... */ + if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) + pg_fatal("Fell out of pipeline mode somehow"); + + /* until we end it, which we can safely do now */ + if (PQexitPipelineMode(conn) != 1) + pg_fatal("attempt to exit pipeline mode failed when it should've succeeded: %s", + PQerrorMessage(conn)); + + if (PQpipelineStatus(conn) != PQ_PIPELINE_OFF) + pg_fatal("exiting pipeline mode didn't seem to work"); + + /*- + * Since we fired the pipelines off without a surrounding xact, the results + * should be: + * + * - Implicit xact started by server around 1st pipeline + * - First insert applied + * - Second statement aborted xact + * - Third insert skipped + * - Sync rolled back first implicit xact + * - Implicit xact created by server around 2nd pipeline + * - insert applied from 2nd pipeline + * - Sync commits 2nd xact + * + * So we should only have the value 3 that we inserted. + */ + res = PQexec(conn, "SELECT itemno FROM pq_pipeline_demo"); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("Expected tuples, got %s: %s", + PQresStatus(PQresultStatus(res)), PQerrorMessage(conn)); + if (PQntuples(res) != 1) + pg_fatal("expected 1 result, got %d", PQntuples(res)); + for (i = 0; i < PQntuples(res); i++) + { + const char *val = PQgetvalue(res, i, 0); + + if (strcmp(val, "3") != 0) + pg_fatal("expected only insert with value 3, got %s", val); + } + + PQclear(res); + + fprintf(stderr, "ok\n"); +} + +/* State machine enum for test_pipelined_insert */ +enum PipelineInsertStep +{ + BI_BEGIN_TX, + BI_DROP_TABLE, + BI_CREATE_TABLE, + BI_PREPARE, + BI_INSERT_ROWS, + BI_COMMIT_TX, + BI_SYNC, + BI_DONE +}; + +static void +test_pipelined_insert(PGconn *conn, int n_rows) +{ + Oid insert_param_oids[2] = {INT4OID, INT8OID}; + const char *insert_params[2]; + char insert_param_0[MAXINTLEN]; + char insert_param_1[MAXINT8LEN]; + enum PipelineInsertStep send_step = BI_BEGIN_TX, + recv_step = BI_BEGIN_TX; + int rows_to_send, + rows_to_receive; + + insert_params[0] = insert_param_0; + insert_params[1] = insert_param_1; + + rows_to_send = rows_to_receive = n_rows; + + /* + * Do a pipelined insert into a table created at the start of the pipeline + */ + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + while (send_step != BI_PREPARE) + { + const char *sql; + + switch (send_step) + { + case BI_BEGIN_TX: + sql = "BEGIN TRANSACTION"; + send_step = BI_DROP_TABLE; + break; + + case BI_DROP_TABLE: + sql = drop_table_sql; + send_step = BI_CREATE_TABLE; + break; + + case BI_CREATE_TABLE: + sql = create_table_sql; + send_step = BI_PREPARE; + break; + + default: + pg_fatal("invalid state"); + sql = NULL; /* keep compiler quiet */ + } + + pg_debug("sending: %s\n", sql); + if (PQsendQueryParams(conn, sql, + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("dispatching %s failed: %s", sql, PQerrorMessage(conn)); + } + + Assert(send_step == BI_PREPARE); + pg_debug("sending: %s\n", insert_sql2); + if (PQsendPrepare(conn, "my_insert", insert_sql2, 2, insert_param_oids) != 1) + pg_fatal("dispatching PREPARE failed: %s", PQerrorMessage(conn)); + send_step = BI_INSERT_ROWS; + + /* + * Now we start inserting. We'll be sending enough data that we could fill + * our output buffer, so to avoid deadlocking we need to enter nonblocking + * mode and consume input while we send more output. As results of each + * query are processed we should pop them to allow processing of the next + * query. There's no need to finish the pipeline before processing + * results. + */ + if (PQsetnonblocking(conn, 1) != 0) + pg_fatal("failed to set nonblocking mode: %s", PQerrorMessage(conn)); + + while (recv_step != BI_DONE) + { + int sock; + fd_set input_mask; + fd_set output_mask; + + sock = PQsocket(conn); + + if (sock < 0) + break; /* shouldn't happen */ + + FD_ZERO(&input_mask); + FD_SET(sock, &input_mask); + FD_ZERO(&output_mask); + FD_SET(sock, &output_mask); + + if (select(sock + 1, &input_mask, &output_mask, NULL, NULL) < 0) + { + fprintf(stderr, "select() failed: %s\n", strerror(errno)); + exit_nicely(conn); + } + + /* + * Process any results, so we keep the server's output buffer free + * flowing and it can continue to process input + */ + if (FD_ISSET(sock, &input_mask)) + { + PQconsumeInput(conn); + + /* Read until we'd block if we tried to read */ + while (!PQisBusy(conn) && recv_step < BI_DONE) + { + PGresult *res; + const char *cmdtag = ""; + const char *description = ""; + int status; + + /* + * Read next result. If no more results from this query, + * advance to the next query + */ + res = PQgetResult(conn); + if (res == NULL) + continue; + + status = PGRES_COMMAND_OK; + switch (recv_step) + { + case BI_BEGIN_TX: + cmdtag = "BEGIN"; + recv_step++; + break; + case BI_DROP_TABLE: + cmdtag = "DROP TABLE"; + recv_step++; + break; + case BI_CREATE_TABLE: + cmdtag = "CREATE TABLE"; + recv_step++; + break; + case BI_PREPARE: + cmdtag = ""; + description = "PREPARE"; + recv_step++; + break; + case BI_INSERT_ROWS: + cmdtag = "INSERT"; + rows_to_receive--; + if (rows_to_receive == 0) + recv_step++; + break; + case BI_COMMIT_TX: + cmdtag = "COMMIT"; + recv_step++; + break; + case BI_SYNC: + cmdtag = ""; + description = "SYNC"; + status = PGRES_PIPELINE_SYNC; + recv_step++; + break; + case BI_DONE: + /* unreachable */ + pg_fatal("unreachable state"); + } + + if (PQresultStatus(res) != status) + pg_fatal("%s reported status %s, expected %s\n" + "Error message: \"%s\"", + description, PQresStatus(PQresultStatus(res)), + PQresStatus(status), PQerrorMessage(conn)); + + if (strncmp(PQcmdStatus(res), cmdtag, strlen(cmdtag)) != 0) + pg_fatal("%s expected command tag '%s', got '%s'", + description, cmdtag, PQcmdStatus(res)); + + pg_debug("Got %s OK\n", cmdtag[0] != '\0' ? cmdtag : description); + + PQclear(res); + } + } + + /* Write more rows and/or the end pipeline message, if needed */ + if (FD_ISSET(sock, &output_mask)) + { + PQflush(conn); + + if (send_step == BI_INSERT_ROWS) + { + snprintf(insert_param_0, MAXINTLEN, "%d", rows_to_send); + /* use up some buffer space with a wide value */ + snprintf(insert_param_1, MAXINT8LEN, "%lld", 1LL << 62); + + if (PQsendQueryPrepared(conn, "my_insert", + 2, insert_params, NULL, NULL, 0) == 1) + { + pg_debug("sent row %d\n", rows_to_send); + + rows_to_send--; + if (rows_to_send == 0) + send_step++; + } + else + { + /* + * in nonblocking mode, so it's OK for an insert to fail + * to send + */ + fprintf(stderr, "WARNING: failed to send insert #%d: %s\n", + rows_to_send, PQerrorMessage(conn)); + } + } + else if (send_step == BI_COMMIT_TX) + { + if (PQsendQueryParams(conn, "COMMIT", + 0, NULL, NULL, NULL, NULL, 0) == 1) + { + pg_debug("sent COMMIT\n"); + send_step++; + } + else + { + fprintf(stderr, "WARNING: failed to send commit: %s\n", + PQerrorMessage(conn)); + } + } + else if (send_step == BI_SYNC) + { + if (PQpipelineSync(conn) == 1) + { + fprintf(stdout, "pipeline sync sent\n"); + send_step++; + } + else + { + fprintf(stderr, "WARNING: pipeline sync failed: %s\n", + PQerrorMessage(conn)); + } + } + } + } + + /* We've got the sync message and the pipeline should be done */ + if (PQexitPipelineMode(conn) != 1) + pg_fatal("attempt to exit pipeline mode failed when it should've succeeded: %s", + PQerrorMessage(conn)); + + if (PQsetnonblocking(conn, 0) != 0) + pg_fatal("failed to clear nonblocking mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +static void +test_prepared(PGconn *conn) +{ + PGresult *res = NULL; + Oid param_oids[1] = {INT4OID}; + Oid expected_oids[4]; + Oid typ; + + fprintf(stderr, "prepared... "); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + if (PQsendPrepare(conn, "select_one", "SELECT $1, '42', $1::numeric, " + "interval '1 sec'", + 1, param_oids) != 1) + pg_fatal("preparing query failed: %s", PQerrorMessage(conn)); + expected_oids[0] = INT4OID; + expected_oids[1] = TEXTOID; + expected_oids[2] = NUMERICOID; + expected_oids[3] = INTERVALOID; + if (PQsendDescribePrepared(conn, "select_one") != 1) + pg_fatal("failed to send describePrepared: %s", PQerrorMessage(conn)); + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); + PQclear(res); + res = PQgetResult(conn); + if (res != NULL) + pg_fatal("expected NULL result"); + + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned NULL"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); + if (PQnfields(res) != lengthof(expected_oids)) + pg_fatal("expected %zd columns, got %d", + lengthof(expected_oids), PQnfields(res)); + for (int i = 0; i < PQnfields(res); i++) + { + typ = PQftype(res, i); + if (typ != expected_oids[i]) + pg_fatal("field %d: expected type %u, got %u", + i, expected_oids[i], typ); + } + PQclear(res); + res = PQgetResult(conn); + if (res != NULL) + pg_fatal("expected NULL result"); + + res = PQgetResult(conn); + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("expected PGRES_PIPELINE_SYNC, got %s", PQresStatus(PQresultStatus(res))); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("could not exit pipeline mode: %s", PQerrorMessage(conn)); + + PQexec(conn, "BEGIN"); + PQexec(conn, "DECLARE cursor_one CURSOR FOR SELECT 1"); + PQenterPipelineMode(conn); + if (PQsendDescribePortal(conn, "cursor_one") != 1) + pg_fatal("PQsendDescribePortal failed: %s", PQerrorMessage(conn)); + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); + + typ = PQftype(res, 0); + if (typ != INT4OID) + pg_fatal("portal: expected type %u, got %u", + INT4OID, typ); + PQclear(res); + res = PQgetResult(conn); + if (res != NULL) + pg_fatal("expected NULL result"); + res = PQgetResult(conn); + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("expected PGRES_PIPELINE_SYNC, got %s", PQresStatus(PQresultStatus(res))); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("could not exit pipeline mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +/* Notice processor: print notices, and count how many we got */ +static void +notice_processor(void *arg, const char *message) +{ + int *n_notices = (int *) arg; + + (*n_notices)++; + fprintf(stderr, "NOTICE %d: %s", *n_notices, message); +} + +/* Verify behavior in "idle" state */ +static void +test_pipeline_idle(PGconn *conn) +{ + PGresult *res; + int n_notices = 0; + + fprintf(stderr, "\npipeline idle...\n"); + + PQsetNoticeProcessor(conn, notice_processor, &n_notices); + + /* Try to exit pipeline mode in pipeline-idle state */ + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + if (PQsendQueryParams(conn, "SELECT 1", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", PQerrorMessage(conn)); + PQsendFlushRequest(conn); + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null when there's a pipeline item: %s", + PQerrorMessage(conn)); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("unexpected result code %s from first pipeline item", + PQresStatus(PQresultStatus(res))); + PQclear(res); + res = PQgetResult(conn); + if (res != NULL) + pg_fatal("did not receive terminating NULL"); + if (PQsendQueryParams(conn, "SELECT 2", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", PQerrorMessage(conn)); + if (PQexitPipelineMode(conn) == 1) + pg_fatal("exiting pipeline succeeded when it shouldn't"); + if (strncmp(PQerrorMessage(conn), "cannot exit pipeline mode", + strlen("cannot exit pipeline mode")) != 0) + pg_fatal("did not get expected error; got: %s", + PQerrorMessage(conn)); + PQsendFlushRequest(conn); + res = PQgetResult(conn); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("unexpected result code %s from second pipeline item", + PQresStatus(PQresultStatus(res))); + PQclear(res); + res = PQgetResult(conn); + if (res != NULL) + pg_fatal("did not receive terminating NULL"); + if (PQexitPipelineMode(conn) != 1) + pg_fatal("exiting pipeline failed: %s", PQerrorMessage(conn)); + + if (n_notices > 0) + pg_fatal("got %d notice(s)", n_notices); + fprintf(stderr, "ok - 1\n"); + + /* Have a WARNING in the middle of a resultset */ + if (PQenterPipelineMode(conn) != 1) + pg_fatal("entering pipeline mode failed: %s", PQerrorMessage(conn)); + if (PQsendQueryParams(conn, "SELECT pg_catalog.pg_advisory_unlock(1,1)", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", PQerrorMessage(conn)); + PQsendFlushRequest(conn); + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("unexpected NULL result received"); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("unexpected result code %s", PQresStatus(PQresultStatus(res))); + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + fprintf(stderr, "ok - 2\n"); +} + +static void +test_simple_pipeline(PGconn *conn) +{ + PGresult *res = NULL; + const char *dummy_params[1] = {"1"}; + Oid dummy_param_oids[1] = {INT4OID}; + + fprintf(stderr, "simple pipeline... "); + + /* + * Enter pipeline mode and dispatch a set of operations, which we'll then + * process the results of as they come in. + * + * For a simple case we should be able to do this without interim + * processing of results since our output buffer will give us enough slush + * to work with and we won't block on sending. So blocking mode is fine. + */ + if (PQisnonblocking(conn)) + pg_fatal("Expected blocking connection mode"); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "SELECT $1", + 1, dummy_param_oids, dummy_params, + NULL, NULL, 0) != 1) + pg_fatal("dispatching SELECT failed: %s", PQerrorMessage(conn)); + + if (PQexitPipelineMode(conn) != 0) + pg_fatal("exiting pipeline mode with work in progress should fail, but succeeded"); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null when there's a pipeline item: %s", + PQerrorMessage(conn)); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("Unexpected result code %s from first pipeline item", + PQresStatus(PQresultStatus(res))); + + PQclear(res); + res = NULL; + + if (PQgetResult(conn) != NULL) + pg_fatal("PQgetResult returned something extra after first query result."); + + /* + * Even though we've processed the result there's still a sync to come and + * we can't exit pipeline mode yet + */ + if (PQexitPipelineMode(conn) != 0) + pg_fatal("exiting pipeline mode after query but before sync succeeded incorrectly"); + + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("PQgetResult returned null when sync result PGRES_PIPELINE_SYNC expected: %s", + PQerrorMessage(conn)); + + if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) + pg_fatal("Unexpected result code %s instead of PGRES_PIPELINE_SYNC, error: %s", + PQresStatus(PQresultStatus(res)), PQerrorMessage(conn)); + + PQclear(res); + res = NULL; + + if (PQgetResult(conn) != NULL) + pg_fatal("PQgetResult returned something extra after pipeline end: %s", + PQresStatus(PQresultStatus(res))); + + /* We're still in pipeline mode... */ + if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) + pg_fatal("Fell out of pipeline mode somehow"); + + /* ... until we end it, which we can safely do now */ + if (PQexitPipelineMode(conn) != 1) + pg_fatal("attempt to exit pipeline mode failed when it should've succeeded: %s", + PQerrorMessage(conn)); + + if (PQpipelineStatus(conn) != PQ_PIPELINE_OFF) + pg_fatal("Exiting pipeline mode didn't seem to work"); + + fprintf(stderr, "ok\n"); +} + +static void +test_singlerowmode(PGconn *conn) +{ + PGresult *res; + int i; + bool pipeline_ended = false; + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", + PQerrorMessage(conn)); + + /* One series of three commands, using single-row mode for the first two. */ + for (i = 0; i < 3; i++) + { + char *param[1]; + + param[0] = psprintf("%d", 44 + i); + + if (PQsendQueryParams(conn, + "SELECT generate_series(42, $1)", + 1, + NULL, + (const char **) param, + NULL, + NULL, + 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + pfree(param[0]); + } + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + for (i = 0; !pipeline_ended; i++) + { + bool first = true; + bool saw_ending_tuplesok; + bool isSingleTuple = false; + + /* Set single row mode for only first 2 SELECT queries */ + if (i < 2) + { + if (PQsetSingleRowMode(conn) != 1) + pg_fatal("PQsetSingleRowMode() failed for i=%d", i); + } + + /* Consume rows for this query */ + saw_ending_tuplesok = false; + while ((res = PQgetResult(conn)) != NULL) + { + ExecStatusType est = PQresultStatus(res); + + if (est == PGRES_PIPELINE_SYNC) + { + fprintf(stderr, "end of pipeline reached\n"); + pipeline_ended = true; + PQclear(res); + if (i != 3) + pg_fatal("Expected three results, got %d", i); + break; + } + + /* Expect SINGLE_TUPLE for queries 0 and 1, TUPLES_OK for 2 */ + if (first) + { + if (i <= 1 && est != PGRES_SINGLE_TUPLE) + pg_fatal("Expected PGRES_SINGLE_TUPLE for query %d, got %s", + i, PQresStatus(est)); + if (i >= 2 && est != PGRES_TUPLES_OK) + pg_fatal("Expected PGRES_TUPLES_OK for query %d, got %s", + i, PQresStatus(est)); + first = false; + } + + fprintf(stderr, "Result status %s for query %d", PQresStatus(est), i); + switch (est) + { + case PGRES_TUPLES_OK: + fprintf(stderr, ", tuples: %d\n", PQntuples(res)); + saw_ending_tuplesok = true; + if (isSingleTuple) + { + if (PQntuples(res) == 0) + fprintf(stderr, "all tuples received in query %d\n", i); + else + pg_fatal("Expected to follow PGRES_SINGLE_TUPLE, but received PGRES_TUPLES_OK directly instead"); + } + break; + + case PGRES_SINGLE_TUPLE: + isSingleTuple = true; + fprintf(stderr, ", %d tuple: %s\n", PQntuples(res), PQgetvalue(res, 0, 0)); + break; + + default: + pg_fatal("unexpected"); + } + PQclear(res); + } + if (!pipeline_ended && !saw_ending_tuplesok) + pg_fatal("didn't get expected terminating TUPLES_OK"); + } + + /* + * Now issue one command, get its results in with single-row mode, then + * issue another command, and get its results in normal mode; make sure + * the single-row mode flag is reset as expected. + */ + if (PQsendQueryParams(conn, "SELECT generate_series(0, 0)", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + if (PQsendFlushRequest(conn) != 1) + pg_fatal("failed to send flush request"); + if (PQsetSingleRowMode(conn) != 1) + pg_fatal("PQsetSingleRowMode() failed"); + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("unexpected NULL"); + if (PQresultStatus(res) != PGRES_SINGLE_TUPLE) + pg_fatal("Expected PGRES_SINGLE_TUPLE, got %s", + PQresStatus(PQresultStatus(res))); + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("unexpected NULL"); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("Expected PGRES_TUPLES_OK, got %s", + PQresStatus(PQresultStatus(res))); + if (PQgetResult(conn) != NULL) + pg_fatal("expected NULL result"); + + if (PQsendQueryParams(conn, "SELECT 1", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + if (PQsendFlushRequest(conn) != 1) + pg_fatal("failed to send flush request"); + res = PQgetResult(conn); + if (res == NULL) + pg_fatal("unexpected NULL"); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("Expected PGRES_TUPLES_OK, got %s", + PQresStatus(PQresultStatus(res))); + if (PQgetResult(conn) != NULL) + pg_fatal("expected NULL result"); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to end pipeline mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +/* + * Simple test to verify that a pipeline is discarded as a whole when there's + * an error, ignoring transaction commands. + */ +static void +test_transaction(PGconn *conn) +{ + PGresult *res; + bool expect_null; + int num_syncs = 0; + + res = PQexec(conn, "DROP TABLE IF EXISTS pq_pipeline_tst;" + "CREATE TABLE pq_pipeline_tst (id int)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("failed to create test table: %s", + PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", + PQerrorMessage(conn)); + if (PQsendPrepare(conn, "rollback", "ROLLBACK", 0, NULL) != 1) + pg_fatal("could not send prepare on pipeline: %s", + PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, + "BEGIN", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + if (PQsendQueryParams(conn, + "SELECT 0/0", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + + /* + * send a ROLLBACK using a prepared stmt. Doesn't work because we need to + * get out of the pipeline-aborted state first. + */ + if (PQsendQueryPrepared(conn, "rollback", 0, NULL, NULL, NULL, 1) != 1) + pg_fatal("failed to execute prepared: %s", + PQerrorMessage(conn)); + + /* This insert fails because we're in pipeline-aborted state */ + if (PQsendQueryParams(conn, + "INSERT INTO pq_pipeline_tst VALUES (1)", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + num_syncs++; + + /* + * This insert fails even though the pipeline got a SYNC, because we're in + * an aborted transaction + */ + if (PQsendQueryParams(conn, + "INSERT INTO pq_pipeline_tst VALUES (2)", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + num_syncs++; + + /* + * Send ROLLBACK using prepared stmt. This one works because we just did + * PQpipelineSync above. + */ + if (PQsendQueryPrepared(conn, "rollback", 0, NULL, NULL, NULL, 1) != 1) + pg_fatal("failed to execute prepared: %s", + PQerrorMessage(conn)); + + /* + * Now that we're out of a transaction and in pipeline-good mode, this + * insert works + */ + if (PQsendQueryParams(conn, + "INSERT INTO pq_pipeline_tst VALUES (3)", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("failed to send query: %s", + PQerrorMessage(conn)); + /* Send two syncs now -- match up to SYNC messages below */ + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + num_syncs++; + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + num_syncs++; + + expect_null = false; + for (int i = 0;; i++) + { + ExecStatusType restype; + + res = PQgetResult(conn); + if (res == NULL) + { + printf("%d: got NULL result\n", i); + if (!expect_null) + pg_fatal("did not expect NULL here"); + expect_null = false; + continue; + } + restype = PQresultStatus(res); + printf("%d: got status %s", i, PQresStatus(restype)); + if (expect_null) + pg_fatal("expected NULL"); + if (restype == PGRES_FATAL_ERROR) + printf("; error: %s", PQerrorMessage(conn)); + else if (restype == PGRES_PIPELINE_ABORTED) + { + printf(": command didn't run because pipeline aborted\n"); + } + else + printf("\n"); + PQclear(res); + + if (restype == PGRES_PIPELINE_SYNC) + num_syncs--; + else + expect_null = true; + if (num_syncs <= 0) + break; + } + if (PQgetResult(conn) != NULL) + pg_fatal("returned something extra after all the syncs: %s", + PQresStatus(PQresultStatus(res))); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to end pipeline mode: %s", PQerrorMessage(conn)); + + /* We expect to find one tuple containing the value "3" */ + res = PQexec(conn, "SELECT * FROM pq_pipeline_tst"); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("failed to obtain result: %s", PQerrorMessage(conn)); + if (PQntuples(res) != 1) + pg_fatal("did not get 1 tuple"); + if (strcmp(PQgetvalue(res, 0, 0), "3") != 0) + pg_fatal("did not get expected tuple"); + PQclear(res); + + fprintf(stderr, "ok\n"); +} + +/* + * In this test mode we send a stream of queries, with one in the middle + * causing an error. Verify that we can still send some more after the + * error and have libpq work properly. + */ +static void +test_uniqviol(PGconn *conn) +{ + int sock = PQsocket(conn); + PGresult *res; + Oid paramTypes[2] = {INT8OID, INT8OID}; + const char *paramValues[2]; + char paramValue0[MAXINT8LEN]; + char paramValue1[MAXINT8LEN]; + int ctr = 0; + int numsent = 0; + int results = 0; + bool read_done = false; + bool write_done = false; + bool error_sent = false; + bool got_error = false; + int switched = 0; + int socketful = 0; + fd_set in_fds; + fd_set out_fds; + + fprintf(stderr, "uniqviol ..."); + + PQsetnonblocking(conn, 1); + + paramValues[0] = paramValue0; + paramValues[1] = paramValue1; + sprintf(paramValue1, "42"); + + res = PQexec(conn, "drop table if exists ppln_uniqviol;" + "create table ppln_uniqviol(id bigint primary key, idata bigint)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("failed to create table: %s", PQerrorMessage(conn)); + + res = PQexec(conn, "begin"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("failed to begin transaction: %s", PQerrorMessage(conn)); + + res = PQprepare(conn, "insertion", + "insert into ppln_uniqviol values ($1, $2) returning id", + 2, paramTypes); + if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("failed to prepare query: %s", PQerrorMessage(conn)); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode"); + + while (!read_done) + { + /* + * Avoid deadlocks by reading everything the server has sent before + * sending anything. (Special precaution is needed here to process + * PQisBusy before testing the socket for read-readiness, because the + * socket does not turn read-ready after "sending" queries in aborted + * pipeline mode.) + */ + while (PQisBusy(conn) == 0) + { + bool new_error; + + if (results >= numsent) + { + if (write_done) + read_done = true; + break; + } + + res = PQgetResult(conn); + new_error = process_result(conn, res, results, numsent); + if (new_error && got_error) + pg_fatal("got two errors"); + got_error |= new_error; + if (results++ >= numsent - 1) + { + if (write_done) + read_done = true; + break; + } + } + + if (read_done) + break; + + FD_ZERO(&out_fds); + FD_SET(sock, &out_fds); + + FD_ZERO(&in_fds); + FD_SET(sock, &in_fds); + + if (select(sock + 1, &in_fds, write_done ? NULL : &out_fds, NULL, NULL) == -1) + { + if (errno == EINTR) + continue; + pg_fatal("select() failed: %m"); + } + + if (FD_ISSET(sock, &in_fds) && PQconsumeInput(conn) == 0) + pg_fatal("PQconsumeInput failed: %s", PQerrorMessage(conn)); + + /* + * If the socket is writable and we haven't finished sending queries, + * send some. + */ + if (!write_done && FD_ISSET(sock, &out_fds)) + { + for (;;) + { + int flush; + + /* + * provoke uniqueness violation exactly once after having + * switched to read mode. + */ + if (switched >= 1 && !error_sent && ctr % socketful >= socketful / 2) + { + sprintf(paramValue0, "%d", numsent / 2); + fprintf(stderr, "E"); + error_sent = true; + } + else + { + fprintf(stderr, "."); + sprintf(paramValue0, "%d", ctr++); + } + + if (PQsendQueryPrepared(conn, "insertion", 2, paramValues, NULL, NULL, 0) != 1) + pg_fatal("failed to execute prepared query: %s", PQerrorMessage(conn)); + numsent++; + + /* Are we done writing? */ + if (socketful != 0 && numsent % socketful == 42 && error_sent) + { + if (PQsendFlushRequest(conn) != 1) + pg_fatal("failed to send flush request"); + write_done = true; + fprintf(stderr, "\ndone writing\n"); + PQflush(conn); + break; + } + + /* is the outgoing socket full? */ + flush = PQflush(conn); + if (flush == -1) + pg_fatal("failed to flush: %s", PQerrorMessage(conn)); + if (flush == 1) + { + if (socketful == 0) + socketful = numsent; + fprintf(stderr, "\nswitch to reading\n"); + switched++; + break; + } + } + } + } + + if (!got_error) + pg_fatal("did not get expected error"); + + fprintf(stderr, "ok\n"); +} + +/* + * Subroutine for test_uniqviol; given a PGresult, print it out and consume + * the expected NULL that should follow it. + * + * Returns true if we read a fatal error message, otherwise false. + */ +static bool +process_result(PGconn *conn, PGresult *res, int results, int numsent) +{ + PGresult *res2; + bool got_error = false; + + if (res == NULL) + pg_fatal("got unexpected NULL"); + + switch (PQresultStatus(res)) + { + case PGRES_FATAL_ERROR: + got_error = true; + fprintf(stderr, "result %d/%d (error): %s\n", results, numsent, PQerrorMessage(conn)); + PQclear(res); + + res2 = PQgetResult(conn); + if (res2 != NULL) + pg_fatal("expected NULL, got %s", + PQresStatus(PQresultStatus(res2))); + break; + + case PGRES_TUPLES_OK: + fprintf(stderr, "result %d/%d: %s\n", results, numsent, PQgetvalue(res, 0, 0)); + PQclear(res); + + res2 = PQgetResult(conn); + if (res2 != NULL) + pg_fatal("expected NULL, got %s", + PQresStatus(PQresultStatus(res2))); + break; + + case PGRES_PIPELINE_ABORTED: + fprintf(stderr, "result %d/%d: pipeline aborted\n", results, numsent); + res2 = PQgetResult(conn); + if (res2 != NULL) + pg_fatal("expected NULL, got %s", + PQresStatus(PQresultStatus(res2))); + break; + + default: + pg_fatal("got unexpected %s", PQresStatus(PQresultStatus(res))); + } + + return got_error; +} + + +static void +usage(const char *progname) +{ + fprintf(stderr, "%s tests libpq's pipeline mode.\n\n", progname); + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s [OPTION] tests\n", progname); + fprintf(stderr, " %s [OPTION] TESTNAME [CONNINFO]\n", progname); + fprintf(stderr, "\nOptions:\n"); + fprintf(stderr, " -t TRACEFILE generate a libpq trace to TRACEFILE\n"); + fprintf(stderr, " -r NUMROWS use NUMROWS as the test size\n"); +} + +static void +print_test_list(void) +{ + printf("disallowed_in_pipeline\n"); + printf("multi_pipelines\n"); + printf("nosync\n"); + printf("pipeline_abort\n"); + printf("pipeline_idle\n"); + printf("pipelined_insert\n"); + printf("prepared\n"); + printf("simple_pipeline\n"); + printf("singlerow\n"); + printf("transaction\n"); + printf("uniqviol\n"); +} + +int +main(int argc, char **argv) +{ + const char *conninfo = ""; + PGconn *conn; + FILE *trace; + char *testname; + int numrows = 10000; + PGresult *res; + int c; + + while ((c = getopt(argc, argv, "t:r:")) != -1) + { + switch (c) + { + case 't': /* trace file */ + tracefile = pg_strdup(optarg); + break; + case 'r': /* numrows */ + errno = 0; + numrows = strtol(optarg, NULL, 10); + if (errno != 0 || numrows <= 0) + { + fprintf(stderr, "couldn't parse \"%s\" as a positive integer\n", + optarg); + exit(1); + } + break; + } + } + + if (optind < argc) + { + testname = pg_strdup(argv[optind]); + optind++; + } + else + { + usage(argv[0]); + exit(1); + } + + if (strcmp(testname, "tests") == 0) + { + print_test_list(); + exit(0); + } + + if (optind < argc) + { + conninfo = pg_strdup(argv[optind]); + optind++; + } + + /* Make a connection to the database */ + conn = PQconnectdb(conninfo); + if (PQstatus(conn) != CONNECTION_OK) + { + fprintf(stderr, "Connection to database failed: %s\n", + PQerrorMessage(conn)); + exit_nicely(conn); + } + + res = PQexec(conn, "SET lc_messages TO \"C\""); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("failed to set lc_messages: %s", PQerrorMessage(conn)); + res = PQexec(conn, "SET force_parallel_mode = off"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("failed to set force_parallel_mode: %s", PQerrorMessage(conn)); + + /* Set the trace file, if requested */ + if (tracefile != NULL) + { + if (strcmp(tracefile, "-") == 0) + trace = stdout; + else + trace = fopen(tracefile, "w"); + if (trace == NULL) + pg_fatal("could not open file \"%s\": %m", tracefile); + + /* Make it line-buffered */ + setvbuf(trace, NULL, PG_IOLBF, 0); + + PQtrace(conn, trace); + PQsetTraceFlags(conn, + PQTRACE_SUPPRESS_TIMESTAMPS | PQTRACE_REGRESS_MODE); + } + + if (strcmp(testname, "disallowed_in_pipeline") == 0) + test_disallowed_in_pipeline(conn); + else if (strcmp(testname, "multi_pipelines") == 0) + test_multi_pipelines(conn); + else if (strcmp(testname, "nosync") == 0) + test_nosync(conn); + else if (strcmp(testname, "pipeline_abort") == 0) + test_pipeline_abort(conn); + else if (strcmp(testname, "pipeline_idle") == 0) + test_pipeline_idle(conn); + else if (strcmp(testname, "pipelined_insert") == 0) + test_pipelined_insert(conn, numrows); + else if (strcmp(testname, "prepared") == 0) + test_prepared(conn); + else if (strcmp(testname, "simple_pipeline") == 0) + test_simple_pipeline(conn); + else if (strcmp(testname, "singlerow") == 0) + test_singlerowmode(conn); + else if (strcmp(testname, "transaction") == 0) + test_transaction(conn); + else if (strcmp(testname, "uniqviol") == 0) + test_uniqviol(conn); + else + { + fprintf(stderr, "\"%s\" is not a recognized test name\n", testname); + exit(1); + } + + /* close the connection to the database and cleanup */ + PQfinish(conn); + return 0; +} diff --git a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl new file mode 100644 index 0000000..0821329 --- /dev/null +++ b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl @@ -0,0 +1,78 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +use strict; +use warnings; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +my $numrows = 700; + +my ($out, $err) = run_command([ 'libpq_pipeline', 'tests' ]); +die "oops: $err" unless $err eq ''; +my @tests = split(/\s+/, $out); + +mkdir "$PostgreSQL::Test::Utils::tmp_check/traces"; + +for my $testname (@tests) +{ + my @extraargs = ('-r', $numrows); + my $cmptrace = grep(/^$testname$/, + qw(simple_pipeline nosync multi_pipelines prepared singlerow + pipeline_abort pipeline_idle transaction + disallowed_in_pipeline)) > 0; + + # For a bunch of tests, generate a libpq trace file too. + my $traceout = + "$PostgreSQL::Test::Utils::tmp_check/traces/$testname.trace"; + if ($cmptrace) + { + push @extraargs, "-t", $traceout; + } + + # Execute the test + $node->command_ok( + [ + 'libpq_pipeline', @extraargs, + $testname, $node->connstr('postgres') + ], + "libpq_pipeline $testname"); + + # Compare the trace, if requested + if ($cmptrace) + { + my $expected; + my $result; + + $expected = slurp_file_eval("traces/$testname.trace"); + next unless $expected ne ""; + $result = slurp_file_eval($traceout); + next unless $result ne ""; + + is($result, $expected, "$testname trace match"); + } +} + +$node->stop('fast'); + +done_testing(); + +sub slurp_file_eval +{ + my $filepath = shift; + my $contents; + + eval { $contents = slurp_file($filepath); }; + if ($@) + { + fail "reading $filepath: $@"; + return ""; + } + return $contents; +} diff --git a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace new file mode 100644 index 0000000..dd6df03 --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace @@ -0,0 +1,6 @@ +F 13 Query "SELECT 1" +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '1' +B 13 CommandComplete "SELECT 1" +B 5 ReadyForQuery I +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace new file mode 100644 index 0000000..4b9ab07 --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace @@ -0,0 +1,23 @@ +F 21 Parse "" "SELECT $1" 1 NNNN +F 19 Bind "" "" 0 1 1 '1' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +F 21 Parse "" "SELECT $1" 1 NNNN +F 19 Bind "" "" 0 1 1 '1' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +B 4 ParseComplete +B 4 BindComplete +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '1' +B 13 CommandComplete "SELECT 1" +B 5 ReadyForQuery I +B 4 ParseComplete +B 4 BindComplete +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '1' +B 13 CommandComplete "SELECT 1" +B 5 ReadyForQuery I +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/nosync.trace b/src/test/modules/libpq_pipeline/traces/nosync.trace new file mode 100644 index 0000000..d99aac6 --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/nosync.trace @@ -0,0 +1,92 @@ +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 34 Parse "" "SELECT repeat('xyzxz', 12)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Flush +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +B 4 ParseComplete +B 4 BindComplete +B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' +B 13 CommandComplete "SELECT 1" +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace new file mode 100644 index 0000000..cf6ccec --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace @@ -0,0 +1,62 @@ +F 42 Query "DROP TABLE IF EXISTS pq_pipeline_demo" +B NN NoticeResponse S "NOTICE" V "NOTICE" C "00000" M "table "pq_pipeline_demo" does not exist, skipping" F "SSSS" L "SSSS" R "SSSS" \x00 +B 15 CommandComplete "DROP TABLE" +B 5 ReadyForQuery I +F 99 Query "CREATE UNLOGGED TABLE pq_pipeline_demo(id serial primary key, itemno integer,int8filler int8);" +B 17 CommandComplete "CREATE TABLE" +B 5 ReadyForQuery I +F 60 Parse "" "INSERT INTO pq_pipeline_demo(itemno) VALUES ($1)" 1 NNNN +F 19 Bind "" "" 0 1 1 '1' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 39 Parse "" "SELECT no_such_function($1)" 1 NNNN +F 19 Bind "" "" 0 1 1 '1' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 60 Parse "" "INSERT INTO pq_pipeline_demo(itemno) VALUES ($1)" 1 NNNN +F 19 Bind "" "" 0 1 1 '2' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +F 60 Parse "" "INSERT INTO pq_pipeline_demo(itemno) VALUES ($1)" 1 NNNN +F 19 Bind "" "" 0 1 1 '3' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +B 4 ParseComplete +B 4 BindComplete +B 4 NoData +B 15 CommandComplete "INSERT 0 1" +B NN ErrorResponse S "ERROR" V "ERROR" C "42883" M "function no_such_function(integer) does not exist" H "No function matches the given name and argument types. You might need to add explicit type casts." P "8" F "SSSS" L "SSSS" R "SSSS" \x00 +B 5 ReadyForQuery I +B 4 ParseComplete +B 4 BindComplete +B 4 NoData +B 15 CommandComplete "INSERT 0 1" +B 5 ReadyForQuery I +F 26 Parse "" "SELECT 1; SELECT 2" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +B NN ErrorResponse S "ERROR" V "ERROR" C "42601" M "cannot insert multiple commands into a prepared statement" F "SSSS" L "SSSS" R "SSSS" \x00 +B 5 ReadyForQuery I +F 54 Parse "" "SELECT 1.0/g FROM generate_series(3, -1, -1) g" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +B 4 ParseComplete +B 4 BindComplete +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 65535 -1 0 +B 32 DataRow 1 22 '0.33333333333333333333' +B 32 DataRow 1 22 '0.50000000000000000000' +B 32 DataRow 1 22 '1.00000000000000000000' +B NN ErrorResponse S "ERROR" V "ERROR" C "22012" M "division by zero" F "SSSS" L "SSSS" R "SSSS" \x00 +B 5 ReadyForQuery I +F 40 Query "SELECT itemno FROM pq_pipeline_demo" +B 31 RowDescription 1 "itemno" NNNN 2 NNNN 4 -1 0 +B 11 DataRow 1 1 '3' +B 13 CommandComplete "SELECT 1" +B 5 ReadyForQuery I +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace b/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace new file mode 100644 index 0000000..83ee415 --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace @@ -0,0 +1,32 @@ +F 16 Parse "" "SELECT 1" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Flush +B 4 ParseComplete +B 4 BindComplete +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '1' +B 13 CommandComplete "SELECT 1" +F 16 Parse "" "SELECT 2" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Flush +B 4 ParseComplete +B 4 BindComplete +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '2' +B 13 CommandComplete "SELECT 1" +F 49 Parse "" "SELECT pg_catalog.pg_advisory_unlock(1,1)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Flush +B 4 ParseComplete +B 4 BindComplete +B 43 RowDescription 1 "pg_advisory_unlock" NNNN 0 NNNN 1 -1 0 +B NN NoticeResponse S "WARNING" V "WARNING" C "01000" M "you don't own a lock of type ExclusiveLock" F "SSSS" L "SSSS" R "SSSS" \x00 +B 11 DataRow 1 1 'f' +B 13 CommandComplete "SELECT 1" +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace new file mode 100644 index 0000000..1a7de5c --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -0,0 +1,18 @@ +F 68 Parse "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN +F 16 Describe S "select_one" +F 4 Sync +B 4 ParseComplete +B 10 ParameterDescription 1 NNNN +B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0 +B 5 ReadyForQuery I +F 10 Query "BEGIN" +B 10 CommandComplete "BEGIN" +B 5 ReadyForQuery T +F 43 Query "DECLARE cursor_one CURSOR FOR SELECT 1" +B 19 CommandComplete "DECLARE CURSOR" +B 5 ReadyForQuery T +F 16 Describe P "cursor_one" +F 4 Sync +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 5 ReadyForQuery T +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace new file mode 100644 index 0000000..5c94749 --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace @@ -0,0 +1,12 @@ +F 21 Parse "" "SELECT $1" 1 NNNN +F 19 Bind "" "" 0 1 1 '1' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +B 4 ParseComplete +B 4 BindComplete +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '1' +B 13 CommandComplete "SELECT 1" +B 5 ReadyForQuery I +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/singlerow.trace b/src/test/modules/libpq_pipeline/traces/singlerow.trace new file mode 100644 index 0000000..83043e1 --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/singlerow.trace @@ -0,0 +1,59 @@ +F 38 Parse "" "SELECT generate_series(42, $1)" 0 +F 20 Bind "" "" 0 1 2 '44' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 38 Parse "" "SELECT generate_series(42, $1)" 0 +F 20 Bind "" "" 0 1 2 '45' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 38 Parse "" "SELECT generate_series(42, $1)" 0 +F 20 Bind "" "" 0 1 2 '46' 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +B 4 ParseComplete +B 4 BindComplete +B 40 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 +B 12 DataRow 1 2 '42' +B 12 DataRow 1 2 '43' +B 12 DataRow 1 2 '44' +B 13 CommandComplete "SELECT 3" +B 4 ParseComplete +B 4 BindComplete +B 40 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 +B 12 DataRow 1 2 '42' +B 12 DataRow 1 2 '43' +B 12 DataRow 1 2 '44' +B 12 DataRow 1 2 '45' +B 13 CommandComplete "SELECT 4" +B 4 ParseComplete +B 4 BindComplete +B 40 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 +B 12 DataRow 1 2 '42' +B 12 DataRow 1 2 '43' +B 12 DataRow 1 2 '44' +B 12 DataRow 1 2 '45' +B 12 DataRow 1 2 '46' +B 13 CommandComplete "SELECT 5" +B 5 ReadyForQuery I +F 36 Parse "" "SELECT generate_series(0, 0)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Flush +B 4 ParseComplete +B 4 BindComplete +B 40 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '0' +B 13 CommandComplete "SELECT 1" +F 16 Parse "" "SELECT 1" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Flush +B 4 ParseComplete +B 4 BindComplete +B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 11 DataRow 1 1 '1' +B 13 CommandComplete "SELECT 1" +F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/transaction.trace b/src/test/modules/libpq_pipeline/traces/transaction.trace new file mode 100644 index 0000000..1dcc237 --- /dev/null +++ b/src/test/modules/libpq_pipeline/traces/transaction.trace @@ -0,0 +1,61 @@ +F 79 Query "DROP TABLE IF EXISTS pq_pipeline_tst;CREATE TABLE pq_pipeline_tst (id int)" +B NN NoticeResponse S "NOTICE" V "NOTICE" C "00000" M "table "pq_pipeline_tst" does not exist, skipping" F "SSSS" L "SSSS" R "SSSS" \x00 +B 15 CommandComplete "DROP TABLE" +B 17 CommandComplete "CREATE TABLE" +B 5 ReadyForQuery I +F 24 Parse "rollback" "ROLLBACK" 0 +F 13 Parse "" "BEGIN" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 18 Parse "" "SELECT 0/0" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 22 Bind "" "rollback" 0 0 1 1 +F 6 Describe P "" +F 9 Execute "" 0 +F 46 Parse "" "INSERT INTO pq_pipeline_tst VALUES (1)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +F 46 Parse "" "INSERT INTO pq_pipeline_tst VALUES (2)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +F 22 Bind "" "rollback" 0 0 1 1 +F 6 Describe P "" +F 9 Execute "" 0 +F 46 Parse "" "INSERT INTO pq_pipeline_tst VALUES (3)" 0 +F 14 Bind "" "" 0 0 1 0 +F 6 Describe P "" +F 9 Execute "" 0 +F 4 Sync +F 4 Sync +B 4 ParseComplete +B 4 ParseComplete +B 4 BindComplete +B 4 NoData +B 10 CommandComplete "BEGIN" +B 4 ParseComplete +B NN ErrorResponse S "ERROR" V "ERROR" C "22012" M "division by zero" F "SSSS" L "SSSS" R "SSSS" \x00 +B 5 ReadyForQuery E +B NN ErrorResponse S "ERROR" V "ERROR" C "25P02" M "current transaction is aborted, commands ignored until end of transaction block" F "SSSS" L "SSSS" R "SSSS" \x00 +B 5 ReadyForQuery E +B 4 BindComplete +B 4 NoData +B 13 CommandComplete "ROLLBACK" +B 4 ParseComplete +B 4 BindComplete +B 4 NoData +B 15 CommandComplete "INSERT 0 1" +B 5 ReadyForQuery I +B 5 ReadyForQuery I +F 34 Query "SELECT * FROM pq_pipeline_tst" +B 27 RowDescription 1 "id" NNNN 1 NNNN 4 -1 0 +B 11 DataRow 1 1 '3' +B 13 CommandComplete "SELECT 1" +B 5 ReadyForQuery I +F 4 Terminate diff --git a/src/test/modules/plsample/.gitignore b/src/test/modules/plsample/.gitignore new file mode 100644 index 0000000..44d119c --- /dev/null +++ b/src/test/modules/plsample/.gitignore @@ -0,0 +1,3 @@ +# Generated subdirectories +/log/ +/results/ diff --git a/src/test/modules/plsample/Makefile b/src/test/modules/plsample/Makefile new file mode 100644 index 0000000..f1bc334 --- /dev/null +++ b/src/test/modules/plsample/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/plsample/Makefile + +MODULES = plsample + +EXTENSION = plsample +DATA = plsample--1.0.sql +PGFILEDESC = "PL/Sample - template for procedural language" + +REGRESS = plsample + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/plsample +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/plsample/README b/src/test/modules/plsample/README new file mode 100644 index 0000000..0ed3193 --- /dev/null +++ b/src/test/modules/plsample/README @@ -0,0 +1,6 @@ +PL/Sample +========= + +PL/Sample is an example template of procedural-language handler. It is +a simple implementation, yet demonstrates some of the things that can be done +to build a fully functional procedural-language handler. diff --git a/src/test/modules/plsample/expected/plsample.out b/src/test/modules/plsample/expected/plsample.out new file mode 100644 index 0000000..8ad5f7a --- /dev/null +++ b/src/test/modules/plsample/expected/plsample.out @@ -0,0 +1,117 @@ +CREATE EXTENSION plsample; +-- Create and test some dummy functions +CREATE FUNCTION plsample_result_text(a1 numeric, a2 text, a3 integer[]) +RETURNS TEXT +AS $$ + Example of source with text result. +$$ LANGUAGE plsample; +SELECT plsample_result_text(1.23, 'abc', '{4, 5, 6}'); +NOTICE: source text of function "plsample_result_text": + Example of source with text result. + +NOTICE: argument: 0; name: a1; value: 1.23 +NOTICE: argument: 1; name: a2; value: abc +NOTICE: argument: 2; name: a3; value: {4,5,6} + plsample_result_text +--------------------------------------- + + + Example of source with text result.+ + +(1 row) + +CREATE FUNCTION plsample_result_void(a1 text[]) +RETURNS VOID +AS $$ + Example of source with void result. +$$ LANGUAGE plsample; +SELECT plsample_result_void('{foo, bar, hoge}'); +NOTICE: source text of function "plsample_result_void": + Example of source with void result. + +NOTICE: argument: 0; name: a1; value: {foo,bar,hoge} + plsample_result_void +---------------------- + +(1 row) + +CREATE FUNCTION my_trigger_func() RETURNS trigger AS $$ +if TD_event == "INSERT" + return TD_NEW +elseif TD_event == "UPDATE" + return TD_NEW +else + return "OK" +end +$$ language plsample; +CREATE TABLE my_table (num integer, description text); +CREATE TRIGGER my_trigger_func BEFORE INSERT OR UPDATE ON my_table + FOR EACH ROW EXECUTE FUNCTION my_trigger_func(); +CREATE TRIGGER my_trigger_func2 AFTER INSERT OR UPDATE ON my_table + FOR EACH ROW EXECUTE FUNCTION my_trigger_func(8); +INSERT INTO my_table (num, description) +VALUES (1, 'first'); +NOTICE: source text of function "my_trigger_func": +if TD_event == "INSERT" + return TD_NEW +elseif TD_event == "UPDATE" + return TD_NEW +else + return "OK" +end + +NOTICE: trigger name: my_trigger_func +NOTICE: trigger relation: my_table +NOTICE: trigger relation schema: public +NOTICE: triggered by INSERT +NOTICE: triggered BEFORE +NOTICE: triggered per row +NOTICE: source text of function "my_trigger_func": +if TD_event == "INSERT" + return TD_NEW +elseif TD_event == "UPDATE" + return TD_NEW +else + return "OK" +end + +NOTICE: trigger name: my_trigger_func2 +NOTICE: trigger relation: my_table +NOTICE: trigger relation schema: public +NOTICE: triggered by INSERT +NOTICE: triggered AFTER +NOTICE: triggered per row +NOTICE: trigger arg[0]: 8 +UPDATE my_table +SET description = 'first, modified once' +WHERE num = 1; +NOTICE: source text of function "my_trigger_func": +if TD_event == "INSERT" + return TD_NEW +elseif TD_event == "UPDATE" + return TD_NEW +else + return "OK" +end + +NOTICE: trigger name: my_trigger_func +NOTICE: trigger relation: my_table +NOTICE: trigger relation schema: public +NOTICE: triggered by UPDATE +NOTICE: triggered BEFORE +NOTICE: triggered per row +NOTICE: source text of function "my_trigger_func": +if TD_event == "INSERT" + return TD_NEW +elseif TD_event == "UPDATE" + return TD_NEW +else + return "OK" +end + +NOTICE: trigger name: my_trigger_func2 +NOTICE: trigger relation: my_table +NOTICE: trigger relation schema: public +NOTICE: triggered by UPDATE +NOTICE: triggered AFTER +NOTICE: triggered per row +NOTICE: trigger arg[0]: 8 diff --git a/src/test/modules/plsample/plsample--1.0.sql b/src/test/modules/plsample/plsample--1.0.sql new file mode 100644 index 0000000..fc5b280 --- /dev/null +++ b/src/test/modules/plsample/plsample--1.0.sql @@ -0,0 +1,14 @@ +/* src/test/modules/plsample/plsample--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION plsample" to load this file. \quit + +CREATE FUNCTION plsample_call_handler() RETURNS language_handler + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE TRUSTED LANGUAGE plsample + HANDLER plsample_call_handler; + +ALTER LANGUAGE plsample OWNER TO @extowner@; + +COMMENT ON LANGUAGE plsample IS 'PL/Sample procedural language'; diff --git a/src/test/modules/plsample/plsample.c b/src/test/modules/plsample/plsample.c new file mode 100644 index 0000000..780db72 --- /dev/null +++ b/src/test/modules/plsample/plsample.c @@ -0,0 +1,354 @@ +/*------------------------------------------------------------------------- + * + * plsample.c + * Handler for the PL/Sample procedural language + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/test/modules/plsample/plsample.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_proc.h" +#include "catalog/pg_type.h" +#include "commands/event_trigger.h" +#include "commands/trigger.h" +#include "executor/spi.h" +#include "funcapi.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(plsample_call_handler); + +static Datum plsample_func_handler(PG_FUNCTION_ARGS); +static HeapTuple plsample_trigger_handler(PG_FUNCTION_ARGS); + +/* + * Handle function, procedure, and trigger calls. + */ +Datum +plsample_call_handler(PG_FUNCTION_ARGS) +{ + Datum retval = (Datum) 0; + + /* + * Many languages will require cleanup that happens even in the event of + * an error. That can happen in the PG_FINALLY block. If none is needed, + * this PG_TRY construct can be omitted. + */ + PG_TRY(); + { + /* + * Determine if called as function or trigger and call appropriate + * subhandler. + */ + if (CALLED_AS_TRIGGER(fcinfo)) + { + /* + * This function has been called as a trigger function, where + * (TriggerData *) fcinfo->context includes the information of the + * context. + */ + retval = PointerGetDatum(plsample_trigger_handler(fcinfo)); + } + else if (CALLED_AS_EVENT_TRIGGER(fcinfo)) + { + /* + * This function is called as an event trigger function, where + * (EventTriggerData *) fcinfo->context includes the information + * of the context. + * + * TODO: provide an example handler. + */ + } + else + { + /* Regular function handler */ + retval = plsample_func_handler(fcinfo); + } + } + PG_FINALLY(); + { + } + PG_END_TRY(); + + return retval; +} + +/* + * plsample_func_handler + * + * Function called by the call handler for function execution. + */ +static Datum +plsample_func_handler(PG_FUNCTION_ARGS) +{ + HeapTuple pl_tuple; + Datum ret; + char *source; + bool isnull; + FmgrInfo *arg_out_func; + Form_pg_type type_struct; + HeapTuple type_tuple; + Form_pg_proc pl_struct; + volatile MemoryContext proc_cxt = NULL; + Oid *argtypes; + char **argnames; + char *argmodes; + char *proname; + Form_pg_type pg_type_entry; + Oid result_typioparam; + Oid prorettype; + FmgrInfo result_in_func; + int numargs; + + /* Fetch the function's pg_proc entry. */ + pl_tuple = SearchSysCache1(PROCOID, + ObjectIdGetDatum(fcinfo->flinfo->fn_oid)); + if (!HeapTupleIsValid(pl_tuple)) + elog(ERROR, "cache lookup failed for function %u", + fcinfo->flinfo->fn_oid); + + /* + * Extract and print the source text of the function. This can be used as + * a base for the function validation and execution. + */ + pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple); + proname = pstrdup(NameStr(pl_struct->proname)); + ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull); + if (isnull) + elog(ERROR, "could not find source text of function \"%s\"", + proname); + source = DatumGetCString(DirectFunctionCall1(textout, ret)); + ereport(NOTICE, + (errmsg("source text of function \"%s\": %s", + proname, source))); + + /* + * Allocate a context that will hold all the Postgres data for the + * procedure. + */ + proc_cxt = AllocSetContextCreate(TopMemoryContext, + "PL/Sample function", + ALLOCSET_SMALL_SIZES); + + arg_out_func = (FmgrInfo *) palloc0(fcinfo->nargs * sizeof(FmgrInfo)); + numargs = get_func_arg_info(pl_tuple, &argtypes, &argnames, &argmodes); + + /* + * Iterate through all of the function arguments, printing each input + * value. + */ + for (int i = 0; i < numargs; i++) + { + Oid argtype = pl_struct->proargtypes.values[i]; + char *value; + + type_tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(argtype)); + if (!HeapTupleIsValid(type_tuple)) + elog(ERROR, "cache lookup failed for type %u", argtype); + + type_struct = (Form_pg_type) GETSTRUCT(type_tuple); + fmgr_info_cxt(type_struct->typoutput, &(arg_out_func[i]), proc_cxt); + ReleaseSysCache(type_tuple); + + value = OutputFunctionCall(&arg_out_func[i], fcinfo->args[i].value); + ereport(NOTICE, + (errmsg("argument: %d; name: %s; value: %s", + i, argnames[i], value))); + } + + /* Type of the result */ + prorettype = pl_struct->prorettype; + ReleaseSysCache(pl_tuple); + + /* + * Get the required information for input conversion of the return value. + * + * If the function uses VOID as result, it is better to return NULL. + * Anyway, let's be honest. This is just a template, so there is not much + * we can do here. This returns NULL except if the result type is text, + * where the result is the source text of the function. + */ + if (prorettype != TEXTOID) + PG_RETURN_NULL(); + + type_tuple = SearchSysCache1(TYPEOID, + ObjectIdGetDatum(prorettype)); + if (!HeapTupleIsValid(type_tuple)) + elog(ERROR, "cache lookup failed for type %u", prorettype); + pg_type_entry = (Form_pg_type) GETSTRUCT(type_tuple); + result_typioparam = getTypeIOParam(type_tuple); + + fmgr_info_cxt(pg_type_entry->typinput, &result_in_func, proc_cxt); + ReleaseSysCache(type_tuple); + + ret = InputFunctionCall(&result_in_func, source, result_typioparam, -1); + PG_RETURN_DATUM(ret); +} + +/* + * plsample_trigger_handler + * + * Function called by the call handler for trigger execution. + */ +static HeapTuple +plsample_trigger_handler(PG_FUNCTION_ARGS) +{ + TriggerData *trigdata = (TriggerData *) fcinfo->context; + char *string; + volatile HeapTuple rettup; + HeapTuple pl_tuple; + Datum ret; + char *source; + bool isnull; + Form_pg_proc pl_struct; + char *proname; + int rc PG_USED_FOR_ASSERTS_ONLY; + + /* Make sure this is being called from a trigger. */ + if (!CALLED_AS_TRIGGER(fcinfo)) + elog(ERROR, "not called by trigger manager"); + + /* Connect to the SPI manager */ + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "could not connect to SPI manager"); + + rc = SPI_register_trigger_data(trigdata); + Assert(rc >= 0); + + /* Fetch the function's pg_proc entry. */ + pl_tuple = SearchSysCache1(PROCOID, + ObjectIdGetDatum(fcinfo->flinfo->fn_oid)); + if (!HeapTupleIsValid(pl_tuple)) + elog(ERROR, "cache lookup failed for function %u", + fcinfo->flinfo->fn_oid); + + /* + * Code Retrieval + * + * Extract and print the source text of the function. This can be used as + * a base for the function validation and execution. + */ + pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple); + proname = pstrdup(NameStr(pl_struct->proname)); + ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull); + if (isnull) + elog(ERROR, "could not find source text of function \"%s\"", + proname); + source = DatumGetCString(DirectFunctionCall1(textout, ret)); + ereport(NOTICE, + (errmsg("source text of function \"%s\": %s", + proname, source))); + + /* + * We're done with the pg_proc tuple, so release it. (Note that the + * "proname" and "source" strings are now standalone copies.) + */ + ReleaseSysCache(pl_tuple); + + /* + * Code Augmentation + * + * The source text may be augmented here, such as by wrapping it as the + * body of a function in the target language, prefixing a parameter list + * with names like TD_name, TD_relid, TD_table_name, TD_table_schema, + * TD_event, TD_when, TD_level, TD_NEW, TD_OLD, and args, using whatever + * types in the target language are convenient. The augmented text can be + * cached in a longer-lived memory context, or, if the target language + * uses a compilation step, that can be done here, caching the result of + * the compilation. + */ + + /* + * Code Execution + * + * Here the function (the possibly-augmented source text, or the result of + * compilation if the target language uses such a step) should be + * executed, after binding values from the TriggerData struct to the + * appropriate parameters. + * + * In this example we just print a lot of info via ereport. + */ + + PG_TRY(); + { + ereport(NOTICE, + (errmsg("trigger name: %s", trigdata->tg_trigger->tgname))); + string = SPI_getrelname(trigdata->tg_relation); + ereport(NOTICE, (errmsg("trigger relation: %s", string))); + + string = SPI_getnspname(trigdata->tg_relation); + ereport(NOTICE, (errmsg("trigger relation schema: %s", string))); + + /* Example handling of different trigger aspects. */ + + if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) + { + ereport(NOTICE, (errmsg("triggered by INSERT"))); + rettup = trigdata->tg_trigtuple; + } + else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) + { + ereport(NOTICE, (errmsg("triggered by DELETE"))); + rettup = trigdata->tg_trigtuple; + } + else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) + { + ereport(NOTICE, (errmsg("triggered by UPDATE"))); + rettup = trigdata->tg_trigtuple; + } + else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) + { + ereport(NOTICE, (errmsg("triggered by TRUNCATE"))); + rettup = trigdata->tg_trigtuple; + } + else + elog(ERROR, "unrecognized event: %u", trigdata->tg_event); + + if (TRIGGER_FIRED_BEFORE(trigdata->tg_event)) + ereport(NOTICE, (errmsg("triggered BEFORE"))); + else if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) + ereport(NOTICE, (errmsg("triggered AFTER"))); + else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event)) + ereport(NOTICE, (errmsg("triggered INSTEAD OF"))); + else + elog(ERROR, "unrecognized when: %u", trigdata->tg_event); + + if (TRIGGER_FIRED_FOR_ROW(trigdata->tg_event)) + ereport(NOTICE, (errmsg("triggered per row"))); + else if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) + ereport(NOTICE, (errmsg("triggered per statement"))); + else + elog(ERROR, "unrecognized level: %u", trigdata->tg_event); + + /* + * Iterate through all of the trigger arguments, printing each input + * value. + */ + for (int i = 0; i < trigdata->tg_trigger->tgnargs; i++) + ereport(NOTICE, + (errmsg("trigger arg[%i]: %s", i, + trigdata->tg_trigger->tgargs[i]))); + } + PG_CATCH(); + { + /* Error cleanup code would go here */ + PG_RE_THROW(); + } + PG_END_TRY(); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish() failed"); + + return rettup; +} diff --git a/src/test/modules/plsample/plsample.control b/src/test/modules/plsample/plsample.control new file mode 100644 index 0000000..1e67251 --- /dev/null +++ b/src/test/modules/plsample/plsample.control @@ -0,0 +1,8 @@ +# plsample extension +comment = 'PL/Sample' +default_version = '1.0' +module_pathname = '$libdir/plsample' +relocatable = false +schema = pg_catalog +superuser = false +trusted = true diff --git a/src/test/modules/plsample/sql/plsample.sql b/src/test/modules/plsample/sql/plsample.sql new file mode 100644 index 0000000..cf652ad --- /dev/null +++ b/src/test/modules/plsample/sql/plsample.sql @@ -0,0 +1,38 @@ +CREATE EXTENSION plsample; +-- Create and test some dummy functions +CREATE FUNCTION plsample_result_text(a1 numeric, a2 text, a3 integer[]) +RETURNS TEXT +AS $$ + Example of source with text result. +$$ LANGUAGE plsample; +SELECT plsample_result_text(1.23, 'abc', '{4, 5, 6}'); + +CREATE FUNCTION plsample_result_void(a1 text[]) +RETURNS VOID +AS $$ + Example of source with void result. +$$ LANGUAGE plsample; +SELECT plsample_result_void('{foo, bar, hoge}'); + +CREATE FUNCTION my_trigger_func() RETURNS trigger AS $$ +if TD_event == "INSERT" + return TD_NEW +elseif TD_event == "UPDATE" + return TD_NEW +else + return "OK" +end +$$ language plsample; + +CREATE TABLE my_table (num integer, description text); +CREATE TRIGGER my_trigger_func BEFORE INSERT OR UPDATE ON my_table + FOR EACH ROW EXECUTE FUNCTION my_trigger_func(); +CREATE TRIGGER my_trigger_func2 AFTER INSERT OR UPDATE ON my_table + FOR EACH ROW EXECUTE FUNCTION my_trigger_func(8); + +INSERT INTO my_table (num, description) +VALUES (1, 'first'); + +UPDATE my_table +SET description = 'first, modified once' +WHERE num = 1; diff --git a/src/test/modules/snapshot_too_old/.gitignore b/src/test/modules/snapshot_too_old/.gitignore new file mode 100644 index 0000000..ba2160b --- /dev/null +++ b/src/test/modules/snapshot_too_old/.gitignore @@ -0,0 +1,3 @@ +# Generated subdirectories +/output_iso/ +/tmp_check_iso/ diff --git a/src/test/modules/snapshot_too_old/Makefile b/src/test/modules/snapshot_too_old/Makefile new file mode 100644 index 0000000..dfb4537 --- /dev/null +++ b/src/test/modules/snapshot_too_old/Makefile @@ -0,0 +1,28 @@ +# src/test/modules/snapshot_too_old/Makefile + +# Note: because we don't tell the Makefile there are any regression tests, +# we have to clean those result files explicitly +EXTRA_CLEAN = $(pg_regress_clean_files) + +ISOLATION = sto_using_cursor sto_using_select sto_using_hash_index +ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/snapshot_too_old/sto.conf + +# Disabled because these tests require "old_snapshot_threshold" >= 0, which +# typical installcheck users do not have (e.g. buildfarm clients). +NO_INSTALLCHECK = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/snapshot_too_old +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +# But it can nonetheless be very helpful to run tests on preexisting +# installation, allow to do so, but only if requested explicitly. +installcheck-force: + $(pg_isolation_regress_installcheck) $(ISOLATION) diff --git a/src/test/modules/snapshot_too_old/expected/sto_using_cursor.out b/src/test/modules/snapshot_too_old/expected/sto_using_cursor.out new file mode 100644 index 0000000..06fe4d0 --- /dev/null +++ b/src/test/modules/snapshot_too_old/expected/sto_using_cursor.out @@ -0,0 +1,19 @@ +Parsed test spec with 2 sessions + +starting permutation: s1decl s1f1 s1sleep s2u s1f2 +step s1decl: DECLARE cursor1 CURSOR FOR SELECT c FROM sto1; +step s1f1: FETCH FIRST FROM cursor1; +c +- +1 +(1 row) + +step s1sleep: SELECT setting, pg_sleep(6) FROM pg_settings WHERE name = 'old_snapshot_threshold'; +setting|pg_sleep +-------+-------- + 0| +(1 row) + +step s2u: UPDATE sto1 SET c = 1001 WHERE c = 1; +step s1f2: FETCH FIRST FROM cursor1; +ERROR: snapshot too old diff --git a/src/test/modules/snapshot_too_old/expected/sto_using_hash_index.out b/src/test/modules/snapshot_too_old/expected/sto_using_hash_index.out new file mode 100644 index 0000000..11f827f --- /dev/null +++ b/src/test/modules/snapshot_too_old/expected/sto_using_hash_index.out @@ -0,0 +1,19 @@ +Parsed test spec with 2 sessions + +starting permutation: noseq s1f1 s2sleep s2u s1f2 +step noseq: SET enable_seqscan = false; +step s1f1: SELECT c FROM sto1 where c = 1000; + c +---- +1000 +(1 row) + +step s2sleep: SELECT setting, pg_sleep(6) FROM pg_settings WHERE name = 'old_snapshot_threshold'; +setting|pg_sleep +-------+-------- + 0| +(1 row) + +step s2u: UPDATE sto1 SET c = 1001 WHERE c = 1000; +step s1f2: SELECT c FROM sto1 where c = 1001; +ERROR: snapshot too old diff --git a/src/test/modules/snapshot_too_old/expected/sto_using_select.out b/src/test/modules/snapshot_too_old/expected/sto_using_select.out new file mode 100644 index 0000000..e910e5c --- /dev/null +++ b/src/test/modules/snapshot_too_old/expected/sto_using_select.out @@ -0,0 +1,18 @@ +Parsed test spec with 2 sessions + +starting permutation: s1f1 s1sleep s2u s1f2 +step s1f1: SELECT c FROM sto1 ORDER BY c LIMIT 1; +c +- +1 +(1 row) + +step s1sleep: SELECT setting, pg_sleep(6) FROM pg_settings WHERE name = 'old_snapshot_threshold'; +setting|pg_sleep +-------+-------- + 0| +(1 row) + +step s2u: UPDATE sto1 SET c = 1001 WHERE c = 1; +step s1f2: SELECT c FROM sto1 ORDER BY c LIMIT 1; +ERROR: snapshot too old diff --git a/src/test/modules/snapshot_too_old/specs/sto_using_cursor.spec b/src/test/modules/snapshot_too_old/specs/sto_using_cursor.spec new file mode 100644 index 0000000..f3677a8 --- /dev/null +++ b/src/test/modules/snapshot_too_old/specs/sto_using_cursor.spec @@ -0,0 +1,38 @@ +# This test provokes a "snapshot too old" error using a cursor. +# +# The sleep is needed because with a threshold of zero a statement could error +# on changes it made. With more normal settings no external delay is needed, +# but we don't want these tests to run long enough to see that, since +# granularity is in minutes. +# +# Since results depend on the value of old_snapshot_threshold, sneak that into +# the line generated by the sleep, so that a surprising value isn't so hard +# to identify. + +setup +{ + CREATE TABLE sto1 (c int NOT NULL); + INSERT INTO sto1 SELECT generate_series(1, 1000); +} +setup +{ + VACUUM ANALYZE sto1; +} + +teardown +{ + DROP TABLE sto1; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step "s1decl" { DECLARE cursor1 CURSOR FOR SELECT c FROM sto1; } +step "s1f1" { FETCH FIRST FROM cursor1; } +step "s1sleep" { SELECT setting, pg_sleep(6) FROM pg_settings WHERE name = 'old_snapshot_threshold'; } +step "s1f2" { FETCH FIRST FROM cursor1; } +teardown { COMMIT; } + +session "s2" +step "s2u" { UPDATE sto1 SET c = 1001 WHERE c = 1; } + +permutation "s1decl" "s1f1" "s1sleep" "s2u" "s1f2" diff --git a/src/test/modules/snapshot_too_old/specs/sto_using_hash_index.spec b/src/test/modules/snapshot_too_old/specs/sto_using_hash_index.spec new file mode 100644 index 0000000..33d91ff --- /dev/null +++ b/src/test/modules/snapshot_too_old/specs/sto_using_hash_index.spec @@ -0,0 +1,31 @@ +# This test is like sto_using_select, except that we test access via a +# hash index. + +setup +{ + CREATE TABLE sto1 (c int NOT NULL); + INSERT INTO sto1 SELECT generate_series(1, 1000); + CREATE INDEX idx_sto1 ON sto1 USING HASH (c); +} +setup +{ + VACUUM ANALYZE sto1; +} + +teardown +{ + DROP TABLE sto1; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step "noseq" { SET enable_seqscan = false; } +step "s1f1" { SELECT c FROM sto1 where c = 1000; } +step "s1f2" { SELECT c FROM sto1 where c = 1001; } +teardown { ROLLBACK; } + +session "s2" +step "s2sleep" { SELECT setting, pg_sleep(6) FROM pg_settings WHERE name = 'old_snapshot_threshold'; } +step "s2u" { UPDATE sto1 SET c = 1001 WHERE c = 1000; } + +permutation "noseq" "s1f1" "s2sleep" "s2u" "s1f2" diff --git a/src/test/modules/snapshot_too_old/specs/sto_using_select.spec b/src/test/modules/snapshot_too_old/specs/sto_using_select.spec new file mode 100644 index 0000000..80a3176 --- /dev/null +++ b/src/test/modules/snapshot_too_old/specs/sto_using_select.spec @@ -0,0 +1,37 @@ +# This test provokes a "snapshot too old" error using SELECT statements. +# +# The sleep is needed because with a threshold of zero a statement could error +# on changes it made. With more normal settings no external delay is needed, +# but we don't want these tests to run long enough to see that, since +# granularity is in minutes. +# +# Since results depend on the value of old_snapshot_threshold, sneak that into +# the line generated by the sleep, so that a surprising value isn't so hard +# to identify. + +setup +{ + CREATE TABLE sto1 (c int NOT NULL); + INSERT INTO sto1 SELECT generate_series(1, 1000); +} +setup +{ + VACUUM ANALYZE sto1; +} + +teardown +{ + DROP TABLE sto1; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step "s1f1" { SELECT c FROM sto1 ORDER BY c LIMIT 1; } +step "s1sleep" { SELECT setting, pg_sleep(6) FROM pg_settings WHERE name = 'old_snapshot_threshold'; } +step "s1f2" { SELECT c FROM sto1 ORDER BY c LIMIT 1; } +teardown { COMMIT; } + +session "s2" +step "s2u" { UPDATE sto1 SET c = 1001 WHERE c = 1; } + +permutation "s1f1" "s1sleep" "s2u" "s1f2" diff --git a/src/test/modules/snapshot_too_old/sto.conf b/src/test/modules/snapshot_too_old/sto.conf new file mode 100644 index 0000000..7eeaeeb --- /dev/null +++ b/src/test/modules/snapshot_too_old/sto.conf @@ -0,0 +1,2 @@ +autovacuum = off +old_snapshot_threshold = 0 diff --git a/src/test/modules/spgist_name_ops/.gitignore b/src/test/modules/spgist_name_ops/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/spgist_name_ops/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/spgist_name_ops/Makefile b/src/test/modules/spgist_name_ops/Makefile new file mode 100644 index 0000000..05b2464 --- /dev/null +++ b/src/test/modules/spgist_name_ops/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/spgist_name_ops/Makefile + +MODULE_big = spgist_name_ops +OBJS = \ + $(WIN32RES) \ + spgist_name_ops.o +PGFILEDESC = "spgist_name_ops - test opclass for SP-GiST" + +EXTENSION = spgist_name_ops +DATA = spgist_name_ops--1.0.sql + +REGRESS = spgist_name_ops + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/spgist_name_ops +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/spgist_name_ops/README b/src/test/modules/spgist_name_ops/README new file mode 100644 index 0000000..119208b --- /dev/null +++ b/src/test/modules/spgist_name_ops/README @@ -0,0 +1,8 @@ +spgist_name_ops implements an SP-GiST operator class that indexes +columns of type "name", but with storage identical to that used +by SP-GiST text_ops. + +This is not terribly useful in itself, perhaps, but it allows +testing cases where the indexed data type is different from the leaf +data type and yet we can reconstruct the original indexed value. +That situation is not tested by any built-in SP-GiST opclass. diff --git a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out new file mode 100644 index 0000000..1ee65ed --- /dev/null +++ b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out @@ -0,0 +1,120 @@ +create extension spgist_name_ops; +select opcname, amvalidate(opc.oid) +from pg_opclass opc join pg_am am on am.oid = opcmethod +where amname = 'spgist' and opcname = 'name_ops'; + opcname | amvalidate +----------+------------ + name_ops | t +(1 row) + +-- warning expected here +select opcname, amvalidate(opc.oid) +from pg_opclass opc join pg_am am on am.oid = opcmethod +where amname = 'spgist' and opcname = 'name_ops_old'; +INFO: SP-GiST leaf data type text does not match declared type name + opcname | amvalidate +--------------+------------ + name_ops_old | f +(1 row) + +create table t(f1 name, f2 integer, f3 text); +create index on t using spgist(f1) include(f2, f3); +\d+ t_f1_f2_f3_idx + Index "public.t_f1_f2_f3_idx" + Column | Type | Key? | Definition | Storage | Stats target +--------+---------+------+------------+----------+-------------- + f1 | text | yes | f1 | extended | + f2 | integer | no | f2 | plain | + f3 | text | no | f3 | extended | +spgist, for table "public.t" + +insert into t select + proname, + case when length(proname) % 2 = 0 then pronargs else null end, + prosrc from pg_proc; +vacuum analyze t; +explain (costs off) +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; + QUERY PLAN +--------------------------------------------------------------------------------------------------- + Sort + Sort Key: f1 + -> Index Only Scan using t_f1_f2_f3_idx on t + Index Cond: ((f1 > 'binary_upgrade_set_n'::name) AND (f1 < 'binary_upgrade_set_p'::name)) +(4 rows) + +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; + f1 | f2 | f3 +------------------------------------------------------+----+------------------------------------------------------ + binary_upgrade_set_next_array_pg_type_oid | | binary_upgrade_set_next_array_pg_type_oid + binary_upgrade_set_next_heap_pg_class_oid | | binary_upgrade_set_next_heap_pg_class_oid + binary_upgrade_set_next_heap_relfilenode | 1 | binary_upgrade_set_next_heap_relfilenode + binary_upgrade_set_next_index_pg_class_oid | 1 | binary_upgrade_set_next_index_pg_class_oid + binary_upgrade_set_next_index_relfilenode | | binary_upgrade_set_next_index_relfilenode + binary_upgrade_set_next_multirange_array_pg_type_oid | 1 | binary_upgrade_set_next_multirange_array_pg_type_oid + binary_upgrade_set_next_multirange_pg_type_oid | 1 | binary_upgrade_set_next_multirange_pg_type_oid + binary_upgrade_set_next_pg_authid_oid | | binary_upgrade_set_next_pg_authid_oid + binary_upgrade_set_next_pg_enum_oid | | binary_upgrade_set_next_pg_enum_oid + binary_upgrade_set_next_pg_tablespace_oid | | binary_upgrade_set_next_pg_tablespace_oid + binary_upgrade_set_next_pg_type_oid | | binary_upgrade_set_next_pg_type_oid + binary_upgrade_set_next_toast_pg_class_oid | 1 | binary_upgrade_set_next_toast_pg_class_oid + binary_upgrade_set_next_toast_relfilenode | | binary_upgrade_set_next_toast_relfilenode +(13 rows) + +-- Verify clean failure when INCLUDE'd columns result in overlength tuple +-- The error message details are platform-dependent, so show only SQLSTATE +\set VERBOSITY sqlstate +insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000)); +ERROR: 54000 +\set VERBOSITY default +drop index t_f1_f2_f3_idx; +create index on t using spgist(f1 name_ops_old) include(f2, f3); +\d+ t_f1_f2_f3_idx + Index "public.t_f1_f2_f3_idx" + Column | Type | Key? | Definition | Storage | Stats target +--------+---------+------+------------+----------+-------------- + f1 | name | yes | f1 | plain | + f2 | integer | no | f2 | plain | + f3 | text | no | f3 | extended | +spgist, for table "public.t" + +explain (costs off) +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; + QUERY PLAN +--------------------------------------------------------------------------------------------------- + Sort + Sort Key: f1 + -> Index Only Scan using t_f1_f2_f3_idx on t + Index Cond: ((f1 > 'binary_upgrade_set_n'::name) AND (f1 < 'binary_upgrade_set_p'::name)) +(4 rows) + +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; + f1 | f2 | f3 +------------------------------------------------------+----+------------------------------------------------------ + binary_upgrade_set_next_array_pg_type_oid | | binary_upgrade_set_next_array_pg_type_oid + binary_upgrade_set_next_heap_pg_class_oid | | binary_upgrade_set_next_heap_pg_class_oid + binary_upgrade_set_next_heap_relfilenode | 1 | binary_upgrade_set_next_heap_relfilenode + binary_upgrade_set_next_index_pg_class_oid | 1 | binary_upgrade_set_next_index_pg_class_oid + binary_upgrade_set_next_index_relfilenode | | binary_upgrade_set_next_index_relfilenode + binary_upgrade_set_next_multirange_array_pg_type_oid | 1 | binary_upgrade_set_next_multirange_array_pg_type_oid + binary_upgrade_set_next_multirange_pg_type_oid | 1 | binary_upgrade_set_next_multirange_pg_type_oid + binary_upgrade_set_next_pg_authid_oid | | binary_upgrade_set_next_pg_authid_oid + binary_upgrade_set_next_pg_enum_oid | | binary_upgrade_set_next_pg_enum_oid + binary_upgrade_set_next_pg_tablespace_oid | | binary_upgrade_set_next_pg_tablespace_oid + binary_upgrade_set_next_pg_type_oid | | binary_upgrade_set_next_pg_type_oid + binary_upgrade_set_next_toast_pg_class_oid | 1 | binary_upgrade_set_next_toast_pg_class_oid + binary_upgrade_set_next_toast_relfilenode | | binary_upgrade_set_next_toast_relfilenode +(13 rows) + +\set VERBOSITY sqlstate +insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000)); +ERROR: 54000 +\set VERBOSITY default diff --git a/src/test/modules/spgist_name_ops/spgist_name_ops--1.0.sql b/src/test/modules/spgist_name_ops/spgist_name_ops--1.0.sql new file mode 100644 index 0000000..432216e --- /dev/null +++ b/src/test/modules/spgist_name_ops/spgist_name_ops--1.0.sql @@ -0,0 +1,54 @@ +/* src/test/modules/spgist_name_ops/spgist_name_ops--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION spgist_name_ops" to load this file. \quit + +CREATE FUNCTION spgist_name_config(internal, internal) +RETURNS void IMMUTABLE PARALLEL SAFE STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION spgist_name_choose(internal, internal) +RETURNS void IMMUTABLE PARALLEL SAFE STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION spgist_name_inner_consistent(internal, internal) +RETURNS void IMMUTABLE PARALLEL SAFE STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION spgist_name_leaf_consistent(internal, internal) +RETURNS boolean IMMUTABLE PARALLEL SAFE STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION spgist_name_compress(name) +RETURNS text IMMUTABLE PARALLEL SAFE STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE OPERATOR CLASS name_ops DEFAULT FOR TYPE name +USING spgist AS + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + FUNCTION 1 spgist_name_config(internal, internal), + FUNCTION 2 spgist_name_choose(internal, internal), + FUNCTION 3 spg_text_picksplit(internal, internal), + FUNCTION 4 spgist_name_inner_consistent(internal, internal), + FUNCTION 5 spgist_name_leaf_consistent(internal, internal), + FUNCTION 6 spgist_name_compress(name), + STORAGE text; + +-- Also test old-style where the STORAGE clause is disallowed +CREATE OPERATOR CLASS name_ops_old FOR TYPE name +USING spgist AS + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + FUNCTION 1 spgist_name_config(internal, internal), + FUNCTION 2 spgist_name_choose(internal, internal), + FUNCTION 3 spg_text_picksplit(internal, internal), + FUNCTION 4 spgist_name_inner_consistent(internal, internal), + FUNCTION 5 spgist_name_leaf_consistent(internal, internal), + FUNCTION 6 spgist_name_compress(name); diff --git a/src/test/modules/spgist_name_ops/spgist_name_ops.c b/src/test/modules/spgist_name_ops/spgist_name_ops.c new file mode 100644 index 0000000..89595fe --- /dev/null +++ b/src/test/modules/spgist_name_ops/spgist_name_ops.c @@ -0,0 +1,501 @@ +/*-------------------------------------------------------------------------- + * + * spgist_name_ops.c + * Test opclass for SP-GiST + * + * This indexes input values of type "name", but the index storage is "text", + * with the same choices as made in the core SP-GiST text_ops opclass. + * Much of the code is identical to src/backend/access/spgist/spgtextproc.c, + * which see for a more detailed header comment. + * + * Unlike spgtextproc.c, we don't bother with collation-aware logic. + * + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/test/modules/spgist_name_ops/spgist_name_ops.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/spgist.h" +#include "catalog/pg_type.h" +#include "utils/datum.h" + +PG_MODULE_MAGIC; + + +PG_FUNCTION_INFO_V1(spgist_name_config); +Datum +spgist_name_config(PG_FUNCTION_ARGS) +{ + /* spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); */ + spgConfigOut *cfg = (spgConfigOut *) PG_GETARG_POINTER(1); + + cfg->prefixType = TEXTOID; + cfg->labelType = INT2OID; + cfg->leafType = TEXTOID; + cfg->canReturnData = true; + cfg->longValuesOK = true; /* suffixing will shorten long values */ + PG_RETURN_VOID(); +} + +/* + * Form a text datum from the given not-necessarily-null-terminated string, + * using short varlena header format if possible + */ +static Datum +formTextDatum(const char *data, int datalen) +{ + char *p; + + p = (char *) palloc(datalen + VARHDRSZ); + + if (datalen + VARHDRSZ_SHORT <= VARATT_SHORT_MAX) + { + SET_VARSIZE_SHORT(p, datalen + VARHDRSZ_SHORT); + if (datalen) + memcpy(p + VARHDRSZ_SHORT, data, datalen); + } + else + { + SET_VARSIZE(p, datalen + VARHDRSZ); + memcpy(p + VARHDRSZ, data, datalen); + } + + return PointerGetDatum(p); +} + +/* + * Find the length of the common prefix of a and b + */ +static int +commonPrefix(const char *a, const char *b, int lena, int lenb) +{ + int i = 0; + + while (i < lena && i < lenb && *a == *b) + { + a++; + b++; + i++; + } + + return i; +} + +/* + * Binary search an array of int16 datums for a match to c + * + * On success, *i gets the match location; on failure, it gets where to insert + */ +static bool +searchChar(Datum *nodeLabels, int nNodes, int16 c, int *i) +{ + int StopLow = 0, + StopHigh = nNodes; + + while (StopLow < StopHigh) + { + int StopMiddle = (StopLow + StopHigh) >> 1; + int16 middle = DatumGetInt16(nodeLabels[StopMiddle]); + + if (c < middle) + StopHigh = StopMiddle; + else if (c > middle) + StopLow = StopMiddle + 1; + else + { + *i = StopMiddle; + return true; + } + } + + *i = StopHigh; + return false; +} + +PG_FUNCTION_INFO_V1(spgist_name_choose); +Datum +spgist_name_choose(PG_FUNCTION_ARGS) +{ + spgChooseIn *in = (spgChooseIn *) PG_GETARG_POINTER(0); + spgChooseOut *out = (spgChooseOut *) PG_GETARG_POINTER(1); + Name inName = DatumGetName(in->datum); + char *inStr = NameStr(*inName); + int inSize = strlen(inStr); + char *prefixStr = NULL; + int prefixSize = 0; + int commonLen = 0; + int16 nodeChar = 0; + int i = 0; + + /* Check for prefix match, set nodeChar to first byte after prefix */ + if (in->hasPrefix) + { + text *prefixText = DatumGetTextPP(in->prefixDatum); + + prefixStr = VARDATA_ANY(prefixText); + prefixSize = VARSIZE_ANY_EXHDR(prefixText); + + commonLen = commonPrefix(inStr + in->level, + prefixStr, + inSize - in->level, + prefixSize); + + if (commonLen == prefixSize) + { + if (inSize - in->level > commonLen) + nodeChar = *(unsigned char *) (inStr + in->level + commonLen); + else + nodeChar = -1; + } + else + { + /* Must split tuple because incoming value doesn't match prefix */ + out->resultType = spgSplitTuple; + + if (commonLen == 0) + { + out->result.splitTuple.prefixHasPrefix = false; + } + else + { + out->result.splitTuple.prefixHasPrefix = true; + out->result.splitTuple.prefixPrefixDatum = + formTextDatum(prefixStr, commonLen); + } + out->result.splitTuple.prefixNNodes = 1; + out->result.splitTuple.prefixNodeLabels = + (Datum *) palloc(sizeof(Datum)); + out->result.splitTuple.prefixNodeLabels[0] = + Int16GetDatum(*(unsigned char *) (prefixStr + commonLen)); + + out->result.splitTuple.childNodeN = 0; + + if (prefixSize - commonLen == 1) + { + out->result.splitTuple.postfixHasPrefix = false; + } + else + { + out->result.splitTuple.postfixHasPrefix = true; + out->result.splitTuple.postfixPrefixDatum = + formTextDatum(prefixStr + commonLen + 1, + prefixSize - commonLen - 1); + } + + PG_RETURN_VOID(); + } + } + else if (inSize > in->level) + { + nodeChar = *(unsigned char *) (inStr + in->level); + } + else + { + nodeChar = -1; + } + + /* Look up nodeChar in the node label array */ + if (searchChar(in->nodeLabels, in->nNodes, nodeChar, &i)) + { + /* + * Descend to existing node. (If in->allTheSame, the core code will + * ignore our nodeN specification here, but that's OK. We still have + * to provide the correct levelAdd and restDatum values, and those are + * the same regardless of which node gets chosen by core.) + */ + int levelAdd; + + out->resultType = spgMatchNode; + out->result.matchNode.nodeN = i; + levelAdd = commonLen; + if (nodeChar >= 0) + levelAdd++; + out->result.matchNode.levelAdd = levelAdd; + if (inSize - in->level - levelAdd > 0) + out->result.matchNode.restDatum = + formTextDatum(inStr + in->level + levelAdd, + inSize - in->level - levelAdd); + else + out->result.matchNode.restDatum = + formTextDatum(NULL, 0); + } + else if (in->allTheSame) + { + /* + * Can't use AddNode action, so split the tuple. The upper tuple has + * the same prefix as before and uses a dummy node label -2 for the + * lower tuple. The lower tuple has no prefix and the same node + * labels as the original tuple. + * + * Note: it might seem tempting to shorten the upper tuple's prefix, + * if it has one, then use its last byte as label for the lower tuple. + * But that doesn't win since we know the incoming value matches the + * whole prefix: we'd just end up splitting the lower tuple again. + */ + out->resultType = spgSplitTuple; + out->result.splitTuple.prefixHasPrefix = in->hasPrefix; + out->result.splitTuple.prefixPrefixDatum = in->prefixDatum; + out->result.splitTuple.prefixNNodes = 1; + out->result.splitTuple.prefixNodeLabels = (Datum *) palloc(sizeof(Datum)); + out->result.splitTuple.prefixNodeLabels[0] = Int16GetDatum(-2); + out->result.splitTuple.childNodeN = 0; + out->result.splitTuple.postfixHasPrefix = false; + } + else + { + /* Add a node for the not-previously-seen nodeChar value */ + out->resultType = spgAddNode; + out->result.addNode.nodeLabel = Int16GetDatum(nodeChar); + out->result.addNode.nodeN = i; + } + + PG_RETURN_VOID(); +} + +/* The picksplit function is identical to the core opclass, so just use that */ + +PG_FUNCTION_INFO_V1(spgist_name_inner_consistent); +Datum +spgist_name_inner_consistent(PG_FUNCTION_ARGS) +{ + spgInnerConsistentIn *in = (spgInnerConsistentIn *) PG_GETARG_POINTER(0); + spgInnerConsistentOut *out = (spgInnerConsistentOut *) PG_GETARG_POINTER(1); + text *reconstructedValue; + text *reconstrText; + int maxReconstrLen; + text *prefixText = NULL; + int prefixSize = 0; + int i; + + /* + * Reconstruct values represented at this tuple, including parent data, + * prefix of this tuple if any, and the node label if it's non-dummy. + * in->level should be the length of the previously reconstructed value, + * and the number of bytes added here is prefixSize or prefixSize + 1. + * + * Recall that reconstructedValues are assumed to be the same type as leaf + * datums, so we must use "text" not "name" for them. + * + * Note: we assume that in->reconstructedValue isn't toasted and doesn't + * have a short varlena header. This is okay because it must have been + * created by a previous invocation of this routine, and we always emit + * long-format reconstructed values. + */ + reconstructedValue = (text *) DatumGetPointer(in->reconstructedValue); + Assert(reconstructedValue == NULL ? in->level == 0 : + VARSIZE_ANY_EXHDR(reconstructedValue) == in->level); + + maxReconstrLen = in->level + 1; + if (in->hasPrefix) + { + prefixText = DatumGetTextPP(in->prefixDatum); + prefixSize = VARSIZE_ANY_EXHDR(prefixText); + maxReconstrLen += prefixSize; + } + + reconstrText = palloc(VARHDRSZ + maxReconstrLen); + SET_VARSIZE(reconstrText, VARHDRSZ + maxReconstrLen); + + if (in->level) + memcpy(VARDATA(reconstrText), + VARDATA(reconstructedValue), + in->level); + if (prefixSize) + memcpy(((char *) VARDATA(reconstrText)) + in->level, + VARDATA_ANY(prefixText), + prefixSize); + /* last byte of reconstrText will be filled in below */ + + /* + * Scan the child nodes. For each one, complete the reconstructed value + * and see if it's consistent with the query. If so, emit an entry into + * the output arrays. + */ + out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); + out->levelAdds = (int *) palloc(sizeof(int) * in->nNodes); + out->reconstructedValues = (Datum *) palloc(sizeof(Datum) * in->nNodes); + out->nNodes = 0; + + for (i = 0; i < in->nNodes; i++) + { + int16 nodeChar = DatumGetInt16(in->nodeLabels[i]); + int thisLen; + bool res = true; + int j; + + /* If nodeChar is a dummy value, don't include it in data */ + if (nodeChar <= 0) + thisLen = maxReconstrLen - 1; + else + { + ((unsigned char *) VARDATA(reconstrText))[maxReconstrLen - 1] = nodeChar; + thisLen = maxReconstrLen; + } + + for (j = 0; j < in->nkeys; j++) + { + StrategyNumber strategy = in->scankeys[j].sk_strategy; + Name inName; + char *inStr; + int inSize; + int r; + + inName = DatumGetName(in->scankeys[j].sk_argument); + inStr = NameStr(*inName); + inSize = strlen(inStr); + + r = memcmp(VARDATA(reconstrText), inStr, + Min(inSize, thisLen)); + + switch (strategy) + { + case BTLessStrategyNumber: + case BTLessEqualStrategyNumber: + if (r > 0) + res = false; + break; + case BTEqualStrategyNumber: + if (r != 0 || inSize < thisLen) + res = false; + break; + case BTGreaterEqualStrategyNumber: + case BTGreaterStrategyNumber: + if (r < 0) + res = false; + break; + default: + elog(ERROR, "unrecognized strategy number: %d", + in->scankeys[j].sk_strategy); + break; + } + + if (!res) + break; /* no need to consider remaining conditions */ + } + + if (res) + { + out->nodeNumbers[out->nNodes] = i; + out->levelAdds[out->nNodes] = thisLen - in->level; + SET_VARSIZE(reconstrText, VARHDRSZ + thisLen); + out->reconstructedValues[out->nNodes] = + datumCopy(PointerGetDatum(reconstrText), false, -1); + out->nNodes++; + } + } + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(spgist_name_leaf_consistent); +Datum +spgist_name_leaf_consistent(PG_FUNCTION_ARGS) +{ + spgLeafConsistentIn *in = (spgLeafConsistentIn *) PG_GETARG_POINTER(0); + spgLeafConsistentOut *out = (spgLeafConsistentOut *) PG_GETARG_POINTER(1); + int level = in->level; + text *leafValue, + *reconstrValue = NULL; + char *fullValue; + int fullLen; + bool res; + int j; + + /* all tests are exact */ + out->recheck = false; + + leafValue = DatumGetTextPP(in->leafDatum); + + /* As above, in->reconstructedValue isn't toasted or short. */ + if (DatumGetPointer(in->reconstructedValue)) + reconstrValue = (text *) DatumGetPointer(in->reconstructedValue); + + Assert(reconstrValue == NULL ? level == 0 : + VARSIZE_ANY_EXHDR(reconstrValue) == level); + + /* Reconstruct the Name represented by this leaf tuple */ + fullValue = palloc0(NAMEDATALEN); + fullLen = level + VARSIZE_ANY_EXHDR(leafValue); + Assert(fullLen < NAMEDATALEN); + if (VARSIZE_ANY_EXHDR(leafValue) == 0 && level > 0) + { + memcpy(fullValue, VARDATA(reconstrValue), + VARSIZE_ANY_EXHDR(reconstrValue)); + } + else + { + if (level) + memcpy(fullValue, VARDATA(reconstrValue), level); + if (VARSIZE_ANY_EXHDR(leafValue) > 0) + memcpy(fullValue + level, VARDATA_ANY(leafValue), + VARSIZE_ANY_EXHDR(leafValue)); + } + out->leafValue = PointerGetDatum(fullValue); + + /* Perform the required comparison(s) */ + res = true; + for (j = 0; j < in->nkeys; j++) + { + StrategyNumber strategy = in->scankeys[j].sk_strategy; + Name queryName = DatumGetName(in->scankeys[j].sk_argument); + char *queryStr = NameStr(*queryName); + int queryLen = strlen(queryStr); + int r; + + /* Non-collation-aware comparison */ + r = memcmp(fullValue, queryStr, Min(queryLen, fullLen)); + + if (r == 0) + { + if (queryLen > fullLen) + r = -1; + else if (queryLen < fullLen) + r = 1; + } + + switch (strategy) + { + case BTLessStrategyNumber: + res = (r < 0); + break; + case BTLessEqualStrategyNumber: + res = (r <= 0); + break; + case BTEqualStrategyNumber: + res = (r == 0); + break; + case BTGreaterEqualStrategyNumber: + res = (r >= 0); + break; + case BTGreaterStrategyNumber: + res = (r > 0); + break; + default: + elog(ERROR, "unrecognized strategy number: %d", + in->scankeys[j].sk_strategy); + res = false; + break; + } + + if (!res) + break; /* no need to consider remaining conditions */ + } + + PG_RETURN_BOOL(res); +} + +PG_FUNCTION_INFO_V1(spgist_name_compress); +Datum +spgist_name_compress(PG_FUNCTION_ARGS) +{ + Name inName = PG_GETARG_NAME(0); + char *inStr = NameStr(*inName); + + PG_RETURN_DATUM(formTextDatum(inStr, strlen(inStr))); +} diff --git a/src/test/modules/spgist_name_ops/spgist_name_ops.control b/src/test/modules/spgist_name_ops/spgist_name_ops.control new file mode 100644 index 0000000..f02df7e --- /dev/null +++ b/src/test/modules/spgist_name_ops/spgist_name_ops.control @@ -0,0 +1,4 @@ +comment = 'Test opclass for SP-GiST' +default_version = '1.0' +module_pathname = '$libdir/spgist_name_ops' +relocatable = true diff --git a/src/test/modules/spgist_name_ops/sql/spgist_name_ops.sql b/src/test/modules/spgist_name_ops/sql/spgist_name_ops.sql new file mode 100644 index 0000000..982f221 --- /dev/null +++ b/src/test/modules/spgist_name_ops/sql/spgist_name_ops.sql @@ -0,0 +1,51 @@ +create extension spgist_name_ops; + +select opcname, amvalidate(opc.oid) +from pg_opclass opc join pg_am am on am.oid = opcmethod +where amname = 'spgist' and opcname = 'name_ops'; + +-- warning expected here +select opcname, amvalidate(opc.oid) +from pg_opclass opc join pg_am am on am.oid = opcmethod +where amname = 'spgist' and opcname = 'name_ops_old'; + +create table t(f1 name, f2 integer, f3 text); +create index on t using spgist(f1) include(f2, f3); +\d+ t_f1_f2_f3_idx + +insert into t select + proname, + case when length(proname) % 2 = 0 then pronargs else null end, + prosrc from pg_proc; +vacuum analyze t; + +explain (costs off) +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; + +-- Verify clean failure when INCLUDE'd columns result in overlength tuple +-- The error message details are platform-dependent, so show only SQLSTATE +\set VERBOSITY sqlstate +insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000)); +\set VERBOSITY default + +drop index t_f1_f2_f3_idx; + +create index on t using spgist(f1 name_ops_old) include(f2, f3); +\d+ t_f1_f2_f3_idx + +explain (costs off) +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; +select * from t + where f1 > 'binary_upgrade_set_n' and f1 < 'binary_upgrade_set_p' + order by 1; + +\set VERBOSITY sqlstate +insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000)); +\set VERBOSITY default diff --git a/src/test/modules/ssl_passphrase_callback/.gitignore b/src/test/modules/ssl_passphrase_callback/.gitignore new file mode 100644 index 0000000..1dbadf7 --- /dev/null +++ b/src/test/modules/ssl_passphrase_callback/.gitignore @@ -0,0 +1 @@ +tmp_check diff --git a/src/test/modules/ssl_passphrase_callback/Makefile b/src/test/modules/ssl_passphrase_callback/Makefile new file mode 100644 index 0000000..a34d7ea --- /dev/null +++ b/src/test/modules/ssl_passphrase_callback/Makefile @@ -0,0 +1,40 @@ +# ssl_passphrase_callback Makefile + +export with_ssl + +MODULE_big = ssl_passphrase_func +OBJS = ssl_passphrase_func.o $(WIN32RES) +PGFILEDESC = "callback function to provide a passphrase" + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/ssl_passphrase_callback +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +SHLIB_LINK += $(filter -lssl -lcrypto -lssleay32 -leay32, $(LIBS)) + +# Targets to generate or remove the ssl certificate and key +# Normally not needed. Don't run these targets in a vpath build, the results +# won't be in the right place if you do. + +# needs to agree with what's in the test script +PASS = FooBaR1 + +.PHONY: ssl-files ssl-files-clean + +ssl-files: + openssl req -new -x509 -days 10000 -nodes -out server.crt \ + -keyout server.ckey -subj "/CN=localhost" + openssl rsa -aes256 -in server.ckey -out server.key -passout pass:$(PASS) + rm server.ckey + +ssl-files-clean: + rm -f server.crt server.key diff --git a/src/test/modules/ssl_passphrase_callback/server.crt b/src/test/modules/ssl_passphrase_callback/server.crt new file mode 100644 index 0000000..b3c4be4 --- /dev/null +++ b/src/test/modules/ssl_passphrase_callback/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUfHgPLNys4V0d0cWrzRHqfs91LFMwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMDMyMTE0MDM1OVoXDTQ3MDgw +NzE0MDM1OVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA2j0PZwmeahBC7QpG7i9/VUVJrLzy+b8oVaqZUO6nlPbY +wuPISYTO/jqc0XDfs/Gb0kccDJ6bPfNfvSnRTG1omE6OO9YjR0u3296l4bWAmYVq +q4SesgQmm1Wy8ODNpeGaoBUwR51OB/gFHFjUlqAjRwOmrTCbDiAsLt7e+cx+W26r +2SrJIweiSJsqaQsMMaqlY2qpHnYgWfqRUTqwXqlno0dXuqBt+KKgqeHMY3w3XS51 +8roOI0+Q9KWsexL/aYnLwMRsHRMZcthhzTK6HD/OrLh9CxURImr4ed9TtsNiZltA +KqLTeGbtS1D2AvFqJU8n5DvtU+26wDrHu6pEM3kSJQIDAQABo1MwUTAdBgNVHQ4E +FgQUkkfa08hDnxYs1UjG2ydCBJs1b2AwHwYDVR0jBBgwFoAUkkfa08hDnxYs1UjG +2ydCBJs1b2AwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAjsJh +p4tCopCA/Pvxupv3VEwGJ+nbH7Zg/hp+o2IWuHBOK1qrkyXBv34h/69bRnWZ5UFV +HxQwL7CjNtjZu9SbpKkaHbZXPWANC9fbPKdBz9fAEwunf33KbZe3dPv/7xbJirMz +e+j5V0LE0Spkr/p89LipXfjGw0jLC8VRTx/vKPnmbiBsCKw5SQKh3w7CcBx84Y6q +Nc27WQ8ReR4W4X1zHGN6kEV4H+yPN2Z9OlSixTiSNvr2mtJQsZa7gK7Wwfm79RN7 +5Kf3l8b6e2BToJwLorpK9mvu41NtwRzl4UoJ1BFJDyhMplFMd8RcwTW6yT2biOFC +lYCajcBoms3IiyqBog== +-----END CERTIFICATE----- diff --git a/src/test/modules/ssl_passphrase_callback/server.key b/src/test/modules/ssl_passphrase_callback/server.key new file mode 100644 index 0000000..1475007 --- /dev/null +++ b/src/test/modules/ssl_passphrase_callback/server.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,DB0E7068D4DCE79FFE63C95B8D8F7CEA + +Y4uvnlWX/kyulqsmt8aWI55vKFdfJL4wEZItL8ZKlQFuZuxC9w0OworyjTdqO38R +v9hwnetZBDgK8kEv6U7wR58mTfwHHCGuxYgSiPZtiW7btS4zu16ePdh8oBEzCxjW +ALrCFt7uvRu5h2AWy/4BgV4gLNVPNB+lJFABtUoiSnUDr7+bcx7UjZ4An6HriGxC +Kg/N1pKjT/xiKOy+yHtrp1Jih5HYDE4i99jPtMuTROf8Uyz7ibyrdd/E7QNvANQN +Cmw4I4Xk4hZ68F0iGU0C0wLND3pWbeYPKorpo3PkI4Du+Aqlg15ae5u8CtU3fXGJ +mq4/qLGAi1sr/45f5P5a3Q8BQpKkCmGopXMaFYOOiaf3YYgD1eVOxLhsCWqUB+O8 +ygcTNRCoKhzY+ULComXp880J3fFk5b92g4Hm1PAO42uDKzhWSnrmCBJ7ynXvnEc+ +JqhiE8Obrp6FBIHvfN26JtHcXTd/bgUMXSh7AXjsotfvPPV0URve9JJG+RnwckeT +K3AYDOQK/lbqDGliNqHg1WiMSA2oHSqDhUMB0Sm0jh6+jxCQlsmSDvPvJfWRo5wY +zbZZZARQnFUaHa9CZVdFxbaPGhYU6vAwxDqi42osSJEdf68Gy2KVXcelqpU/2dKk +aHfTgAWOsajbgt9p+0369TeZb39+zLODdDJnvZYiu1pTASHP5VrJ2xHhu5zOdjXm +GafYiPwYBM280wkIVQ0HsTX7BViU2R/7W3FqflXgQvBiraVQVwHyaX4bOU1a3rzg +emHNLTCpRamT0i/D0tkEPgS42bWSVi9ko5Mn9yb+qToBjAOLVUOAOs9Bv3qxawhI +XFbBDZ7DS59l2yV6eQkrG7DUCLDf4dv4WZeBnhrPe/Jg8HKcsKcJYV3cejZh8sgu +XHeCU50+jpJDfTZVPW3TjZWmrTqStGwF1UFpj+tTsTcX+OHAY/shFs3bBZulAsMy +5UWZWzyWHMWr/wbxW7dbhTb1gNmOgpQQz9dunSgcZ8umzSGLa0ZGmnQj9P/kZkQA +RenuswH5O7CK/MDmf3J6svwyLt/jULmH26MZTcNu7igT6dj3VMSwkoQQaaQdtmzb +glzN3uqf8qM+CEjV8dxlt8fv6KJV7gvoYfPAz+1pp5DVJBmRo/+b4e/d4QTV9iWS +ScBYdonc9WXcrjmExX9+Wf/K/IKfLnKLIi2MZ3pwr1n7yY+dMeF6iREYSjFVIpZd +MH3G9/SxTrqR7X/eHjwdv1UupYYyaDag8wpVn1RMCb0xYqh2/QP1k0pQycckL0WQ +lieXibEuQhV/heXcqt83G6pGqLImc6YPYU46jdGpPIMyOK+ZSqJTHUWHfRMQTIMz +varR2M3uhHvwUFzmvjLh/o6I3r0a0Rl1MztpYfjBV6MS4BKYfraWZ0kxCyV+e6tz +O7vD0P5W2qm6b89Md3nqjUcbOM8AojcfBl3xpQrpSdgJ25YJBoJ9L2I2pIMNCK/x +yDNEJl7yP87fdHfXZm2VoUXclDUYHyNys9Rtv9NSr+VNkIMcqrCHEgpAxwQQ5NsO +/vOZe3wjhXXLyRO7Nh5W8jojw3xcb9c9avFUWUvM2BaS4vEYcItUoF4QuHohrCwk +-----END RSA PRIVATE KEY----- diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c new file mode 100644 index 0000000..e9f2329 --- /dev/null +++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c @@ -0,0 +1,85 @@ +/*------------------------------------------------------------------------- + * + * ssl_passphrase_func.c + * + * Loadable PostgreSQL module fetch an ssl passphrase for the server cert. + * instead of calling an external program. This implementation just hands + * back the configured password rot13'd. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include <float.h> +#include <stdio.h> + +#include "libpq/libpq.h" +#include "libpq/libpq-be.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +static char *ssl_passphrase = NULL; + +/* callback function */ +static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata); + +/* hook function to set the callback */ +static void set_rot13(SSL_CTX *context, bool isServerStart); + +/* + * Module load callback + */ +void +_PG_init(void) +{ + /* Define custom GUC variable. */ + DefineCustomStringVariable("ssl_passphrase.passphrase", + "passphrase before transformation", + NULL, + &ssl_passphrase, + NULL, + PGC_SIGHUP, + 0, /* no flags required */ + NULL, + NULL, + NULL); + + MarkGUCPrefixReserved("ssl_passphrase"); + + if (ssl_passphrase) + openssl_tls_init_hook = set_rot13; +} + +static void +set_rot13(SSL_CTX *context, bool isServerStart) +{ + /* warn if the user has set ssl_passphrase_command */ + if (ssl_passphrase_command[0]) + ereport(WARNING, + (errmsg("ssl_passphrase_command setting ignored by ssl_passphrase_func module"))); + + SSL_CTX_set_default_passwd_cb(context, rot13_passphrase); +} + +static int +rot13_passphrase(char *buf, int size, int rwflag, void *userdata) +{ + + Assert(ssl_passphrase != NULL); + strlcpy(buf, ssl_passphrase, size); + for (char *p = buf; *p; p++) + { + char c = *p; + + if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) + *p = c + 13; + else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) + *p = c - 13; + } + + return strlen(buf); +} diff --git a/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl b/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl new file mode 100644 index 0000000..5be5ac3 --- /dev/null +++ b/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl @@ -0,0 +1,78 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +use strict; +use warnings; + +use File::Copy; + +use PostgreSQL::Test::Utils; +use Test::More; +use PostgreSQL::Test::Cluster; + +unless (($ENV{with_ssl} || "") eq 'openssl') +{ + plan skip_all => 'OpenSSL not supported by this build'; +} + +my $clearpass = "FooBaR1"; +my $rot13pass = "SbbOnE1"; + +# see the Makefile for how the certificate and key have been generated + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf('postgresql.conf', + "ssl_passphrase.passphrase = '$rot13pass'"); +$node->append_conf('postgresql.conf', + "shared_preload_libraries = 'ssl_passphrase_func'"); +$node->append_conf('postgresql.conf', "ssl = 'on'"); + +my $ddir = $node->data_dir; + +# install certificate and protected key +copy("server.crt", $ddir); +copy("server.key", $ddir); +chmod 0600, "$ddir/server.key"; + +$node->start; + +# if the server is running we must have successfully transformed the passphrase +ok(-e "$ddir/postmaster.pid", "postgres started"); + +$node->stop('fast'); + +# should get a warning if ssl_passphrase_command is set +my $log = $node->rotate_logfile(); + +$node->append_conf('postgresql.conf', + "ssl_passphrase_command = 'echo spl0tz'"); + +$node->start; + +$node->stop('fast'); + +my $log_contents = slurp_file($log); + +like( + $log_contents, + qr/WARNING.*ssl_passphrase_command setting ignored by ssl_passphrase_func module/, + "ssl_passphrase_command set warning"); + +# set the wrong passphrase +$node->append_conf('postgresql.conf', "ssl_passphrase.passphrase = 'blurfl'"); + +# try to start the server again +my $ret = + PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $node->data_dir, '-l', + $node->logfile, 'start'); + + +# with a bad passphrase the server should not start +ok($ret, "pg_ctl fails with bad passphrase"); +ok(!-e "$ddir/postmaster.pid", "postgres not started with bad passphrase"); + +# just in case +$node->stop('fast'); + +done_testing(); diff --git a/src/test/modules/test_bloomfilter/.gitignore b/src/test/modules/test_bloomfilter/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_bloomfilter/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_bloomfilter/Makefile b/src/test/modules/test_bloomfilter/Makefile new file mode 100644 index 0000000..c8b7890 --- /dev/null +++ b/src/test/modules/test_bloomfilter/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_bloomfilter/Makefile + +MODULE_big = test_bloomfilter +OBJS = \ + $(WIN32RES) \ + test_bloomfilter.o +PGFILEDESC = "test_bloomfilter - test code for Bloom filter library" + +EXTENSION = test_bloomfilter +DATA = test_bloomfilter--1.0.sql + +REGRESS = test_bloomfilter + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_bloomfilter +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_bloomfilter/README b/src/test/modules/test_bloomfilter/README new file mode 100644 index 0000000..4c05efe --- /dev/null +++ b/src/test/modules/test_bloomfilter/README @@ -0,0 +1,68 @@ +test_bloomfilter overview +========================= + +test_bloomfilter is a test harness module for testing Bloom filter library set +membership operations. It consists of a single SQL-callable function, +test_bloomfilter(), plus a regression test that calls test_bloomfilter(). +Membership tests are performed against a dataset that the test harness module +generates. + +The test_bloomfilter() function displays instrumentation at DEBUG1 elog level +(WARNING when the false positive rate exceeds a 1% threshold). This can be +used to get a sense of the performance characteristics of the Postgres Bloom +filter implementation under varied conditions. + +Bitset size +----------- + +The main bloomfilter.c criteria for sizing its bitset is that the false +positive rate should not exceed 2% when sufficient bloom_work_mem is available +(and the caller-supplied estimate of the number of elements turns out to have +been accurate). A 1% - 2% rate is currently assumed to be suitable for all +Bloom filter callers. + +With an optimal K (number of hash functions), Bloom filters should only have a +1% false positive rate with just 9.6 bits of memory per element. The Postgres +implementation's 2% worst case guarantee exists because there is a need for +some slop due to implementation inflexibility in bitset sizing. Since the +bitset size is always actually kept to a power of two number of bits, callers +can have their bloom_work_mem argument truncated down by almost half. +In practice, callers that make a point of passing a bloom_work_mem that is an +exact power of two bitset size (such as test_bloomfilter.c) will actually get +the "9.6 bits per element" 1% false positive rate. + +Testing strategy +---------------- + +Our approach to regression testing is to test that a Bloom filter has only a 1% +false positive rate for a single bitset size (2 ^ 23, or 1MB). We test a +dataset with 838,861 elements, which works out at 10 bits of memory per +element. We round up from 9.6 bits to 10 bits to make sure that we reliably +get under 1% for regression testing. Note that a random seed is used in the +regression tests because the exact false positive rate is inconsistent across +platforms. Inconsistent hash function behavior is something that the +regression tests need to be tolerant of anyway. + +test_bloomfilter() SQL-callable function +======================================== + +The SQL-callable function test_bloomfilter() provides the following arguments: + +* "power" is the power of two used to size the Bloom filter's bitset. + +The minimum valid argument value is 23 (2^23 bits), or 1MB of memory. The +maximum valid argument value is 32, or 512MB of memory. + +* "nelements" is the number of elements to generate for testing purposes. + +* "seed" is a seed value for hashing. + +A value < 0 is interpreted as "use random seed". Varying the seed value (or +specifying -1) should result in small variations in the total number of false +positives. + +* "tests" is the number of tests to run. + +This may be increased when it's useful to perform many tests in an interactive +session. It only makes sense to perform multiple tests when a random seed is +used. diff --git a/src/test/modules/test_bloomfilter/expected/test_bloomfilter.out b/src/test/modules/test_bloomfilter/expected/test_bloomfilter.out new file mode 100644 index 0000000..21c0688 --- /dev/null +++ b/src/test/modules/test_bloomfilter/expected/test_bloomfilter.out @@ -0,0 +1,22 @@ +CREATE EXTENSION test_bloomfilter; +-- See README for explanation of arguments: +SELECT test_bloomfilter(power => 23, + nelements => 838861, + seed => -1, + tests => 1); + test_bloomfilter +------------------ + +(1 row) + +-- Equivalent "10 bits per element" tests for all possible bitset sizes: +-- +-- SELECT test_bloomfilter(24, 1677722) +-- SELECT test_bloomfilter(25, 3355443) +-- SELECT test_bloomfilter(26, 6710886) +-- SELECT test_bloomfilter(27, 13421773) +-- SELECT test_bloomfilter(28, 26843546) +-- SELECT test_bloomfilter(29, 53687091) +-- SELECT test_bloomfilter(30, 107374182) +-- SELECT test_bloomfilter(31, 214748365) +-- SELECT test_bloomfilter(32, 429496730) diff --git a/src/test/modules/test_bloomfilter/sql/test_bloomfilter.sql b/src/test/modules/test_bloomfilter/sql/test_bloomfilter.sql new file mode 100644 index 0000000..9ec159c --- /dev/null +++ b/src/test/modules/test_bloomfilter/sql/test_bloomfilter.sql @@ -0,0 +1,19 @@ +CREATE EXTENSION test_bloomfilter; + +-- See README for explanation of arguments: +SELECT test_bloomfilter(power => 23, + nelements => 838861, + seed => -1, + tests => 1); + +-- Equivalent "10 bits per element" tests for all possible bitset sizes: +-- +-- SELECT test_bloomfilter(24, 1677722) +-- SELECT test_bloomfilter(25, 3355443) +-- SELECT test_bloomfilter(26, 6710886) +-- SELECT test_bloomfilter(27, 13421773) +-- SELECT test_bloomfilter(28, 26843546) +-- SELECT test_bloomfilter(29, 53687091) +-- SELECT test_bloomfilter(30, 107374182) +-- SELECT test_bloomfilter(31, 214748365) +-- SELECT test_bloomfilter(32, 429496730) diff --git a/src/test/modules/test_bloomfilter/test_bloomfilter--1.0.sql b/src/test/modules/test_bloomfilter/test_bloomfilter--1.0.sql new file mode 100644 index 0000000..7682318 --- /dev/null +++ b/src/test/modules/test_bloomfilter/test_bloomfilter--1.0.sql @@ -0,0 +1,11 @@ +/* src/test/modules/test_bloomfilter/test_bloomfilter--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_bloomfilter" to load this file. \quit + +CREATE FUNCTION test_bloomfilter(power integer, + nelements bigint, + seed integer DEFAULT -1, + tests integer DEFAULT 1) +RETURNS pg_catalog.void STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_bloomfilter/test_bloomfilter.c b/src/test/modules/test_bloomfilter/test_bloomfilter.c new file mode 100644 index 0000000..415b96c --- /dev/null +++ b/src/test/modules/test_bloomfilter/test_bloomfilter.c @@ -0,0 +1,138 @@ +/*-------------------------------------------------------------------------- + * + * test_bloomfilter.c + * Test false positive rate of Bloom filter. + * + * Copyright (c) 2018-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_bloomfilter/test_bloomfilter.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/pg_prng.h" +#include "fmgr.h" +#include "lib/bloomfilter.h" +#include "miscadmin.h" + +PG_MODULE_MAGIC; + +/* Fits decimal representation of PG_INT64_MIN + 2 bytes: */ +#define MAX_ELEMENT_BYTES 21 +/* False positive rate WARNING threshold (1%): */ +#define FPOSITIVE_THRESHOLD 0.01 + + +/* + * Populate an empty Bloom filter with "nelements" dummy strings. + */ +static void +populate_with_dummy_strings(bloom_filter *filter, int64 nelements) +{ + char element[MAX_ELEMENT_BYTES]; + int64 i; + + for (i = 0; i < nelements; i++) + { + CHECK_FOR_INTERRUPTS(); + + snprintf(element, sizeof(element), "i" INT64_FORMAT, i); + bloom_add_element(filter, (unsigned char *) element, strlen(element)); + } +} + +/* + * Returns number of strings that are indicated as probably appearing in Bloom + * filter that were in fact never added by populate_with_dummy_strings(). + * These are false positives. + */ +static int64 +nfalsepos_for_missing_strings(bloom_filter *filter, int64 nelements) +{ + char element[MAX_ELEMENT_BYTES]; + int64 nfalsepos = 0; + int64 i; + + for (i = 0; i < nelements; i++) + { + CHECK_FOR_INTERRUPTS(); + + snprintf(element, sizeof(element), "M" INT64_FORMAT, i); + if (!bloom_lacks_element(filter, (unsigned char *) element, + strlen(element))) + nfalsepos++; + } + + return nfalsepos; +} + +static void +create_and_test_bloom(int power, int64 nelements, int callerseed) +{ + int bloom_work_mem; + uint64 seed; + int64 nfalsepos; + bloom_filter *filter; + + bloom_work_mem = (1L << power) / 8L / 1024L; + + elog(DEBUG1, "bloom_work_mem (KB): %d", bloom_work_mem); + + /* + * Generate random seed, or use caller's. Seed should always be a + * positive value less than or equal to PG_INT32_MAX, to ensure that any + * random seed can be recreated through callerseed if the need arises. + */ + seed = callerseed < 0 ? pg_prng_int32p(&pg_global_prng_state) : callerseed; + + /* Create Bloom filter, populate it, and report on false positive rate */ + filter = bloom_create(nelements, bloom_work_mem, seed); + populate_with_dummy_strings(filter, nelements); + nfalsepos = nfalsepos_for_missing_strings(filter, nelements); + + ereport((nfalsepos > nelements * FPOSITIVE_THRESHOLD) ? WARNING : DEBUG1, + (errmsg_internal("seed: " UINT64_FORMAT " false positives: " INT64_FORMAT " (%.6f%%) bitset %.2f%% set", + seed, nfalsepos, (double) nfalsepos / nelements, + 100.0 * bloom_prop_bits_set(filter)))); + + bloom_free(filter); +} + +PG_FUNCTION_INFO_V1(test_bloomfilter); + +/* + * SQL-callable entry point to perform all tests. + * + * If a 1% false positive threshold is not met, emits WARNINGs. + * + * See README for details of arguments. + */ +Datum +test_bloomfilter(PG_FUNCTION_ARGS) +{ + int power = PG_GETARG_INT32(0); + int64 nelements = PG_GETARG_INT64(1); + int seed = PG_GETARG_INT32(2); + int tests = PG_GETARG_INT32(3); + int i; + + if (power < 23 || power > 32) + elog(ERROR, "power argument must be between 23 and 32 inclusive"); + + if (tests <= 0) + elog(ERROR, "invalid number of tests: %d", tests); + + if (nelements < 0) + elog(ERROR, "invalid number of elements: %d", tests); + + for (i = 0; i < tests; i++) + { + elog(DEBUG1, "beginning test #%d...", i + 1); + + create_and_test_bloom(power, nelements, seed); + } + + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_bloomfilter/test_bloomfilter.control b/src/test/modules/test_bloomfilter/test_bloomfilter.control new file mode 100644 index 0000000..99e56ee --- /dev/null +++ b/src/test/modules/test_bloomfilter/test_bloomfilter.control @@ -0,0 +1,4 @@ +comment = 'Test code for Bloom filter library' +default_version = '1.0' +module_pathname = '$libdir/test_bloomfilter' +relocatable = true diff --git a/src/test/modules/test_ddl_deparse/.gitignore b/src/test/modules/test_ddl_deparse/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_ddl_deparse/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_ddl_deparse/Makefile b/src/test/modules/test_ddl_deparse/Makefile new file mode 100644 index 0000000..3a57a95 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/Makefile @@ -0,0 +1,43 @@ +# src/test/modules/test_ddl_deparse/Makefile + +MODULES = test_ddl_deparse +PGFILEDESC = "test_ddl_deparse - regression testing for DDL deparsing" + +EXTENSION = test_ddl_deparse +DATA = test_ddl_deparse--1.0.sql + +# test_ddl_deparse must be first +REGRESS = test_ddl_deparse \ + create_extension \ + create_schema \ + create_type \ + create_conversion \ + create_domain \ + create_sequence_1 \ + create_table \ + create_transform \ + alter_table \ + create_view \ + create_trigger \ + create_rule \ + comment_on \ + alter_function \ + alter_sequence \ + alter_ts_config \ + alter_type_enum \ + opfamily \ + defprivs \ + matviews + +EXTRA_INSTALL = contrib/pg_stat_statements + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_ddl_deparse +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_ddl_deparse/README b/src/test/modules/test_ddl_deparse/README new file mode 100644 index 0000000..b12a129 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/README @@ -0,0 +1,8 @@ +test_ddl_deparse is an example of how to use the pg_ddl_command datatype. +It is not intended to do anything useful on its own; rather, it is a +demonstration of how to use the datatype, and to provide some unit tests for +it. + +The functions in this extension are intended to be able to process some +part of the struct and produce some readable output, preferably handling +all possible cases so that SQL test code can be written. diff --git a/src/test/modules/test_ddl_deparse/expected/alter_extension.out b/src/test/modules/test_ddl_deparse/expected/alter_extension.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/alter_extension.out diff --git a/src/test/modules/test_ddl_deparse/expected/alter_function.out b/src/test/modules/test_ddl_deparse/expected/alter_function.out new file mode 100644 index 0000000..69a3742 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/alter_function.out @@ -0,0 +1,15 @@ +-- +-- ALTER_FUNCTION +-- +ALTER FUNCTION plpgsql_function_trigger_1 () + SET SCHEMA foo; +NOTICE: DDL test: type simple, tag ALTER FUNCTION +ALTER FUNCTION foo.plpgsql_function_trigger_1() + COST 10; +NOTICE: DDL test: type simple, tag ALTER FUNCTION +CREATE ROLE regress_alter_function_role; +ALTER FUNCTION plpgsql_function_trigger_2() + OWNER TO regress_alter_function_role; +ERROR: function plpgsql_function_trigger_2() does not exist +DROP OWNED BY regress_alter_function_role; +DROP ROLE regress_alter_function_role; diff --git a/src/test/modules/test_ddl_deparse/expected/alter_sequence.out b/src/test/modules/test_ddl_deparse/expected/alter_sequence.out new file mode 100644 index 0000000..319f36f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/alter_sequence.out @@ -0,0 +1,15 @@ +-- +-- ALTER_SEQUENCE +-- +ALTER SEQUENCE fkey_table_seq + MINVALUE 10 + START 20 + CACHE 1 + NO CYCLE; +NOTICE: DDL test: type simple, tag ALTER SEQUENCE +ALTER SEQUENCE fkey_table_seq + RENAME TO fkey_table_seq_renamed; +NOTICE: DDL test: type simple, tag ALTER SEQUENCE +ALTER SEQUENCE fkey_table_seq_renamed + SET SCHEMA foo; +NOTICE: DDL test: type simple, tag ALTER SEQUENCE diff --git a/src/test/modules/test_ddl_deparse/expected/alter_table.out b/src/test/modules/test_ddl_deparse/expected/alter_table.out new file mode 100644 index 0000000..141060f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/alter_table.out @@ -0,0 +1,29 @@ +CREATE TABLE parent ( + a int +); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE child () INHERITS (parent); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE grandchild () INHERITS (child); +NOTICE: DDL test: type simple, tag CREATE TABLE +ALTER TABLE parent ADD COLUMN b serial; +NOTICE: DDL test: type simple, tag CREATE SEQUENCE +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: ADD COLUMN (and recurse) +NOTICE: DDL test: type simple, tag ALTER SEQUENCE +ALTER TABLE parent RENAME COLUMN b TO c; +NOTICE: DDL test: type simple, tag ALTER TABLE +ALTER TABLE parent ADD CONSTRAINT a_pos CHECK (a > 0); +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: ADD CONSTRAINT (and recurse) +CREATE TABLE part ( + a int +) PARTITION BY RANGE (a); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE part1 PARTITION OF part FOR VALUES FROM (1) to (100); +NOTICE: DDL test: type simple, tag CREATE TABLE +ALTER TABLE part ADD PRIMARY KEY (a); +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: SET NOT NULL +NOTICE: subcommand: SET NOT NULL +NOTICE: subcommand: ADD INDEX diff --git a/src/test/modules/test_ddl_deparse/expected/alter_ts_config.out b/src/test/modules/test_ddl_deparse/expected/alter_ts_config.out new file mode 100644 index 0000000..afc352f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/alter_ts_config.out @@ -0,0 +1,8 @@ +-- +-- ALTER TEXT SEARCH CONFIGURATION +-- +CREATE TEXT SEARCH CONFIGURATION en (copy=english); +NOTICE: DDL test: type simple, tag CREATE TEXT SEARCH CONFIGURATION +ALTER TEXT SEARCH CONFIGURATION en + ALTER MAPPING FOR host, email, url, sfloat WITH simple; +NOTICE: DDL test: type alter text search configuration, tag ALTER TEXT SEARCH CONFIGURATION diff --git a/src/test/modules/test_ddl_deparse/expected/alter_type_enum.out b/src/test/modules/test_ddl_deparse/expected/alter_type_enum.out new file mode 100644 index 0000000..74107c2 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/alter_type_enum.out @@ -0,0 +1,7 @@ +--- +--- ALTER_TYPE_ENUM +--- +ALTER TYPE enum_test ADD VALUE 'zzz' AFTER 'baz'; +NOTICE: DDL test: type simple, tag ALTER TYPE +ALTER TYPE enum_test ADD VALUE 'aaa' BEFORE 'foo'; +NOTICE: DDL test: type simple, tag ALTER TYPE diff --git a/src/test/modules/test_ddl_deparse/expected/comment_on.out b/src/test/modules/test_ddl_deparse/expected/comment_on.out new file mode 100644 index 0000000..129eff9 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/comment_on.out @@ -0,0 +1,23 @@ +-- +-- COMMENT_ON +-- +COMMENT ON SCHEMA foo IS 'This is schema foo'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON TYPE enum_test IS 'ENUM test'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON TYPE int2range IS 'RANGE test'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON DOMAIN japanese_postal_code IS 'DOMAIN test'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON SEQUENCE fkey_table_seq IS 'SEQUENCE test'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON TABLE datatype_table IS 'This table should contain all native datatypes'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON VIEW datatype_view IS 'This is a view'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON FUNCTION c_function_test() IS 'FUNCTION test'; +ERROR: function c_function_test() does not exist +COMMENT ON TRIGGER trigger_1 ON datatype_table IS 'TRIGGER test'; +NOTICE: DDL test: type simple, tag COMMENT +COMMENT ON RULE rule_1 ON datatype_table IS 'RULE test'; +NOTICE: DDL test: type simple, tag COMMENT diff --git a/src/test/modules/test_ddl_deparse/expected/create_conversion.out b/src/test/modules/test_ddl_deparse/expected/create_conversion.out new file mode 100644 index 0000000..e8697cf --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_conversion.out @@ -0,0 +1,6 @@ +--- +--- CREATE_CONVERSION +--- +-- Simple test should suffice for this +CREATE CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8; +NOTICE: DDL test: type simple, tag CREATE CONVERSION diff --git a/src/test/modules/test_ddl_deparse/expected/create_domain.out b/src/test/modules/test_ddl_deparse/expected/create_domain.out new file mode 100644 index 0000000..2e7f585 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_domain.out @@ -0,0 +1,11 @@ +--- +--- CREATE_DOMAIN +--- +CREATE DOMAIN domainvarchar VARCHAR(5); +NOTICE: DDL test: type simple, tag CREATE DOMAIN +CREATE DOMAIN japanese_postal_code AS TEXT +CHECK( + VALUE ~ '^\d{3}$' +OR VALUE ~ '^\d{3}-\d{4}$' +); +NOTICE: DDL test: type simple, tag CREATE DOMAIN diff --git a/src/test/modules/test_ddl_deparse/expected/create_extension.out b/src/test/modules/test_ddl_deparse/expected/create_extension.out new file mode 100644 index 0000000..4042e02 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_extension.out @@ -0,0 +1,5 @@ +--- +--- CREATE_EXTENSION +--- +CREATE EXTENSION pg_stat_statements; +NOTICE: DDL test: type simple, tag CREATE EXTENSION diff --git a/src/test/modules/test_ddl_deparse/expected/create_function.out b/src/test/modules/test_ddl_deparse/expected/create_function.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_function.out diff --git a/src/test/modules/test_ddl_deparse/expected/create_operator.out b/src/test/modules/test_ddl_deparse/expected/create_operator.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_operator.out diff --git a/src/test/modules/test_ddl_deparse/expected/create_rule.out b/src/test/modules/test_ddl_deparse/expected/create_rule.out new file mode 100644 index 0000000..fe3d047 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_rule.out @@ -0,0 +1,30 @@ +--- +--- CREATE_RULE +--- +CREATE RULE rule_1 AS + ON INSERT + TO datatype_table + DO NOTHING; +NOTICE: DDL test: type simple, tag CREATE RULE +CREATE RULE rule_2 AS + ON UPDATE + TO datatype_table + DO INSERT INTO unlogged_table (id) VALUES(NEW.id); +NOTICE: DDL test: type simple, tag CREATE RULE +CREATE RULE rule_3 AS + ON DELETE + TO datatype_table + DO ALSO NOTHING; +NOTICE: DDL test: type simple, tag CREATE RULE +CREATE RULE "_RETURN" AS + ON SELECT + TO like_datatype_table + DO INSTEAD + SELECT * FROM datatype_view; +NOTICE: DDL test: type simple, tag CREATE RULE +CREATE RULE rule_3 AS + ON DELETE + TO like_datatype_table + WHERE id < 100 + DO ALSO NOTHING; +NOTICE: DDL test: type simple, tag CREATE RULE diff --git a/src/test/modules/test_ddl_deparse/expected/create_schema.out b/src/test/modules/test_ddl_deparse/expected/create_schema.out new file mode 100644 index 0000000..8ab4eb0 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_schema.out @@ -0,0 +1,19 @@ +-- +-- CREATE_SCHEMA +-- +CREATE SCHEMA foo; +NOTICE: DDL test: type simple, tag CREATE SCHEMA +CREATE SCHEMA IF NOT EXISTS bar; +NOTICE: DDL test: type simple, tag CREATE SCHEMA +CREATE SCHEMA baz; +NOTICE: DDL test: type simple, tag CREATE SCHEMA +-- Will not be created, and will not be handled by the +-- event trigger +CREATE SCHEMA IF NOT EXISTS baz; +NOTICE: schema "baz" already exists, skipping +CREATE SCHEMA element_test + CREATE TABLE foo (id int) + CREATE VIEW bar AS SELECT * FROM foo; +NOTICE: DDL test: type simple, tag CREATE SCHEMA +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE VIEW diff --git a/src/test/modules/test_ddl_deparse/expected/create_sequence_1.out b/src/test/modules/test_ddl_deparse/expected/create_sequence_1.out new file mode 100644 index 0000000..5837ea4 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_sequence_1.out @@ -0,0 +1,11 @@ +-- +-- CREATE_SEQUENCE +-- +CREATE SEQUENCE fkey_table_seq + INCREMENT BY 1 + MINVALUE 0 + MAXVALUE 1000000 + START 10 + CACHE 10 + CYCLE; +NOTICE: DDL test: type simple, tag CREATE SEQUENCE diff --git a/src/test/modules/test_ddl_deparse/expected/create_table.out b/src/test/modules/test_ddl_deparse/expected/create_table.out new file mode 100644 index 0000000..0f2a2c1 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_table.out @@ -0,0 +1,164 @@ +-- +-- CREATE_TABLE +-- +-- Datatypes +CREATE TABLE datatype_table ( + id SERIAL, + id_big BIGSERIAL, + is_small SMALLSERIAL, + v_bytea BYTEA, + v_smallint SMALLINT, + v_int INT, + v_bigint BIGINT, + v_char CHAR(1), + v_varchar VARCHAR(10), + v_text TEXT, + v_bool BOOLEAN, + v_inet INET, + v_cidr CIDR, + v_macaddr MACADDR, + v_numeric NUMERIC(1,0), + v_real REAL, + v_float FLOAT(1), + v_float8 FLOAT8, + v_money MONEY, + v_tsquery TSQUERY, + v_tsvector TSVECTOR, + v_date DATE, + v_time TIME, + v_time_tz TIME WITH TIME ZONE, + v_timestamp TIMESTAMP, + v_timestamp_tz TIMESTAMP WITH TIME ZONE, + v_interval INTERVAL, + v_bit BIT, + v_bit4 BIT(4), + v_varbit VARBIT, + v_varbit4 VARBIT(4), + v_box BOX, + v_circle CIRCLE, + v_lseg LSEG, + v_path PATH, + v_point POINT, + v_polygon POLYGON, + v_json JSON, + v_xml XML, + v_uuid UUID, + v_pg_snapshot pg_snapshot, + v_enum ENUM_TEST, + v_postal_code japanese_postal_code, + v_int2range int2range, + PRIMARY KEY (id), + UNIQUE (id_big) +); +NOTICE: DDL test: type simple, tag CREATE SEQUENCE +NOTICE: DDL test: type simple, tag CREATE SEQUENCE +NOTICE: DDL test: type simple, tag CREATE SEQUENCE +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type simple, tag ALTER SEQUENCE +NOTICE: DDL test: type simple, tag ALTER SEQUENCE +NOTICE: DDL test: type simple, tag ALTER SEQUENCE +-- Constraint definitions +CREATE TABLE IF NOT EXISTS fkey_table ( + id INT NOT NULL DEFAULT nextval('fkey_table_seq'::REGCLASS), + datatype_id INT NOT NULL REFERENCES datatype_table(id), + big_id BIGINT NOT NULL, + sometext TEXT COLLATE "POSIX", + check_col_1 INT NOT NULL CHECK(check_col_1 < 10), + check_col_2 INT NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fkey_big_id + FOREIGN KEY (big_id) + REFERENCES datatype_table(id_big), + EXCLUDE USING btree (check_col_2 WITH =) +); +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: ADD CONSTRAINT (and recurse) +NOTICE: subcommand: ADD CONSTRAINT (and recurse) +-- Typed table +CREATE TABLE employees OF employee_type ( + PRIMARY KEY (name), + salary WITH OPTIONS DEFAULT 1000 +); +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: SET NOT NULL +NOTICE: DDL test: type simple, tag CREATE INDEX +-- Inheritance +CREATE TABLE person ( + id INT NOT NULL PRIMARY KEY, + name text, + age int4, + location point +); +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +CREATE TABLE emp ( + salary int4, + manager name +) INHERITS (person); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE student ( + gpa float8 +) INHERITS (person); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE stud_emp ( + percent int4 +) INHERITS (emp, student); +NOTICE: merging multiple inherited definitions of column "id" +NOTICE: merging multiple inherited definitions of column "name" +NOTICE: merging multiple inherited definitions of column "age" +NOTICE: merging multiple inherited definitions of column "location" +NOTICE: DDL test: type simple, tag CREATE TABLE +-- Storage parameters +CREATE TABLE storage ( + id INT +) WITH ( + fillfactor = 10, + autovacuum_enabled = FALSE +); +NOTICE: DDL test: type simple, tag CREATE TABLE +-- LIKE +CREATE TABLE like_datatype_table ( + LIKE datatype_table + EXCLUDING ALL +); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE like_fkey_table ( + LIKE fkey_table + INCLUDING DEFAULTS + INCLUDING INDEXES + INCLUDING STORAGE +); +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: ALTER COLUMN SET DEFAULT (precooked) +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type simple, tag CREATE INDEX +-- Volatile table types +CREATE UNLOGGED TABLE unlogged_table ( + id INT PRIMARY KEY +); +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +CREATE TEMP TABLE temp_table ( + id INT PRIMARY KEY +); +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +CREATE TEMP TABLE temp_table_commit_delete ( + id INT PRIMARY KEY +) +ON COMMIT DELETE ROWS; +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +CREATE TEMP TABLE temp_table_commit_drop ( + id INT PRIMARY KEY +) +ON COMMIT DROP; +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX diff --git a/src/test/modules/test_ddl_deparse/expected/create_transform.out b/src/test/modules/test_ddl_deparse/expected/create_transform.out new file mode 100644 index 0000000..5066051 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_transform.out @@ -0,0 +1,15 @@ +-- +-- CREATE_TRANSFORM +-- +-- Create a dummy transform +-- The function FROM SQL should have internal as single argument as well +-- as return type. The function TO SQL should have as single argument +-- internal and as return argument the datatype of the transform done. +-- We choose some random built-in functions that have the right signature. +-- This won't actually be used, because the SQL function language +-- doesn't implement transforms (there would be no point). +CREATE TRANSFORM FOR int LANGUAGE SQL ( + FROM SQL WITH FUNCTION prsd_lextype(internal), + TO SQL WITH FUNCTION int4recv(internal)); +NOTICE: DDL test: type simple, tag CREATE TRANSFORM +DROP TRANSFORM FOR int LANGUAGE SQL; diff --git a/src/test/modules/test_ddl_deparse/expected/create_trigger.out b/src/test/modules/test_ddl_deparse/expected/create_trigger.out new file mode 100644 index 0000000..c89c847 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_trigger.out @@ -0,0 +1,18 @@ +--- +--- CREATE_TRIGGER +--- +CREATE FUNCTION plpgsql_function_trigger_1() + RETURNS TRIGGER + LANGUAGE plpgsql +AS $$ +BEGIN + RETURN NEW; +END; +$$; +NOTICE: DDL test: type simple, tag CREATE FUNCTION +CREATE TRIGGER trigger_1 + BEFORE INSERT OR UPDATE + ON datatype_table + FOR EACH ROW + EXECUTE PROCEDURE plpgsql_function_trigger_1(); +NOTICE: DDL test: type simple, tag CREATE TRIGGER diff --git a/src/test/modules/test_ddl_deparse/expected/create_type.out b/src/test/modules/test_ddl_deparse/expected/create_type.out new file mode 100644 index 0000000..dadbc8f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_type.out @@ -0,0 +1,24 @@ +--- +--- CREATE_TYPE +--- +CREATE FUNCTION text_w_default_in(cstring) + RETURNS text_w_default + AS 'textin' + LANGUAGE internal STABLE STRICT; +NOTICE: type "text_w_default" is not yet defined +DETAIL: Creating a shell type definition. +NOTICE: DDL test: type simple, tag CREATE FUNCTION +CREATE FUNCTION text_w_default_out(text_w_default) + RETURNS cstring + AS 'textout' + LANGUAGE internal STABLE STRICT ; +NOTICE: argument type text_w_default is only a shell +NOTICE: DDL test: type simple, tag CREATE FUNCTION +CREATE TYPE employee_type AS (name TEXT, salary NUMERIC); +NOTICE: DDL test: type simple, tag CREATE TYPE +CREATE TYPE enum_test AS ENUM ('foo', 'bar', 'baz'); +NOTICE: DDL test: type simple, tag CREATE TYPE +CREATE TYPE int2range AS RANGE ( + SUBTYPE = int2 +); +NOTICE: DDL test: type simple, tag CREATE TYPE diff --git a/src/test/modules/test_ddl_deparse/expected/create_view.out b/src/test/modules/test_ddl_deparse/expected/create_view.out new file mode 100644 index 0000000..2ae4e2d --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/create_view.out @@ -0,0 +1,19 @@ +-- +-- CREATE_VIEW +-- +CREATE VIEW static_view AS + SELECT 'foo'::TEXT AS col; +NOTICE: DDL test: type simple, tag CREATE VIEW +CREATE OR REPLACE VIEW static_view AS + SELECT 'bar'::TEXT AS col; +NOTICE: DDL test: type simple, tag CREATE VIEW +NOTICE: DDL test: type alter table, tag CREATE VIEW +NOTICE: subcommand: REPLACE RELOPTIONS +CREATE VIEW datatype_view AS + SELECT * FROM datatype_table; +NOTICE: DDL test: type simple, tag CREATE VIEW +CREATE RECURSIVE VIEW nums_1_100 (n) AS + VALUES (1) +UNION ALL + SELECT n+1 FROM nums_1_100 WHERE n < 100; +NOTICE: DDL test: type simple, tag CREATE VIEW diff --git a/src/test/modules/test_ddl_deparse/expected/defprivs.out b/src/test/modules/test_ddl_deparse/expected/defprivs.out new file mode 100644 index 0000000..66b2680 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/defprivs.out @@ -0,0 +1,6 @@ +-- +-- ALTER DEFAULT PRIVILEGES +-- +ALTER DEFAULT PRIVILEGES IN SCHEMA public + REVOKE ALL PRIVILEGES ON TABLES FROM public; +NOTICE: DDL test: type alter default privileges, tag ALTER DEFAULT PRIVILEGES diff --git a/src/test/modules/test_ddl_deparse/expected/matviews.out b/src/test/modules/test_ddl_deparse/expected/matviews.out new file mode 100644 index 0000000..69a5627 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/matviews.out @@ -0,0 +1,8 @@ +-- +-- Materialized views +-- +CREATE MATERIALIZED VIEW ddl_deparse_mv AS + SELECT * FROM datatype_table LIMIT 1 WITH NO DATA; +NOTICE: DDL test: type simple, tag CREATE MATERIALIZED VIEW +REFRESH MATERIALIZED VIEW ddl_deparse_mv; +NOTICE: DDL test: type simple, tag REFRESH MATERIALIZED VIEW diff --git a/src/test/modules/test_ddl_deparse/expected/opfamily.out b/src/test/modules/test_ddl_deparse/expected/opfamily.out new file mode 100644 index 0000000..c7e3a23 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/opfamily.out @@ -0,0 +1,68 @@ +-- copied from equivclass.sql +create type int8alias1; +NOTICE: DDL test: type simple, tag CREATE TYPE +create function int8alias1in(cstring) returns int8alias1 + strict immutable language internal as 'int8in'; +NOTICE: return type int8alias1 is only a shell +NOTICE: DDL test: type simple, tag CREATE FUNCTION +create function int8alias1out(int8alias1) returns cstring + strict immutable language internal as 'int8out'; +NOTICE: argument type int8alias1 is only a shell +NOTICE: DDL test: type simple, tag CREATE FUNCTION +create type int8alias1 ( + input = int8alias1in, + output = int8alias1out, + like = int8 +); +NOTICE: DDL test: type simple, tag CREATE TYPE +create type int8alias2; +NOTICE: DDL test: type simple, tag CREATE TYPE +create function int8alias2in(cstring) returns int8alias2 + strict immutable language internal as 'int8in'; +NOTICE: return type int8alias2 is only a shell +NOTICE: DDL test: type simple, tag CREATE FUNCTION +create function int8alias2out(int8alias2) returns cstring + strict immutable language internal as 'int8out'; +NOTICE: argument type int8alias2 is only a shell +NOTICE: DDL test: type simple, tag CREATE FUNCTION +create type int8alias2 ( + input = int8alias2in, + output = int8alias2out, + like = int8 +); +NOTICE: DDL test: type simple, tag CREATE TYPE +create cast (int8 as int8alias1) without function; +NOTICE: DDL test: type simple, tag CREATE CAST +create cast (int8 as int8alias2) without function; +NOTICE: DDL test: type simple, tag CREATE CAST +create cast (int8alias1 as int8) without function; +NOTICE: DDL test: type simple, tag CREATE CAST +create cast (int8alias2 as int8) without function; +NOTICE: DDL test: type simple, tag CREATE CAST +create function int8alias1eq(int8alias1, int8alias1) returns bool + strict immutable language internal as 'int8eq'; +NOTICE: DDL test: type simple, tag CREATE FUNCTION +create operator = ( + procedure = int8alias1eq, + leftarg = int8alias1, rightarg = int8alias1, + commutator = =, + restrict = eqsel, join = eqjoinsel, + merges +); +NOTICE: DDL test: type simple, tag CREATE OPERATOR +alter operator family integer_ops using btree add + operator 3 = (int8alias1, int8alias1); +NOTICE: DDL test: type alter operator family, tag ALTER OPERATOR FAMILY +-- copied from alter_table.sql +create type ctype as (f1 int, f2 text); +NOTICE: DDL test: type simple, tag CREATE TYPE +create function same(ctype, ctype) returns boolean language sql +as 'select $1.f1 is not distinct from $2.f1 and $1.f2 is not distinct from $2.f2'; +NOTICE: DDL test: type simple, tag CREATE FUNCTION +create operator =(procedure = same, leftarg = ctype, rightarg = ctype); +NOTICE: DDL test: type simple, tag CREATE OPERATOR +create operator class ctype_hash_ops + default for type ctype using hash as + operator 1 =(ctype, ctype); +NOTICE: DDL test: type simple, tag CREATE OPERATOR FAMILY +NOTICE: DDL test: type create operator class, tag CREATE OPERATOR CLASS diff --git a/src/test/modules/test_ddl_deparse/expected/test_ddl_deparse.out b/src/test/modules/test_ddl_deparse/expected/test_ddl_deparse.out new file mode 100644 index 0000000..4a5ea9e --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/test_ddl_deparse.out @@ -0,0 +1,40 @@ +CREATE EXTENSION test_ddl_deparse; +CREATE OR REPLACE FUNCTION test_ddl_deparse() + RETURNS event_trigger LANGUAGE plpgsql AS +$$ +DECLARE + r record; + r2 record; + cmdtype text; + objtype text; + tag text; +BEGIN + FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() + LOOP + -- verify that tags match + tag = public.get_command_tag(r.command); + IF tag <> r.command_tag THEN + RAISE NOTICE 'tag % doesn''t match %', tag, r.command_tag; + END IF; + + -- log the operation + cmdtype = public.get_command_type(r.command); + IF cmdtype <> 'grant' THEN + RAISE NOTICE 'DDL test: type %, tag %', cmdtype, tag; + ELSE + RAISE NOTICE 'DDL test: type %, object type %', cmdtype, r.object_type; + END IF; + + -- if alter table, log more + IF cmdtype = 'alter table' THEN + FOR r2 IN SELECT * + FROM unnest(public.get_altertable_subcmdtypes(r.command)) + LOOP + RAISE NOTICE ' subcommand: %', r2.unnest; + END LOOP; + END IF; + END LOOP; +END; +$$; +CREATE EVENT TRIGGER test_ddl_deparse +ON ddl_command_end EXECUTE PROCEDURE test_ddl_deparse(); diff --git a/src/test/modules/test_ddl_deparse/sql/alter_function.sql b/src/test/modules/test_ddl_deparse/sql/alter_function.sql new file mode 100644 index 0000000..45c8d1e --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/alter_function.sql @@ -0,0 +1,17 @@ +-- +-- ALTER_FUNCTION +-- + +ALTER FUNCTION plpgsql_function_trigger_1 () + SET SCHEMA foo; + +ALTER FUNCTION foo.plpgsql_function_trigger_1() + COST 10; + +CREATE ROLE regress_alter_function_role; + +ALTER FUNCTION plpgsql_function_trigger_2() + OWNER TO regress_alter_function_role; + +DROP OWNED BY regress_alter_function_role; +DROP ROLE regress_alter_function_role; diff --git a/src/test/modules/test_ddl_deparse/sql/alter_sequence.sql b/src/test/modules/test_ddl_deparse/sql/alter_sequence.sql new file mode 100644 index 0000000..9b2799f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/alter_sequence.sql @@ -0,0 +1,15 @@ +-- +-- ALTER_SEQUENCE +-- + +ALTER SEQUENCE fkey_table_seq + MINVALUE 10 + START 20 + CACHE 1 + NO CYCLE; + +ALTER SEQUENCE fkey_table_seq + RENAME TO fkey_table_seq_renamed; + +ALTER SEQUENCE fkey_table_seq_renamed + SET SCHEMA foo; diff --git a/src/test/modules/test_ddl_deparse/sql/alter_table.sql b/src/test/modules/test_ddl_deparse/sql/alter_table.sql new file mode 100644 index 0000000..dec53a0 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/alter_table.sql @@ -0,0 +1,21 @@ +CREATE TABLE parent ( + a int +); + +CREATE TABLE child () INHERITS (parent); + +CREATE TABLE grandchild () INHERITS (child); + +ALTER TABLE parent ADD COLUMN b serial; + +ALTER TABLE parent RENAME COLUMN b TO c; + +ALTER TABLE parent ADD CONSTRAINT a_pos CHECK (a > 0); + +CREATE TABLE part ( + a int +) PARTITION BY RANGE (a); + +CREATE TABLE part1 PARTITION OF part FOR VALUES FROM (1) to (100); + +ALTER TABLE part ADD PRIMARY KEY (a); diff --git a/src/test/modules/test_ddl_deparse/sql/alter_ts_config.sql b/src/test/modules/test_ddl_deparse/sql/alter_ts_config.sql new file mode 100644 index 0000000..ac13e21 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/alter_ts_config.sql @@ -0,0 +1,8 @@ +-- +-- ALTER TEXT SEARCH CONFIGURATION +-- + +CREATE TEXT SEARCH CONFIGURATION en (copy=english); + +ALTER TEXT SEARCH CONFIGURATION en + ALTER MAPPING FOR host, email, url, sfloat WITH simple; diff --git a/src/test/modules/test_ddl_deparse/sql/alter_type_enum.sql b/src/test/modules/test_ddl_deparse/sql/alter_type_enum.sql new file mode 100644 index 0000000..8999b38 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/alter_type_enum.sql @@ -0,0 +1,6 @@ +--- +--- ALTER_TYPE_ENUM +--- + +ALTER TYPE enum_test ADD VALUE 'zzz' AFTER 'baz'; +ALTER TYPE enum_test ADD VALUE 'aaa' BEFORE 'foo'; diff --git a/src/test/modules/test_ddl_deparse/sql/comment_on.sql b/src/test/modules/test_ddl_deparse/sql/comment_on.sql new file mode 100644 index 0000000..fc29a73 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/comment_on.sql @@ -0,0 +1,14 @@ +-- +-- COMMENT_ON +-- + +COMMENT ON SCHEMA foo IS 'This is schema foo'; +COMMENT ON TYPE enum_test IS 'ENUM test'; +COMMENT ON TYPE int2range IS 'RANGE test'; +COMMENT ON DOMAIN japanese_postal_code IS 'DOMAIN test'; +COMMENT ON SEQUENCE fkey_table_seq IS 'SEQUENCE test'; +COMMENT ON TABLE datatype_table IS 'This table should contain all native datatypes'; +COMMENT ON VIEW datatype_view IS 'This is a view'; +COMMENT ON FUNCTION c_function_test() IS 'FUNCTION test'; +COMMENT ON TRIGGER trigger_1 ON datatype_table IS 'TRIGGER test'; +COMMENT ON RULE rule_1 ON datatype_table IS 'RULE test'; diff --git a/src/test/modules/test_ddl_deparse/sql/create_conversion.sql b/src/test/modules/test_ddl_deparse/sql/create_conversion.sql new file mode 100644 index 0000000..813c66d --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_conversion.sql @@ -0,0 +1,6 @@ +--- +--- CREATE_CONVERSION +--- + +-- Simple test should suffice for this +CREATE CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8; diff --git a/src/test/modules/test_ddl_deparse/sql/create_domain.sql b/src/test/modules/test_ddl_deparse/sql/create_domain.sql new file mode 100644 index 0000000..6ab5525 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_domain.sql @@ -0,0 +1,10 @@ +--- +--- CREATE_DOMAIN +--- +CREATE DOMAIN domainvarchar VARCHAR(5); + +CREATE DOMAIN japanese_postal_code AS TEXT +CHECK( + VALUE ~ '^\d{3}$' +OR VALUE ~ '^\d{3}-\d{4}$' +); diff --git a/src/test/modules/test_ddl_deparse/sql/create_extension.sql b/src/test/modules/test_ddl_deparse/sql/create_extension.sql new file mode 100644 index 0000000..d23e7fd --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_extension.sql @@ -0,0 +1,5 @@ +--- +--- CREATE_EXTENSION +--- + +CREATE EXTENSION pg_stat_statements; diff --git a/src/test/modules/test_ddl_deparse/sql/create_rule.sql b/src/test/modules/test_ddl_deparse/sql/create_rule.sql new file mode 100644 index 0000000..60ac151 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_rule.sql @@ -0,0 +1,31 @@ +--- +--- CREATE_RULE +--- + + +CREATE RULE rule_1 AS + ON INSERT + TO datatype_table + DO NOTHING; + +CREATE RULE rule_2 AS + ON UPDATE + TO datatype_table + DO INSERT INTO unlogged_table (id) VALUES(NEW.id); + +CREATE RULE rule_3 AS + ON DELETE + TO datatype_table + DO ALSO NOTHING; + +CREATE RULE "_RETURN" AS + ON SELECT + TO like_datatype_table + DO INSTEAD + SELECT * FROM datatype_view; + +CREATE RULE rule_3 AS + ON DELETE + TO like_datatype_table + WHERE id < 100 + DO ALSO NOTHING; diff --git a/src/test/modules/test_ddl_deparse/sql/create_schema.sql b/src/test/modules/test_ddl_deparse/sql/create_schema.sql new file mode 100644 index 0000000..f314dc2 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_schema.sql @@ -0,0 +1,17 @@ +-- +-- CREATE_SCHEMA +-- + +CREATE SCHEMA foo; + +CREATE SCHEMA IF NOT EXISTS bar; + +CREATE SCHEMA baz; + +-- Will not be created, and will not be handled by the +-- event trigger +CREATE SCHEMA IF NOT EXISTS baz; + +CREATE SCHEMA element_test + CREATE TABLE foo (id int) + CREATE VIEW bar AS SELECT * FROM foo; diff --git a/src/test/modules/test_ddl_deparse/sql/create_sequence_1.sql b/src/test/modules/test_ddl_deparse/sql/create_sequence_1.sql new file mode 100644 index 0000000..9e6743f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_sequence_1.sql @@ -0,0 +1,11 @@ +-- +-- CREATE_SEQUENCE +-- + +CREATE SEQUENCE fkey_table_seq + INCREMENT BY 1 + MINVALUE 0 + MAXVALUE 1000000 + START 10 + CACHE 10 + CYCLE; diff --git a/src/test/modules/test_ddl_deparse/sql/create_table.sql b/src/test/modules/test_ddl_deparse/sql/create_table.sql new file mode 100644 index 0000000..39cdb9d --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_table.sql @@ -0,0 +1,142 @@ +-- +-- CREATE_TABLE +-- + +-- Datatypes +CREATE TABLE datatype_table ( + id SERIAL, + id_big BIGSERIAL, + is_small SMALLSERIAL, + v_bytea BYTEA, + v_smallint SMALLINT, + v_int INT, + v_bigint BIGINT, + v_char CHAR(1), + v_varchar VARCHAR(10), + v_text TEXT, + v_bool BOOLEAN, + v_inet INET, + v_cidr CIDR, + v_macaddr MACADDR, + v_numeric NUMERIC(1,0), + v_real REAL, + v_float FLOAT(1), + v_float8 FLOAT8, + v_money MONEY, + v_tsquery TSQUERY, + v_tsvector TSVECTOR, + v_date DATE, + v_time TIME, + v_time_tz TIME WITH TIME ZONE, + v_timestamp TIMESTAMP, + v_timestamp_tz TIMESTAMP WITH TIME ZONE, + v_interval INTERVAL, + v_bit BIT, + v_bit4 BIT(4), + v_varbit VARBIT, + v_varbit4 VARBIT(4), + v_box BOX, + v_circle CIRCLE, + v_lseg LSEG, + v_path PATH, + v_point POINT, + v_polygon POLYGON, + v_json JSON, + v_xml XML, + v_uuid UUID, + v_pg_snapshot pg_snapshot, + v_enum ENUM_TEST, + v_postal_code japanese_postal_code, + v_int2range int2range, + PRIMARY KEY (id), + UNIQUE (id_big) +); + +-- Constraint definitions + +CREATE TABLE IF NOT EXISTS fkey_table ( + id INT NOT NULL DEFAULT nextval('fkey_table_seq'::REGCLASS), + datatype_id INT NOT NULL REFERENCES datatype_table(id), + big_id BIGINT NOT NULL, + sometext TEXT COLLATE "POSIX", + check_col_1 INT NOT NULL CHECK(check_col_1 < 10), + check_col_2 INT NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fkey_big_id + FOREIGN KEY (big_id) + REFERENCES datatype_table(id_big), + EXCLUDE USING btree (check_col_2 WITH =) +); + +-- Typed table + +CREATE TABLE employees OF employee_type ( + PRIMARY KEY (name), + salary WITH OPTIONS DEFAULT 1000 +); + +-- Inheritance +CREATE TABLE person ( + id INT NOT NULL PRIMARY KEY, + name text, + age int4, + location point +); + +CREATE TABLE emp ( + salary int4, + manager name +) INHERITS (person); + + +CREATE TABLE student ( + gpa float8 +) INHERITS (person); + +CREATE TABLE stud_emp ( + percent int4 +) INHERITS (emp, student); + + +-- Storage parameters + +CREATE TABLE storage ( + id INT +) WITH ( + fillfactor = 10, + autovacuum_enabled = FALSE +); + +-- LIKE + +CREATE TABLE like_datatype_table ( + LIKE datatype_table + EXCLUDING ALL +); + +CREATE TABLE like_fkey_table ( + LIKE fkey_table + INCLUDING DEFAULTS + INCLUDING INDEXES + INCLUDING STORAGE +); + + +-- Volatile table types +CREATE UNLOGGED TABLE unlogged_table ( + id INT PRIMARY KEY +); + +CREATE TEMP TABLE temp_table ( + id INT PRIMARY KEY +); + +CREATE TEMP TABLE temp_table_commit_delete ( + id INT PRIMARY KEY +) +ON COMMIT DELETE ROWS; + +CREATE TEMP TABLE temp_table_commit_drop ( + id INT PRIMARY KEY +) +ON COMMIT DROP; diff --git a/src/test/modules/test_ddl_deparse/sql/create_transform.sql b/src/test/modules/test_ddl_deparse/sql/create_transform.sql new file mode 100644 index 0000000..970d89e --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_transform.sql @@ -0,0 +1,16 @@ +-- +-- CREATE_TRANSFORM +-- + +-- Create a dummy transform +-- The function FROM SQL should have internal as single argument as well +-- as return type. The function TO SQL should have as single argument +-- internal and as return argument the datatype of the transform done. +-- We choose some random built-in functions that have the right signature. +-- This won't actually be used, because the SQL function language +-- doesn't implement transforms (there would be no point). +CREATE TRANSFORM FOR int LANGUAGE SQL ( + FROM SQL WITH FUNCTION prsd_lextype(internal), + TO SQL WITH FUNCTION int4recv(internal)); + +DROP TRANSFORM FOR int LANGUAGE SQL; diff --git a/src/test/modules/test_ddl_deparse/sql/create_trigger.sql b/src/test/modules/test_ddl_deparse/sql/create_trigger.sql new file mode 100644 index 0000000..fc0aef7 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_trigger.sql @@ -0,0 +1,18 @@ +--- +--- CREATE_TRIGGER +--- + +CREATE FUNCTION plpgsql_function_trigger_1() + RETURNS TRIGGER + LANGUAGE plpgsql +AS $$ +BEGIN + RETURN NEW; +END; +$$; + +CREATE TRIGGER trigger_1 + BEFORE INSERT OR UPDATE + ON datatype_table + FOR EACH ROW + EXECUTE PROCEDURE plpgsql_function_trigger_1(); diff --git a/src/test/modules/test_ddl_deparse/sql/create_type.sql b/src/test/modules/test_ddl_deparse/sql/create_type.sql new file mode 100644 index 0000000..a387cfd --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_type.sql @@ -0,0 +1,21 @@ +--- +--- CREATE_TYPE +--- + +CREATE FUNCTION text_w_default_in(cstring) + RETURNS text_w_default + AS 'textin' + LANGUAGE internal STABLE STRICT; + +CREATE FUNCTION text_w_default_out(text_w_default) + RETURNS cstring + AS 'textout' + LANGUAGE internal STABLE STRICT ; + +CREATE TYPE employee_type AS (name TEXT, salary NUMERIC); + +CREATE TYPE enum_test AS ENUM ('foo', 'bar', 'baz'); + +CREATE TYPE int2range AS RANGE ( + SUBTYPE = int2 +); diff --git a/src/test/modules/test_ddl_deparse/sql/create_view.sql b/src/test/modules/test_ddl_deparse/sql/create_view.sql new file mode 100644 index 0000000..030b76f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/create_view.sql @@ -0,0 +1,17 @@ +-- +-- CREATE_VIEW +-- + +CREATE VIEW static_view AS + SELECT 'foo'::TEXT AS col; + +CREATE OR REPLACE VIEW static_view AS + SELECT 'bar'::TEXT AS col; + +CREATE VIEW datatype_view AS + SELECT * FROM datatype_table; + +CREATE RECURSIVE VIEW nums_1_100 (n) AS + VALUES (1) +UNION ALL + SELECT n+1 FROM nums_1_100 WHERE n < 100; diff --git a/src/test/modules/test_ddl_deparse/sql/defprivs.sql b/src/test/modules/test_ddl_deparse/sql/defprivs.sql new file mode 100644 index 0000000..a0fb4c2 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/defprivs.sql @@ -0,0 +1,6 @@ +-- +-- ALTER DEFAULT PRIVILEGES +-- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public + REVOKE ALL PRIVILEGES ON TABLES FROM public; diff --git a/src/test/modules/test_ddl_deparse/sql/matviews.sql b/src/test/modules/test_ddl_deparse/sql/matviews.sql new file mode 100644 index 0000000..6e22c52 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/matviews.sql @@ -0,0 +1,8 @@ +-- +-- Materialized views +-- + +CREATE MATERIALIZED VIEW ddl_deparse_mv AS + SELECT * FROM datatype_table LIMIT 1 WITH NO DATA; + +REFRESH MATERIALIZED VIEW ddl_deparse_mv; diff --git a/src/test/modules/test_ddl_deparse/sql/opfamily.sql b/src/test/modules/test_ddl_deparse/sql/opfamily.sql new file mode 100644 index 0000000..b2bacbb --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/opfamily.sql @@ -0,0 +1,52 @@ +-- copied from equivclass.sql +create type int8alias1; +create function int8alias1in(cstring) returns int8alias1 + strict immutable language internal as 'int8in'; +create function int8alias1out(int8alias1) returns cstring + strict immutable language internal as 'int8out'; +create type int8alias1 ( + input = int8alias1in, + output = int8alias1out, + like = int8 +); + +create type int8alias2; +create function int8alias2in(cstring) returns int8alias2 + strict immutable language internal as 'int8in'; +create function int8alias2out(int8alias2) returns cstring + strict immutable language internal as 'int8out'; +create type int8alias2 ( + input = int8alias2in, + output = int8alias2out, + like = int8 +); + +create cast (int8 as int8alias1) without function; +create cast (int8 as int8alias2) without function; +create cast (int8alias1 as int8) without function; +create cast (int8alias2 as int8) without function; + +create function int8alias1eq(int8alias1, int8alias1) returns bool + strict immutable language internal as 'int8eq'; +create operator = ( + procedure = int8alias1eq, + leftarg = int8alias1, rightarg = int8alias1, + commutator = =, + restrict = eqsel, join = eqjoinsel, + merges +); +alter operator family integer_ops using btree add + operator 3 = (int8alias1, int8alias1); + + +-- copied from alter_table.sql +create type ctype as (f1 int, f2 text); + +create function same(ctype, ctype) returns boolean language sql +as 'select $1.f1 is not distinct from $2.f1 and $1.f2 is not distinct from $2.f2'; + +create operator =(procedure = same, leftarg = ctype, rightarg = ctype); + +create operator class ctype_hash_ops + default for type ctype using hash as + operator 1 =(ctype, ctype); diff --git a/src/test/modules/test_ddl_deparse/sql/test_ddl_deparse.sql b/src/test/modules/test_ddl_deparse/sql/test_ddl_deparse.sql new file mode 100644 index 0000000..e257a21 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/test_ddl_deparse.sql @@ -0,0 +1,42 @@ +CREATE EXTENSION test_ddl_deparse; + +CREATE OR REPLACE FUNCTION test_ddl_deparse() + RETURNS event_trigger LANGUAGE plpgsql AS +$$ +DECLARE + r record; + r2 record; + cmdtype text; + objtype text; + tag text; +BEGIN + FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() + LOOP + -- verify that tags match + tag = public.get_command_tag(r.command); + IF tag <> r.command_tag THEN + RAISE NOTICE 'tag % doesn''t match %', tag, r.command_tag; + END IF; + + -- log the operation + cmdtype = public.get_command_type(r.command); + IF cmdtype <> 'grant' THEN + RAISE NOTICE 'DDL test: type %, tag %', cmdtype, tag; + ELSE + RAISE NOTICE 'DDL test: type %, object type %', cmdtype, r.object_type; + END IF; + + -- if alter table, log more + IF cmdtype = 'alter table' THEN + FOR r2 IN SELECT * + FROM unnest(public.get_altertable_subcmdtypes(r.command)) + LOOP + RAISE NOTICE ' subcommand: %', r2.unnest; + END LOOP; + END IF; + END LOOP; +END; +$$; + +CREATE EVENT TRIGGER test_ddl_deparse +ON ddl_command_end EXECUTE PROCEDURE test_ddl_deparse(); diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql b/src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql new file mode 100644 index 0000000..093005a --- /dev/null +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql @@ -0,0 +1,16 @@ +/* src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ddl_deparse" to load this file. \quit + +CREATE FUNCTION get_command_type(pg_ddl_command) + RETURNS text IMMUTABLE STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION get_command_tag(pg_ddl_command) + RETURNS text IMMUTABLE STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION get_altertable_subcmdtypes(pg_ddl_command) + RETURNS text[] IMMUTABLE STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c new file mode 100644 index 0000000..9476c3f --- /dev/null +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c @@ -0,0 +1,296 @@ +/*---------------------------------------------------------------------- + * test_ddl_deparse.c + * Support functions for the test_ddl_deparse module + * + * Copyright (c) 2014-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_ddl_deparse/test_ddl_deparse.c + *---------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_type.h" +#include "tcop/deparse_utility.h" +#include "tcop/utility.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(get_command_type); +PG_FUNCTION_INFO_V1(get_command_tag); +PG_FUNCTION_INFO_V1(get_altertable_subcmdtypes); + +/* + * Return the textual representation of the struct type used to represent a + * command in struct CollectedCommand format. + */ +Datum +get_command_type(PG_FUNCTION_ARGS) +{ + CollectedCommand *cmd = (CollectedCommand *) PG_GETARG_POINTER(0); + const char *type; + + switch (cmd->type) + { + case SCT_Simple: + type = "simple"; + break; + case SCT_AlterTable: + type = "alter table"; + break; + case SCT_Grant: + type = "grant"; + break; + case SCT_AlterOpFamily: + type = "alter operator family"; + break; + case SCT_AlterDefaultPrivileges: + type = "alter default privileges"; + break; + case SCT_CreateOpClass: + type = "create operator class"; + break; + case SCT_AlterTSConfig: + type = "alter text search configuration"; + break; + default: + type = "unknown command type"; + break; + } + + PG_RETURN_TEXT_P(cstring_to_text(type)); +} + +/* + * Return the command tag corresponding to a parse node contained in a + * CollectedCommand struct. + */ +Datum +get_command_tag(PG_FUNCTION_ARGS) +{ + CollectedCommand *cmd = (CollectedCommand *) PG_GETARG_POINTER(0); + + if (!cmd->parsetree) + PG_RETURN_NULL(); + + PG_RETURN_TEXT_P(cstring_to_text(CreateCommandName(cmd->parsetree))); +} + +/* + * Return a text array representation of the subcommands of an ALTER TABLE + * command. + */ +Datum +get_altertable_subcmdtypes(PG_FUNCTION_ARGS) +{ + CollectedCommand *cmd = (CollectedCommand *) PG_GETARG_POINTER(0); + ArrayBuildState *astate = NULL; + ListCell *cell; + + if (cmd->type != SCT_AlterTable) + elog(ERROR, "command is not ALTER TABLE"); + + foreach(cell, cmd->d.alterTable.subcmds) + { + CollectedATSubcmd *sub = lfirst(cell); + AlterTableCmd *subcmd = castNode(AlterTableCmd, sub->parsetree); + const char *strtype; + + switch (subcmd->subtype) + { + case AT_AddColumn: + strtype = "ADD COLUMN"; + break; + case AT_AddColumnRecurse: + strtype = "ADD COLUMN (and recurse)"; + break; + case AT_AddColumnToView: + strtype = "ADD COLUMN TO VIEW"; + break; + case AT_ColumnDefault: + strtype = "ALTER COLUMN SET DEFAULT"; + break; + case AT_CookedColumnDefault: + strtype = "ALTER COLUMN SET DEFAULT (precooked)"; + break; + case AT_DropNotNull: + strtype = "DROP NOT NULL"; + break; + case AT_SetNotNull: + strtype = "SET NOT NULL"; + break; + case AT_CheckNotNull: + strtype = "CHECK NOT NULL"; + break; + case AT_SetStatistics: + strtype = "SET STATS"; + break; + case AT_SetOptions: + strtype = "SET OPTIONS"; + break; + case AT_ResetOptions: + strtype = "RESET OPTIONS"; + break; + case AT_SetStorage: + strtype = "SET STORAGE"; + break; + case AT_DropColumn: + strtype = "DROP COLUMN"; + break; + case AT_DropColumnRecurse: + strtype = "DROP COLUMN (and recurse)"; + break; + case AT_AddIndex: + strtype = "ADD INDEX"; + break; + case AT_ReAddIndex: + strtype = "(re) ADD INDEX"; + break; + case AT_AddConstraint: + strtype = "ADD CONSTRAINT"; + break; + case AT_AddConstraintRecurse: + strtype = "ADD CONSTRAINT (and recurse)"; + break; + case AT_ReAddConstraint: + strtype = "(re) ADD CONSTRAINT"; + break; + case AT_AlterConstraint: + strtype = "ALTER CONSTRAINT"; + break; + case AT_ValidateConstraint: + strtype = "VALIDATE CONSTRAINT"; + break; + case AT_ValidateConstraintRecurse: + strtype = "VALIDATE CONSTRAINT (and recurse)"; + break; + case AT_AddIndexConstraint: + strtype = "ADD CONSTRAINT (using index)"; + break; + case AT_DropConstraint: + strtype = "DROP CONSTRAINT"; + break; + case AT_DropConstraintRecurse: + strtype = "DROP CONSTRAINT (and recurse)"; + break; + case AT_ReAddComment: + strtype = "(re) ADD COMMENT"; + break; + case AT_AlterColumnType: + strtype = "ALTER COLUMN SET TYPE"; + break; + case AT_AlterColumnGenericOptions: + strtype = "ALTER COLUMN SET OPTIONS"; + break; + case AT_ChangeOwner: + strtype = "CHANGE OWNER"; + break; + case AT_ClusterOn: + strtype = "CLUSTER"; + break; + case AT_DropCluster: + strtype = "DROP CLUSTER"; + break; + case AT_SetLogged: + strtype = "SET LOGGED"; + break; + case AT_SetUnLogged: + strtype = "SET UNLOGGED"; + break; + case AT_DropOids: + strtype = "DROP OIDS"; + break; + case AT_SetTableSpace: + strtype = "SET TABLESPACE"; + break; + case AT_SetRelOptions: + strtype = "SET RELOPTIONS"; + break; + case AT_ResetRelOptions: + strtype = "RESET RELOPTIONS"; + break; + case AT_ReplaceRelOptions: + strtype = "REPLACE RELOPTIONS"; + break; + case AT_EnableTrig: + strtype = "ENABLE TRIGGER"; + break; + case AT_EnableAlwaysTrig: + strtype = "ENABLE TRIGGER (always)"; + break; + case AT_EnableReplicaTrig: + strtype = "ENABLE TRIGGER (replica)"; + break; + case AT_DisableTrig: + strtype = "DISABLE TRIGGER"; + break; + case AT_EnableTrigAll: + strtype = "ENABLE TRIGGER (all)"; + break; + case AT_DisableTrigAll: + strtype = "DISABLE TRIGGER (all)"; + break; + case AT_EnableTrigUser: + strtype = "ENABLE TRIGGER (user)"; + break; + case AT_DisableTrigUser: + strtype = "DISABLE TRIGGER (user)"; + break; + case AT_EnableRule: + strtype = "ENABLE RULE"; + break; + case AT_EnableAlwaysRule: + strtype = "ENABLE RULE (always)"; + break; + case AT_EnableReplicaRule: + strtype = "ENABLE RULE (replica)"; + break; + case AT_DisableRule: + strtype = "DISABLE RULE"; + break; + case AT_AddInherit: + strtype = "ADD INHERIT"; + break; + case AT_DropInherit: + strtype = "DROP INHERIT"; + break; + case AT_AddOf: + strtype = "OF"; + break; + case AT_DropOf: + strtype = "NOT OF"; + break; + case AT_ReplicaIdentity: + strtype = "REPLICA IDENTITY"; + break; + case AT_EnableRowSecurity: + strtype = "ENABLE ROW SECURITY"; + break; + case AT_DisableRowSecurity: + strtype = "DISABLE ROW SECURITY"; + break; + case AT_ForceRowSecurity: + strtype = "FORCE ROW SECURITY"; + break; + case AT_NoForceRowSecurity: + strtype = "NO FORCE ROW SECURITY"; + break; + case AT_GenericOptions: + strtype = "SET OPTIONS"; + break; + default: + strtype = "unrecognized"; + break; + } + + astate = + accumArrayResult(astate, CStringGetTextDatum(strtype), + false, TEXTOID, CurrentMemoryContext); + } + + if (astate == NULL) + elog(ERROR, "empty alter table subcommand list"); + + PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate, CurrentMemoryContext)); +} diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.control b/src/test/modules/test_ddl_deparse/test_ddl_deparse.control new file mode 100644 index 0000000..09112ee --- /dev/null +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.control @@ -0,0 +1,4 @@ +comment = 'Test code for DDL deparse feature' +default_version = '1.0' +module_pathname = '$libdir/test_ddl_deparse' +relocatable = true diff --git a/src/test/modules/test_extensions/.gitignore b/src/test/modules/test_extensions/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_extensions/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile new file mode 100644 index 0000000..6796c6b --- /dev/null +++ b/src/test/modules/test_extensions/Makefile @@ -0,0 +1,34 @@ +# src/test/modules/test_extensions/Makefile + +MODULE = test_extensions +PGFILEDESC = "test_extensions - regression testing for EXTENSION support" + +EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ + test_ext7 test_ext8 test_ext_cine test_ext_cor \ + test_ext_cyclic1 test_ext_cyclic2 \ + test_ext_extschema \ + test_ext_evttrig +DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ + test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \ + test_ext7--1.0.sql test_ext7--1.0--2.0.sql test_ext8--1.0.sql \ + test_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ + test_ext_cor--1.0.sql \ + test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ + test_ext_extschema--1.0.sql \ + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + +REGRESS = test_extensions test_extdepend + +# force C locale for output stability +NO_LOCALE = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_extensions +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_extensions/expected/test_extdepend.out b/src/test/modules/test_extensions/expected/test_extdepend.out new file mode 100644 index 0000000..0b62015 --- /dev/null +++ b/src/test/modules/test_extensions/expected/test_extdepend.out @@ -0,0 +1,188 @@ +-- +-- test ALTER THING name DEPENDS ON EXTENSION +-- +-- Common setup for all tests +CREATE TABLE test_extdep_commands (command text); +COPY test_extdep_commands FROM stdin; +SELECT * FROM test_extdep_commands; + command +------------------------------------------------------------------------- + CREATE SCHEMA test_ext + CREATE EXTENSION test_ext5 SCHEMA test_ext + SET search_path TO test_ext + CREATE TABLE a (a1 int) + + CREATE FUNCTION b() RETURNS TRIGGER LANGUAGE plpgsql AS + + $$ BEGIN NEW.a1 := NEW.a1 + 42; RETURN NEW; END; $$ + ALTER FUNCTION b() DEPENDS ON EXTENSION test_ext5 + + CREATE TRIGGER c BEFORE INSERT ON a FOR EACH ROW EXECUTE PROCEDURE b() + ALTER TRIGGER c ON a DEPENDS ON EXTENSION test_ext5 + + CREATE MATERIALIZED VIEW d AS SELECT * FROM a + ALTER MATERIALIZED VIEW d DEPENDS ON EXTENSION test_ext5 + + CREATE INDEX e ON a (a1) + ALTER INDEX e DEPENDS ON EXTENSION test_ext5 + RESET search_path +(17 rows) + +-- First, test that dependent objects go away when the extension is dropped. +SELECT * FROM test_extdep_commands \gexec + CREATE SCHEMA test_ext + CREATE EXTENSION test_ext5 SCHEMA test_ext + SET search_path TO test_ext + CREATE TABLE a (a1 int) + + CREATE FUNCTION b() RETURNS TRIGGER LANGUAGE plpgsql AS + $$ BEGIN NEW.a1 := NEW.a1 + 42; RETURN NEW; END; $$ + ALTER FUNCTION b() DEPENDS ON EXTENSION test_ext5 + + CREATE TRIGGER c BEFORE INSERT ON a FOR EACH ROW EXECUTE PROCEDURE b() + ALTER TRIGGER c ON a DEPENDS ON EXTENSION test_ext5 + + CREATE MATERIALIZED VIEW d AS SELECT * FROM a + ALTER MATERIALIZED VIEW d DEPENDS ON EXTENSION test_ext5 + + CREATE INDEX e ON a (a1) + ALTER INDEX e DEPENDS ON EXTENSION test_ext5 + RESET search_path +-- A dependent object made dependent again has no effect +ALTER FUNCTION test_ext.b() DEPENDS ON EXTENSION test_ext5; +-- make sure we have the right dependencies on the extension +SELECT deptype, p.* + FROM pg_depend, pg_identify_object(classid, objid, objsubid) AS p + WHERE refclassid = 'pg_extension'::regclass AND + refobjid = (SELECT oid FROM pg_extension WHERE extname = 'test_ext5') +ORDER BY type; + deptype | type | schema | name | identity +---------+-------------------+----------+------+----------------- + x | function | test_ext | | test_ext.b() + x | index | test_ext | e | test_ext.e + x | materialized view | test_ext | d | test_ext.d + x | trigger | | | c on test_ext.a +(4 rows) + +DROP EXTENSION test_ext5; +-- anything still depending on the table? +SELECT deptype, i.* + FROM pg_catalog.pg_depend, pg_identify_object(classid, objid, objsubid) i +WHERE refclassid='pg_class'::regclass AND + refobjid='test_ext.a'::regclass AND NOT deptype IN ('i', 'a'); + deptype | type | schema | name | identity +---------+------+--------+------+---------- +(0 rows) + +DROP SCHEMA test_ext CASCADE; +NOTICE: drop cascades to table test_ext.a +-- Second test: If we drop the table, the objects are dropped too and no +-- vestige remains in pg_depend. +SELECT * FROM test_extdep_commands \gexec + CREATE SCHEMA test_ext + CREATE EXTENSION test_ext5 SCHEMA test_ext + SET search_path TO test_ext + CREATE TABLE a (a1 int) + + CREATE FUNCTION b() RETURNS TRIGGER LANGUAGE plpgsql AS + $$ BEGIN NEW.a1 := NEW.a1 + 42; RETURN NEW; END; $$ + ALTER FUNCTION b() DEPENDS ON EXTENSION test_ext5 + + CREATE TRIGGER c BEFORE INSERT ON a FOR EACH ROW EXECUTE PROCEDURE b() + ALTER TRIGGER c ON a DEPENDS ON EXTENSION test_ext5 + + CREATE MATERIALIZED VIEW d AS SELECT * FROM a + ALTER MATERIALIZED VIEW d DEPENDS ON EXTENSION test_ext5 + + CREATE INDEX e ON a (a1) + ALTER INDEX e DEPENDS ON EXTENSION test_ext5 + RESET search_path +DROP TABLE test_ext.a; -- should fail, require cascade +ERROR: cannot drop table test_ext.a because other objects depend on it +DETAIL: materialized view test_ext.d depends on table test_ext.a +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE test_ext.a CASCADE; +NOTICE: drop cascades to materialized view test_ext.d +-- anything still depending on the extension? Should be only function b() +SELECT deptype, i.* + FROM pg_catalog.pg_depend, pg_identify_object(classid, objid, objsubid) i + WHERE refclassid='pg_extension'::regclass AND + refobjid=(SELECT oid FROM pg_extension WHERE extname='test_ext5'); + deptype | type | schema | name | identity +---------+----------+----------+------+-------------- + x | function | test_ext | | test_ext.b() +(1 row) + +DROP EXTENSION test_ext5; +DROP SCHEMA test_ext CASCADE; +-- Third test: we can drop the objects individually +SELECT * FROM test_extdep_commands \gexec + CREATE SCHEMA test_ext + CREATE EXTENSION test_ext5 SCHEMA test_ext + SET search_path TO test_ext + CREATE TABLE a (a1 int) + + CREATE FUNCTION b() RETURNS TRIGGER LANGUAGE plpgsql AS + $$ BEGIN NEW.a1 := NEW.a1 + 42; RETURN NEW; END; $$ + ALTER FUNCTION b() DEPENDS ON EXTENSION test_ext5 + + CREATE TRIGGER c BEFORE INSERT ON a FOR EACH ROW EXECUTE PROCEDURE b() + ALTER TRIGGER c ON a DEPENDS ON EXTENSION test_ext5 + + CREATE MATERIALIZED VIEW d AS SELECT * FROM a + ALTER MATERIALIZED VIEW d DEPENDS ON EXTENSION test_ext5 + + CREATE INDEX e ON a (a1) + ALTER INDEX e DEPENDS ON EXTENSION test_ext5 + RESET search_path +SET search_path TO test_ext; +DROP TRIGGER c ON a; +DROP FUNCTION b(); +DROP MATERIALIZED VIEW d; +DROP INDEX e; +SELECT deptype, i.* + FROM pg_catalog.pg_depend, pg_identify_object(classid, objid, objsubid) i + WHERE (refclassid='pg_extension'::regclass AND + refobjid=(SELECT oid FROM pg_extension WHERE extname='test_ext5')) + OR (refclassid='pg_class'::regclass AND refobjid='test_ext.a'::regclass) + AND NOT deptype IN ('i', 'a'); + deptype | type | schema | name | identity +---------+------+--------+------+---------- +(0 rows) + +DROP TABLE a; +RESET search_path; +DROP SCHEMA test_ext CASCADE; +NOTICE: drop cascades to extension test_ext5 +-- Fourth test: we can mark the objects as dependent, then unmark; then the +-- drop of the extension does nothing +SELECT * FROM test_extdep_commands \gexec + CREATE SCHEMA test_ext + CREATE EXTENSION test_ext5 SCHEMA test_ext + SET search_path TO test_ext + CREATE TABLE a (a1 int) + + CREATE FUNCTION b() RETURNS TRIGGER LANGUAGE plpgsql AS + $$ BEGIN NEW.a1 := NEW.a1 + 42; RETURN NEW; END; $$ + ALTER FUNCTION b() DEPENDS ON EXTENSION test_ext5 + + CREATE TRIGGER c BEFORE INSERT ON a FOR EACH ROW EXECUTE PROCEDURE b() + ALTER TRIGGER c ON a DEPENDS ON EXTENSION test_ext5 + + CREATE MATERIALIZED VIEW d AS SELECT * FROM a + ALTER MATERIALIZED VIEW d DEPENDS ON EXTENSION test_ext5 + + CREATE INDEX e ON a (a1) + ALTER INDEX e DEPENDS ON EXTENSION test_ext5 + RESET search_path +SET search_path TO test_ext; +ALTER FUNCTION b() NO DEPENDS ON EXTENSION test_ext5; +ALTER TRIGGER c ON a NO DEPENDS ON EXTENSION test_ext5; +ALTER MATERIALIZED VIEW d NO DEPENDS ON EXTENSION test_ext5; +ALTER INDEX e NO DEPENDS ON EXTENSION test_ext5; +DROP EXTENSION test_ext5; +DROP TRIGGER c ON a; +DROP FUNCTION b(); +DROP MATERIALIZED VIEW d; +DROP INDEX e; +DROP SCHEMA test_ext CASCADE; +NOTICE: drop cascades to table a diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out new file mode 100644 index 0000000..4ed9eba --- /dev/null +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -0,0 +1,322 @@ +CREATE SCHEMA has$dollar; +-- test some errors +CREATE EXTENSION test_ext1; +ERROR: required extension "test_ext2" is not installed +HINT: Use CREATE EXTENSION ... CASCADE to install required extensions too. +CREATE EXTENSION test_ext1 SCHEMA test_ext1; +ERROR: schema "test_ext1" does not exist +CREATE EXTENSION test_ext1 SCHEMA test_ext; +ERROR: schema "test_ext" does not exist +CREATE EXTENSION test_ext1 SCHEMA has$dollar; +ERROR: extension "test_ext1" must be installed in schema "test_ext1" +-- finally success +CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE; +NOTICE: installing required extension "test_ext2" +NOTICE: installing required extension "test_ext3" +NOTICE: installing required extension "test_ext5" +NOTICE: installing required extension "test_ext4" +SELECT extname, nspname, extversion, extrelocatable FROM pg_extension e, pg_namespace n WHERE extname LIKE 'test_ext%' AND e.extnamespace = n.oid ORDER BY 1; + extname | nspname | extversion | extrelocatable +-----------+------------+------------+---------------- + test_ext1 | test_ext1 | 1.0 | f + test_ext2 | has$dollar | 1.0 | t + test_ext3 | has$dollar | 1.0 | t + test_ext4 | has$dollar | 1.0 | t + test_ext5 | has$dollar | 1.0 | t +(5 rows) + +CREATE EXTENSION test_ext_cyclic1 CASCADE; +NOTICE: installing required extension "test_ext_cyclic2" +ERROR: cyclic dependency detected between extensions "test_ext_cyclic1" and "test_ext_cyclic2" +DROP SCHEMA has$dollar CASCADE; +NOTICE: drop cascades to 5 other objects +DETAIL: drop cascades to extension test_ext3 +drop cascades to extension test_ext5 +drop cascades to extension test_ext2 +drop cascades to extension test_ext4 +drop cascades to extension test_ext1 +CREATE SCHEMA has$dollar; +CREATE EXTENSION test_ext6; +DROP EXTENSION test_ext6; +CREATE EXTENSION test_ext6; +-- test dropping of member tables that own extensions: +-- this table will be absorbed into test_ext7 +create table old_table1 (col1 serial primary key); +create extension test_ext7; +\dx+ test_ext7 +Objects in extension "test_ext7" + Object description +------------------------------- + sequence ext7_table1_col1_seq + sequence ext7_table2_col2_seq + sequence old_table1_col1_seq + table ext7_table1 + table ext7_table2 + table old_table1 +(6 rows) + +alter extension test_ext7 update to '2.0'; +\dx+ test_ext7 +Objects in extension "test_ext7" + Object description +------------------------------- + sequence ext7_table2_col2_seq + table ext7_table2 +(2 rows) + +-- test handling of temp objects created by extensions +create extension test_ext8; +-- \dx+ would expose a variable pg_temp_nn schema name, so we can't use it here +select regexp_replace(pg_describe_object(classid, objid, objsubid), + 'pg_temp_\d+', 'pg_temp', 'g') as "Object description" +from pg_depend +where refclassid = 'pg_extension'::regclass and deptype = 'e' and + refobjid = (select oid from pg_extension where extname = 'test_ext8') +order by 1; + Object description +----------------------------------------- + function ext8_even(posint) + function pg_temp.ext8_temp_even(posint) + table ext8_table1 + table ext8_temp_table1 + type posint +(5 rows) + +-- Should be possible to drop and recreate this extension +drop extension test_ext8; +create extension test_ext8; +select regexp_replace(pg_describe_object(classid, objid, objsubid), + 'pg_temp_\d+', 'pg_temp', 'g') as "Object description" +from pg_depend +where refclassid = 'pg_extension'::regclass and deptype = 'e' and + refobjid = (select oid from pg_extension where extname = 'test_ext8') +order by 1; + Object description +----------------------------------------- + function ext8_even(posint) + function pg_temp.ext8_temp_even(posint) + table ext8_table1 + table ext8_temp_table1 + type posint +(5 rows) + +-- here we want to start a new session and wait till old one is gone +select pg_backend_pid() as oldpid \gset +\c - +do 'declare c int = 0; +begin + while (select count(*) from pg_stat_activity where pid = ' + :'oldpid' + ') > 0 loop c := c + 1; perform pg_stat_clear_snapshot(); end loop; + raise log ''test_extensions looped % times'', c; +end'; +-- extension should now contain no temp objects +\dx+ test_ext8 +Objects in extension "test_ext8" + Object description +---------------------------- + function ext8_even(posint) + table ext8_table1 + type posint +(3 rows) + +-- dropping it should still work +drop extension test_ext8; +-- Test creation of extension in temporary schema with two-phase commit, +-- which should not work. This function wrapper is useful for portability. +-- Avoid noise caused by CONTEXT and NOTICE messages including the temporary +-- schema name. +\set SHOW_CONTEXT never +SET client_min_messages TO 'warning'; +-- First enforce presence of temporary schema. +CREATE TEMP TABLE test_ext4_tab (); +CREATE OR REPLACE FUNCTION create_extension_with_temp_schema() + RETURNS VOID AS $$ + DECLARE + tmpschema text; + query text; + BEGIN + SELECT INTO tmpschema pg_my_temp_schema()::regnamespace; + query := 'CREATE EXTENSION test_ext4 SCHEMA ' || tmpschema || ' CASCADE;'; + RAISE NOTICE 'query %', query; + EXECUTE query; + END; $$ LANGUAGE plpgsql; +BEGIN; +SELECT create_extension_with_temp_schema(); + create_extension_with_temp_schema +----------------------------------- + +(1 row) + +PREPARE TRANSACTION 'twophase_extension'; +ERROR: cannot PREPARE a transaction that has operated on temporary objects +-- Clean up +DROP TABLE test_ext4_tab; +DROP FUNCTION create_extension_with_temp_schema(); +RESET client_min_messages; +\unset SHOW_CONTEXT +-- Test case of an event trigger run in an extension upgrade script. +-- See: https://postgr.es/m/20200902193715.6e0269d4@firost +CREATE EXTENSION test_ext_evttrig; +ALTER EXTENSION test_ext_evttrig UPDATE TO '2.0'; +DROP EXTENSION test_ext_evttrig; +-- It's generally bad style to use CREATE OR REPLACE unnecessarily. +-- Test what happens if an extension does it anyway. +-- Replacing a shell type or operator is sort of like CREATE OR REPLACE; +-- check that too. +CREATE FUNCTION ext_cor_func() RETURNS text + AS $$ SELECT 'ext_cor_func: original'::text $$ LANGUAGE sql; +CREATE EXTENSION test_ext_cor; -- fail +ERROR: function ext_cor_func() is not a member of extension "test_ext_cor" +DETAIL: An extension is not allowed to replace an object that it does not own. +SELECT ext_cor_func(); + ext_cor_func +------------------------ + ext_cor_func: original +(1 row) + +DROP FUNCTION ext_cor_func(); +CREATE VIEW ext_cor_view AS + SELECT 'ext_cor_view: original'::text AS col; +CREATE EXTENSION test_ext_cor; -- fail +ERROR: view ext_cor_view is not a member of extension "test_ext_cor" +DETAIL: An extension is not allowed to replace an object that it does not own. +SELECT ext_cor_func(); +ERROR: function ext_cor_func() does not exist +LINE 1: SELECT ext_cor_func(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM ext_cor_view; + col +------------------------ + ext_cor_view: original +(1 row) + +DROP VIEW ext_cor_view; +CREATE TYPE test_ext_type; +CREATE EXTENSION test_ext_cor; -- fail +ERROR: type test_ext_type is not a member of extension "test_ext_cor" +DETAIL: An extension is not allowed to replace an object that it does not own. +DROP TYPE test_ext_type; +-- this makes a shell "point <<@@ polygon" operator too +CREATE OPERATOR @@>> ( PROCEDURE = poly_contain_pt, + LEFTARG = polygon, RIGHTARG = point, + COMMUTATOR = <<@@ ); +CREATE EXTENSION test_ext_cor; -- fail +ERROR: operator <<@@(point,polygon) is not a member of extension "test_ext_cor" +DETAIL: An extension is not allowed to replace an object that it does not own. +DROP OPERATOR <<@@ (point, polygon); +CREATE EXTENSION test_ext_cor; -- now it should work +SELECT ext_cor_func(); + ext_cor_func +------------------------------ + ext_cor_func: from extension +(1 row) + +SELECT * FROM ext_cor_view; + col +------------------------------ + ext_cor_view: from extension +(1 row) + +SELECT 'x'::test_ext_type; + test_ext_type +--------------- + x +(1 row) + +SELECT point(0,0) <<@@ polygon(circle(point(0,0),1)); + ?column? +---------- + t +(1 row) + +\dx+ test_ext_cor +Objects in extension "test_ext_cor" + Object description +------------------------------ + function ext_cor_func() + operator <<@@(point,polygon) + type test_ext_type + view ext_cor_view +(4 rows) + +-- +-- CREATE IF NOT EXISTS is an entirely unsound thing for an extension +-- to be doing, but let's at least plug the major security hole in it. +-- +CREATE COLLATION ext_cine_coll + ( LC_COLLATE = "C", LC_CTYPE = "C" ); +CREATE EXTENSION test_ext_cine; -- fail +ERROR: collation ext_cine_coll is not a member of extension "test_ext_cine" +DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +DROP COLLATION ext_cine_coll; +CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1; +CREATE EXTENSION test_ext_cine; -- fail +ERROR: materialized view ext_cine_mv is not a member of extension "test_ext_cine" +DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +DROP MATERIALIZED VIEW ext_cine_mv; +CREATE FOREIGN DATA WRAPPER dummy; +CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy; +CREATE EXTENSION test_ext_cine; -- fail +ERROR: server ext_cine_srv is not a member of extension "test_ext_cine" +DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +DROP SERVER ext_cine_srv; +CREATE SCHEMA ext_cine_schema; +CREATE EXTENSION test_ext_cine; -- fail +ERROR: schema ext_cine_schema is not a member of extension "test_ext_cine" +DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +DROP SCHEMA ext_cine_schema; +CREATE SEQUENCE ext_cine_seq; +CREATE EXTENSION test_ext_cine; -- fail +ERROR: sequence ext_cine_seq is not a member of extension "test_ext_cine" +DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +DROP SEQUENCE ext_cine_seq; +CREATE TABLE ext_cine_tab1 (x int); +CREATE EXTENSION test_ext_cine; -- fail +ERROR: table ext_cine_tab1 is not a member of extension "test_ext_cine" +DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +DROP TABLE ext_cine_tab1; +CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y; +CREATE EXTENSION test_ext_cine; -- fail +ERROR: table ext_cine_tab2 is not a member of extension "test_ext_cine" +DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +DROP TABLE ext_cine_tab2; +CREATE EXTENSION test_ext_cine; +\dx+ test_ext_cine +Objects in extension "test_ext_cine" + Object description +----------------------------------- + collation ext_cine_coll + foreign-data wrapper ext_cine_fdw + materialized view ext_cine_mv + schema ext_cine_schema + sequence ext_cine_seq + server ext_cine_srv + table ext_cine_tab1 + table ext_cine_tab2 +(8 rows) + +ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; +\dx+ test_ext_cine +Objects in extension "test_ext_cine" + Object description +----------------------------------- + collation ext_cine_coll + foreign-data wrapper ext_cine_fdw + materialized view ext_cine_mv + schema ext_cine_schema + sequence ext_cine_seq + server ext_cine_srv + table ext_cine_tab1 + table ext_cine_tab2 + table ext_cine_tab3 +(9 rows) + +-- +-- Test @extschema@ syntax. +-- +CREATE SCHEMA "has space"; +CREATE EXTENSION test_ext_extschema SCHEMA has$dollar; +ERROR: invalid character in extension "test_ext_extschema" schema: must not contain any of ""$'\" +CREATE EXTENSION test_ext_extschema SCHEMA "has space"; diff --git a/src/test/modules/test_extensions/sql/test_extdepend.sql b/src/test/modules/test_extensions/sql/test_extdepend.sql new file mode 100644 index 0000000..63240a1 --- /dev/null +++ b/src/test/modules/test_extensions/sql/test_extdepend.sql @@ -0,0 +1,90 @@ +-- +-- test ALTER THING name DEPENDS ON EXTENSION +-- + +-- Common setup for all tests +CREATE TABLE test_extdep_commands (command text); +COPY test_extdep_commands FROM stdin; + CREATE SCHEMA test_ext + CREATE EXTENSION test_ext5 SCHEMA test_ext + SET search_path TO test_ext + CREATE TABLE a (a1 int) + + CREATE FUNCTION b() RETURNS TRIGGER LANGUAGE plpgsql AS\n $$ BEGIN NEW.a1 := NEW.a1 + 42; RETURN NEW; END; $$ + ALTER FUNCTION b() DEPENDS ON EXTENSION test_ext5 + + CREATE TRIGGER c BEFORE INSERT ON a FOR EACH ROW EXECUTE PROCEDURE b() + ALTER TRIGGER c ON a DEPENDS ON EXTENSION test_ext5 + + CREATE MATERIALIZED VIEW d AS SELECT * FROM a + ALTER MATERIALIZED VIEW d DEPENDS ON EXTENSION test_ext5 + + CREATE INDEX e ON a (a1) + ALTER INDEX e DEPENDS ON EXTENSION test_ext5 + RESET search_path +\. + +SELECT * FROM test_extdep_commands; +-- First, test that dependent objects go away when the extension is dropped. +SELECT * FROM test_extdep_commands \gexec +-- A dependent object made dependent again has no effect +ALTER FUNCTION test_ext.b() DEPENDS ON EXTENSION test_ext5; +-- make sure we have the right dependencies on the extension +SELECT deptype, p.* + FROM pg_depend, pg_identify_object(classid, objid, objsubid) AS p + WHERE refclassid = 'pg_extension'::regclass AND + refobjid = (SELECT oid FROM pg_extension WHERE extname = 'test_ext5') +ORDER BY type; +DROP EXTENSION test_ext5; +-- anything still depending on the table? +SELECT deptype, i.* + FROM pg_catalog.pg_depend, pg_identify_object(classid, objid, objsubid) i +WHERE refclassid='pg_class'::regclass AND + refobjid='test_ext.a'::regclass AND NOT deptype IN ('i', 'a'); +DROP SCHEMA test_ext CASCADE; + +-- Second test: If we drop the table, the objects are dropped too and no +-- vestige remains in pg_depend. +SELECT * FROM test_extdep_commands \gexec +DROP TABLE test_ext.a; -- should fail, require cascade +DROP TABLE test_ext.a CASCADE; +-- anything still depending on the extension? Should be only function b() +SELECT deptype, i.* + FROM pg_catalog.pg_depend, pg_identify_object(classid, objid, objsubid) i + WHERE refclassid='pg_extension'::regclass AND + refobjid=(SELECT oid FROM pg_extension WHERE extname='test_ext5'); +DROP EXTENSION test_ext5; +DROP SCHEMA test_ext CASCADE; + +-- Third test: we can drop the objects individually +SELECT * FROM test_extdep_commands \gexec +SET search_path TO test_ext; +DROP TRIGGER c ON a; +DROP FUNCTION b(); +DROP MATERIALIZED VIEW d; +DROP INDEX e; + +SELECT deptype, i.* + FROM pg_catalog.pg_depend, pg_identify_object(classid, objid, objsubid) i + WHERE (refclassid='pg_extension'::regclass AND + refobjid=(SELECT oid FROM pg_extension WHERE extname='test_ext5')) + OR (refclassid='pg_class'::regclass AND refobjid='test_ext.a'::regclass) + AND NOT deptype IN ('i', 'a'); +DROP TABLE a; +RESET search_path; +DROP SCHEMA test_ext CASCADE; + +-- Fourth test: we can mark the objects as dependent, then unmark; then the +-- drop of the extension does nothing +SELECT * FROM test_extdep_commands \gexec +SET search_path TO test_ext; +ALTER FUNCTION b() NO DEPENDS ON EXTENSION test_ext5; +ALTER TRIGGER c ON a NO DEPENDS ON EXTENSION test_ext5; +ALTER MATERIALIZED VIEW d NO DEPENDS ON EXTENSION test_ext5; +ALTER INDEX e NO DEPENDS ON EXTENSION test_ext5; +DROP EXTENSION test_ext5; +DROP TRIGGER c ON a; +DROP FUNCTION b(); +DROP MATERIALIZED VIEW d; +DROP INDEX e; +DROP SCHEMA test_ext CASCADE; diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql new file mode 100644 index 0000000..212fd9b --- /dev/null +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -0,0 +1,220 @@ +CREATE SCHEMA has$dollar; + +-- test some errors +CREATE EXTENSION test_ext1; +CREATE EXTENSION test_ext1 SCHEMA test_ext1; +CREATE EXTENSION test_ext1 SCHEMA test_ext; +CREATE EXTENSION test_ext1 SCHEMA has$dollar; + +-- finally success +CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE; + +SELECT extname, nspname, extversion, extrelocatable FROM pg_extension e, pg_namespace n WHERE extname LIKE 'test_ext%' AND e.extnamespace = n.oid ORDER BY 1; + +CREATE EXTENSION test_ext_cyclic1 CASCADE; + +DROP SCHEMA has$dollar CASCADE; +CREATE SCHEMA has$dollar; + +CREATE EXTENSION test_ext6; +DROP EXTENSION test_ext6; +CREATE EXTENSION test_ext6; + +-- test dropping of member tables that own extensions: +-- this table will be absorbed into test_ext7 +create table old_table1 (col1 serial primary key); +create extension test_ext7; +\dx+ test_ext7 +alter extension test_ext7 update to '2.0'; +\dx+ test_ext7 + +-- test handling of temp objects created by extensions +create extension test_ext8; + +-- \dx+ would expose a variable pg_temp_nn schema name, so we can't use it here +select regexp_replace(pg_describe_object(classid, objid, objsubid), + 'pg_temp_\d+', 'pg_temp', 'g') as "Object description" +from pg_depend +where refclassid = 'pg_extension'::regclass and deptype = 'e' and + refobjid = (select oid from pg_extension where extname = 'test_ext8') +order by 1; + +-- Should be possible to drop and recreate this extension +drop extension test_ext8; +create extension test_ext8; + +select regexp_replace(pg_describe_object(classid, objid, objsubid), + 'pg_temp_\d+', 'pg_temp', 'g') as "Object description" +from pg_depend +where refclassid = 'pg_extension'::regclass and deptype = 'e' and + refobjid = (select oid from pg_extension where extname = 'test_ext8') +order by 1; + +-- here we want to start a new session and wait till old one is gone +select pg_backend_pid() as oldpid \gset +\c - +do 'declare c int = 0; +begin + while (select count(*) from pg_stat_activity where pid = ' + :'oldpid' + ') > 0 loop c := c + 1; perform pg_stat_clear_snapshot(); end loop; + raise log ''test_extensions looped % times'', c; +end'; + +-- extension should now contain no temp objects +\dx+ test_ext8 + +-- dropping it should still work +drop extension test_ext8; + +-- Test creation of extension in temporary schema with two-phase commit, +-- which should not work. This function wrapper is useful for portability. + +-- Avoid noise caused by CONTEXT and NOTICE messages including the temporary +-- schema name. +\set SHOW_CONTEXT never +SET client_min_messages TO 'warning'; +-- First enforce presence of temporary schema. +CREATE TEMP TABLE test_ext4_tab (); +CREATE OR REPLACE FUNCTION create_extension_with_temp_schema() + RETURNS VOID AS $$ + DECLARE + tmpschema text; + query text; + BEGIN + SELECT INTO tmpschema pg_my_temp_schema()::regnamespace; + query := 'CREATE EXTENSION test_ext4 SCHEMA ' || tmpschema || ' CASCADE;'; + RAISE NOTICE 'query %', query; + EXECUTE query; + END; $$ LANGUAGE plpgsql; +BEGIN; +SELECT create_extension_with_temp_schema(); +PREPARE TRANSACTION 'twophase_extension'; +-- Clean up +DROP TABLE test_ext4_tab; +DROP FUNCTION create_extension_with_temp_schema(); +RESET client_min_messages; +\unset SHOW_CONTEXT + +-- Test case of an event trigger run in an extension upgrade script. +-- See: https://postgr.es/m/20200902193715.6e0269d4@firost +CREATE EXTENSION test_ext_evttrig; +ALTER EXTENSION test_ext_evttrig UPDATE TO '2.0'; +DROP EXTENSION test_ext_evttrig; + +-- It's generally bad style to use CREATE OR REPLACE unnecessarily. +-- Test what happens if an extension does it anyway. +-- Replacing a shell type or operator is sort of like CREATE OR REPLACE; +-- check that too. + +CREATE FUNCTION ext_cor_func() RETURNS text + AS $$ SELECT 'ext_cor_func: original'::text $$ LANGUAGE sql; + +CREATE EXTENSION test_ext_cor; -- fail + +SELECT ext_cor_func(); + +DROP FUNCTION ext_cor_func(); + +CREATE VIEW ext_cor_view AS + SELECT 'ext_cor_view: original'::text AS col; + +CREATE EXTENSION test_ext_cor; -- fail + +SELECT ext_cor_func(); + +SELECT * FROM ext_cor_view; + +DROP VIEW ext_cor_view; + +CREATE TYPE test_ext_type; + +CREATE EXTENSION test_ext_cor; -- fail + +DROP TYPE test_ext_type; + +-- this makes a shell "point <<@@ polygon" operator too +CREATE OPERATOR @@>> ( PROCEDURE = poly_contain_pt, + LEFTARG = polygon, RIGHTARG = point, + COMMUTATOR = <<@@ ); + +CREATE EXTENSION test_ext_cor; -- fail + +DROP OPERATOR <<@@ (point, polygon); + +CREATE EXTENSION test_ext_cor; -- now it should work + +SELECT ext_cor_func(); + +SELECT * FROM ext_cor_view; + +SELECT 'x'::test_ext_type; + +SELECT point(0,0) <<@@ polygon(circle(point(0,0),1)); + +\dx+ test_ext_cor + +-- +-- CREATE IF NOT EXISTS is an entirely unsound thing for an extension +-- to be doing, but let's at least plug the major security hole in it. +-- + +CREATE COLLATION ext_cine_coll + ( LC_COLLATE = "C", LC_CTYPE = "C" ); + +CREATE EXTENSION test_ext_cine; -- fail + +DROP COLLATION ext_cine_coll; + +CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1; + +CREATE EXTENSION test_ext_cine; -- fail + +DROP MATERIALIZED VIEW ext_cine_mv; + +CREATE FOREIGN DATA WRAPPER dummy; + +CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy; + +CREATE EXTENSION test_ext_cine; -- fail + +DROP SERVER ext_cine_srv; + +CREATE SCHEMA ext_cine_schema; + +CREATE EXTENSION test_ext_cine; -- fail + +DROP SCHEMA ext_cine_schema; + +CREATE SEQUENCE ext_cine_seq; + +CREATE EXTENSION test_ext_cine; -- fail + +DROP SEQUENCE ext_cine_seq; + +CREATE TABLE ext_cine_tab1 (x int); + +CREATE EXTENSION test_ext_cine; -- fail + +DROP TABLE ext_cine_tab1; + +CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y; + +CREATE EXTENSION test_ext_cine; -- fail + +DROP TABLE ext_cine_tab2; + +CREATE EXTENSION test_ext_cine; + +\dx+ test_ext_cine + +ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; + +\dx+ test_ext_cine + +-- +-- Test @extschema@ syntax. +-- +CREATE SCHEMA "has space"; +CREATE EXTENSION test_ext_extschema SCHEMA has$dollar; +CREATE EXTENSION test_ext_extschema SCHEMA "has space"; diff --git a/src/test/modules/test_extensions/test_ext1--1.0.sql b/src/test/modules/test_extensions/test_ext1--1.0.sql new file mode 100644 index 0000000..9a4bb1b --- /dev/null +++ b/src/test/modules/test_extensions/test_ext1--1.0.sql @@ -0,0 +1,3 @@ +/* src/test/modules/test_extensions/test_ext1--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext1" to load this file. \quit diff --git a/src/test/modules/test_extensions/test_ext1.control b/src/test/modules/test_extensions/test_ext1.control new file mode 100644 index 0000000..9c069df --- /dev/null +++ b/src/test/modules/test_extensions/test_ext1.control @@ -0,0 +1,5 @@ +comment = 'Test extension 1' +default_version = '1.0' +schema = 'test_ext1' +relocatable = false +requires = 'test_ext2,test_ext4' diff --git a/src/test/modules/test_extensions/test_ext2--1.0.sql b/src/test/modules/test_extensions/test_ext2--1.0.sql new file mode 100644 index 0000000..0f6d4ec --- /dev/null +++ b/src/test/modules/test_extensions/test_ext2--1.0.sql @@ -0,0 +1,3 @@ +/* src/test/modules/test_extensions/test_ext2--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext2" to load this file. \quit diff --git a/src/test/modules/test_extensions/test_ext2.control b/src/test/modules/test_extensions/test_ext2.control new file mode 100644 index 0000000..946b7d5 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext2.control @@ -0,0 +1,4 @@ +comment = 'Test extension 2' +default_version = '1.0' +relocatable = true +requires = 'test_ext3,test_ext5' diff --git a/src/test/modules/test_extensions/test_ext3--1.0.sql b/src/test/modules/test_extensions/test_ext3--1.0.sql new file mode 100644 index 0000000..4fcb63d --- /dev/null +++ b/src/test/modules/test_extensions/test_ext3--1.0.sql @@ -0,0 +1,9 @@ +/* src/test/modules/test_extensions/test_ext3--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext3" to load this file. \quit + +CREATE TABLE test_ext3_table (col_old INT); + +ALTER TABLE test_ext3_table RENAME col_old TO col_new; + +UPDATE test_ext3_table SET col_new = 0; diff --git a/src/test/modules/test_extensions/test_ext3.control b/src/test/modules/test_extensions/test_ext3.control new file mode 100644 index 0000000..5f1afe7 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext3.control @@ -0,0 +1,3 @@ +comment = 'Test extension 3' +default_version = '1.0' +relocatable = true diff --git a/src/test/modules/test_extensions/test_ext4--1.0.sql b/src/test/modules/test_extensions/test_ext4--1.0.sql new file mode 100644 index 0000000..19f051f --- /dev/null +++ b/src/test/modules/test_extensions/test_ext4--1.0.sql @@ -0,0 +1,3 @@ +/* src/test/modules/test_extensions/test_ext4--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext4" to load this file. \quit diff --git a/src/test/modules/test_extensions/test_ext4.control b/src/test/modules/test_extensions/test_ext4.control new file mode 100644 index 0000000..fc62591 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext4.control @@ -0,0 +1,4 @@ +comment = 'Test extension 4' +default_version = '1.0' +relocatable = true +requires = 'test_ext5' diff --git a/src/test/modules/test_extensions/test_ext5--1.0.sql b/src/test/modules/test_extensions/test_ext5--1.0.sql new file mode 100644 index 0000000..baf6ef8 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext5--1.0.sql @@ -0,0 +1,3 @@ +/* src/test/modules/test_extensions/test_ext5--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext5" to load this file. \quit diff --git a/src/test/modules/test_extensions/test_ext5.control b/src/test/modules/test_extensions/test_ext5.control new file mode 100644 index 0000000..51bc57e --- /dev/null +++ b/src/test/modules/test_extensions/test_ext5.control @@ -0,0 +1,3 @@ +comment = 'Test extension 5' +default_version = '1.0' +relocatable = true diff --git a/src/test/modules/test_extensions/test_ext6--1.0.sql b/src/test/modules/test_extensions/test_ext6--1.0.sql new file mode 100644 index 0000000..65a4fc5 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext6--1.0.sql @@ -0,0 +1 @@ +grant usage on schema @extschema@ to public; diff --git a/src/test/modules/test_extensions/test_ext6.control b/src/test/modules/test_extensions/test_ext6.control new file mode 100644 index 0000000..04b2146 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext6.control @@ -0,0 +1,5 @@ +comment = 'test_ext6' +default_version = '1.0' +relocatable = false +superuser = true +schema = 'test_ext6' diff --git a/src/test/modules/test_extensions/test_ext7--1.0--2.0.sql b/src/test/modules/test_extensions/test_ext7--1.0--2.0.sql new file mode 100644 index 0000000..50e3dca --- /dev/null +++ b/src/test/modules/test_extensions/test_ext7--1.0--2.0.sql @@ -0,0 +1,8 @@ +/* src/test/modules/test_extensions/test_ext7--1.0--2.0.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext7 UPDATE TO '2.0'" to load this file. \quit + +-- drop some tables with serial columns +drop table ext7_table1; +drop table old_table1; diff --git a/src/test/modules/test_extensions/test_ext7--1.0.sql b/src/test/modules/test_extensions/test_ext7--1.0.sql new file mode 100644 index 0000000..0c2d72a --- /dev/null +++ b/src/test/modules/test_extensions/test_ext7--1.0.sql @@ -0,0 +1,13 @@ +/* src/test/modules/test_extensions/test_ext7--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext7" to load this file. \quit + +-- link some existing serial-owning table to the extension +alter extension test_ext7 add table old_table1; +alter extension test_ext7 add sequence old_table1_col1_seq; + +-- ordinary member tables with serial columns +create table ext7_table1 (col1 serial primary key); + +create table ext7_table2 (col2 serial primary key); diff --git a/src/test/modules/test_extensions/test_ext7.control b/src/test/modules/test_extensions/test_ext7.control new file mode 100644 index 0000000..b58df53 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext7.control @@ -0,0 +1,4 @@ +comment = 'Test extension 7' +default_version = '1.0' +schema = 'public' +relocatable = false diff --git a/src/test/modules/test_extensions/test_ext8--1.0.sql b/src/test/modules/test_extensions/test_ext8--1.0.sql new file mode 100644 index 0000000..1561ffe --- /dev/null +++ b/src/test/modules/test_extensions/test_ext8--1.0.sql @@ -0,0 +1,21 @@ +/* src/test/modules/test_extensions/test_ext8--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext8" to load this file. \quit + +-- create some random data type +create domain posint as int check (value > 0); + +-- use it in regular and temporary tables and functions + +create table ext8_table1 (f1 posint); + +create temp table ext8_temp_table1 (f1 posint); + +create function ext8_even (posint) returns bool as + 'select ($1 % 2) = 0' language sql; + +create function pg_temp.ext8_temp_even (posint) returns bool as + 'select ($1 % 2) = 0' language sql; + +-- we intentionally don't drop the temp objects before exiting diff --git a/src/test/modules/test_extensions/test_ext8.control b/src/test/modules/test_extensions/test_ext8.control new file mode 100644 index 0000000..70f8caa --- /dev/null +++ b/src/test/modules/test_extensions/test_ext8.control @@ -0,0 +1,4 @@ +comment = 'Test extension 8' +default_version = '1.0' +schema = 'public' +relocatable = false diff --git a/src/test/modules/test_extensions/test_ext_cine--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_cine--1.0--1.1.sql new file mode 100644 index 0000000..6dadfd2 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cine--1.0--1.1.sql @@ -0,0 +1,26 @@ +/* src/test/modules/test_extensions/test_ext_cine--1.0--1.1.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_cine UPDATE TO '1.1'" to load this file. \quit + +-- +-- These are the same commands as in the 1.0 script; we expect them +-- to do nothing. +-- + +CREATE COLLATION IF NOT EXISTS ext_cine_coll + ( LC_COLLATE = "POSIX", LC_CTYPE = "POSIX" ); + +CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1; + +CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw; + +CREATE SCHEMA IF NOT EXISTS ext_cine_schema; + +CREATE SEQUENCE IF NOT EXISTS ext_cine_seq; + +CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int); + +CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y; + +-- just to verify the script ran +CREATE TABLE ext_cine_tab3 (z int); diff --git a/src/test/modules/test_extensions/test_ext_cine--1.0.sql b/src/test/modules/test_extensions/test_ext_cine--1.0.sql new file mode 100644 index 0000000..01408ff --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cine--1.0.sql @@ -0,0 +1,25 @@ +/* src/test/modules/test_extensions/test_ext_cine--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext_cine" to load this file. \quit + +-- +-- CREATE IF NOT EXISTS is an entirely unsound thing for an extension +-- to be doing, but let's at least plug the major security hole in it. +-- + +CREATE COLLATION IF NOT EXISTS ext_cine_coll + ( LC_COLLATE = "POSIX", LC_CTYPE = "POSIX" ); + +CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1; + +CREATE FOREIGN DATA WRAPPER ext_cine_fdw; + +CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw; + +CREATE SCHEMA IF NOT EXISTS ext_cine_schema; + +CREATE SEQUENCE IF NOT EXISTS ext_cine_seq; + +CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int); + +CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y; diff --git a/src/test/modules/test_extensions/test_ext_cine.control b/src/test/modules/test_extensions/test_ext_cine.control new file mode 100644 index 0000000..ced713b --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cine.control @@ -0,0 +1,3 @@ +comment = 'Test extension using CREATE IF NOT EXISTS' +default_version = '1.0' +relocatable = true diff --git a/src/test/modules/test_extensions/test_ext_cor--1.0.sql b/src/test/modules/test_extensions/test_ext_cor--1.0.sql new file mode 100644 index 0000000..2e8d89c --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cor--1.0.sql @@ -0,0 +1,20 @@ +/* src/test/modules/test_extensions/test_ext_cor--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext_cor" to load this file. \quit + +-- It's generally bad style to use CREATE OR REPLACE unnecessarily. +-- Test what happens if an extension does it anyway. + +CREATE OR REPLACE FUNCTION ext_cor_func() RETURNS text + AS $$ SELECT 'ext_cor_func: from extension'::text $$ LANGUAGE sql; + +CREATE OR REPLACE VIEW ext_cor_view AS + SELECT 'ext_cor_view: from extension'::text AS col; + +-- These are for testing replacement of a shell type/operator, which works +-- enough like an implicit OR REPLACE to be important to check. + +CREATE TYPE test_ext_type AS ENUM('x', 'y'); + +CREATE OPERATOR <<@@ ( PROCEDURE = pt_contained_poly, + LEFTARG = point, RIGHTARG = polygon ); diff --git a/src/test/modules/test_extensions/test_ext_cor.control b/src/test/modules/test_extensions/test_ext_cor.control new file mode 100644 index 0000000..0e972e5 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cor.control @@ -0,0 +1,3 @@ +comment = 'Test extension using CREATE OR REPLACE' +default_version = '1.0' +relocatable = true diff --git a/src/test/modules/test_extensions/test_ext_cyclic1--1.0.sql b/src/test/modules/test_extensions/test_ext_cyclic1--1.0.sql new file mode 100644 index 0000000..81bdaf4 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cyclic1--1.0.sql @@ -0,0 +1,3 @@ +/* src/test/modules/test_extensions/test_ext_cyclic1--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext_cyclic1" to load this file. \quit diff --git a/src/test/modules/test_extensions/test_ext_cyclic1.control b/src/test/modules/test_extensions/test_ext_cyclic1.control new file mode 100644 index 0000000..aaab403 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cyclic1.control @@ -0,0 +1,4 @@ +comment = 'Test extension cyclic 1' +default_version = '1.0' +relocatable = true +requires = 'test_ext_cyclic2' diff --git a/src/test/modules/test_extensions/test_ext_cyclic2--1.0.sql b/src/test/modules/test_extensions/test_ext_cyclic2--1.0.sql new file mode 100644 index 0000000..ae2b3e9 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cyclic2--1.0.sql @@ -0,0 +1,3 @@ +/* src/test/modules/test_extensions/test_ext_cyclic2--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext_cyclic2" to load this file. \quit diff --git a/src/test/modules/test_extensions/test_ext_cyclic2.control b/src/test/modules/test_extensions/test_ext_cyclic2.control new file mode 100644 index 0000000..1e28f96 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_cyclic2.control @@ -0,0 +1,4 @@ +comment = 'Test extension cyclic 2' +default_version = '1.0' +relocatable = true +requires = 'test_ext_cyclic1' diff --git a/src/test/modules/test_extensions/test_ext_evttrig--1.0--2.0.sql b/src/test/modules/test_extensions/test_ext_evttrig--1.0--2.0.sql new file mode 100644 index 0000000..fdd2f35 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_evttrig--1.0--2.0.sql @@ -0,0 +1,7 @@ +/* src/test/modules/test_extensions/test_event_trigger--1.0--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_event_trigger UPDATE TO '2.0'" to load this file. \quit + +-- Test extension upgrade with event trigger. +ALTER EVENT TRIGGER table_rewrite_trg DISABLE; +ALTER TABLE t DROP COLUMN id; diff --git a/src/test/modules/test_extensions/test_ext_evttrig--1.0.sql b/src/test/modules/test_extensions/test_ext_evttrig--1.0.sql new file mode 100644 index 0000000..0071712 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_evttrig--1.0.sql @@ -0,0 +1,16 @@ +/* src/test/modules/test_extensions/test_event_trigger--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_event_trigger" to load this file. \quit + +-- Base table with event trigger, used in a regression test involving +-- extension upgrades. +CREATE TABLE t (id text); +CREATE OR REPLACE FUNCTION _evt_table_rewrite_fnct() +RETURNS EVENT_TRIGGER LANGUAGE plpgsql AS +$$ + BEGIN + END; +$$; +CREATE EVENT TRIGGER table_rewrite_trg + ON table_rewrite + EXECUTE PROCEDURE _evt_table_rewrite_fnct(); diff --git a/src/test/modules/test_extensions/test_ext_evttrig.control b/src/test/modules/test_extensions/test_ext_evttrig.control new file mode 100644 index 0000000..915fae6 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_evttrig.control @@ -0,0 +1,3 @@ +comment = 'Test extension - event trigger' +default_version = '1.0' +relocatable = true diff --git a/src/test/modules/test_extensions/test_ext_extschema--1.0.sql b/src/test/modules/test_extensions/test_ext_extschema--1.0.sql new file mode 100644 index 0000000..aed5383 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_extschema--1.0.sql @@ -0,0 +1,5 @@ +/* src/test/modules/test_extensions/test_ext_extschema--1.0.sql */ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ext_extschema" to load this file. \quit + +SELECT 1 AS @extschema@; diff --git a/src/test/modules/test_extensions/test_ext_extschema.control b/src/test/modules/test_extensions/test_ext_extschema.control new file mode 100644 index 0000000..b124d49 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_extschema.control @@ -0,0 +1,3 @@ +comment = 'test @extschema@' +default_version = '1.0' +relocatable = false diff --git a/src/test/modules/test_ginpostinglist/.gitignore b/src/test/modules/test_ginpostinglist/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_ginpostinglist/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_ginpostinglist/Makefile b/src/test/modules/test_ginpostinglist/Makefile new file mode 100644 index 0000000..51b941b --- /dev/null +++ b/src/test/modules/test_ginpostinglist/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_ginpostinglist/Makefile + +MODULE_big = test_ginpostinglist +OBJS = \ + $(WIN32RES) \ + test_ginpostinglist.o +PGFILEDESC = "test_ginpostinglist - test code for src/backend/access/gin//ginpostinglist.c" + +EXTENSION = test_ginpostinglist +DATA = test_ginpostinglist--1.0.sql + +REGRESS = test_ginpostinglist + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_ginpostinglist +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_ginpostinglist/README b/src/test/modules/test_ginpostinglist/README new file mode 100644 index 0000000..66684dd --- /dev/null +++ b/src/test/modules/test_ginpostinglist/README @@ -0,0 +1,2 @@ +test_ginpostinglist contains unit tests for the GIN posting list code in +src/backend/access/gin/ginpostinglist.c. diff --git a/src/test/modules/test_ginpostinglist/expected/test_ginpostinglist.out b/src/test/modules/test_ginpostinglist/expected/test_ginpostinglist.out new file mode 100644 index 0000000..4d0beae --- /dev/null +++ b/src/test/modules/test_ginpostinglist/expected/test_ginpostinglist.out @@ -0,0 +1,19 @@ +CREATE EXTENSION test_ginpostinglist; +-- +-- All the logic is in the test_ginpostinglist() function. It will throw +-- a error if something fails. +-- +SELECT test_ginpostinglist(); +NOTICE: testing with (0, 1), (0, 2), max 14 bytes +NOTICE: encoded 2 item pointers to 10 bytes +NOTICE: testing with (0, 1), (0, 291), max 14 bytes +NOTICE: encoded 2 item pointers to 10 bytes +NOTICE: testing with (0, 1), (4294967294, 291), max 14 bytes +NOTICE: encoded 1 item pointers to 8 bytes +NOTICE: testing with (0, 1), (4294967294, 291), max 16 bytes +NOTICE: encoded 2 item pointers to 16 bytes + test_ginpostinglist +--------------------- + +(1 row) + diff --git a/src/test/modules/test_ginpostinglist/sql/test_ginpostinglist.sql b/src/test/modules/test_ginpostinglist/sql/test_ginpostinglist.sql new file mode 100644 index 0000000..b8cab7a --- /dev/null +++ b/src/test/modules/test_ginpostinglist/sql/test_ginpostinglist.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION test_ginpostinglist; + +-- +-- All the logic is in the test_ginpostinglist() function. It will throw +-- a error if something fails. +-- +SELECT test_ginpostinglist(); diff --git a/src/test/modules/test_ginpostinglist/test_ginpostinglist--1.0.sql b/src/test/modules/test_ginpostinglist/test_ginpostinglist--1.0.sql new file mode 100644 index 0000000..37396a4 --- /dev/null +++ b/src/test/modules/test_ginpostinglist/test_ginpostinglist--1.0.sql @@ -0,0 +1,8 @@ +/* src/test/modules/test_ginpostinglist/test_ginpostinglist--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ginpostinglist" to load this file. \quit + +CREATE FUNCTION test_ginpostinglist() +RETURNS pg_catalog.void STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_ginpostinglist/test_ginpostinglist.c b/src/test/modules/test_ginpostinglist/test_ginpostinglist.c new file mode 100644 index 0000000..9a23009 --- /dev/null +++ b/src/test/modules/test_ginpostinglist/test_ginpostinglist.c @@ -0,0 +1,96 @@ +/*-------------------------------------------------------------------------- + * + * test_ginpostinglist.c + * Test varbyte-encoding in ginpostinglist.c + * + * Copyright (c) 2019-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_ginpostinglist/test_ginpostinglist.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/gin_private.h" +#include "access/ginblock.h" +#include "access/htup_details.h" +#include "fmgr.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_ginpostinglist); + +/* + * Encodes a pair of TIDs, and decodes it back. The first TID is always + * (0, 1), the second one is formed from the blk/off arguments. The 'maxsize' + * argument is passed to ginCompressPostingList(); it can be used to test the + * overflow checks. + * + * The reason that we test a pair, instead of just a single TID, is that + * the GinPostingList stores the first TID as is, and the varbyte-encoding + * is only used for the deltas between TIDs. So testing a single TID would + * not exercise the varbyte encoding at all. + * + * This function prints NOTICEs to describe what is tested, and how large the + * resulting GinPostingList is. Any incorrect results, e.g. if the encode + + * decode round trip doesn't return the original input, are reported as + * ERRORs. + */ +static void +test_itemptr_pair(BlockNumber blk, OffsetNumber off, int maxsize) +{ + ItemPointerData orig_itemptrs[2]; + ItemPointer decoded_itemptrs; + GinPostingList *pl; + int nwritten; + int ndecoded; + + elog(NOTICE, "testing with (%u, %d), (%u, %d), max %d bytes", + 0, 1, blk, off, maxsize); + ItemPointerSet(&orig_itemptrs[0], 0, 1); + ItemPointerSet(&orig_itemptrs[1], blk, off); + + /* Encode, and decode it back */ + pl = ginCompressPostingList(orig_itemptrs, 2, maxsize, &nwritten); + elog(NOTICE, "encoded %d item pointers to %zu bytes", + nwritten, SizeOfGinPostingList(pl)); + + if (SizeOfGinPostingList(pl) > maxsize) + elog(ERROR, "overflow: result was %zu bytes, max %d", + SizeOfGinPostingList(pl), maxsize); + + decoded_itemptrs = ginPostingListDecode(pl, &ndecoded); + if (nwritten != ndecoded) + elog(NOTICE, "encoded %d itemptrs, %d came back", nwritten, ndecoded); + + /* Check the result */ + if (!ItemPointerEquals(&orig_itemptrs[0], &decoded_itemptrs[0])) + elog(ERROR, "mismatch on first itemptr: (%u, %d) vs (%u, %d)", + 0, 1, + ItemPointerGetBlockNumber(&decoded_itemptrs[0]), + ItemPointerGetOffsetNumber(&decoded_itemptrs[0])); + + if (ndecoded == 2 && + !ItemPointerEquals(&orig_itemptrs[0], &decoded_itemptrs[0])) + { + elog(ERROR, "mismatch on second itemptr: (%u, %d) vs (%u, %d)", + 0, 1, + ItemPointerGetBlockNumber(&decoded_itemptrs[0]), + ItemPointerGetOffsetNumber(&decoded_itemptrs[0])); + } +} + +/* + * SQL-callable entry point to perform all tests. + */ +Datum +test_ginpostinglist(PG_FUNCTION_ARGS) +{ + test_itemptr_pair(0, 2, 14); + test_itemptr_pair(0, MaxHeapTuplesPerPage, 14); + test_itemptr_pair(MaxBlockNumber, MaxHeapTuplesPerPage, 14); + test_itemptr_pair(MaxBlockNumber, MaxHeapTuplesPerPage, 16); + + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_ginpostinglist/test_ginpostinglist.control b/src/test/modules/test_ginpostinglist/test_ginpostinglist.control new file mode 100644 index 0000000..e4f5a7c --- /dev/null +++ b/src/test/modules/test_ginpostinglist/test_ginpostinglist.control @@ -0,0 +1,4 @@ +comment = 'Test code for ginpostinglist.c' +default_version = '1.0' +module_pathname = '$libdir/test_ginpostinglist' +relocatable = true diff --git a/src/test/modules/test_integerset/.gitignore b/src/test/modules/test_integerset/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_integerset/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_integerset/Makefile b/src/test/modules/test_integerset/Makefile new file mode 100644 index 0000000..799c17c --- /dev/null +++ b/src/test/modules/test_integerset/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_integerset/Makefile + +MODULE_big = test_integerset +OBJS = \ + $(WIN32RES) \ + test_integerset.o +PGFILEDESC = "test_integerset - test code for src/backend/lib/integerset.c" + +EXTENSION = test_integerset +DATA = test_integerset--1.0.sql + +REGRESS = test_integerset + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_integerset +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_integerset/README b/src/test/modules/test_integerset/README new file mode 100644 index 0000000..a8b2718 --- /dev/null +++ b/src/test/modules/test_integerset/README @@ -0,0 +1,7 @@ +test_integerset contains unit tests for testing the integer set implementation +in src/backend/lib/integerset.c. + +The tests verify the correctness of the implementation, but they can also be +used as a micro-benchmark. If you set the 'intset_test_stats' flag in +test_integerset.c, the tests will print extra information about execution time +and memory usage. diff --git a/src/test/modules/test_integerset/expected/test_integerset.out b/src/test/modules/test_integerset/expected/test_integerset.out new file mode 100644 index 0000000..822dd03 --- /dev/null +++ b/src/test/modules/test_integerset/expected/test_integerset.out @@ -0,0 +1,31 @@ +CREATE EXTENSION test_integerset; +-- +-- All the logic is in the test_integerset() function. It will throw +-- an error if something fails. +-- +SELECT test_integerset(); +NOTICE: testing intset with empty set +NOTICE: testing intset with distances > 2^60 between values +NOTICE: testing intset with single value 0 +NOTICE: testing intset with single value 1 +NOTICE: testing intset with single value 18446744073709551614 +NOTICE: testing intset with single value 18446744073709551615 +NOTICE: testing intset with value 0, and all between 1000 and 2000 +NOTICE: testing intset with value 1, and all between 1000 and 2000 +NOTICE: testing intset with value 1, and all between 1000 and 2000000 +NOTICE: testing intset with value 18446744073709551614, and all between 1000 and 2000 +NOTICE: testing intset with value 18446744073709551615, and all between 1000 and 2000 +NOTICE: testing intset with pattern "all ones" +NOTICE: testing intset with pattern "alternating bits" +NOTICE: testing intset with pattern "clusters of ten" +NOTICE: testing intset with pattern "clusters of hundred" +NOTICE: testing intset with pattern "one-every-64k" +NOTICE: testing intset with pattern "sparse" +NOTICE: testing intset with pattern "single values, distance > 2^32" +NOTICE: testing intset with pattern "clusters, distance > 2^32" +NOTICE: testing intset with pattern "clusters, distance > 2^60" + test_integerset +----------------- + +(1 row) + diff --git a/src/test/modules/test_integerset/sql/test_integerset.sql b/src/test/modules/test_integerset/sql/test_integerset.sql new file mode 100644 index 0000000..9d970dd --- /dev/null +++ b/src/test/modules/test_integerset/sql/test_integerset.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION test_integerset; + +-- +-- All the logic is in the test_integerset() function. It will throw +-- an error if something fails. +-- +SELECT test_integerset(); diff --git a/src/test/modules/test_integerset/test_integerset--1.0.sql b/src/test/modules/test_integerset/test_integerset--1.0.sql new file mode 100644 index 0000000..d6d5a3f --- /dev/null +++ b/src/test/modules/test_integerset/test_integerset--1.0.sql @@ -0,0 +1,8 @@ +/* src/test/modules/test_integerset/test_integerset--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_integerset" to load this file. \quit + +CREATE FUNCTION test_integerset() +RETURNS pg_catalog.void STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_integerset/test_integerset.c b/src/test/modules/test_integerset/test_integerset.c new file mode 100644 index 0000000..578d2e8 --- /dev/null +++ b/src/test/modules/test_integerset/test_integerset.c @@ -0,0 +1,623 @@ +/*-------------------------------------------------------------------------- + * + * test_integerset.c + * Test integer set data structure. + * + * Copyright (c) 2019-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_integerset/test_integerset.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/pg_prng.h" +#include "fmgr.h" +#include "lib/integerset.h" +#include "miscadmin.h" +#include "nodes/bitmapset.h" +#include "storage/block.h" +#include "storage/itemptr.h" +#include "utils/memutils.h" +#include "utils/timestamp.h" + +/* + * If you enable this, the "pattern" tests will print information about + * how long populating, probing, and iterating the test set takes, and + * how much memory the test set consumed. That can be used as + * micro-benchmark of various operations and input patterns (you might + * want to increase the number of values used in each of the test, if + * you do that, to reduce noise). + * + * The information is printed to the server's stderr, mostly because + * that's where MemoryContextStats() output goes. + */ +static const bool intset_test_stats = false; + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_integerset); + +/* + * A struct to define a pattern of integers, for use with the test_pattern() + * function. + */ +typedef struct +{ + char *test_name; /* short name of the test, for humans */ + char *pattern_str; /* a bit pattern */ + uint64 spacing; /* pattern repeats at this interval */ + uint64 num_values; /* number of integers to set in total */ +} test_spec; + +static const test_spec test_specs[] = { + { + "all ones", "1111111111", + 10, 10000000 + }, + { + "alternating bits", "0101010101", + 10, 10000000 + }, + { + "clusters of ten", "1111111111", + 10000, 10000000 + }, + { + "clusters of hundred", + "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + 10000, 100000000 + }, + { + "one-every-64k", "1", + 65536, 10000000 + }, + { + "sparse", "100000000000000000000000000000001", + 10000000, 10000000 + }, + { + "single values, distance > 2^32", "1", + UINT64CONST(10000000000), 1000000 + }, + { + "clusters, distance > 2^32", "10101010", + UINT64CONST(10000000000), 10000000 + }, + { + "clusters, distance > 2^60", "10101010", + UINT64CONST(2000000000000000000), + 23 /* can't be much higher than this, or we + * overflow uint64 */ + } +}; + +static void test_pattern(const test_spec *spec); +static void test_empty(void); +static void test_single_value(uint64 value); +static void check_with_filler(IntegerSet *intset, uint64 x, uint64 value, uint64 filler_min, uint64 filler_max); +static void test_single_value_and_filler(uint64 value, uint64 filler_min, uint64 filler_max); +static void test_huge_distances(void); + +/* + * SQL-callable entry point to perform all tests. + */ +Datum +test_integerset(PG_FUNCTION_ARGS) +{ + /* Tests for various corner cases */ + test_empty(); + test_huge_distances(); + test_single_value(0); + test_single_value(1); + test_single_value(PG_UINT64_MAX - 1); + test_single_value(PG_UINT64_MAX); + test_single_value_and_filler(0, 1000, 2000); + test_single_value_and_filler(1, 1000, 2000); + test_single_value_and_filler(1, 1000, 2000000); + test_single_value_and_filler(PG_UINT64_MAX - 1, 1000, 2000); + test_single_value_and_filler(PG_UINT64_MAX, 1000, 2000); + + /* Test different test patterns, with lots of entries */ + for (int i = 0; i < lengthof(test_specs); i++) + { + test_pattern(&test_specs[i]); + } + + PG_RETURN_VOID(); +} + +/* + * Test with a repeating pattern, defined by the 'spec'. + */ +static void +test_pattern(const test_spec *spec) +{ + IntegerSet *intset; + MemoryContext intset_ctx; + MemoryContext old_ctx; + TimestampTz starttime; + TimestampTz endtime; + uint64 n; + uint64 last_int; + int patternlen; + uint64 *pattern_values; + uint64 pattern_num_values; + + elog(NOTICE, "testing intset with pattern \"%s\"", spec->test_name); + if (intset_test_stats) + fprintf(stderr, "-----\ntesting intset with pattern \"%s\"\n", spec->test_name); + + /* Pre-process the pattern, creating an array of integers from it. */ + patternlen = strlen(spec->pattern_str); + pattern_values = palloc(patternlen * sizeof(uint64)); + pattern_num_values = 0; + for (int i = 0; i < patternlen; i++) + { + if (spec->pattern_str[i] == '1') + pattern_values[pattern_num_values++] = i; + } + + /* + * Allocate the integer set. + * + * Allocate it in a separate memory context, so that we can print its + * memory usage easily. (intset_create() creates a memory context of its + * own, too, but we don't have direct access to it, so we cannot call + * MemoryContextStats() on it directly). + */ + intset_ctx = AllocSetContextCreate(CurrentMemoryContext, + "intset test", + ALLOCSET_SMALL_SIZES); + MemoryContextSetIdentifier(intset_ctx, spec->test_name); + old_ctx = MemoryContextSwitchTo(intset_ctx); + intset = intset_create(); + MemoryContextSwitchTo(old_ctx); + + /* + * Add values to the set. + */ + starttime = GetCurrentTimestamp(); + + n = 0; + last_int = 0; + while (n < spec->num_values) + { + uint64 x = 0; + + for (int i = 0; i < pattern_num_values && n < spec->num_values; i++) + { + x = last_int + pattern_values[i]; + + intset_add_member(intset, x); + n++; + } + last_int += spec->spacing; + } + + endtime = GetCurrentTimestamp(); + + if (intset_test_stats) + fprintf(stderr, "added " UINT64_FORMAT " values in %d ms\n", + spec->num_values, (int) (endtime - starttime) / 1000); + + /* + * Print stats on the amount of memory used. + * + * We print the usage reported by intset_memory_usage(), as well as the + * stats from the memory context. They should be in the same ballpark, + * but it's hard to automate testing that, so if you're making changes to + * the implementation, just observe that manually. + */ + if (intset_test_stats) + { + uint64 mem_usage; + + /* + * Also print memory usage as reported by intset_memory_usage(). It + * should be in the same ballpark as the usage reported by + * MemoryContextStats(). + */ + mem_usage = intset_memory_usage(intset); + fprintf(stderr, "intset_memory_usage() reported " UINT64_FORMAT " (%0.2f bytes / integer)\n", + mem_usage, (double) mem_usage / spec->num_values); + + MemoryContextStats(intset_ctx); + } + + /* Check that intset_get_num_entries works */ + n = intset_num_entries(intset); + if (n != spec->num_values) + elog(ERROR, "intset_num_entries returned " UINT64_FORMAT ", expected " UINT64_FORMAT, n, spec->num_values); + + /* + * Test random-access probes with intset_is_member() + */ + starttime = GetCurrentTimestamp(); + + for (n = 0; n < 100000; n++) + { + bool b; + bool expected; + uint64 x; + + /* + * Pick next value to probe at random. We limit the probes to the + * last integer that we added to the set, plus an arbitrary constant + * (1000). There's no point in probing the whole 0 - 2^64 range, if + * only a small part of the integer space is used. We would very + * rarely hit values that are actually in the set. + */ + x = pg_prng_uint64_range(&pg_global_prng_state, 0, last_int + 1000); + + /* Do we expect this value to be present in the set? */ + if (x >= last_int) + expected = false; + else + { + uint64 idx = x % spec->spacing; + + if (idx >= patternlen) + expected = false; + else if (spec->pattern_str[idx] == '1') + expected = true; + else + expected = false; + } + + /* Is it present according to intset_is_member() ? */ + b = intset_is_member(intset, x); + + if (b != expected) + elog(ERROR, "mismatch at " UINT64_FORMAT ": %d vs %d", x, b, expected); + } + endtime = GetCurrentTimestamp(); + if (intset_test_stats) + fprintf(stderr, "probed " UINT64_FORMAT " values in %d ms\n", + n, (int) (endtime - starttime) / 1000); + + /* + * Test iterator + */ + starttime = GetCurrentTimestamp(); + + intset_begin_iterate(intset); + n = 0; + last_int = 0; + while (n < spec->num_values) + { + for (int i = 0; i < pattern_num_values && n < spec->num_values; i++) + { + uint64 expected = last_int + pattern_values[i]; + uint64 x; + + if (!intset_iterate_next(intset, &x)) + break; + + if (x != expected) + elog(ERROR, "iterate returned wrong value; got " UINT64_FORMAT ", expected " UINT64_FORMAT, x, expected); + n++; + } + last_int += spec->spacing; + } + endtime = GetCurrentTimestamp(); + if (intset_test_stats) + fprintf(stderr, "iterated " UINT64_FORMAT " values in %d ms\n", + n, (int) (endtime - starttime) / 1000); + + if (n < spec->num_values) + elog(ERROR, "iterator stopped short after " UINT64_FORMAT " entries, expected " UINT64_FORMAT, n, spec->num_values); + if (n > spec->num_values) + elog(ERROR, "iterator returned " UINT64_FORMAT " entries, " UINT64_FORMAT " was expected", n, spec->num_values); + + MemoryContextDelete(intset_ctx); +} + +/* + * Test with a set containing a single integer. + */ +static void +test_single_value(uint64 value) +{ + IntegerSet *intset; + uint64 x; + uint64 num_entries; + bool found; + + elog(NOTICE, "testing intset with single value " UINT64_FORMAT, value); + + /* Create the set. */ + intset = intset_create(); + intset_add_member(intset, value); + + /* Test intset_get_num_entries() */ + num_entries = intset_num_entries(intset); + if (num_entries != 1) + elog(ERROR, "intset_num_entries returned " UINT64_FORMAT ", expected 1", num_entries); + + /* + * Test intset_is_member() at various special values, like 0 and maximum + * possible 64-bit integer, as well as the value itself. + */ + if (intset_is_member(intset, 0) != (value == 0)) + elog(ERROR, "intset_is_member failed for 0"); + if (intset_is_member(intset, 1) != (value == 1)) + elog(ERROR, "intset_is_member failed for 1"); + if (intset_is_member(intset, PG_UINT64_MAX) != (value == PG_UINT64_MAX)) + elog(ERROR, "intset_is_member failed for PG_UINT64_MAX"); + if (intset_is_member(intset, value) != true) + elog(ERROR, "intset_is_member failed for the tested value"); + + /* + * Test iterator + */ + intset_begin_iterate(intset); + found = intset_iterate_next(intset, &x); + if (!found || x != value) + elog(ERROR, "intset_iterate_next failed for " UINT64_FORMAT, x); + + found = intset_iterate_next(intset, &x); + if (found) + elog(ERROR, "intset_iterate_next failed " UINT64_FORMAT, x); +} + +/* + * Test with an integer set that contains: + * + * - a given single 'value', and + * - all integers between 'filler_min' and 'filler_max'. + * + * This exercises different codepaths than testing just with a single value, + * because the implementation buffers newly-added values. If we add just a + * single value to the set, we won't test the internal B-tree code at all, + * just the code that deals with the buffer. + */ +static void +test_single_value_and_filler(uint64 value, uint64 filler_min, uint64 filler_max) +{ + IntegerSet *intset; + uint64 x; + bool found; + uint64 *iter_expected; + uint64 n = 0; + uint64 num_entries = 0; + uint64 mem_usage; + + elog(NOTICE, "testing intset with value " UINT64_FORMAT ", and all between " UINT64_FORMAT " and " UINT64_FORMAT, + value, filler_min, filler_max); + + intset = intset_create(); + + iter_expected = palloc(sizeof(uint64) * (filler_max - filler_min + 1)); + if (value < filler_min) + { + intset_add_member(intset, value); + iter_expected[n++] = value; + } + + for (x = filler_min; x < filler_max; x++) + { + intset_add_member(intset, x); + iter_expected[n++] = x; + } + + if (value >= filler_max) + { + intset_add_member(intset, value); + iter_expected[n++] = value; + } + + /* Test intset_get_num_entries() */ + num_entries = intset_num_entries(intset); + if (num_entries != n) + elog(ERROR, "intset_num_entries returned " UINT64_FORMAT ", expected " UINT64_FORMAT, num_entries, n); + + /* + * Test intset_is_member() at various spots, at and around the values that + * we expect to be set, as well as 0 and the maximum possible value. + */ + check_with_filler(intset, 0, + value, filler_min, filler_max); + check_with_filler(intset, 1, + value, filler_min, filler_max); + check_with_filler(intset, filler_min - 1, + value, filler_min, filler_max); + check_with_filler(intset, filler_min, + value, filler_min, filler_max); + check_with_filler(intset, filler_min + 1, + value, filler_min, filler_max); + check_with_filler(intset, value - 1, + value, filler_min, filler_max); + check_with_filler(intset, value, + value, filler_min, filler_max); + check_with_filler(intset, value + 1, + value, filler_min, filler_max); + check_with_filler(intset, filler_max - 1, + value, filler_min, filler_max); + check_with_filler(intset, filler_max, + value, filler_min, filler_max); + check_with_filler(intset, filler_max + 1, + value, filler_min, filler_max); + check_with_filler(intset, PG_UINT64_MAX - 1, + value, filler_min, filler_max); + check_with_filler(intset, PG_UINT64_MAX, + value, filler_min, filler_max); + + intset_begin_iterate(intset); + for (uint64 i = 0; i < n; i++) + { + found = intset_iterate_next(intset, &x); + if (!found || x != iter_expected[i]) + elog(ERROR, "intset_iterate_next failed for " UINT64_FORMAT, x); + } + found = intset_iterate_next(intset, &x); + if (found) + elog(ERROR, "intset_iterate_next failed " UINT64_FORMAT, x); + + mem_usage = intset_memory_usage(intset); + if (mem_usage < 5000 || mem_usage > 500000000) + elog(ERROR, "intset_memory_usage() reported suspicious value: " UINT64_FORMAT, mem_usage); +} + +/* + * Helper function for test_single_value_and_filler. + * + * Calls intset_is_member() for value 'x', and checks that the result is what + * we expect. + */ +static void +check_with_filler(IntegerSet *intset, uint64 x, + uint64 value, uint64 filler_min, uint64 filler_max) +{ + bool expected; + bool actual; + + expected = (x == value || (filler_min <= x && x < filler_max)); + + actual = intset_is_member(intset, x); + + if (actual != expected) + elog(ERROR, "intset_is_member failed for " UINT64_FORMAT, x); +} + +/* + * Test empty set + */ +static void +test_empty(void) +{ + IntegerSet *intset; + uint64 x; + + elog(NOTICE, "testing intset with empty set"); + + intset = intset_create(); + + /* Test intset_is_member() */ + if (intset_is_member(intset, 0) != false) + elog(ERROR, "intset_is_member on empty set returned true"); + if (intset_is_member(intset, 1) != false) + elog(ERROR, "intset_is_member on empty set returned true"); + if (intset_is_member(intset, PG_UINT64_MAX) != false) + elog(ERROR, "intset_is_member on empty set returned true"); + + /* Test iterator */ + intset_begin_iterate(intset); + if (intset_iterate_next(intset, &x)) + elog(ERROR, "intset_iterate_next on empty set returned a value (" UINT64_FORMAT ")", x); +} + +/* + * Test with integers that are more than 2^60 apart. + * + * The Simple-8b encoding used by the set implementation can only encode + * values up to 2^60. That makes large differences like this interesting + * to test. + */ +static void +test_huge_distances(void) +{ + IntegerSet *intset; + uint64 values[1000]; + int num_values = 0; + uint64 val = 0; + bool found; + uint64 x; + + elog(NOTICE, "testing intset with distances > 2^60 between values"); + + val = 0; + values[num_values++] = val; + + /* Test differences on both sides of the 2^60 boundary. */ + val += UINT64CONST(1152921504606846976) - 1; /* 2^60 - 1 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976) - 1; /* 2^60 - 1 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976); /* 2^60 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976); /* 2^60 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976); /* 2^60 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976) + 1; /* 2^60 + 1 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976) + 1; /* 2^60 + 1 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976) + 1; /* 2^60 + 1 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976) + 2; /* 2^60 + 2 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976) + 2; /* 2^60 + 2 */ + values[num_values++] = val; + + val += UINT64CONST(1152921504606846976); /* 2^60 */ + values[num_values++] = val; + + /* + * We're now very close to 2^64, so can't add large values anymore. But + * add more smaller values to the end, to make sure that all the above + * values get flushed and packed into the tree structure. + */ + while (num_values < 1000) + { + val += pg_prng_uint32(&pg_global_prng_state); + values[num_values++] = val; + } + + /* Create an IntegerSet using these values */ + intset = intset_create(); + for (int i = 0; i < num_values; i++) + intset_add_member(intset, values[i]); + + /* + * Test intset_is_member() around each of these values + */ + for (int i = 0; i < num_values; i++) + { + uint64 x = values[i]; + bool expected; + bool result; + + if (x > 0) + { + expected = (values[i - 1] == x - 1); + result = intset_is_member(intset, x - 1); + if (result != expected) + elog(ERROR, "intset_is_member failed for " UINT64_FORMAT, x - 1); + } + + result = intset_is_member(intset, x); + if (result != true) + elog(ERROR, "intset_is_member failed for " UINT64_FORMAT, x); + + expected = (i != num_values - 1) ? (values[i + 1] == x + 1) : false; + result = intset_is_member(intset, x + 1); + if (result != expected) + elog(ERROR, "intset_is_member failed for " UINT64_FORMAT, x + 1); + } + + /* + * Test iterator + */ + intset_begin_iterate(intset); + for (int i = 0; i < num_values; i++) + { + found = intset_iterate_next(intset, &x); + if (!found || x != values[i]) + elog(ERROR, "intset_iterate_next failed for " UINT64_FORMAT, x); + } + found = intset_iterate_next(intset, &x); + if (found) + elog(ERROR, "intset_iterate_next failed " UINT64_FORMAT, x); +} diff --git a/src/test/modules/test_integerset/test_integerset.control b/src/test/modules/test_integerset/test_integerset.control new file mode 100644 index 0000000..7d20c2d --- /dev/null +++ b/src/test/modules/test_integerset/test_integerset.control @@ -0,0 +1,4 @@ +comment = 'Test code for integerset' +default_version = '1.0' +module_pathname = '$libdir/test_integerset' +relocatable = true diff --git a/src/test/modules/test_misc/.gitignore b/src/test/modules/test_misc/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_misc/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile new file mode 100644 index 0000000..39c6c20 --- /dev/null +++ b/src/test/modules/test_misc/Makefile @@ -0,0 +1,14 @@ +# src/test/modules/test_misc/Makefile + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_misc +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_misc/README b/src/test/modules/test_misc/README new file mode 100644 index 0000000..4876733 --- /dev/null +++ b/src/test/modules/test_misc/README @@ -0,0 +1,4 @@ +This directory doesn't actually contain any extension module. + +What it is is a home for otherwise-unclassified TAP tests that exercise core +server features. We might equally well have called it, say, src/test/misc. diff --git a/src/test/modules/test_misc/t/001_constraint_validation.pl b/src/test/modules/test_misc/t/001_constraint_validation.pl new file mode 100644 index 0000000..3b9fc66 --- /dev/null +++ b/src/test/modules/test_misc/t/001_constraint_validation.pl @@ -0,0 +1,315 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Verify that ALTER TABLE optimizes certain operations as expected + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize a test cluster +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init(); +# Turn message level up to DEBUG1 so that we get the messages we want to see +$node->append_conf('postgresql.conf', 'client_min_messages = DEBUG1'); +$node->start; + +# Run a SQL command and return psql's stderr (including debug messages) +sub run_sql_command +{ + my $sql = shift; + my $stderr; + + $node->psql( + 'postgres', + $sql, + stderr => \$stderr, + on_error_die => 1, + on_error_stop => 1); + return $stderr; +} + +# Check whether result of run_sql_command shows that we did a verify pass +sub is_table_verified +{ + my $output = shift; + return index($output, 'DEBUG: verifying table') != -1; +} + +my $output; + +note "test alter table set not null"; + +run_sql_command( + 'create table atacc1 (test_a int, test_b int); + insert into atacc1 values (1, 2);'); + +$output = run_sql_command('alter table atacc1 alter test_a set not null;'); +ok(is_table_verified($output), + 'column test_a without constraint will scan table'); + +run_sql_command( + 'alter table atacc1 alter test_a drop not null; + alter table atacc1 add constraint atacc1_constr_a_valid + check(test_a is not null);'); + +# normal run will verify table data +$output = run_sql_command('alter table atacc1 alter test_a set not null;'); +ok(!is_table_verified($output), 'with constraint will not scan table'); +ok( $output =~ + m/existing constraints on column "atacc1.test_a" are sufficient to prove that it does not contain nulls/, + 'test_a proved by constraints'); + +run_sql_command('alter table atacc1 alter test_a drop not null;'); + +# we have check only for test_a column, so we need verify table for test_b +$output = run_sql_command( + 'alter table atacc1 alter test_b set not null, alter test_a set not null;' +); +ok(is_table_verified($output), 'table was scanned'); +# we may miss debug message for test_a constraint because we need verify table due test_b +ok( !( $output =~ + m/existing constraints on column "atacc1.test_b" are sufficient to prove that it does not contain nulls/ + ), + 'test_b not proved by wrong constraints'); +run_sql_command( + 'alter table atacc1 alter test_a drop not null, alter test_b drop not null;' +); + +# test with both columns having check constraints +run_sql_command( + 'alter table atacc1 add constraint atacc1_constr_b_valid check(test_b is not null);' +); +$output = run_sql_command( + 'alter table atacc1 alter test_b set not null, alter test_a set not null;' +); +ok(!is_table_verified($output), 'table was not scanned for both columns'); +ok( $output =~ + m/existing constraints on column "atacc1.test_a" are sufficient to prove that it does not contain nulls/, + 'test_a proved by constraints'); +ok( $output =~ + m/existing constraints on column "atacc1.test_b" are sufficient to prove that it does not contain nulls/, + 'test_b proved by constraints'); +run_sql_command('drop table atacc1;'); + +note "test alter table attach partition"; + +run_sql_command( + 'CREATE TABLE list_parted2 ( + a int, + b char + ) PARTITION BY LIST (a); + CREATE TABLE part_3_4 ( + LIKE list_parted2, + CONSTRAINT check_a CHECK (a IN (3)));'); + +# need NOT NULL to skip table scan +$output = run_sql_command( + 'ALTER TABLE list_parted2 ATTACH PARTITION part_3_4 FOR VALUES IN (3, 4);' +); +ok(is_table_verified($output), 'table part_3_4 scanned'); + +run_sql_command( + 'ALTER TABLE list_parted2 DETACH PARTITION part_3_4; + ALTER TABLE part_3_4 ALTER a SET NOT NULL;'); + +$output = run_sql_command( + 'ALTER TABLE list_parted2 ATTACH PARTITION part_3_4 FOR VALUES IN (3, 4);' +); +ok(!is_table_verified($output), 'table part_3_4 not scanned'); +ok( $output =~ + m/partition constraint for table "part_3_4" is implied by existing constraints/, + 'part_3_4 verified by existing constraints'); + +# test attach default partition +run_sql_command( + 'CREATE TABLE list_parted2_def ( + LIKE list_parted2, + CONSTRAINT check_a CHECK (a IN (5, 6)));'); +$output = run_sql_command( + 'ALTER TABLE list_parted2 ATTACH PARTITION list_parted2_def default;'); +ok(!is_table_verified($output), 'table list_parted2_def not scanned'); +ok( $output =~ + m/partition constraint for table "list_parted2_def" is implied by existing constraints/, + 'list_parted2_def verified by existing constraints'); + +$output = run_sql_command( + 'CREATE TABLE part_55_66 PARTITION OF list_parted2 FOR VALUES IN (55, 66);' +); +ok(!is_table_verified($output), 'table list_parted2_def not scanned'); +ok( $output =~ + m/updated partition constraint for default partition "list_parted2_def" is implied by existing constraints/, + 'updated partition constraint for default partition list_parted2_def'); + +# test attach another partitioned table +run_sql_command( + 'CREATE TABLE part_5 ( + LIKE list_parted2 + ) PARTITION BY LIST (b); + CREATE TABLE part_5_a PARTITION OF part_5 FOR VALUES IN (\'a\'); + ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 5);' +); +$output = run_sql_command( + 'ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5);'); +ok(!($output =~ m/verifying table "part_5"/), 'table part_5 not scanned'); +ok($output =~ m/verifying table "list_parted2_def"/, + 'list_parted2_def scanned'); +ok( $output =~ + m/partition constraint for table "part_5" is implied by existing constraints/, + 'part_5 verified by existing constraints'); + +run_sql_command( + 'ALTER TABLE list_parted2 DETACH PARTITION part_5; + ALTER TABLE part_5 DROP CONSTRAINT check_a;'); + +# scan should again be skipped, even though NOT NULL is now a column property +run_sql_command( + 'ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5)), + ALTER a SET NOT NULL;' +); +$output = run_sql_command( + 'ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5);'); +ok(!($output =~ m/verifying table "part_5"/), 'table part_5 not scanned'); +ok($output =~ m/verifying table "list_parted2_def"/, + 'list_parted2_def scanned'); +ok( $output =~ + m/partition constraint for table "part_5" is implied by existing constraints/, + 'part_5 verified by existing constraints'); + +# Check the case where attnos of the partitioning columns in the table being +# attached differs from the parent. It should not affect the constraint- +# checking logic that allows to skip the scan. +run_sql_command( + 'CREATE TABLE part_6 ( + c int, + LIKE list_parted2, + CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 6) + ); + ALTER TABLE part_6 DROP c;'); +$output = run_sql_command( + 'ALTER TABLE list_parted2 ATTACH PARTITION part_6 FOR VALUES IN (6);'); +ok(!($output =~ m/verifying table "part_6"/), 'table part_6 not scanned'); +ok($output =~ m/verifying table "list_parted2_def"/, + 'list_parted2_def scanned'); +ok( $output =~ + m/partition constraint for table "part_6" is implied by existing constraints/, + 'part_6 verified by existing constraints'); + +# Similar to above, but the table being attached is a partitioned table +# whose partition has still different attnos for the root partitioning +# columns. +run_sql_command( + 'CREATE TABLE part_7 ( + LIKE list_parted2, + CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7) + ) PARTITION BY LIST (b); + CREATE TABLE part_7_a_null ( + c int, + d int, + e int, + LIKE list_parted2, -- a will have attnum = 4 + CONSTRAINT check_b CHECK (b IS NULL OR b = \'a\'), + CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7) + ); + ALTER TABLE part_7_a_null DROP c, DROP d, DROP e;'); + +$output = run_sql_command( + 'ALTER TABLE part_7 ATTACH PARTITION part_7_a_null FOR VALUES IN (\'a\', null);' +); +ok(!is_table_verified($output), 'table not scanned'); +ok( $output =~ + m/partition constraint for table "part_7_a_null" is implied by existing constraints/, + 'part_7_a_null verified by existing constraints'); +$output = run_sql_command( + 'ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);'); +ok(!is_table_verified($output), 'tables not scanned'); +ok( $output =~ + m/partition constraint for table "part_7" is implied by existing constraints/, + 'part_7 verified by existing constraints'); +ok( $output =~ + m/updated partition constraint for default partition "list_parted2_def" is implied by existing constraints/, + 'updated partition constraint for default partition list_parted2_def'); + +run_sql_command( + 'CREATE TABLE range_parted ( + a int, + b int + ) PARTITION BY RANGE (a, b); + CREATE TABLE range_part1 ( + a int NOT NULL CHECK (a = 1), + b int NOT NULL);'); + +$output = run_sql_command( + 'ALTER TABLE range_parted ATTACH PARTITION range_part1 FOR VALUES FROM (1, 1) TO (1, 10);' +); +ok(is_table_verified($output), 'table range_part1 scanned'); +ok( !( $output =~ + m/partition constraint for table "range_part1" is implied by existing constraints/ + ), + 'range_part1 not verified by existing constraints'); + +run_sql_command( + 'CREATE TABLE range_part2 ( + a int NOT NULL CHECK (a = 1), + b int NOT NULL CHECK (b >= 10 and b < 18) +);'); +$output = run_sql_command( + 'ALTER TABLE range_parted ATTACH PARTITION range_part2 FOR VALUES FROM (1, 10) TO (1, 20);' +); +ok(!is_table_verified($output), 'table range_part2 not scanned'); +ok( $output =~ + m/partition constraint for table "range_part2" is implied by existing constraints/, + 'range_part2 verified by existing constraints'); + +# If a partitioned table being created or an existing table being attached +# as a partition does not have a constraint that would allow validation scan +# to be skipped, but an individual partition does, then the partition's +# validation scan is skipped. +run_sql_command( + 'CREATE TABLE quuux (a int, b text) PARTITION BY LIST (a); + CREATE TABLE quuux_default PARTITION OF quuux DEFAULT PARTITION BY LIST (b); + CREATE TABLE quuux_default1 PARTITION OF quuux_default ( + CONSTRAINT check_1 CHECK (a IS NOT NULL AND a = 1) + ) FOR VALUES IN (\'b\'); + CREATE TABLE quuux1 (a int, b text);'); + +$output = run_sql_command( + 'ALTER TABLE quuux ATTACH PARTITION quuux1 FOR VALUES IN (1);'); +ok(is_table_verified($output), 'quuux1 table scanned'); +ok( !( $output =~ + m/partition constraint for table "quuux1" is implied by existing constraints/ + ), + 'quuux1 verified by existing constraints'); + +run_sql_command('CREATE TABLE quuux2 (a int, b text);'); +$output = run_sql_command( + 'ALTER TABLE quuux ATTACH PARTITION quuux2 FOR VALUES IN (2);'); +ok(!($output =~ m/verifying table "quuux_default1"/), + 'quuux_default1 not scanned'); +ok($output =~ m/verifying table "quuux2"/, 'quuux2 scanned'); +ok( $output =~ + m/updated partition constraint for default partition "quuux_default1" is implied by existing constraints/, + 'updated partition constraint for default partition quuux_default1'); +run_sql_command('DROP TABLE quuux1, quuux2;'); + +# should validate for quuux1, but not for quuux2 +$output = run_sql_command( + 'CREATE TABLE quuux1 PARTITION OF quuux FOR VALUES IN (1);'); +ok(!is_table_verified($output), 'tables not scanned'); +ok( !( $output =~ + m/partition constraint for table "quuux1" is implied by existing constraints/ + ), + 'quuux1 verified by existing constraints'); +$output = run_sql_command( + 'CREATE TABLE quuux2 PARTITION OF quuux FOR VALUES IN (2);'); +ok(!is_table_verified($output), 'tables not scanned'); +ok( $output =~ + m/updated partition constraint for default partition "quuux_default1" is implied by existing constraints/, + 'updated partition constraint for default partition quuux_default1'); +run_sql_command('DROP TABLE quuux;'); + +$node->stop('fast'); + +done_testing(); diff --git a/src/test/modules/test_misc/t/002_tablespace.pl b/src/test/modules/test_misc/t/002_tablespace.pl new file mode 100644 index 0000000..95cd2b7 --- /dev/null +++ b/src/test/modules/test_misc/t/002_tablespace.pl @@ -0,0 +1,95 @@ +# Simple tablespace tests that can't be replicated on the same host +# due to the use of absolute paths, so we keep them out of the regular +# regression tests. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +# Create a couple of directories to use as tablespaces. +my $basedir = $node->basedir(); +my $TS1_LOCATION = "$basedir/ts1"; +$TS1_LOCATION =~ s/\/\.\//\//g; # collapse foo/./bar to foo/bar +mkdir($TS1_LOCATION); +my $TS2_LOCATION = "$basedir/ts2"; +$TS2_LOCATION =~ s/\/\.\//\//g; +mkdir($TS2_LOCATION); + +my $result; + +# Create a tablespace with an absolute path +$result = $node->psql('postgres', + "CREATE TABLESPACE regress_ts1 LOCATION '$TS1_LOCATION'"); +ok($result == 0, 'create tablespace with absolute path'); + +# Can't create a tablespace where there is one already +$result = $node->psql('postgres', + "CREATE TABLESPACE regress_ts1 LOCATION '$TS1_LOCATION'"); +ok($result != 0, 'clobber tablespace with absolute path'); + +# Create table in it +$result = $node->psql('postgres', "CREATE TABLE t () TABLESPACE regress_ts1"); +ok($result == 0, 'create table in tablespace with absolute path'); + +# Can't drop a tablespace that still has a table in it +$result = $node->psql('postgres', "DROP TABLESPACE regress_ts1"); +ok($result != 0, 'drop tablespace with absolute path'); + +# Drop the table +$result = $node->psql('postgres', "DROP TABLE t"); +ok($result == 0, 'drop table in tablespace with absolute path'); + +# Drop the tablespace +$result = $node->psql('postgres', "DROP TABLESPACE regress_ts1"); +ok($result == 0, 'drop tablespace with absolute path'); + +# Create two absolute tablespaces and two in-place tablespaces, so we can +# testing various kinds of tablespace moves. +$result = $node->psql('postgres', + "CREATE TABLESPACE regress_ts1 LOCATION '$TS1_LOCATION'"); +ok($result == 0, 'create tablespace 1 with absolute path'); +$result = $node->psql('postgres', + "CREATE TABLESPACE regress_ts2 LOCATION '$TS2_LOCATION'"); +ok($result == 0, 'create tablespace 2 with absolute path'); +$result = $node->psql('postgres', + "SET allow_in_place_tablespaces=on; CREATE TABLESPACE regress_ts3 LOCATION ''" +); +ok($result == 0, 'create tablespace 3 with in-place directory'); +$result = $node->psql('postgres', + "SET allow_in_place_tablespaces=on; CREATE TABLESPACE regress_ts4 LOCATION ''" +); +ok($result == 0, 'create tablespace 4 with in-place directory'); + +# Create a table and test moving between absolute and in-place tablespaces +$result = $node->psql('postgres', "CREATE TABLE t () TABLESPACE regress_ts1"); +ok($result == 0, 'create table in tablespace 1'); +$result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts2"); +ok($result == 0, 'move table abs->abs'); +$result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts3"); +ok($result == 0, 'move table abs->in-place'); +$result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts4"); +ok($result == 0, 'move table in-place->in-place'); +$result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts1"); +ok($result == 0, 'move table in-place->abs'); + +# Drop everything +$result = $node->psql('postgres', "DROP TABLE t"); +ok($result == 0, 'create table in tablespace 1'); +$result = $node->psql('postgres', "DROP TABLESPACE regress_ts1"); +ok($result == 0, 'drop tablespace 1'); +$result = $node->psql('postgres', "DROP TABLESPACE regress_ts2"); +ok($result == 0, 'drop tablespace 2'); +$result = $node->psql('postgres', "DROP TABLESPACE regress_ts3"); +ok($result == 0, 'drop tablespace 3'); +$result = $node->psql('postgres', "DROP TABLESPACE regress_ts4"); +ok($result == 0, 'drop tablespace 4'); + +$node->stop; + +done_testing(); diff --git a/src/test/modules/test_misc/t/003_check_guc.pl b/src/test/modules/test_misc/t/003_check_guc.pl new file mode 100644 index 0000000..20c2dc3 --- /dev/null +++ b/src/test/modules/test_misc/t/003_check_guc.pl @@ -0,0 +1,110 @@ +# Tests to cross-check the consistency of GUC parameters with +# postgresql.conf.sample. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +# Grab the names of all the parameters that can be listed in the +# configuration sample file. config_file is an exception, it is not +# in postgresql.conf.sample but is part of the lists from guc.c. +# Custom GUCs loaded by extensions are excluded. +my $all_params = $node->safe_psql( + 'postgres', + "SELECT name + FROM pg_settings + WHERE NOT 'NOT_IN_SAMPLE' = ANY (pg_settings_get_flags(name)) AND + name <> 'config_file' AND category <> 'Customized Options' + ORDER BY 1"); +# Note the lower-case conversion, for consistency. +my @all_params_array = split("\n", lc($all_params)); + +# Grab the names of all parameters marked as NOT_IN_SAMPLE. +my $not_in_sample = $node->safe_psql( + 'postgres', + "SELECT name + FROM pg_settings + WHERE 'NOT_IN_SAMPLE' = ANY (pg_settings_get_flags(name)) + ORDER BY 1"); +my @not_in_sample_array = split("\n", lc($not_in_sample)); + +# use the sample file from the temp install +my $share_dir = $node->config_data('--sharedir'); +my $sample_file = "$share_dir/postgresql.conf.sample"; + +# List of all the GUCs found in the sample file. +my @gucs_in_file; + +# Read the sample file line-by-line, checking its contents to build a list +# of everything known as a GUC. +my $num_tests = 0; +open(my $contents, '<', $sample_file) + || die "Could not open $sample_file: $!"; +while (my $line = <$contents>) +{ + # Check if this line matches a GUC parameter: + # - Each parameter is preceded by "#", but not "# " in the sample + # file. + # - Valid configuration options are followed immediately by " = ", + # with one space before and after the equal sign. + if ($line =~ m/^#?([_[:alpha:]]+) = .*/) + { + # Lower-case conversion matters for some of the GUCs. + my $param_name = lc($1); + + # Ignore some exceptions. + next if $param_name eq "include"; + next if $param_name eq "include_dir"; + next if $param_name eq "include_if_exists"; + + # Update the list of GUCs found in the sample file, for the + # follow-up tests. + push @gucs_in_file, $param_name; + } +} + +close $contents; + +# Cross-check that all the GUCs found in the sample file match the ones +# fetched above. This maps the arrays to a hash, making the creation of +# each exclude and intersection list easier. +my %gucs_in_file_hash = map { $_ => 1 } @gucs_in_file; +my %all_params_hash = map { $_ => 1 } @all_params_array; +my %not_in_sample_hash = map { $_ => 1 } @not_in_sample_array; + +my @missing_from_file = grep(!$gucs_in_file_hash{$_}, @all_params_array); +is(scalar(@missing_from_file), + 0, "no parameters missing from postgresql.conf.sample"); + +my @missing_from_list = grep(!$all_params_hash{$_}, @gucs_in_file); +is(scalar(@missing_from_list), 0, "no parameters missing from guc.c"); + +my @sample_intersect = grep($not_in_sample_hash{$_}, @gucs_in_file); +is(scalar(@sample_intersect), + 0, "no parameters marked as NOT_IN_SAMPLE in postgresql.conf.sample"); + +# These would log some information only on errors. +foreach my $param (@missing_from_file) +{ + print("found GUC $param in guc.c, missing from postgresql.conf.sample\n"); +} +foreach my $param (@missing_from_list) +{ + print( + "found GUC $param in postgresql.conf.sample, with incorrect info in guc.c\n" + ); +} +foreach my $param (@sample_intersect) +{ + print( + "found GUC $param in postgresql.conf.sample, marked as NOT_IN_SAMPLE\n" + ); +} + +done_testing(); diff --git a/src/test/modules/test_oat_hooks/.gitignore b/src/test/modules/test_oat_hooks/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_oat_hooks/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_oat_hooks/Makefile b/src/test/modules/test_oat_hooks/Makefile new file mode 100644 index 0000000..2b5d268 --- /dev/null +++ b/src/test/modules/test_oat_hooks/Makefile @@ -0,0 +1,26 @@ +# src/test/modules/test_oat_hooks/Makefile + +MODULE_big = test_oat_hooks +OBJS = \ + $(WIN32RES) \ + test_oat_hooks.o +PGFILEDESC = "test_oat_hooks - example use of object access hooks" + +REGRESS = test_oat_hooks + +# disable installcheck for now +NO_INSTALLCHECK = 1 +# and also for now force NO_LOCALE and UTF8 +ENCODING = UTF8 +NO_LOCALE = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_oat_hooks +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_oat_hooks/README b/src/test/modules/test_oat_hooks/README new file mode 100644 index 0000000..70c792a --- /dev/null +++ b/src/test/modules/test_oat_hooks/README @@ -0,0 +1,86 @@ +OVERVIEW +======== + +This test module, "test_oat_hooks", is an example of how to use the object +access hooks (OAT) to enforce mandatory access controls (MAC). + +The testing strategy is as follows: When this module loads, it registers hooks +of various types. (See below.) GUCs are defined to control each hook, +determining whether the hook allows or denies actions for which it fires. A +single additional GUC controls the verbosity of the hooks. GUCs default to +permissive/quiet, which allows the module to load without generating noise in +the log or denying any activity in the run-up to the regression test beginning. +When the test begins, it uses SET commands to turn on logging and to control +each hook's permissive/restrictive behavior. Various SQL statements are run +under both superuser and ordinary user permissions. The output is compared +against the expected output to verify that the hooks behaved and fired in the +order by expect. + +Because users may care about the firing order of other system hooks relative to +OAT hooks, ProcessUtility hooks and ExecutorCheckPerms hooks are also +registered by this module, with their own logging and allow/deny behavior. + + +SUSET test configuration GUCs +============================= + +The following configuration parameters (GUCs) control this test module's Object +Access Type (OAT), Process Utility and Executor Check Permissions hooks. The +general pattern is that each hook has a corresponding GUC which controls +whether the hook will allow or deny operations for which the hook gets called. +A real-world OAT hook should certainly provide more fine-grained control than +merely "allow-all" vs. "deny-all", but for testing this is sufficient. + +Note that even when these hooks allow an action, the core permissions system +may still refuse the action. The firing order of the hooks relative to the +core permissions system can be inferred from which NOTICE messages get emitted +before an action is refused. + +Each hook applies the allow vs. deny setting to all operations performed by +non-superusers. + +- test_oat_hooks.deny_set_variable + + Controls whether the object_access_hook_str MAC function rejects attempts to + set a configuration parameter. + +- test_oat_hooks.deny_alter_system + + Controls whether the object_access_hook_str MAC function rejects attempts to + alter system set a configuration parameter. + +- test_oat_hooks.deny_object_access + + Controls whether the object_access_hook MAC function rejects all operations + for which it is called. + +- test_oat_hooks.deny_exec_perms + + Controls whether the exec_check_perms MAC function rejects all operations for + which it is called. + +- test_oat_hooks.deny_utility_commands + + Controls whether the ProcessUtility_hook function rejects all operations for + which it is called. + +- test_oat_hooks.audit + + Controls whether each hook logs NOTICE messages for each attempt, along with + success or failure status. Note that clearing or setting this GUC may itself + generate NOTICE messages appearing before but not after, or after but not + before, the new setting takes effect. + + +Functions +========= + +The module registers hooks by the following names: + +- REGRESS_object_access_hook + +- REGRESS_object_access_hook_str + +- REGRESS_exec_check_perms + +- REGRESS_utility_command diff --git a/src/test/modules/test_oat_hooks/expected/test_oat_hooks.out b/src/test/modules/test_oat_hooks/expected/test_oat_hooks.out new file mode 100644 index 0000000..194ee7d --- /dev/null +++ b/src/test/modules/test_oat_hooks/expected/test_oat_hooks.out @@ -0,0 +1,309 @@ +-- This test script fails if debug_discard_caches is enabled, because cache +-- flushes cause extra calls of the OAT hook in recomputeNamespacePath, +-- resulting in more NOTICE messages than are in the expected output. +SET debug_discard_caches = 0; +-- Creating privileges on a placeholder GUC should create entries in the +-- pg_parameter_acl catalog which conservatively grant no privileges to public. +CREATE ROLE regress_role_joe; +GRANT SET ON PARAMETER test_oat_hooks.user_var1 TO regress_role_joe; +GRANT SET ON PARAMETER test_oat_hooks.super_var1 TO regress_role_joe; +-- SET commands fire both the ProcessUtility_hook and the +-- object_access_hook_str. Since the auditing GUC starts out false, we miss the +-- initial "attempting" audit message from the ProcessUtility_hook, but we +-- should thereafter see the audit messages. +LOAD 'test_oat_hooks'; +SET test_oat_hooks.audit = true; +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [test_oat_hooks.audit] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [test_oat_hooks.audit] +NOTICE: in process utility: superuser finished SET +-- Creating privileges on an existent custom GUC should create precisely the +-- right privileges, not overly conservative ones. +GRANT SET ON PARAMETER test_oat_hooks.user_var2 TO regress_role_joe; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +GRANT SET ON PARAMETER test_oat_hooks.super_var2 TO regress_role_joe; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +-- Granting multiple privileges on a parameter should be reported correctly to +-- the OAT hook, but beware that WITH GRANT OPTION is not represented. +GRANT SET, ALTER SYSTEM ON PARAMETER none.such TO regress_role_joe; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +GRANT SET, ALTER SYSTEM ON PARAMETER another.bogus TO regress_role_joe WITH GRANT OPTION; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +-- Check when the hooks fire relative to dependency based abort of a drop +DROP ROLE regress_role_joe; +NOTICE: in process utility: superuser attempting DROP ROLE +NOTICE: in object access: superuser attempting drop (subId=0x0) [] +NOTICE: in object access: superuser finished drop (subId=0x0) [] +ERROR: role "regress_role_joe" cannot be dropped because some objects depend on it +DETAIL: privileges for parameter test_oat_hooks.user_var1 +privileges for parameter test_oat_hooks.super_var1 +privileges for parameter test_oat_hooks.user_var2 +privileges for parameter test_oat_hooks.super_var2 +privileges for parameter none.such +privileges for parameter another.bogus +-- Check the behavior of the hooks relative to do-nothing grants and revokes +GRANT SET ON PARAMETER maintenance_work_mem TO PUBLIC; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +REVOKE SET ON PARAMETER maintenance_work_mem FROM PUBLIC; +NOTICE: in process utility: superuser attempting REVOKE +NOTICE: in process utility: superuser finished REVOKE +REVOKE ALTER SYSTEM ON PARAMETER maintenance_work_mem FROM PUBLIC; +NOTICE: in process utility: superuser attempting REVOKE +NOTICE: in process utility: superuser finished REVOKE +-- Check the behavior of the hooks relative to unrecognized parameters +GRANT ALL ON PARAMETER "none.such" TO PUBLIC; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +-- Check relative to an operation that causes the catalog entry to be deleted +REVOKE ALL ON PARAMETER "none.such" FROM PUBLIC; +NOTICE: in process utility: superuser attempting REVOKE +NOTICE: in process utility: superuser finished REVOKE +-- Create objects for use in the test +CREATE USER regress_test_user; +NOTICE: in process utility: superuser attempting CREATE ROLE +NOTICE: in object access: superuser attempting create (subId=0x0) [explicit] +NOTICE: in object access: superuser finished create (subId=0x0) [explicit] +NOTICE: in process utility: superuser finished CREATE ROLE +CREATE TABLE regress_test_table (t text); +NOTICE: in process utility: superuser attempting CREATE TABLE +NOTICE: in object access: superuser attempting namespace search (subId=0x0) [no report on violation, allowed] +LINE 1: CREATE TABLE regress_test_table (t text); + ^ +NOTICE: in object access: superuser finished namespace search (subId=0x0) [no report on violation, allowed] +LINE 1: CREATE TABLE regress_test_table (t text); + ^ +NOTICE: in object access: superuser attempting create (subId=0x0) [explicit] +NOTICE: in object access: superuser finished create (subId=0x0) [explicit] +NOTICE: in object access: superuser attempting create (subId=0x0) [explicit] +NOTICE: in object access: superuser finished create (subId=0x0) [explicit] +NOTICE: in object access: superuser attempting create (subId=0x0) [explicit] +NOTICE: in object access: superuser finished create (subId=0x0) [explicit] +NOTICE: in object access: superuser attempting create (subId=0x0) [internal] +NOTICE: in object access: superuser finished create (subId=0x0) [internal] +NOTICE: in object access: superuser attempting create (subId=0x0) [internal] +NOTICE: in object access: superuser finished create (subId=0x0) [internal] +NOTICE: in process utility: superuser finished CREATE TABLE +GRANT SELECT ON Table regress_test_table TO public; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +CREATE FUNCTION regress_test_func (t text) RETURNS text AS $$ + SELECT $1; +$$ LANGUAGE sql; +NOTICE: in process utility: superuser attempting CREATE FUNCTION +NOTICE: in object access: superuser attempting create (subId=0x0) [explicit] +NOTICE: in object access: superuser finished create (subId=0x0) [explicit] +NOTICE: in process utility: superuser finished CREATE FUNCTION +GRANT EXECUTE ON FUNCTION regress_test_func (text) TO public; +NOTICE: in process utility: superuser attempting GRANT +NOTICE: in process utility: superuser finished GRANT +-- Do a few things as superuser +SELECT * FROM regress_test_table; +NOTICE: in executor check perms: superuser attempting execute +NOTICE: in executor check perms: superuser finished execute + t +--- +(0 rows) + +SELECT regress_test_func('arg'); +NOTICE: in executor check perms: superuser attempting execute +NOTICE: in executor check perms: superuser finished execute + regress_test_func +------------------- + arg +(1 row) + +SET work_mem = 8192; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [work_mem] +NOTICE: in process utility: superuser finished SET +RESET work_mem; +NOTICE: in process utility: superuser attempting RESET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [work_mem] +NOTICE: in process utility: superuser finished RESET +ALTER SYSTEM SET work_mem = 8192; +NOTICE: in process utility: superuser attempting ALTER SYSTEM +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x2000, alter system) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x2000, alter system) [work_mem] +NOTICE: in process utility: superuser finished ALTER SYSTEM +ALTER SYSTEM RESET work_mem; +NOTICE: in process utility: superuser attempting ALTER SYSTEM +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x2000, alter system) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x2000, alter system) [work_mem] +NOTICE: in process utility: superuser finished ALTER SYSTEM +-- Do those same things as non-superuser +SET SESSION AUTHORIZATION regress_test_user; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: non-superuser attempting alter (subId=0x1000, set) [session_authorization] +NOTICE: in object_access_hook_str: non-superuser finished alter (subId=0x1000, set) [session_authorization] +NOTICE: in process utility: non-superuser finished SET +SELECT * FROM regress_test_table; +NOTICE: in object access: non-superuser attempting namespace search (subId=0x0) [no report on violation, allowed] +LINE 1: SELECT * FROM regress_test_table; + ^ +NOTICE: in object access: non-superuser finished namespace search (subId=0x0) [no report on violation, allowed] +LINE 1: SELECT * FROM regress_test_table; + ^ +NOTICE: in executor check perms: non-superuser attempting execute +NOTICE: in executor check perms: non-superuser finished execute + t +--- +(0 rows) + +SELECT regress_test_func('arg'); +NOTICE: in executor check perms: non-superuser attempting execute +NOTICE: in executor check perms: non-superuser finished execute + regress_test_func +------------------- + arg +(1 row) + +SET work_mem = 8192; +NOTICE: in process utility: non-superuser attempting SET +NOTICE: in object_access_hook_str: non-superuser attempting alter (subId=0x1000, set) [work_mem] +NOTICE: in object_access_hook_str: non-superuser finished alter (subId=0x1000, set) [work_mem] +NOTICE: in process utility: non-superuser finished SET +RESET work_mem; +NOTICE: in process utility: non-superuser attempting RESET +NOTICE: in object_access_hook_str: non-superuser attempting alter (subId=0x1000, set) [work_mem] +NOTICE: in object_access_hook_str: non-superuser finished alter (subId=0x1000, set) [work_mem] +NOTICE: in process utility: non-superuser finished RESET +ALTER SYSTEM SET work_mem = 8192; +NOTICE: in process utility: non-superuser attempting ALTER SYSTEM +ERROR: permission denied to set parameter "work_mem" +ALTER SYSTEM RESET work_mem; +NOTICE: in process utility: non-superuser attempting ALTER SYSTEM +ERROR: permission denied to set parameter "work_mem" +SET test_oat_hooks.user_var1 = true; +NOTICE: in process utility: non-superuser attempting SET +NOTICE: in object_access_hook_str: non-superuser attempting alter (subId=0x1000, set) [test_oat_hooks.user_var1] +NOTICE: in object_access_hook_str: non-superuser finished alter (subId=0x1000, set) [test_oat_hooks.user_var1] +NOTICE: in process utility: non-superuser finished SET +SET test_oat_hooks.super_var1 = true; +NOTICE: in process utility: non-superuser attempting SET +ERROR: permission denied to set parameter "test_oat_hooks.super_var1" +ALTER SYSTEM SET test_oat_hooks.user_var1 = true; +NOTICE: in process utility: non-superuser attempting ALTER SYSTEM +ERROR: permission denied to set parameter "test_oat_hooks.user_var1" +ALTER SYSTEM SET test_oat_hooks.super_var1 = true; +NOTICE: in process utility: non-superuser attempting ALTER SYSTEM +ERROR: permission denied to set parameter "test_oat_hooks.super_var1" +SET test_oat_hooks.user_var2 = true; +NOTICE: in process utility: non-superuser attempting SET +NOTICE: in object_access_hook_str: non-superuser attempting alter (subId=0x1000, set) [test_oat_hooks.user_var2] +NOTICE: in object_access_hook_str: non-superuser finished alter (subId=0x1000, set) [test_oat_hooks.user_var2] +NOTICE: in process utility: non-superuser finished SET +SET test_oat_hooks.super_var2 = true; +NOTICE: in process utility: non-superuser attempting SET +ERROR: permission denied to set parameter "test_oat_hooks.super_var2" +ALTER SYSTEM SET test_oat_hooks.user_var2 = true; +NOTICE: in process utility: non-superuser attempting ALTER SYSTEM +ERROR: permission denied to set parameter "test_oat_hooks.user_var2" +ALTER SYSTEM SET test_oat_hooks.super_var2 = true; +NOTICE: in process utility: non-superuser attempting ALTER SYSTEM +ERROR: permission denied to set parameter "test_oat_hooks.super_var2" +RESET SESSION AUTHORIZATION; +NOTICE: in process utility: non-superuser attempting RESET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [session_authorization] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [session_authorization] +NOTICE: in process utility: superuser finished RESET +-- Turn off non-superuser permissions +SET test_oat_hooks.deny_set_variable = true; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [test_oat_hooks.deny_set_variable] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [test_oat_hooks.deny_set_variable] +NOTICE: in process utility: superuser finished SET +SET test_oat_hooks.deny_alter_system = true; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [test_oat_hooks.deny_alter_system] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [test_oat_hooks.deny_alter_system] +NOTICE: in process utility: superuser finished SET +SET test_oat_hooks.deny_object_access = true; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [test_oat_hooks.deny_object_access] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [test_oat_hooks.deny_object_access] +NOTICE: in process utility: superuser finished SET +SET test_oat_hooks.deny_exec_perms = true; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [test_oat_hooks.deny_exec_perms] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [test_oat_hooks.deny_exec_perms] +NOTICE: in process utility: superuser finished SET +SET test_oat_hooks.deny_utility_commands = true; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [test_oat_hooks.deny_utility_commands] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [test_oat_hooks.deny_utility_commands] +NOTICE: in process utility: superuser finished SET +-- Try again as non-superuser with permissions denied +SET SESSION AUTHORIZATION regress_test_user; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: non-superuser attempting alter (subId=0x1000, set) [session_authorization] +ERROR: permission denied: set session_authorization +SELECT * FROM regress_test_table; +NOTICE: in object access: superuser attempting namespace search (subId=0x0) [no report on violation, allowed] +LINE 1: SELECT * FROM regress_test_table; + ^ +NOTICE: in object access: superuser finished namespace search (subId=0x0) [no report on violation, allowed] +LINE 1: SELECT * FROM regress_test_table; + ^ +NOTICE: in executor check perms: superuser attempting execute +NOTICE: in executor check perms: superuser finished execute + t +--- +(0 rows) + +SELECT regress_test_func('arg'); +NOTICE: in executor check perms: superuser attempting execute +NOTICE: in executor check perms: superuser finished execute + regress_test_func +------------------- + arg +(1 row) + +SET work_mem = 8192; +NOTICE: in process utility: superuser attempting SET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [work_mem] +NOTICE: in process utility: superuser finished SET +RESET work_mem; +NOTICE: in process utility: superuser attempting RESET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [work_mem] +NOTICE: in process utility: superuser finished RESET +ALTER SYSTEM SET work_mem = 8192; +NOTICE: in process utility: superuser attempting ALTER SYSTEM +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x2000, alter system) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x2000, alter system) [work_mem] +NOTICE: in process utility: superuser finished ALTER SYSTEM +ALTER SYSTEM RESET work_mem; +NOTICE: in process utility: superuser attempting ALTER SYSTEM +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x2000, alter system) [work_mem] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x2000, alter system) [work_mem] +NOTICE: in process utility: superuser finished ALTER SYSTEM +-- Clean up +RESET SESSION AUTHORIZATION; +NOTICE: in process utility: superuser attempting RESET +NOTICE: in object_access_hook_str: superuser attempting alter (subId=0x1000, set) [session_authorization] +NOTICE: in object_access_hook_str: superuser finished alter (subId=0x1000, set) [session_authorization] +NOTICE: in process utility: superuser finished RESET +SET test_oat_hooks.audit = false; +NOTICE: in process utility: superuser attempting SET +DROP ROLE regress_role_joe; -- fails +ERROR: role "regress_role_joe" cannot be dropped because some objects depend on it +DETAIL: privileges for parameter test_oat_hooks.user_var1 +privileges for parameter test_oat_hooks.super_var1 +privileges for parameter test_oat_hooks.user_var2 +privileges for parameter test_oat_hooks.super_var2 +privileges for parameter none.such +privileges for parameter another.bogus +REVOKE ALL PRIVILEGES ON PARAMETER + none.such, another.bogus, + test_oat_hooks.user_var1, test_oat_hooks.super_var1, + test_oat_hooks.user_var2, test_oat_hooks.super_var2 + FROM regress_role_joe; +DROP ROLE regress_role_joe; +DROP ROLE regress_test_user; diff --git a/src/test/modules/test_oat_hooks/sql/test_oat_hooks.sql b/src/test/modules/test_oat_hooks/sql/test_oat_hooks.sql new file mode 100644 index 0000000..ebbd6a1 --- /dev/null +++ b/src/test/modules/test_oat_hooks/sql/test_oat_hooks.sql @@ -0,0 +1,107 @@ +-- This test script fails if debug_discard_caches is enabled, because cache +-- flushes cause extra calls of the OAT hook in recomputeNamespacePath, +-- resulting in more NOTICE messages than are in the expected output. +SET debug_discard_caches = 0; + +-- Creating privileges on a placeholder GUC should create entries in the +-- pg_parameter_acl catalog which conservatively grant no privileges to public. +CREATE ROLE regress_role_joe; +GRANT SET ON PARAMETER test_oat_hooks.user_var1 TO regress_role_joe; +GRANT SET ON PARAMETER test_oat_hooks.super_var1 TO regress_role_joe; + +-- SET commands fire both the ProcessUtility_hook and the +-- object_access_hook_str. Since the auditing GUC starts out false, we miss the +-- initial "attempting" audit message from the ProcessUtility_hook, but we +-- should thereafter see the audit messages. +LOAD 'test_oat_hooks'; +SET test_oat_hooks.audit = true; + +-- Creating privileges on an existent custom GUC should create precisely the +-- right privileges, not overly conservative ones. +GRANT SET ON PARAMETER test_oat_hooks.user_var2 TO regress_role_joe; +GRANT SET ON PARAMETER test_oat_hooks.super_var2 TO regress_role_joe; + +-- Granting multiple privileges on a parameter should be reported correctly to +-- the OAT hook, but beware that WITH GRANT OPTION is not represented. +GRANT SET, ALTER SYSTEM ON PARAMETER none.such TO regress_role_joe; +GRANT SET, ALTER SYSTEM ON PARAMETER another.bogus TO regress_role_joe WITH GRANT OPTION; + +-- Check when the hooks fire relative to dependency based abort of a drop +DROP ROLE regress_role_joe; + +-- Check the behavior of the hooks relative to do-nothing grants and revokes +GRANT SET ON PARAMETER maintenance_work_mem TO PUBLIC; +REVOKE SET ON PARAMETER maintenance_work_mem FROM PUBLIC; +REVOKE ALTER SYSTEM ON PARAMETER maintenance_work_mem FROM PUBLIC; + +-- Check the behavior of the hooks relative to unrecognized parameters +GRANT ALL ON PARAMETER "none.such" TO PUBLIC; + +-- Check relative to an operation that causes the catalog entry to be deleted +REVOKE ALL ON PARAMETER "none.such" FROM PUBLIC; + +-- Create objects for use in the test +CREATE USER regress_test_user; +CREATE TABLE regress_test_table (t text); +GRANT SELECT ON Table regress_test_table TO public; +CREATE FUNCTION regress_test_func (t text) RETURNS text AS $$ + SELECT $1; +$$ LANGUAGE sql; +GRANT EXECUTE ON FUNCTION regress_test_func (text) TO public; + +-- Do a few things as superuser +SELECT * FROM regress_test_table; +SELECT regress_test_func('arg'); +SET work_mem = 8192; +RESET work_mem; +ALTER SYSTEM SET work_mem = 8192; +ALTER SYSTEM RESET work_mem; + +-- Do those same things as non-superuser +SET SESSION AUTHORIZATION regress_test_user; +SELECT * FROM regress_test_table; +SELECT regress_test_func('arg'); +SET work_mem = 8192; +RESET work_mem; +ALTER SYSTEM SET work_mem = 8192; +ALTER SYSTEM RESET work_mem; + +SET test_oat_hooks.user_var1 = true; +SET test_oat_hooks.super_var1 = true; +ALTER SYSTEM SET test_oat_hooks.user_var1 = true; +ALTER SYSTEM SET test_oat_hooks.super_var1 = true; +SET test_oat_hooks.user_var2 = true; +SET test_oat_hooks.super_var2 = true; +ALTER SYSTEM SET test_oat_hooks.user_var2 = true; +ALTER SYSTEM SET test_oat_hooks.super_var2 = true; + +RESET SESSION AUTHORIZATION; + +-- Turn off non-superuser permissions +SET test_oat_hooks.deny_set_variable = true; +SET test_oat_hooks.deny_alter_system = true; +SET test_oat_hooks.deny_object_access = true; +SET test_oat_hooks.deny_exec_perms = true; +SET test_oat_hooks.deny_utility_commands = true; + +-- Try again as non-superuser with permissions denied +SET SESSION AUTHORIZATION regress_test_user; +SELECT * FROM regress_test_table; +SELECT regress_test_func('arg'); +SET work_mem = 8192; +RESET work_mem; +ALTER SYSTEM SET work_mem = 8192; +ALTER SYSTEM RESET work_mem; + +-- Clean up +RESET SESSION AUTHORIZATION; + +SET test_oat_hooks.audit = false; +DROP ROLE regress_role_joe; -- fails +REVOKE ALL PRIVILEGES ON PARAMETER + none.such, another.bogus, + test_oat_hooks.user_var1, test_oat_hooks.super_var1, + test_oat_hooks.user_var2, test_oat_hooks.super_var2 + FROM regress_role_joe; +DROP ROLE regress_role_joe; +DROP ROLE regress_test_user; diff --git a/src/test/modules/test_oat_hooks/test_oat_hooks.c b/src/test/modules/test_oat_hooks/test_oat_hooks.c new file mode 100644 index 0000000..5e387fa --- /dev/null +++ b/src/test/modules/test_oat_hooks/test_oat_hooks.c @@ -0,0 +1,520 @@ +/*-------------------------------------------------------------------------- + * + * test_oat_hooks.c + * Code for testing mandatory access control (MAC) using object access hooks. + * + * Copyright (c) 2015-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_oat_hooks/test_oat_hooks.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/parallel.h" +#include "catalog/dependency.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_proc.h" +#include "executor/executor.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "tcop/utility.h" + +PG_MODULE_MAGIC; + +/* + * GUCs controlling which operations to deny + */ +static bool REGRESS_deny_set_variable = false; +static bool REGRESS_deny_alter_system = false; +static bool REGRESS_deny_object_access = false; +static bool REGRESS_deny_exec_perms = false; +static bool REGRESS_deny_utility_commands = false; +static bool REGRESS_audit = false; + +/* + * GUCs for testing privileges on USERSET and SUSET variables, + * with and without privileges granted prior to module load. + */ +static bool REGRESS_userset_variable1 = false; +static bool REGRESS_userset_variable2 = false; +static bool REGRESS_suset_variable1 = false; +static bool REGRESS_suset_variable2 = false; + +/* Saved hook values */ +static object_access_hook_type next_object_access_hook = NULL; +static object_access_hook_type_str next_object_access_hook_str = NULL; +static ExecutorCheckPerms_hook_type next_exec_check_perms_hook = NULL; +static ProcessUtility_hook_type next_ProcessUtility_hook = NULL; + +/* Test Object Access Type Hook hooks */ +static void REGRESS_object_access_hook_str(ObjectAccessType access, + Oid classId, const char *objName, + int subId, void *arg); +static void REGRESS_object_access_hook(ObjectAccessType access, Oid classId, + Oid objectId, int subId, void *arg); +static bool REGRESS_exec_check_perms(List *rangeTabls, bool do_abort); +static void REGRESS_utility_command(PlannedStmt *pstmt, + const char *queryString, bool readOnlyTree, + ProcessUtilityContext context, + ParamListInfo params, + QueryEnvironment *queryEnv, + DestReceiver *dest, QueryCompletion *qc); + +/* Helper functions */ +static char *accesstype_to_string(ObjectAccessType access, int subId); +static char *accesstype_arg_to_string(ObjectAccessType access, void *arg); + + +void _PG_init(void); + +/* + * Module load callback + */ +void +_PG_init(void) +{ + /* + * test_oat_hooks.deny_set_variable = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.deny_set_variable", + "Deny non-superuser set permissions", + NULL, + ®RESS_deny_set_variable, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + /* + * test_oat_hooks.deny_alter_system = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.deny_alter_system", + "Deny non-superuser alter system set permissions", + NULL, + ®RESS_deny_alter_system, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + /* + * test_oat_hooks.deny_object_access = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.deny_object_access", + "Deny non-superuser object access permissions", + NULL, + ®RESS_deny_object_access, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + /* + * test_oat_hooks.deny_exec_perms = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.deny_exec_perms", + "Deny non-superuser exec permissions", + NULL, + ®RESS_deny_exec_perms, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + /* + * test_oat_hooks.deny_utility_commands = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.deny_utility_commands", + "Deny non-superuser utility commands", + NULL, + ®RESS_deny_utility_commands, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + /* + * test_oat_hooks.audit = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.audit", + "Turn on/off debug audit messages", + NULL, + ®RESS_audit, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + /* + * test_oat_hooks.user_var{1,2} = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.user_var1", + "Dummy parameter settable by public", + NULL, + ®RESS_userset_variable1, + false, + PGC_USERSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("test_oat_hooks.user_var2", + "Dummy parameter settable by public", + NULL, + ®RESS_userset_variable2, + false, + PGC_USERSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + /* + * test_oat_hooks.super_var{1,2} = (on|off) + */ + DefineCustomBoolVariable("test_oat_hooks.super_var1", + "Dummy parameter settable by superuser", + NULL, + ®RESS_suset_variable1, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("test_oat_hooks.super_var2", + "Dummy parameter settable by superuser", + NULL, + ®RESS_suset_variable2, + false, + PGC_SUSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL, + NULL); + + MarkGUCPrefixReserved("test_oat_hooks"); + + /* Object access hook */ + next_object_access_hook = object_access_hook; + object_access_hook = REGRESS_object_access_hook; + + /* Object access hook str */ + next_object_access_hook_str = object_access_hook_str; + object_access_hook_str = REGRESS_object_access_hook_str; + + /* DML permission check */ + next_exec_check_perms_hook = ExecutorCheckPerms_hook; + ExecutorCheckPerms_hook = REGRESS_exec_check_perms; + + /* ProcessUtility hook */ + next_ProcessUtility_hook = ProcessUtility_hook; + ProcessUtility_hook = REGRESS_utility_command; +} + +static void +emit_audit_message(const char *type, const char *hook, char *action, char *objName) +{ + /* + * Ensure that audit messages are not duplicated by only emitting them + * from a leader process, not a worker process. This makes the test + * results deterministic even if run with force_parallel_mode = regress. + */ + if (REGRESS_audit && !IsParallelWorker()) + { + const char *who = superuser_arg(GetUserId()) ? "superuser" : "non-superuser"; + + if (objName) + ereport(NOTICE, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("in %s: %s %s %s [%s]", hook, who, type, action, objName))); + else + ereport(NOTICE, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("in %s: %s %s %s", hook, who, type, action))); + } + + if (action) + pfree(action); + if (objName) + pfree(objName); +} + +static void +audit_attempt(const char *hook, char *action, char *objName) +{ + emit_audit_message("attempting", hook, action, objName); +} + +static void +audit_success(const char *hook, char *action, char *objName) +{ + emit_audit_message("finished", hook, action, objName); +} + +static void +audit_failure(const char *hook, char *action, char *objName) +{ + emit_audit_message("denied", hook, action, objName); +} + +static void +REGRESS_object_access_hook_str(ObjectAccessType access, Oid classId, const char *objName, int subId, void *arg) +{ + audit_attempt("object_access_hook_str", + accesstype_to_string(access, subId), + pstrdup(objName)); + + if (next_object_access_hook_str) + { + (*next_object_access_hook_str) (access, classId, objName, subId, arg); + } + + switch (access) + { + case OAT_POST_ALTER: + if ((subId & ACL_SET) && (subId & ACL_ALTER_SYSTEM)) + { + if (REGRESS_deny_set_variable && !superuser_arg(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: all privileges %s", objName))); + } + else if (subId & ACL_SET) + { + if (REGRESS_deny_set_variable && !superuser_arg(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: set %s", objName))); + } + else if (subId & ACL_ALTER_SYSTEM) + { + if (REGRESS_deny_alter_system && !superuser_arg(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: alter system set %s", objName))); + } + else + elog(ERROR, "Unknown ParameterAclRelationId subId: %d", subId); + break; + default: + break; + } + + audit_success("object_access_hook_str", + accesstype_to_string(access, subId), + pstrdup(objName)); +} + +static void +REGRESS_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg) +{ + audit_attempt("object access", + accesstype_to_string(access, 0), + accesstype_arg_to_string(access, arg)); + + if (REGRESS_deny_object_access && !superuser_arg(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: %s [%s]", + accesstype_to_string(access, 0), + accesstype_arg_to_string(access, arg)))); + + /* Forward to next hook in the chain */ + if (next_object_access_hook) + (*next_object_access_hook) (access, classId, objectId, subId, arg); + + audit_success("object access", + accesstype_to_string(access, 0), + accesstype_arg_to_string(access, arg)); +} + +static bool +REGRESS_exec_check_perms(List *rangeTabls, bool do_abort) +{ + bool am_super = superuser_arg(GetUserId()); + bool allow = true; + + audit_attempt("executor check perms", pstrdup("execute"), NULL); + + /* Perform our check */ + allow = !REGRESS_deny_exec_perms || am_super; + if (do_abort && !allow) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: %s", "execute"))); + + /* Forward to next hook in the chain */ + if (next_exec_check_perms_hook && + !(*next_exec_check_perms_hook) (rangeTabls, do_abort)) + allow = false; + + if (allow) + audit_success("executor check perms", + pstrdup("execute"), + NULL); + else + audit_failure("executor check perms", + pstrdup("execute"), + NULL); + + return allow; +} + +static void +REGRESS_utility_command(PlannedStmt *pstmt, + const char *queryString, + bool readOnlyTree, + ProcessUtilityContext context, + ParamListInfo params, + QueryEnvironment *queryEnv, + DestReceiver *dest, + QueryCompletion *qc) +{ + Node *parsetree = pstmt->utilityStmt; + const char *action = GetCommandTagName(CreateCommandTag(parsetree)); + + audit_attempt("process utility", + pstrdup(action), + NULL); + + /* Check permissions */ + if (REGRESS_deny_utility_commands && !superuser_arg(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: %s", action))); + + /* Forward to next hook in the chain */ + if (next_ProcessUtility_hook) + (*next_ProcessUtility_hook) (pstmt, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + else + standard_ProcessUtility(pstmt, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + + /* We're done */ + audit_success("process utility", + pstrdup(action), + NULL); +} + +static char * +accesstype_to_string(ObjectAccessType access, int subId) +{ + const char *type; + + switch (access) + { + case OAT_POST_CREATE: + type = "create"; + break; + case OAT_DROP: + type = "drop"; + break; + case OAT_POST_ALTER: + type = "alter"; + break; + case OAT_NAMESPACE_SEARCH: + type = "namespace search"; + break; + case OAT_FUNCTION_EXECUTE: + type = "execute"; + break; + case OAT_TRUNCATE: + type = "truncate"; + break; + default: + type = "UNRECOGNIZED ObjectAccessType"; + } + + if ((subId & ACL_SET) && (subId & ACL_ALTER_SYSTEM)) + return psprintf("%s (subId=0x%x, all privileges)", type, subId); + if (subId & ACL_SET) + return psprintf("%s (subId=0x%x, set)", type, subId); + if (subId & ACL_ALTER_SYSTEM) + return psprintf("%s (subId=0x%x, alter system)", type, subId); + + return psprintf("%s (subId=0x%x)", type, subId); +} + +static char * +accesstype_arg_to_string(ObjectAccessType access, void *arg) +{ + if (arg == NULL) + return pstrdup("extra info null"); + + switch (access) + { + case OAT_POST_CREATE: + { + ObjectAccessPostCreate *pc_arg = (ObjectAccessPostCreate *) arg; + + return pstrdup(pc_arg->is_internal ? "internal" : "explicit"); + } + break; + case OAT_DROP: + { + ObjectAccessDrop *drop_arg = (ObjectAccessDrop *) arg; + + return psprintf("%s%s%s%s%s%s", + ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL) + ? "internal action," : ""), + ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL) + ? "concurrent drop," : ""), + ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL) + ? "suppress notices," : ""), + ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL) + ? "keep original object," : ""), + ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL) + ? "keep extensions," : ""), + ((drop_arg->dropflags & PERFORM_DELETION_INTERNAL) + ? "normal concurrent drop," : "")); + } + break; + case OAT_POST_ALTER: + { + ObjectAccessPostAlter *pa_arg = (ObjectAccessPostAlter *) arg; + + return psprintf("%s %s auxiliary object", + (pa_arg->is_internal ? "internal" : "explicit"), + (OidIsValid(pa_arg->auxiliary_id) ? "with" : "without")); + } + break; + case OAT_NAMESPACE_SEARCH: + { + ObjectAccessNamespaceSearch *ns_arg = (ObjectAccessNamespaceSearch *) arg; + + return psprintf("%s, %s", + (ns_arg->ereport_on_violation ? "report on violation" : "no report on violation"), + (ns_arg->result ? "allowed" : "denied")); + } + break; + case OAT_TRUNCATE: + case OAT_FUNCTION_EXECUTE: + /* hook takes no arg. */ + return pstrdup("unexpected extra info pointer received"); + default: + return pstrdup("cannot parse extra info for unrecognized access type"); + } + + return pstrdup("unknown"); +} diff --git a/src/test/modules/test_parser/.gitignore b/src/test/modules/test_parser/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_parser/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_parser/Makefile b/src/test/modules/test_parser/Makefile new file mode 100644 index 0000000..5327080 --- /dev/null +++ b/src/test/modules/test_parser/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_parser/Makefile + +MODULE_big = test_parser +OBJS = \ + $(WIN32RES) \ + test_parser.o +PGFILEDESC = "test_parser - example of a custom parser for full-text search" + +EXTENSION = test_parser +DATA = test_parser--1.0.sql + +REGRESS = test_parser + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_parser +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_parser/README b/src/test/modules/test_parser/README new file mode 100644 index 0000000..0a11ec8 --- /dev/null +++ b/src/test/modules/test_parser/README @@ -0,0 +1,61 @@ +test_parser is an example of a custom parser for full-text +search. It doesn't do anything especially useful, but can serve as +a starting point for developing your own parser. + +test_parser recognizes words separated by white space, +and returns just two token types: + +mydb=# SELECT * FROM ts_token_type('testparser'); + tokid | alias | description +-------+-------+--------------- + 3 | word | Word + 12 | blank | Space symbols +(2 rows) + +These token numbers have been chosen to be compatible with the default +parser's numbering. This allows us to use its headline() +function, thus keeping the example simple. + +Usage +===== + +Installing the test_parser extension creates a text search +parser testparser. It has no user-configurable parameters. + +You can test the parser with, for example, + +mydb=# SELECT * FROM ts_parse('testparser', 'That''s my first own parser'); + tokid | token +-------+-------- + 3 | That's + 12 | + 3 | my + 12 | + 3 | first + 12 | + 3 | own + 12 | + 3 | parser + +Real-world use requires setting up a text search configuration +that uses the parser. For example, + +mydb=# CREATE TEXT SEARCH CONFIGURATION testcfg ( PARSER = testparser ); +CREATE TEXT SEARCH CONFIGURATION + +mydb=# ALTER TEXT SEARCH CONFIGURATION testcfg +mydb-# ADD MAPPING FOR word WITH english_stem; +ALTER TEXT SEARCH CONFIGURATION + +mydb=# SELECT to_tsvector('testcfg', 'That''s my first own parser'); + to_tsvector +------------------------------- + 'that':1 'first':3 'parser':5 +(1 row) + +mydb=# SELECT ts_headline('testcfg', 'Supernovae stars are the brightest phenomena in galaxies', +mydb(# to_tsquery('testcfg', 'star')); + ts_headline +----------------------------------------------------------------- + Supernovae <b>stars</b> are the brightest phenomena in galaxies +(1 row) diff --git a/src/test/modules/test_parser/expected/test_parser.out b/src/test/modules/test_parser/expected/test_parser.out new file mode 100644 index 0000000..8a49bc0 --- /dev/null +++ b/src/test/modules/test_parser/expected/test_parser.out @@ -0,0 +1,44 @@ +CREATE EXTENSION test_parser; +-- make test configuration using parser +CREATE TEXT SEARCH CONFIGURATION testcfg (PARSER = testparser); +ALTER TEXT SEARCH CONFIGURATION testcfg ADD MAPPING FOR word WITH simple; +-- ts_parse +SELECT * FROM ts_parse('testparser', 'That''s simple parser can''t parse urls like http://some.url/here/'); + tokid | token +-------+----------------------- + 3 | That's + 12 | + 3 | simple + 12 | + 3 | parser + 12 | + 3 | can't + 12 | + 3 | parse + 12 | + 3 | urls + 12 | + 3 | like + 12 | + 3 | http://some.url/here/ +(15 rows) + +SELECT to_tsvector('testcfg','That''s my first own parser'); + to_tsvector +------------------------------------------------- + 'first':3 'my':2 'own':4 'parser':5 'that''s':1 +(1 row) + +SELECT to_tsquery('testcfg', 'star'); + to_tsquery +------------ + 'star' +(1 row) + +SELECT ts_headline('testcfg','Supernovae stars are the brightest phenomena in galaxies', + to_tsquery('testcfg', 'stars')); + ts_headline +----------------------------------------------------------------- + Supernovae <b>stars</b> are the brightest phenomena in galaxies +(1 row) + diff --git a/src/test/modules/test_parser/sql/test_parser.sql b/src/test/modules/test_parser/sql/test_parser.sql new file mode 100644 index 0000000..1f21504 --- /dev/null +++ b/src/test/modules/test_parser/sql/test_parser.sql @@ -0,0 +1,18 @@ +CREATE EXTENSION test_parser; + +-- make test configuration using parser + +CREATE TEXT SEARCH CONFIGURATION testcfg (PARSER = testparser); + +ALTER TEXT SEARCH CONFIGURATION testcfg ADD MAPPING FOR word WITH simple; + +-- ts_parse + +SELECT * FROM ts_parse('testparser', 'That''s simple parser can''t parse urls like http://some.url/here/'); + +SELECT to_tsvector('testcfg','That''s my first own parser'); + +SELECT to_tsquery('testcfg', 'star'); + +SELECT ts_headline('testcfg','Supernovae stars are the brightest phenomena in galaxies', + to_tsquery('testcfg', 'stars')); diff --git a/src/test/modules/test_parser/test_parser--1.0.sql b/src/test/modules/test_parser/test_parser--1.0.sql new file mode 100644 index 0000000..56bb244 --- /dev/null +++ b/src/test/modules/test_parser/test_parser--1.0.sql @@ -0,0 +1,32 @@ +/* src/test/modules/test_parser/test_parser--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_parser" to load this file. \quit + +CREATE FUNCTION testprs_start(internal, int4) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION testprs_getlexeme(internal, internal, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION testprs_end(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION testprs_lextype(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE TEXT SEARCH PARSER testparser ( + START = testprs_start, + GETTOKEN = testprs_getlexeme, + END = testprs_end, + HEADLINE = pg_catalog.prsd_headline, + LEXTYPES = testprs_lextype +); diff --git a/src/test/modules/test_parser/test_parser.c b/src/test/modules/test_parser/test_parser.c new file mode 100644 index 0000000..ec1e1b6 --- /dev/null +++ b/src/test/modules/test_parser/test_parser.c @@ -0,0 +1,127 @@ +/*------------------------------------------------------------------------- + * + * test_parser.c + * Simple example of a text search parser + * + * Copyright (c) 2007-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_parser/test_parser.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "fmgr.h" + +PG_MODULE_MAGIC; + +/* + * types + */ + +/* self-defined type */ +typedef struct +{ + char *buffer; /* text to parse */ + int len; /* length of the text in buffer */ + int pos; /* position of the parser */ +} ParserState; + +typedef struct +{ + int lexid; + char *alias; + char *descr; +} LexDescr; + +/* + * functions + */ +PG_FUNCTION_INFO_V1(testprs_start); +PG_FUNCTION_INFO_V1(testprs_getlexeme); +PG_FUNCTION_INFO_V1(testprs_end); +PG_FUNCTION_INFO_V1(testprs_lextype); + +Datum +testprs_start(PG_FUNCTION_ARGS) +{ + ParserState *pst = (ParserState *) palloc0(sizeof(ParserState)); + + pst->buffer = (char *) PG_GETARG_POINTER(0); + pst->len = PG_GETARG_INT32(1); + pst->pos = 0; + + PG_RETURN_POINTER(pst); +} + +Datum +testprs_getlexeme(PG_FUNCTION_ARGS) +{ + ParserState *pst = (ParserState *) PG_GETARG_POINTER(0); + char **t = (char **) PG_GETARG_POINTER(1); + int *tlen = (int *) PG_GETARG_POINTER(2); + int startpos = pst->pos; + int type; + + *t = pst->buffer + pst->pos; + + if (pst->pos < pst->len && + (pst->buffer)[pst->pos] == ' ') + { + /* blank type */ + type = 12; + /* go to the next non-space character */ + while (pst->pos < pst->len && + (pst->buffer)[pst->pos] == ' ') + (pst->pos)++; + } + else + { + /* word type */ + type = 3; + /* go to the next space character */ + while (pst->pos < pst->len && + (pst->buffer)[pst->pos] != ' ') + (pst->pos)++; + } + + *tlen = pst->pos - startpos; + + /* we are finished if (*tlen == 0) */ + if (*tlen == 0) + type = 0; + + PG_RETURN_INT32(type); +} + +Datum +testprs_end(PG_FUNCTION_ARGS) +{ + ParserState *pst = (ParserState *) PG_GETARG_POINTER(0); + + pfree(pst); + PG_RETURN_VOID(); +} + +Datum +testprs_lextype(PG_FUNCTION_ARGS) +{ + /* + * Remarks: - we have to return the blanks for headline reason - we use + * the same lexids like Teodor in the default word parser; in this way we + * can reuse the headline function of the default word parser. + */ + LexDescr *descr = (LexDescr *) palloc(sizeof(LexDescr) * (2 + 1)); + + /* there are only two types in this parser */ + descr[0].lexid = 3; + descr[0].alias = pstrdup("word"); + descr[0].descr = pstrdup("Word"); + descr[1].lexid = 12; + descr[1].alias = pstrdup("blank"); + descr[1].descr = pstrdup("Space symbols"); + descr[2].lexid = 0; + + PG_RETURN_POINTER(descr); +} diff --git a/src/test/modules/test_parser/test_parser.control b/src/test/modules/test_parser/test_parser.control new file mode 100644 index 0000000..36b26b2 --- /dev/null +++ b/src/test/modules/test_parser/test_parser.control @@ -0,0 +1,5 @@ +# test_parser extension +comment = 'example of a custom parser for full-text search' +default_version = '1.0' +module_pathname = '$libdir/test_parser' +relocatable = true diff --git a/src/test/modules/test_pg_dump/.gitignore b/src/test/modules/test_pg_dump/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_pg_dump/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_pg_dump/Makefile b/src/test/modules/test_pg_dump/Makefile new file mode 100644 index 0000000..6123b99 --- /dev/null +++ b/src/test/modules/test_pg_dump/Makefile @@ -0,0 +1,21 @@ +# src/test/modules/test_pg_dump/Makefile + +MODULE = test_pg_dump +PGFILEDESC = "test_pg_dump - Test pg_dump with an extension" + +EXTENSION = test_pg_dump +DATA = test_pg_dump--1.0.sql + +REGRESS = test_pg_dump +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_pg_dump +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_pg_dump/README b/src/test/modules/test_pg_dump/README new file mode 100644 index 0000000..b7c2e33 --- /dev/null +++ b/src/test/modules/test_pg_dump/README @@ -0,0 +1,4 @@ +test_pg_dump is an extension explicitly to test pg_dump when +extensions are present in the system. + +We also make use of this module to test ALTER EXTENSION ADD/DROP. diff --git a/src/test/modules/test_pg_dump/expected/test_pg_dump.out b/src/test/modules/test_pg_dump/expected/test_pg_dump.out new file mode 100644 index 0000000..f14f3a6 --- /dev/null +++ b/src/test/modules/test_pg_dump/expected/test_pg_dump.out @@ -0,0 +1,93 @@ +CREATE ROLE regress_dump_test_role; +CREATE EXTENSION test_pg_dump; +ALTER EXTENSION test_pg_dump ADD DATABASE postgres; -- error +ERROR: cannot add an object of this type to an extension +CREATE TABLE test_pg_dump_t1 (c1 int, junk text); +ALTER TABLE test_pg_dump_t1 DROP COLUMN junk; -- to exercise dropped-col cases +CREATE VIEW test_pg_dump_v1 AS SELECT * FROM test_pg_dump_t1; +CREATE MATERIALIZED VIEW test_pg_dump_mv1 AS SELECT * FROM test_pg_dump_t1; +CREATE SCHEMA test_pg_dump_s1; +CREATE TYPE test_pg_dump_e1 AS ENUM ('abc', 'def'); +CREATE AGGREGATE newavg ( + sfunc = int4_avg_accum, basetype = int4, stype = _int8, + finalfunc = int8_avg, + initcond1 = '{0,0}' +); +CREATE FUNCTION test_pg_dump(int) RETURNS int AS $$ +BEGIN +RETURN abs($1); +END +$$ LANGUAGE plpgsql IMMUTABLE; +CREATE OPERATOR ==== ( + LEFTARG = int, + RIGHTARG = int, + PROCEDURE = int4eq, + COMMUTATOR = ==== +); +CREATE ACCESS METHOD gist2 TYPE INDEX HANDLER gisthandler; +CREATE TYPE casttesttype; +CREATE FUNCTION casttesttype_in(cstring) + RETURNS casttesttype + AS 'textin' + LANGUAGE internal STRICT IMMUTABLE; +NOTICE: return type casttesttype is only a shell +CREATE FUNCTION casttesttype_out(casttesttype) + RETURNS cstring + AS 'textout' + LANGUAGE internal STRICT IMMUTABLE; +NOTICE: argument type casttesttype is only a shell +CREATE TYPE casttesttype ( + internallength = variable, + input = casttesttype_in, + output = casttesttype_out, + alignment = int4 +); +CREATE CAST (text AS casttesttype) WITHOUT FUNCTION; +CREATE FOREIGN DATA WRAPPER dummy; +CREATE SERVER s0 FOREIGN DATA WRAPPER dummy; +CREATE FOREIGN TABLE ft1 ( + c1 integer OPTIONS ("param 1" 'val1') NOT NULL, + c2 text OPTIONS (param2 'val2', param3 'val3') CHECK (c2 <> ''), + c3 date, + CHECK (c3 BETWEEN '1994-01-01'::date AND '1994-01-31'::date) +) SERVER s0 OPTIONS (delimiter ',', quote '"', "be quoted" 'value'); +REVOKE EXECUTE ON FUNCTION test_pg_dump(int) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION test_pg_dump(int) TO regress_dump_test_role; +GRANT SELECT (c1) ON test_pg_dump_t1 TO regress_dump_test_role; +GRANT SELECT ON test_pg_dump_v1 TO regress_dump_test_role; +GRANT USAGE ON FOREIGN DATA WRAPPER dummy TO regress_dump_test_role; +GRANT USAGE ON FOREIGN SERVER s0 TO regress_dump_test_role; +GRANT SELECT (c1) ON ft1 TO regress_dump_test_role; +GRANT SELECT ON ft1 TO regress_dump_test_role; +GRANT UPDATE ON test_pg_dump_mv1 TO regress_dump_test_role; +GRANT USAGE ON SCHEMA test_pg_dump_s1 TO regress_dump_test_role; +GRANT USAGE ON TYPE test_pg_dump_e1 TO regress_dump_test_role; +ALTER EXTENSION test_pg_dump ADD ACCESS METHOD gist2; +ALTER EXTENSION test_pg_dump ADD AGGREGATE newavg(int4); +ALTER EXTENSION test_pg_dump ADD CAST (text AS casttesttype); +ALTER EXTENSION test_pg_dump ADD FOREIGN DATA WRAPPER dummy; +ALTER EXTENSION test_pg_dump ADD FOREIGN TABLE ft1; +ALTER EXTENSION test_pg_dump ADD MATERIALIZED VIEW test_pg_dump_mv1; +ALTER EXTENSION test_pg_dump ADD OPERATOR ==== (int, int); +ALTER EXTENSION test_pg_dump ADD SCHEMA test_pg_dump_s1; +ALTER EXTENSION test_pg_dump ADD SERVER s0; +ALTER EXTENSION test_pg_dump ADD FUNCTION test_pg_dump(int); +ALTER EXTENSION test_pg_dump ADD TABLE test_pg_dump_t1; +ALTER EXTENSION test_pg_dump ADD TYPE test_pg_dump_e1; +ALTER EXTENSION test_pg_dump ADD VIEW test_pg_dump_v1; +REVOKE SELECT (c1) ON test_pg_dump_t1 FROM regress_dump_test_role; +REVOKE SELECT ON test_pg_dump_v1 FROM regress_dump_test_role; +REVOKE USAGE ON FOREIGN DATA WRAPPER dummy FROM regress_dump_test_role; +ALTER EXTENSION test_pg_dump DROP ACCESS METHOD gist2; +ALTER EXTENSION test_pg_dump DROP AGGREGATE newavg(int4); +ALTER EXTENSION test_pg_dump DROP CAST (text AS casttesttype); +ALTER EXTENSION test_pg_dump DROP FOREIGN DATA WRAPPER dummy; +ALTER EXTENSION test_pg_dump DROP FOREIGN TABLE ft1; +ALTER EXTENSION test_pg_dump DROP FUNCTION test_pg_dump(int); +ALTER EXTENSION test_pg_dump DROP MATERIALIZED VIEW test_pg_dump_mv1; +ALTER EXTENSION test_pg_dump DROP OPERATOR ==== (int, int); +ALTER EXTENSION test_pg_dump DROP SCHEMA test_pg_dump_s1; +ALTER EXTENSION test_pg_dump DROP SERVER s0; +ALTER EXTENSION test_pg_dump DROP TABLE test_pg_dump_t1; +ALTER EXTENSION test_pg_dump DROP TYPE test_pg_dump_e1; +ALTER EXTENSION test_pg_dump DROP VIEW test_pg_dump_v1; diff --git a/src/test/modules/test_pg_dump/sql/test_pg_dump.sql b/src/test/modules/test_pg_dump/sql/test_pg_dump.sql new file mode 100644 index 0000000..a61a7c8 --- /dev/null +++ b/src/test/modules/test_pg_dump/sql/test_pg_dump.sql @@ -0,0 +1,108 @@ +CREATE ROLE regress_dump_test_role; +CREATE EXTENSION test_pg_dump; + +ALTER EXTENSION test_pg_dump ADD DATABASE postgres; -- error + +CREATE TABLE test_pg_dump_t1 (c1 int, junk text); +ALTER TABLE test_pg_dump_t1 DROP COLUMN junk; -- to exercise dropped-col cases +CREATE VIEW test_pg_dump_v1 AS SELECT * FROM test_pg_dump_t1; +CREATE MATERIALIZED VIEW test_pg_dump_mv1 AS SELECT * FROM test_pg_dump_t1; +CREATE SCHEMA test_pg_dump_s1; +CREATE TYPE test_pg_dump_e1 AS ENUM ('abc', 'def'); + +CREATE AGGREGATE newavg ( + sfunc = int4_avg_accum, basetype = int4, stype = _int8, + finalfunc = int8_avg, + initcond1 = '{0,0}' +); + +CREATE FUNCTION test_pg_dump(int) RETURNS int AS $$ +BEGIN +RETURN abs($1); +END +$$ LANGUAGE plpgsql IMMUTABLE; + +CREATE OPERATOR ==== ( + LEFTARG = int, + RIGHTARG = int, + PROCEDURE = int4eq, + COMMUTATOR = ==== +); + +CREATE ACCESS METHOD gist2 TYPE INDEX HANDLER gisthandler; + +CREATE TYPE casttesttype; + +CREATE FUNCTION casttesttype_in(cstring) + RETURNS casttesttype + AS 'textin' + LANGUAGE internal STRICT IMMUTABLE; +CREATE FUNCTION casttesttype_out(casttesttype) + RETURNS cstring + AS 'textout' + LANGUAGE internal STRICT IMMUTABLE; + +CREATE TYPE casttesttype ( + internallength = variable, + input = casttesttype_in, + output = casttesttype_out, + alignment = int4 +); + +CREATE CAST (text AS casttesttype) WITHOUT FUNCTION; + +CREATE FOREIGN DATA WRAPPER dummy; + +CREATE SERVER s0 FOREIGN DATA WRAPPER dummy; + +CREATE FOREIGN TABLE ft1 ( + c1 integer OPTIONS ("param 1" 'val1') NOT NULL, + c2 text OPTIONS (param2 'val2', param3 'val3') CHECK (c2 <> ''), + c3 date, + CHECK (c3 BETWEEN '1994-01-01'::date AND '1994-01-31'::date) +) SERVER s0 OPTIONS (delimiter ',', quote '"', "be quoted" 'value'); + +REVOKE EXECUTE ON FUNCTION test_pg_dump(int) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION test_pg_dump(int) TO regress_dump_test_role; + +GRANT SELECT (c1) ON test_pg_dump_t1 TO regress_dump_test_role; +GRANT SELECT ON test_pg_dump_v1 TO regress_dump_test_role; +GRANT USAGE ON FOREIGN DATA WRAPPER dummy TO regress_dump_test_role; +GRANT USAGE ON FOREIGN SERVER s0 TO regress_dump_test_role; +GRANT SELECT (c1) ON ft1 TO regress_dump_test_role; +GRANT SELECT ON ft1 TO regress_dump_test_role; +GRANT UPDATE ON test_pg_dump_mv1 TO regress_dump_test_role; +GRANT USAGE ON SCHEMA test_pg_dump_s1 TO regress_dump_test_role; +GRANT USAGE ON TYPE test_pg_dump_e1 TO regress_dump_test_role; + +ALTER EXTENSION test_pg_dump ADD ACCESS METHOD gist2; +ALTER EXTENSION test_pg_dump ADD AGGREGATE newavg(int4); +ALTER EXTENSION test_pg_dump ADD CAST (text AS casttesttype); +ALTER EXTENSION test_pg_dump ADD FOREIGN DATA WRAPPER dummy; +ALTER EXTENSION test_pg_dump ADD FOREIGN TABLE ft1; +ALTER EXTENSION test_pg_dump ADD MATERIALIZED VIEW test_pg_dump_mv1; +ALTER EXTENSION test_pg_dump ADD OPERATOR ==== (int, int); +ALTER EXTENSION test_pg_dump ADD SCHEMA test_pg_dump_s1; +ALTER EXTENSION test_pg_dump ADD SERVER s0; +ALTER EXTENSION test_pg_dump ADD FUNCTION test_pg_dump(int); +ALTER EXTENSION test_pg_dump ADD TABLE test_pg_dump_t1; +ALTER EXTENSION test_pg_dump ADD TYPE test_pg_dump_e1; +ALTER EXTENSION test_pg_dump ADD VIEW test_pg_dump_v1; + +REVOKE SELECT (c1) ON test_pg_dump_t1 FROM regress_dump_test_role; +REVOKE SELECT ON test_pg_dump_v1 FROM regress_dump_test_role; +REVOKE USAGE ON FOREIGN DATA WRAPPER dummy FROM regress_dump_test_role; + +ALTER EXTENSION test_pg_dump DROP ACCESS METHOD gist2; +ALTER EXTENSION test_pg_dump DROP AGGREGATE newavg(int4); +ALTER EXTENSION test_pg_dump DROP CAST (text AS casttesttype); +ALTER EXTENSION test_pg_dump DROP FOREIGN DATA WRAPPER dummy; +ALTER EXTENSION test_pg_dump DROP FOREIGN TABLE ft1; +ALTER EXTENSION test_pg_dump DROP FUNCTION test_pg_dump(int); +ALTER EXTENSION test_pg_dump DROP MATERIALIZED VIEW test_pg_dump_mv1; +ALTER EXTENSION test_pg_dump DROP OPERATOR ==== (int, int); +ALTER EXTENSION test_pg_dump DROP SCHEMA test_pg_dump_s1; +ALTER EXTENSION test_pg_dump DROP SERVER s0; +ALTER EXTENSION test_pg_dump DROP TABLE test_pg_dump_t1; +ALTER EXTENSION test_pg_dump DROP TYPE test_pg_dump_e1; +ALTER EXTENSION test_pg_dump DROP VIEW test_pg_dump_v1; diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl new file mode 100644 index 0000000..f5da6bf --- /dev/null +++ b/src/test/modules/test_pg_dump/t/001_base.pl @@ -0,0 +1,844 @@ + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +use strict; +use warnings; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $tempdir = PostgreSQL::Test::Utils::tempdir; + +############################################################### +# This structure is based off of the src/bin/pg_dump/t test +# suite. +############################################################### +# Definition of the pg_dump runs to make. +# +# Each of these runs are named and those names are used below +# to define how each test should (or shouldn't) treat a result +# from a given run. +# +# test_key indicates that a given run should simply use the same +# set of like/unlike tests as another run, and which run that is. +# +# dump_cmd is the pg_dump command to run, which is an array of +# the full command and arguments to run. Note that this is run +# using $node->command_ok(), so the port does not need to be +# specified and is pulled from $PGPORT, which is set by the +# PostgreSQL::Test::Cluster system. +# +# restore_cmd is the pg_restore command to run, if any. Note +# that this should generally be used when the pg_dump goes to +# a non-text file and that the restore can then be used to +# generate a text file to run through the tests from the +# non-text file generated by pg_dump. +# +# TODO: Have pg_restore actually restore to an independent +# database and then pg_dump *that* database (or something along +# those lines) to validate that part of the process. + +my %pgdump_runs = ( + binary_upgrade => { + dump_cmd => [ + 'pg_dump', '--no-sync', + "--file=$tempdir/binary_upgrade.sql", '--schema-only', + '--binary-upgrade', '--dbname=postgres', + ], + }, + clean => { + dump_cmd => [ + 'pg_dump', "--file=$tempdir/clean.sql", + '-c', '--no-sync', + '--dbname=postgres', + ], + }, + clean_if_exists => { + dump_cmd => [ + 'pg_dump', + '--no-sync', + "--file=$tempdir/clean_if_exists.sql", + '-c', + '--if-exists', + '--encoding=UTF8', # no-op, just tests that option is accepted + 'postgres', + ], + }, + createdb => { + dump_cmd => [ + 'pg_dump', + '--no-sync', + "--file=$tempdir/createdb.sql", + '-C', + '-R', # no-op, just for testing + 'postgres', + ], + }, + data_only => { + dump_cmd => [ + 'pg_dump', + '--no-sync', + "--file=$tempdir/data_only.sql", + '-a', + '-v', # no-op, just make sure it works + 'postgres', + ], + }, + defaults => { + dump_cmd => [ 'pg_dump', '-f', "$tempdir/defaults.sql", 'postgres', ], + }, + defaults_custom_format => { + test_key => 'defaults', + dump_cmd => [ + 'pg_dump', '--no-sync', '-Fc', '-Z6', + "--file=$tempdir/defaults_custom_format.dump", 'postgres', + ], + restore_cmd => [ + 'pg_restore', + "--file=$tempdir/defaults_custom_format.sql", + "$tempdir/defaults_custom_format.dump", + ], + }, + defaults_dir_format => { + test_key => 'defaults', + dump_cmd => [ + 'pg_dump', '--no-sync', '-Fd', + "--file=$tempdir/defaults_dir_format", 'postgres', + ], + restore_cmd => [ + 'pg_restore', + "--file=$tempdir/defaults_dir_format.sql", + "$tempdir/defaults_dir_format", + ], + }, + defaults_parallel => { + test_key => 'defaults', + dump_cmd => [ + 'pg_dump', '--no-sync', '-Fd', '-j2', + "--file=$tempdir/defaults_parallel", 'postgres', + ], + restore_cmd => [ + 'pg_restore', + "--file=$tempdir/defaults_parallel.sql", + "$tempdir/defaults_parallel", + ], + }, + defaults_tar_format => { + test_key => 'defaults', + dump_cmd => [ + 'pg_dump', '--no-sync', '-Ft', + "--file=$tempdir/defaults_tar_format.tar", 'postgres', + ], + restore_cmd => [ + 'pg_restore', + "--file=$tempdir/defaults_tar_format.sql", + "$tempdir/defaults_tar_format.tar", + ], + }, + exclude_table => { + dump_cmd => [ + 'pg_dump', + '--exclude-table=regress_table_dumpable', + "--file=$tempdir/exclude_table.sql", + 'postgres', + ], + }, + extension_schema => { + dump_cmd => [ + 'pg_dump', '--schema=public', + "--file=$tempdir/extension_schema.sql", 'postgres', + ], + }, + pg_dumpall_globals => { + dump_cmd => [ + 'pg_dumpall', '--no-sync', + "--file=$tempdir/pg_dumpall_globals.sql", '-g', + ], + }, + no_privs => { + dump_cmd => [ + 'pg_dump', '--no-sync', + "--file=$tempdir/no_privs.sql", '-x', + 'postgres', + ], + }, + no_owner => { + dump_cmd => [ + 'pg_dump', '--no-sync', + "--file=$tempdir/no_owner.sql", '-O', + 'postgres', + ], + }, + schema_only => { + dump_cmd => [ + 'pg_dump', '--no-sync', "--file=$tempdir/schema_only.sql", + '-s', 'postgres', + ], + }, + section_pre_data => { + dump_cmd => [ + 'pg_dump', '--no-sync', + "--file=$tempdir/section_pre_data.sql", '--section=pre-data', + 'postgres', + ], + }, + section_data => { + dump_cmd => [ + 'pg_dump', '--no-sync', + "--file=$tempdir/section_data.sql", '--section=data', + 'postgres', + ], + }, + section_post_data => { + dump_cmd => [ + 'pg_dump', '--no-sync', "--file=$tempdir/section_post_data.sql", + '--section=post-data', 'postgres', + ], + }, + with_extension => { + dump_cmd => [ + 'pg_dump', '--no-sync', "--file=$tempdir/with_extension.sql", + '--extension=test_pg_dump', 'postgres', + ], + }, + + # plgsql in the list blocks the dump of extension test_pg_dump + without_extension => { + dump_cmd => [ + 'pg_dump', '--no-sync', "--file=$tempdir/without_extension.sql", + '--extension=plpgsql', 'postgres', + ], + }, + + # plgsql in the list of extensions blocks the dump of extension + # test_pg_dump. "public" is the schema used by the extension + # test_pg_dump, but none of its objects should be dumped. + without_extension_explicit_schema => { + dump_cmd => [ + 'pg_dump', + '--no-sync', + "--file=$tempdir/without_extension_explicit_schema.sql", + '--extension=plpgsql', + '--schema=public', + 'postgres', + ], + }, + + # plgsql in the list of extensions blocks the dump of extension + # test_pg_dump, but not the dump of objects not dependent on the + # extension located on a schema maintained by the extension. + without_extension_internal_schema => { + dump_cmd => [ + 'pg_dump', + '--no-sync', + "--file=$tempdir/without_extension_internal_schema.sql", + '--extension=plpgsql', + '--schema=regress_pg_dump_schema', + 'postgres', + ], + },); + +############################################################### +# Definition of the tests to run. +# +# Each test is defined using the log message that will be used. +# +# A regexp should be defined for each test which provides the +# basis for the test. That regexp will be run against the output +# file of each of the runs which the test is to be run against +# and the success of the result will depend on if the regexp +# result matches the expected 'like' or 'unlike' case. +# The runs listed as 'like' will be checked if they match the +# regexp and, if so, the test passes. All runs which are not +# listed as 'like' will be checked to ensure they don't match +# the regexp; if they do, the test will fail. +# +# The below hashes provide convenience sets of runs. Individual +# runs can be excluded from a general hash by placing that run +# into the 'unlike' section. +# +# There can then be a 'create_sql' and 'create_order' for a +# given test. The 'create_sql' commands are collected up in +# 'create_order' and then run against the database prior to any +# of the pg_dump runs happening. This is what "seeds" the +# system with objects to be dumped out. +# +# Building of this hash takes a bit of time as all of the regexps +# included in it are compiled. This greatly improves performance +# as the regexps are used for each run the test applies to. + +# Tests which are considered 'full' dumps by pg_dump, but there +# are flags used to exclude specific items (ACLs, blobs, etc). +my %full_runs = ( + binary_upgrade => 1, + clean => 1, + clean_if_exists => 1, + createdb => 1, + defaults => 1, + exclude_table => 1, + no_privs => 1, + no_owner => 1, + with_extension => 1, + without_extension => 1); + +my %tests = ( + 'ALTER EXTENSION test_pg_dump' => { + create_order => 9, + create_sql => + 'ALTER EXTENSION test_pg_dump ADD TABLE regress_pg_dump_table_added;', + regexp => qr/^ + \QCREATE TABLE public.regress_pg_dump_table_added (\E + \n\s+\Qcol1 integer NOT NULL,\E + \n\s+\Qcol2 integer\E + \n\);\n/xm, + like => { binary_upgrade => 1, }, + }, + + 'CREATE EXTENSION test_pg_dump' => { + create_order => 2, + create_sql => 'CREATE EXTENSION test_pg_dump;', + regexp => qr/^ + \QCREATE EXTENSION IF NOT EXISTS test_pg_dump WITH SCHEMA public;\E + \n/xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { binary_upgrade => 1, without_extension => 1 }, + }, + + 'CREATE ROLE regress_dump_test_role' => { + create_order => 1, + create_sql => 'CREATE ROLE regress_dump_test_role;', + regexp => qr/^CREATE ROLE regress_dump_test_role;\n/m, + like => { pg_dumpall_globals => 1, }, + }, + + 'GRANT ALTER SYSTEM ON PARAMETER full_page_writes TO regress_dump_test_role' + => { + create_order => 2, + create_sql => + 'GRANT ALTER SYSTEM ON PARAMETER full_page_writes TO regress_dump_test_role;', + regexp => + + qr/^GRANT ALTER SYSTEM ON PARAMETER full_page_writes TO regress_dump_test_role;/m, + like => { pg_dumpall_globals => 1, }, + }, + + 'GRANT ALL ON PARAMETER Custom.Knob TO regress_dump_test_role WITH GRANT OPTION' + => { + create_order => 2, + create_sql => + 'GRANT SET, ALTER SYSTEM ON PARAMETER Custom.Knob TO regress_dump_test_role WITH GRANT OPTION;', + regexp => + # "set" plus "alter system" is "all" privileges on parameters + qr/^GRANT ALL ON PARAMETER "custom.knob" TO regress_dump_test_role WITH GRANT OPTION;/m, + like => { pg_dumpall_globals => 1, }, + }, + + 'GRANT ALL ON PARAMETER DateStyle TO regress_dump_test_role' => { + create_order => 2, + create_sql => + 'GRANT ALL ON PARAMETER "DateStyle" TO regress_dump_test_role WITH GRANT OPTION; REVOKE GRANT OPTION FOR ALL ON PARAMETER DateStyle FROM regress_dump_test_role;', + regexp => + # The revoke simplifies the ultimate grant so as to not include "with grant option" + qr/^GRANT ALL ON PARAMETER datestyle TO regress_dump_test_role;/m, + like => { pg_dumpall_globals => 1, }, + }, + + 'CREATE SCHEMA public' => { + regexp => qr/^CREATE SCHEMA public;/m, + like => { + extension_schema => 1, + without_extension_explicit_schema => 1, + }, + }, + + 'CREATE SEQUENCE regress_pg_dump_table_col1_seq' => { + regexp => qr/^ + \QCREATE SEQUENCE public.regress_pg_dump_table_col1_seq\E + \n\s+\QAS integer\E + \n\s+\QSTART WITH 1\E + \n\s+\QINCREMENT BY 1\E + \n\s+\QNO MINVALUE\E + \n\s+\QNO MAXVALUE\E + \n\s+\QCACHE 1;\E + \n/xm, + like => { binary_upgrade => 1, }, + }, + + 'CREATE TABLE regress_pg_dump_table_added' => { + create_order => 7, + create_sql => + 'CREATE TABLE regress_pg_dump_table_added (col1 int not null, col2 int);', + regexp => qr/^ + \QCREATE TABLE public.regress_pg_dump_table_added (\E + \n\s+\Qcol1 integer NOT NULL,\E + \n\s+\Qcol2 integer\E + \n\);\n/xm, + like => { binary_upgrade => 1, }, + }, + + 'CREATE SEQUENCE regress_pg_dump_seq' => { + regexp => qr/^ + \QCREATE SEQUENCE public.regress_pg_dump_seq\E + \n\s+\QSTART WITH 1\E + \n\s+\QINCREMENT BY 1\E + \n\s+\QNO MINVALUE\E + \n\s+\QNO MAXVALUE\E + \n\s+\QCACHE 1;\E + \n/xm, + like => { binary_upgrade => 1, }, + }, + + 'SETVAL SEQUENCE regress_seq_dumpable' => { + create_order => 6, + create_sql => qq{SELECT nextval('regress_seq_dumpable');}, + regexp => qr/^ + \QSELECT pg_catalog.setval('public.regress_seq_dumpable', 1, true);\E + \n/xm, + like => { + %full_runs, + data_only => 1, + section_data => 1, + extension_schema => 1, + }, + unlike => { without_extension => 1, }, + }, + + 'CREATE TABLE regress_pg_dump_table' => { + regexp => qr/^ + \QCREATE TABLE public.regress_pg_dump_table (\E + \n\s+\Qcol1 integer NOT NULL,\E + \n\s+\Qcol2 integer,\E + \n\s+\QCONSTRAINT regress_pg_dump_table_col2_check CHECK ((col2 > 0))\E + \n\);\n/xm, + like => { binary_upgrade => 1, }, + }, + + 'COPY public.regress_table_dumpable (col1)' => { + regexp => qr/^ + \QCOPY public.regress_table_dumpable (col1) FROM stdin;\E + \n/xm, + like => { + %full_runs, + data_only => 1, + section_data => 1, + extension_schema => 1, + }, + unlike => { + binary_upgrade => 1, + exclude_table => 1, + without_extension => 1, + }, + }, + + 'REVOKE ALL ON FUNCTION wgo_then_no_access' => { + create_order => 3, + create_sql => q{ + DO $$BEGIN EXECUTE format( + 'REVOKE ALL ON FUNCTION wgo_then_no_access() + FROM pg_signal_backend, public, %I', + (SELECT usename + FROM pg_user JOIN pg_proc ON proowner = usesysid + WHERE proname = 'wgo_then_no_access')); END$$;}, + regexp => qr/^ + \QREVOKE ALL ON FUNCTION public.wgo_then_no_access() FROM PUBLIC;\E + \n\QREVOKE ALL ON FUNCTION public.wgo_then_no_access() FROM \E.*; + \n\QREVOKE ALL ON FUNCTION public.wgo_then_no_access() FROM pg_signal_backend;\E + /xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { no_privs => 1, without_extension => 1, }, + }, + + 'REVOKE GRANT OPTION FOR UPDATE ON SEQUENCE wgo_then_regular' => { + create_order => 3, + create_sql => 'REVOKE GRANT OPTION FOR UPDATE ON SEQUENCE + wgo_then_regular FROM pg_signal_backend;', + regexp => qr/^ + \QREVOKE ALL ON SEQUENCE public.wgo_then_regular FROM pg_signal_backend;\E + \n\QGRANT SELECT,UPDATE ON SEQUENCE public.wgo_then_regular TO pg_signal_backend;\E + \n\QGRANT USAGE ON SEQUENCE public.wgo_then_regular TO pg_signal_backend WITH GRANT OPTION;\E + /xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { no_privs => 1, without_extension => 1, }, + }, + + 'CREATE ACCESS METHOD regress_test_am' => { + regexp => qr/^ + \QCREATE ACCESS METHOD regress_test_am TYPE INDEX HANDLER bthandler;\E + \n/xm, + like => { binary_upgrade => 1, }, + }, + + 'COMMENT ON EXTENSION test_pg_dump' => { + regexp => qr/^ + \QCOMMENT ON EXTENSION test_pg_dump \E + \QIS 'Test pg_dump with an extension';\E + \n/xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { without_extension => 1, }, + }, + + 'GRANT SELECT regress_pg_dump_table_added pre-ALTER EXTENSION' => { + create_order => 8, + create_sql => + 'GRANT SELECT ON regress_pg_dump_table_added TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON TABLE public.regress_pg_dump_table_added TO regress_dump_test_role;\E + \n/xm, + like => { binary_upgrade => 1, }, + }, + + 'REVOKE SELECT regress_pg_dump_table_added post-ALTER EXTENSION' => { + create_order => 10, + create_sql => + 'REVOKE SELECT ON regress_pg_dump_table_added FROM regress_dump_test_role;', + regexp => qr/^ + \QREVOKE SELECT ON TABLE public.regress_pg_dump_table_added FROM regress_dump_test_role;\E + \n/xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { no_privs => 1, without_extension => 1, }, + }, + + 'GRANT SELECT ON TABLE regress_pg_dump_table' => { + regexp => qr/^ + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(true);\E\n + \QGRANT SELECT ON TABLE public.regress_pg_dump_table TO regress_dump_test_role;\E\n + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(false);\E + \n/xms, + like => { binary_upgrade => 1, }, + }, + + 'GRANT SELECT(col1) ON regress_pg_dump_table' => { + regexp => qr/^ + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(true);\E\n + \QGRANT SELECT(col1) ON TABLE public.regress_pg_dump_table TO PUBLIC;\E\n + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(false);\E + \n/xms, + like => { binary_upgrade => 1, }, + }, + + 'GRANT SELECT(col2) ON regress_pg_dump_table TO regress_dump_test_role' + => { + create_order => 4, + create_sql => 'GRANT SELECT(col2) ON regress_pg_dump_table + TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT(col2) ON TABLE public.regress_pg_dump_table TO regress_dump_test_role;\E + \n/xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { no_privs => 1, without_extension => 1 }, + }, + + 'GRANT USAGE ON regress_pg_dump_table_col1_seq TO regress_dump_test_role' + => { + create_order => 5, + create_sql => 'GRANT USAGE ON SEQUENCE regress_pg_dump_table_col1_seq + TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT USAGE ON SEQUENCE public.regress_pg_dump_table_col1_seq TO regress_dump_test_role;\E + \n/xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { no_privs => 1, without_extension => 1, }, + }, + + 'GRANT USAGE ON regress_pg_dump_seq TO regress_dump_test_role' => { + regexp => qr/^ + \QGRANT USAGE ON SEQUENCE public.regress_pg_dump_seq TO regress_dump_test_role;\E + \n/xm, + like => { binary_upgrade => 1, }, + }, + + 'REVOKE SELECT(col1) ON regress_pg_dump_table' => { + create_order => 3, + create_sql => 'REVOKE SELECT(col1) ON regress_pg_dump_table + FROM PUBLIC;', + regexp => qr/^ + \QREVOKE SELECT(col1) ON TABLE public.regress_pg_dump_table FROM PUBLIC;\E + \n/xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + }, + unlike => { no_privs => 1, without_extension => 1, }, + }, + + # Objects included in extension part of a schema created by this extension */ + 'CREATE TABLE regress_pg_dump_schema.test_table' => { + regexp => qr/^ + \QCREATE TABLE regress_pg_dump_schema.test_table (\E + \n\s+\Qcol1 integer,\E + \n\s+\Qcol2 integer,\E + \n\s+\QCONSTRAINT test_table_col2_check CHECK ((col2 > 0))\E + \n\);\n/xm, + like => { binary_upgrade => 1, }, + }, + + 'GRANT SELECT ON regress_pg_dump_schema.test_table' => { + regexp => qr/^ + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(true);\E\n + \QGRANT SELECT ON TABLE regress_pg_dump_schema.test_table TO regress_dump_test_role;\E\n + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(false);\E + \n/xms, + like => { binary_upgrade => 1, }, + }, + + 'CREATE SEQUENCE regress_pg_dump_schema.test_seq' => { + regexp => qr/^ + \QCREATE SEQUENCE regress_pg_dump_schema.test_seq\E + \n\s+\QSTART WITH 1\E + \n\s+\QINCREMENT BY 1\E + \n\s+\QNO MINVALUE\E + \n\s+\QNO MAXVALUE\E + \n\s+\QCACHE 1;\E + \n/xm, + like => { binary_upgrade => 1, }, + }, + + 'GRANT USAGE ON regress_pg_dump_schema.test_seq' => { + regexp => qr/^ + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(true);\E\n + \QGRANT USAGE ON SEQUENCE regress_pg_dump_schema.test_seq TO regress_dump_test_role;\E\n + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(false);\E + \n/xms, + like => { binary_upgrade => 1, }, + }, + + 'CREATE TYPE regress_pg_dump_schema.test_type' => { + regexp => qr/^ + \QCREATE TYPE regress_pg_dump_schema.test_type AS (\E + \n\s+\Qcol1 integer\E + \n\);\n/xm, + like => { binary_upgrade => 1, }, + }, + + 'GRANT USAGE ON regress_pg_dump_schema.test_type' => { + regexp => qr/^ + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(true);\E\n + \QGRANT ALL ON TYPE regress_pg_dump_schema.test_type TO regress_dump_test_role;\E\n + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(false);\E + \n/xms, + like => { binary_upgrade => 1, }, + }, + + 'CREATE FUNCTION regress_pg_dump_schema.test_func' => { + regexp => qr/^ + \QCREATE FUNCTION regress_pg_dump_schema.test_func() RETURNS integer\E + \n\s+\QLANGUAGE sql\E + \n/xm, + like => { binary_upgrade => 1, }, + }, + + 'GRANT ALL ON regress_pg_dump_schema.test_func' => { + regexp => qr/^ + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(true);\E\n + \QGRANT ALL ON FUNCTION regress_pg_dump_schema.test_func() TO regress_dump_test_role;\E\n + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(false);\E + \n/xms, + like => { binary_upgrade => 1, }, + }, + + 'CREATE AGGREGATE regress_pg_dump_schema.test_agg' => { + regexp => qr/^ + \QCREATE AGGREGATE regress_pg_dump_schema.test_agg(smallint) (\E + \n\s+\QSFUNC = int2_sum,\E + \n\s+\QSTYPE = bigint\E + \n\);\n/xm, + like => { binary_upgrade => 1, }, + }, + + 'GRANT ALL ON regress_pg_dump_schema.test_agg' => { + regexp => qr/^ + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(true);\E\n + \QGRANT ALL ON FUNCTION regress_pg_dump_schema.test_agg(smallint) TO regress_dump_test_role;\E\n + \QSELECT pg_catalog.binary_upgrade_set_record_init_privs(false);\E + \n/xms, + like => { binary_upgrade => 1, }, + }, + + 'ALTER INDEX pkey DEPENDS ON extension' => { + create_order => 11, + create_sql => + 'CREATE TABLE regress_pg_dump_schema.extdependtab (col1 integer primary key, col2 int); + CREATE INDEX ON regress_pg_dump_schema.extdependtab (col2); + ALTER INDEX regress_pg_dump_schema.extdependtab_col2_idx DEPENDS ON EXTENSION test_pg_dump; + ALTER INDEX regress_pg_dump_schema.extdependtab_pkey DEPENDS ON EXTENSION test_pg_dump;', + regexp => qr/^ + \QALTER INDEX regress_pg_dump_schema.extdependtab_pkey DEPENDS ON EXTENSION test_pg_dump;\E\n + /xms, + like => {%pgdump_runs}, + unlike => { + data_only => 1, + extension_schema => 1, + pg_dumpall_globals => 1, + section_data => 1, + section_pre_data => 1, + # Excludes this schema as extension is not listed. + without_extension_explicit_schema => 1, + }, + }, + + 'ALTER INDEX idx DEPENDS ON extension' => { + regexp => qr/^ + \QALTER INDEX regress_pg_dump_schema.extdependtab_col2_idx DEPENDS ON EXTENSION test_pg_dump;\E\n + /xms, + like => {%pgdump_runs}, + unlike => { + data_only => 1, + extension_schema => 1, + pg_dumpall_globals => 1, + section_data => 1, + section_pre_data => 1, + # Excludes this schema as extension is not listed. + without_extension_explicit_schema => 1, + }, + }, + + # Objects not included in extension, part of schema created by extension + 'CREATE TABLE regress_pg_dump_schema.external_tab' => { + create_order => 4, + create_sql => 'CREATE TABLE regress_pg_dump_schema.external_tab + (col1 int);', + regexp => qr/^ + \QCREATE TABLE regress_pg_dump_schema.external_tab (\E + \n\s+\Qcol1 integer\E + \n\);\n/xm, + like => { + %full_runs, + schema_only => 1, + section_pre_data => 1, + # Excludes the extension and keeps the schema's data. + without_extension_internal_schema => 1, + }, + },); + +######################################### +# Create a PG instance to test actually dumping from + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +my $port = $node->port; + +######################################### +# Set up schemas, tables, etc, to be dumped. + +# Build up the create statements +my $create_sql = ''; + +foreach my $test ( + sort { + if ($tests{$a}->{create_order} and $tests{$b}->{create_order}) + { + $tests{$a}->{create_order} <=> $tests{$b}->{create_order}; + } + elsif ($tests{$a}->{create_order}) + { + -1; + } + elsif ($tests{$b}->{create_order}) + { + 1; + } + else + { + 0; + } + } keys %tests) +{ + if ($tests{$test}->{create_sql}) + { + $create_sql .= $tests{$test}->{create_sql}; + } +} + +# Send the combined set of commands to psql +$node->safe_psql('postgres', $create_sql); + +######################################### +# Run all runs + +foreach my $run (sort keys %pgdump_runs) +{ + + my $test_key = $run; + + $node->command_ok(\@{ $pgdump_runs{$run}->{dump_cmd} }, + "$run: pg_dump runs"); + + if ($pgdump_runs{$run}->{restore_cmd}) + { + $node->command_ok(\@{ $pgdump_runs{$run}->{restore_cmd} }, + "$run: pg_restore runs"); + } + + if ($pgdump_runs{$run}->{test_key}) + { + $test_key = $pgdump_runs{$run}->{test_key}; + } + + my $output_file = slurp_file("$tempdir/${run}.sql"); + + ######################################### + # Run all tests where this run is included + # as either a 'like' or 'unlike' test. + + foreach my $test (sort keys %tests) + { + # Run the test listed as a like, unless it is specifically noted + # as an unlike (generally due to an explicit exclusion or similar). + if ($tests{$test}->{like}->{$test_key} + && !defined($tests{$test}->{unlike}->{$test_key})) + { + if (!ok($output_file =~ $tests{$test}->{regexp}, + "$run: should dump $test")) + { + diag("Review $run results in $tempdir"); + } + } + else + { + if (!ok($output_file !~ $tests{$test}->{regexp}, + "$run: should not dump $test")) + { + diag("Review $run results in $tempdir"); + } + } + } +} + +######################################### +# Stop the database instance, which will be removed at the end of the tests. + +$node->stop('fast'); + +done_testing(); diff --git a/src/test/modules/test_pg_dump/test_pg_dump--1.0.sql b/src/test/modules/test_pg_dump/test_pg_dump--1.0.sql new file mode 100644 index 0000000..110f7ee --- /dev/null +++ b/src/test/modules/test_pg_dump/test_pg_dump--1.0.sql @@ -0,0 +1,62 @@ +/* src/test/modules/test_pg_dump/test_pg_dump--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_pg_dump" to load this file. \quit + +CREATE TABLE regress_pg_dump_table ( + col1 serial, + col2 int check (col2 > 0) +); + +CREATE SEQUENCE regress_pg_dump_seq; + +CREATE SEQUENCE regress_seq_dumpable; +SELECT pg_catalog.pg_extension_config_dump('regress_seq_dumpable', ''); + +CREATE TABLE regress_table_dumpable ( + col1 int check (col1 > 0) +); +SELECT pg_catalog.pg_extension_config_dump('regress_table_dumpable', ''); + +CREATE SCHEMA regress_pg_dump_schema; + +GRANT USAGE ON regress_pg_dump_seq TO regress_dump_test_role; + +GRANT SELECT ON regress_pg_dump_table TO regress_dump_test_role; +GRANT SELECT(col1) ON regress_pg_dump_table TO public; + +GRANT SELECT(col2) ON regress_pg_dump_table TO regress_dump_test_role; +REVOKE SELECT(col2) ON regress_pg_dump_table FROM regress_dump_test_role; + +CREATE FUNCTION wgo_then_no_access() RETURNS int LANGUAGE SQL AS 'SELECT 1'; +GRANT ALL ON FUNCTION wgo_then_no_access() + TO pg_signal_backend WITH GRANT OPTION; + +CREATE SEQUENCE wgo_then_regular; +GRANT ALL ON SEQUENCE wgo_then_regular TO pg_signal_backend WITH GRANT OPTION; +REVOKE GRANT OPTION FOR SELECT ON SEQUENCE wgo_then_regular + FROM pg_signal_backend; + +CREATE ACCESS METHOD regress_test_am TYPE INDEX HANDLER bthandler; + +-- Create a set of objects that are part of the schema created by +-- this extension. +CREATE TABLE regress_pg_dump_schema.test_table ( + col1 int, + col2 int check (col2 > 0) +); +GRANT SELECT ON regress_pg_dump_schema.test_table TO regress_dump_test_role; + +CREATE SEQUENCE regress_pg_dump_schema.test_seq; +GRANT USAGE ON regress_pg_dump_schema.test_seq TO regress_dump_test_role; + +CREATE TYPE regress_pg_dump_schema.test_type AS (col1 int); +GRANT USAGE ON TYPE regress_pg_dump_schema.test_type TO regress_dump_test_role; + +CREATE FUNCTION regress_pg_dump_schema.test_func () RETURNS int +AS 'SELECT 1;' LANGUAGE SQL; +GRANT EXECUTE ON FUNCTION regress_pg_dump_schema.test_func() TO regress_dump_test_role; + +CREATE AGGREGATE regress_pg_dump_schema.test_agg(int2) +(SFUNC = int2_sum, STYPE = int8); +GRANT EXECUTE ON FUNCTION regress_pg_dump_schema.test_agg(int2) TO regress_dump_test_role; diff --git a/src/test/modules/test_pg_dump/test_pg_dump.control b/src/test/modules/test_pg_dump/test_pg_dump.control new file mode 100644 index 0000000..fe3450d --- /dev/null +++ b/src/test/modules/test_pg_dump/test_pg_dump.control @@ -0,0 +1,3 @@ +comment = 'Test pg_dump with an extension' +default_version = '1.0' +relocatable = true diff --git a/src/test/modules/test_predtest/.gitignore b/src/test/modules/test_predtest/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_predtest/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_predtest/Makefile b/src/test/modules/test_predtest/Makefile new file mode 100644 index 0000000..a235e2a --- /dev/null +++ b/src/test/modules/test_predtest/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_predtest/Makefile + +MODULE_big = test_predtest +OBJS = \ + $(WIN32RES) \ + test_predtest.o +PGFILEDESC = "test_predtest - test code for optimizer/util/predtest.c" + +EXTENSION = test_predtest +DATA = test_predtest--1.0.sql + +REGRESS = test_predtest + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_predtest +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_predtest/README b/src/test/modules/test_predtest/README new file mode 100644 index 0000000..2c9bec0 --- /dev/null +++ b/src/test/modules/test_predtest/README @@ -0,0 +1,28 @@ +test_predtest is a module for checking the correctness of the optimizer's +predicate-proof logic, in src/backend/optimizer/util/predtest.c. + +The module provides a function that allows direct application of +predtest.c's exposed functions, predicate_implied_by() and +predicate_refuted_by(), to arbitrary boolean expressions, with direct +inspection of the results. This could be done indirectly by checking +planner results, but it can be difficult to construct end-to-end test +cases that prove that the expected results were obtained. + +In general, the use of this function is like + select * from test_predtest('query string') +where the query string must be a SELECT returning two boolean +columns, for example + + select * from test_predtest($$ + select x, not x + from (values (false), (true), (null)) as v(x) + $$); + +The function parses and plans the given query, and then applies the +predtest.c code to the two boolean expressions in the SELECT list, to see +if the first expression can be proven or refuted by the second. It also +executes the query, and checks the resulting rows to see whether any +claimed implication or refutation relationship actually holds. If the +query is designed to exercise the expressions on a full set of possible +input values, as in the example above, then this provides a mechanical +cross-check as to whether the proof code has given a correct answer. diff --git a/src/test/modules/test_predtest/expected/test_predtest.out b/src/test/modules/test_predtest/expected/test_predtest.out new file mode 100644 index 0000000..6d21bcd --- /dev/null +++ b/src/test/modules/test_predtest/expected/test_predtest.out @@ -0,0 +1,1096 @@ +CREATE EXTENSION test_predtest; +-- Make output more legible +\pset expanded on +-- Test data +-- all combinations of four boolean values +create table booleans as +select + case i%3 when 0 then true when 1 then false else null end as x, + case (i/3)%3 when 0 then true when 1 then false else null end as y, + case (i/9)%3 when 0 then true when 1 then false else null end as z, + case (i/27)%3 when 0 then true when 1 then false else null end as w +from generate_series(0, 3*3*3*3-1) i; +-- all combinations of two integers 0..9, plus null +create table integers as +select + case i%11 when 10 then null else i%11 end as x, + case (i/11)%11 when 10 then null else (i/11)%11 end as y +from generate_series(0, 11*11-1) i; +-- and a simple strict function that's opaque to the optimizer +create function strictf(bool, bool) returns bool +language plpgsql as $$begin return $1 and not $2; end$$ strict; +-- a simple function to make arrays opaque to the optimizer +create function opaque_array(int[]) returns int[] +language plpgsql as $$begin return $1; end$$ strict; +-- Basic proof rules for single boolean variables +select * from test_predtest($$ +select x, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x, not x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select not x, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select not x, not x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x is not null, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x is not null, x is null +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is null, x is not null +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is not true, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x, x is not true +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | t + +select * from test_predtest($$ +select x is false, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x, x is false +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is unknown, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x, x is unknown +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | t +s_r_holds | f +w_r_holds | t + +-- Assorted not-so-trivial refutation rules +select * from test_predtest($$ +select x is null, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x, x is null +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | t +s_i_holds | f +w_i_holds | t +s_r_holds | f +w_r_holds | t + +select * from test_predtest($$ +select strictf(x,y), x is null +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | t +s_i_holds | f +w_i_holds | t +s_r_holds | f +w_r_holds | t + +select * from test_predtest($$ +select (x is not null) is not true, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select strictf(x,y), (x is not null) is false +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | t +s_i_holds | f +w_i_holds | t +s_r_holds | f +w_r_holds | t + +select * from test_predtest($$ +select x > y, (y < x) is false +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +-- Tests involving AND/OR constructs +select * from test_predtest($$ +select x, x and y +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select not x, x and y +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x, not x and y +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x or y, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x and y, x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x and y, not x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x and y, y and x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select not y, y and x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x or y, y or x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x or y or z, x or z +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x and z, x and y and z +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select z or w, x or y +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select z and w, x or y +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x, (x and y) or (x and z) +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select (x and y) or z, y and x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select (not x or not y) and z, y and x +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select y or x, (x or y) and z +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select not x and not y, (x or y) and z +from booleans +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +-- Tests using btree operator knowledge +select * from test_predtest($$ +select x <= y, x < y +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= y, x > y +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x <= y, y >= x +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= y, y > x and y < x+2 +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= 5, x <= 7 +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= 5, x > 7 +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x <= 5, 7 > x +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select 5 >= x, 7 > x +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select 5 >= x, x > 7 +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select 5 = x, x = 7 +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is not null, x > 7 +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x is not null, int4lt(x,8) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x is null, x > 7 +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is null, int4lt(x,8) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is not null, x < 'foo' +from (values + ('aaa'::varchar), ('zzz'::varchar), (null)) as v(x) +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- Cases using ScalarArrayOpExpr +select * from test_predtest($$ +select x <= 5, x in (1,3,5) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= 5, x in (1,3,5,7) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= 5, x in (1,3,5,null) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x in (null,1,3,5,7), x in (1,3,5) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= 5, x < all(array[1,3,5]) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | t +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x <= y, x = any(array[1,3,y]) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- In these tests, we want to prevent predtest.c from breaking down the +-- ScalarArrayOpExpr into an AND/OR tree, so as to exercise the logic +-- that handles ScalarArrayOpExpr directly. We use opaque_array() if +-- possible, otherwise an array longer than MAX_SAOP_ARRAY_SIZE. +-- ScalarArrayOpExpr implies scalar IS NOT NULL +select * from test_predtest($$ +select x is not null, x = any(opaque_array(array[1])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- but for ALL, we have to be able to prove the array nonempty +select * from test_predtest($$ +select x is not null, x <> all(opaque_array(array[1])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x is not null, x <> all(array[ + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53, + 54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78, + 79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101 +]) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +select * from test_predtest($$ +select x is not null, x <> all(array[ + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53, + 54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78, + 79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,y +]) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- check empty-array cases +select * from test_predtest($$ +select x is not null, x = any(opaque_array(array[]::int[])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | t +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | t +w_i_holds | t +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is not null, x <> all(opaque_array(array[]::int[])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- same thing under a strict function doesn't prove it +select * from test_predtest($$ +select x is not null, strictf(true, x = any(opaque_array(array[]::int[]))) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- ScalarArrayOpExpr refutes scalar IS NULL +select * from test_predtest($$ +select x is null, x = any(opaque_array(array[1])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +-- but for ALL, we have to be able to prove the array nonempty +select * from test_predtest($$ +select x is null, x <> all(opaque_array(array[1])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is null, x <> all(array[ + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53, + 54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78, + 79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101 +]) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | f +w_i_holds | f +s_r_holds | t +w_r_holds | t + +-- check empty-array cases +select * from test_predtest($$ +select x is null, x = any(opaque_array(array[]::int[])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | t +weak_refuted_by | t +s_i_holds | t +w_i_holds | t +s_r_holds | t +w_r_holds | t + +select * from test_predtest($$ +select x is null, x <> all(opaque_array(array[]::int[])) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- same thing under a strict function doesn't prove it +select * from test_predtest($$ +select x is null, strictf(true, x = any(opaque_array(array[]::int[]))) +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | f +s_r_holds | f +w_r_holds | f + +-- Also, nullness of the scalar weakly refutes a SAOP +select * from test_predtest($$ +select x = any(opaque_array(array[1])), x is null +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | t +s_i_holds | f +w_i_holds | t +s_r_holds | f +w_r_holds | t + +-- as does nullness of the array +select * from test_predtest($$ +select x = any(opaque_array(array[y])), array[y] is null +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | t +s_i_holds | t +w_i_holds | t +s_r_holds | t +w_r_holds | t + +-- ... unless we need to prove array empty +select * from test_predtest($$ +select x = all(opaque_array(array[1])), x is null +from integers +$$); +-[ RECORD 1 ]-----+-- +strong_implied_by | f +weak_implied_by | f +strong_refuted_by | f +weak_refuted_by | f +s_i_holds | f +w_i_holds | t +s_r_holds | f +w_r_holds | t + diff --git a/src/test/modules/test_predtest/sql/test_predtest.sql b/src/test/modules/test_predtest/sql/test_predtest.sql new file mode 100644 index 0000000..072eb5b --- /dev/null +++ b/src/test/modules/test_predtest/sql/test_predtest.sql @@ -0,0 +1,442 @@ +CREATE EXTENSION test_predtest; + +-- Make output more legible +\pset expanded on + +-- Test data + +-- all combinations of four boolean values +create table booleans as +select + case i%3 when 0 then true when 1 then false else null end as x, + case (i/3)%3 when 0 then true when 1 then false else null end as y, + case (i/9)%3 when 0 then true when 1 then false else null end as z, + case (i/27)%3 when 0 then true when 1 then false else null end as w +from generate_series(0, 3*3*3*3-1) i; + +-- all combinations of two integers 0..9, plus null +create table integers as +select + case i%11 when 10 then null else i%11 end as x, + case (i/11)%11 when 10 then null else (i/11)%11 end as y +from generate_series(0, 11*11-1) i; + +-- and a simple strict function that's opaque to the optimizer +create function strictf(bool, bool) returns bool +language plpgsql as $$begin return $1 and not $2; end$$ strict; + +-- a simple function to make arrays opaque to the optimizer +create function opaque_array(int[]) returns int[] +language plpgsql as $$begin return $1; end$$ strict; + +-- Basic proof rules for single boolean variables + +select * from test_predtest($$ +select x, x +from booleans +$$); + +select * from test_predtest($$ +select x, not x +from booleans +$$); + +select * from test_predtest($$ +select not x, x +from booleans +$$); + +select * from test_predtest($$ +select not x, not x +from booleans +$$); + +select * from test_predtest($$ +select x is not null, x +from booleans +$$); + +select * from test_predtest($$ +select x is not null, x is null +from integers +$$); + +select * from test_predtest($$ +select x is null, x is not null +from integers +$$); + +select * from test_predtest($$ +select x is not true, x +from booleans +$$); + +select * from test_predtest($$ +select x, x is not true +from booleans +$$); + +select * from test_predtest($$ +select x is false, x +from booleans +$$); + +select * from test_predtest($$ +select x, x is false +from booleans +$$); + +select * from test_predtest($$ +select x is unknown, x +from booleans +$$); + +select * from test_predtest($$ +select x, x is unknown +from booleans +$$); + +-- Assorted not-so-trivial refutation rules + +select * from test_predtest($$ +select x is null, x +from booleans +$$); + +select * from test_predtest($$ +select x, x is null +from booleans +$$); + +select * from test_predtest($$ +select strictf(x,y), x is null +from booleans +$$); + +select * from test_predtest($$ +select (x is not null) is not true, x +from booleans +$$); + +select * from test_predtest($$ +select strictf(x,y), (x is not null) is false +from booleans +$$); + +select * from test_predtest($$ +select x > y, (y < x) is false +from integers +$$); + +-- Tests involving AND/OR constructs + +select * from test_predtest($$ +select x, x and y +from booleans +$$); + +select * from test_predtest($$ +select not x, x and y +from booleans +$$); + +select * from test_predtest($$ +select x, not x and y +from booleans +$$); + +select * from test_predtest($$ +select x or y, x +from booleans +$$); + +select * from test_predtest($$ +select x and y, x +from booleans +$$); + +select * from test_predtest($$ +select x and y, not x +from booleans +$$); + +select * from test_predtest($$ +select x and y, y and x +from booleans +$$); + +select * from test_predtest($$ +select not y, y and x +from booleans +$$); + +select * from test_predtest($$ +select x or y, y or x +from booleans +$$); + +select * from test_predtest($$ +select x or y or z, x or z +from booleans +$$); + +select * from test_predtest($$ +select x and z, x and y and z +from booleans +$$); + +select * from test_predtest($$ +select z or w, x or y +from booleans +$$); + +select * from test_predtest($$ +select z and w, x or y +from booleans +$$); + +select * from test_predtest($$ +select x, (x and y) or (x and z) +from booleans +$$); + +select * from test_predtest($$ +select (x and y) or z, y and x +from booleans +$$); + +select * from test_predtest($$ +select (not x or not y) and z, y and x +from booleans +$$); + +select * from test_predtest($$ +select y or x, (x or y) and z +from booleans +$$); + +select * from test_predtest($$ +select not x and not y, (x or y) and z +from booleans +$$); + +-- Tests using btree operator knowledge + +select * from test_predtest($$ +select x <= y, x < y +from integers +$$); + +select * from test_predtest($$ +select x <= y, x > y +from integers +$$); + +select * from test_predtest($$ +select x <= y, y >= x +from integers +$$); + +select * from test_predtest($$ +select x <= y, y > x and y < x+2 +from integers +$$); + +select * from test_predtest($$ +select x <= 5, x <= 7 +from integers +$$); + +select * from test_predtest($$ +select x <= 5, x > 7 +from integers +$$); + +select * from test_predtest($$ +select x <= 5, 7 > x +from integers +$$); + +select * from test_predtest($$ +select 5 >= x, 7 > x +from integers +$$); + +select * from test_predtest($$ +select 5 >= x, x > 7 +from integers +$$); + +select * from test_predtest($$ +select 5 = x, x = 7 +from integers +$$); + +select * from test_predtest($$ +select x is not null, x > 7 +from integers +$$); + +select * from test_predtest($$ +select x is not null, int4lt(x,8) +from integers +$$); + +select * from test_predtest($$ +select x is null, x > 7 +from integers +$$); + +select * from test_predtest($$ +select x is null, int4lt(x,8) +from integers +$$); + +select * from test_predtest($$ +select x is not null, x < 'foo' +from (values + ('aaa'::varchar), ('zzz'::varchar), (null)) as v(x) +$$); + +-- Cases using ScalarArrayOpExpr + +select * from test_predtest($$ +select x <= 5, x in (1,3,5) +from integers +$$); + +select * from test_predtest($$ +select x <= 5, x in (1,3,5,7) +from integers +$$); + +select * from test_predtest($$ +select x <= 5, x in (1,3,5,null) +from integers +$$); + +select * from test_predtest($$ +select x in (null,1,3,5,7), x in (1,3,5) +from integers +$$); + +select * from test_predtest($$ +select x <= 5, x < all(array[1,3,5]) +from integers +$$); + +select * from test_predtest($$ +select x <= y, x = any(array[1,3,y]) +from integers +$$); + +-- In these tests, we want to prevent predtest.c from breaking down the +-- ScalarArrayOpExpr into an AND/OR tree, so as to exercise the logic +-- that handles ScalarArrayOpExpr directly. We use opaque_array() if +-- possible, otherwise an array longer than MAX_SAOP_ARRAY_SIZE. + +-- ScalarArrayOpExpr implies scalar IS NOT NULL +select * from test_predtest($$ +select x is not null, x = any(opaque_array(array[1])) +from integers +$$); + +-- but for ALL, we have to be able to prove the array nonempty +select * from test_predtest($$ +select x is not null, x <> all(opaque_array(array[1])) +from integers +$$); + +select * from test_predtest($$ +select x is not null, x <> all(array[ + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53, + 54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78, + 79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101 +]) +from integers +$$); + +select * from test_predtest($$ +select x is not null, x <> all(array[ + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53, + 54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78, + 79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,y +]) +from integers +$$); + +-- check empty-array cases +select * from test_predtest($$ +select x is not null, x = any(opaque_array(array[]::int[])) +from integers +$$); + +select * from test_predtest($$ +select x is not null, x <> all(opaque_array(array[]::int[])) +from integers +$$); + +-- same thing under a strict function doesn't prove it +select * from test_predtest($$ +select x is not null, strictf(true, x = any(opaque_array(array[]::int[]))) +from integers +$$); + +-- ScalarArrayOpExpr refutes scalar IS NULL +select * from test_predtest($$ +select x is null, x = any(opaque_array(array[1])) +from integers +$$); + +-- but for ALL, we have to be able to prove the array nonempty +select * from test_predtest($$ +select x is null, x <> all(opaque_array(array[1])) +from integers +$$); + +select * from test_predtest($$ +select x is null, x <> all(array[ + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53, + 54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78, + 79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101 +]) +from integers +$$); + +-- check empty-array cases +select * from test_predtest($$ +select x is null, x = any(opaque_array(array[]::int[])) +from integers +$$); + +select * from test_predtest($$ +select x is null, x <> all(opaque_array(array[]::int[])) +from integers +$$); + +-- same thing under a strict function doesn't prove it +select * from test_predtest($$ +select x is null, strictf(true, x = any(opaque_array(array[]::int[]))) +from integers +$$); + +-- Also, nullness of the scalar weakly refutes a SAOP +select * from test_predtest($$ +select x = any(opaque_array(array[1])), x is null +from integers +$$); + +-- as does nullness of the array +select * from test_predtest($$ +select x = any(opaque_array(array[y])), array[y] is null +from integers +$$); + +-- ... unless we need to prove array empty +select * from test_predtest($$ +select x = all(opaque_array(array[1])), x is null +from integers +$$); diff --git a/src/test/modules/test_predtest/test_predtest--1.0.sql b/src/test/modules/test_predtest/test_predtest--1.0.sql new file mode 100644 index 0000000..11e1444 --- /dev/null +++ b/src/test/modules/test_predtest/test_predtest--1.0.sql @@ -0,0 +1,16 @@ +/* src/test/modules/test_predtest/test_predtest--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_predtest" to load this file. \quit + +CREATE FUNCTION test_predtest(query text, + OUT strong_implied_by bool, + OUT weak_implied_by bool, + OUT strong_refuted_by bool, + OUT weak_refuted_by bool, + OUT s_i_holds bool, + OUT w_i_holds bool, + OUT s_r_holds bool, + OUT w_r_holds bool) +STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_predtest/test_predtest.c b/src/test/modules/test_predtest/test_predtest.c new file mode 100644 index 0000000..3b19e0e --- /dev/null +++ b/src/test/modules/test_predtest/test_predtest.c @@ -0,0 +1,218 @@ +/*-------------------------------------------------------------------------- + * + * test_predtest.c + * Test correctness of optimizer's predicate proof logic. + * + * Copyright (c) 2018-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_predtest/test_predtest.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "catalog/pg_type.h" +#include "executor/spi.h" +#include "funcapi.h" +#include "nodes/makefuncs.h" +#include "optimizer/optimizer.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +/* + * test_predtest(query text) returns record + */ +PG_FUNCTION_INFO_V1(test_predtest); + +Datum +test_predtest(PG_FUNCTION_ARGS) +{ + text *txt = PG_GETARG_TEXT_PP(0); + char *query_string = text_to_cstring(txt); + SPIPlanPtr spiplan; + int spirc; + TupleDesc tupdesc; + bool s_i_holds, + w_i_holds, + s_r_holds, + w_r_holds; + CachedPlan *cplan; + PlannedStmt *stmt; + Plan *plan; + Expr *clause1; + Expr *clause2; + bool strong_implied_by, + weak_implied_by, + strong_refuted_by, + weak_refuted_by; + Datum values[8]; + bool nulls[8]; + int i; + + /* We use SPI to parse, plan, and execute the test query */ + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + /* + * First, plan and execute the query, and inspect the results. To the + * extent that the query fully exercises the two expressions, this + * provides an experimental indication of whether implication or + * refutation holds. + */ + spiplan = SPI_prepare(query_string, 0, NULL); + if (spiplan == NULL) + elog(ERROR, "SPI_prepare failed for \"%s\"", query_string); + + spirc = SPI_execute_plan(spiplan, NULL, NULL, true, 0); + if (spirc != SPI_OK_SELECT) + elog(ERROR, "failed to execute \"%s\"", query_string); + tupdesc = SPI_tuptable->tupdesc; + if (tupdesc->natts != 2 || + TupleDescAttr(tupdesc, 0)->atttypid != BOOLOID || + TupleDescAttr(tupdesc, 1)->atttypid != BOOLOID) + elog(ERROR, "query must yield two boolean columns"); + + s_i_holds = w_i_holds = s_r_holds = w_r_holds = true; + for (i = 0; i < SPI_processed; i++) + { + HeapTuple tup = SPI_tuptable->vals[i]; + Datum dat; + bool isnull; + char c1, + c2; + + /* Extract column values in a 3-way representation */ + dat = SPI_getbinval(tup, tupdesc, 1, &isnull); + if (isnull) + c1 = 'n'; + else if (DatumGetBool(dat)) + c1 = 't'; + else + c1 = 'f'; + + dat = SPI_getbinval(tup, tupdesc, 2, &isnull); + if (isnull) + c2 = 'n'; + else if (DatumGetBool(dat)) + c2 = 't'; + else + c2 = 'f'; + + /* Check for violations of various proof conditions */ + + /* strong implication: truth of c2 implies truth of c1 */ + if (c2 == 't' && c1 != 't') + s_i_holds = false; + /* weak implication: non-falsity of c2 implies non-falsity of c1 */ + if (c2 != 'f' && c1 == 'f') + w_i_holds = false; + /* strong refutation: truth of c2 implies falsity of c1 */ + if (c2 == 't' && c1 != 'f') + s_r_holds = false; + /* weak refutation: truth of c2 implies non-truth of c1 */ + if (c2 == 't' && c1 == 't') + w_r_holds = false; + } + + /* + * Now, dig the clause querytrees out of the plan, and see what predtest.c + * does with them. + */ + cplan = SPI_plan_get_cached_plan(spiplan); + + if (list_length(cplan->stmt_list) != 1) + elog(ERROR, "failed to decipher query plan"); + stmt = linitial_node(PlannedStmt, cplan->stmt_list); + if (stmt->commandType != CMD_SELECT) + elog(ERROR, "failed to decipher query plan"); + plan = stmt->planTree; + Assert(list_length(plan->targetlist) >= 2); + clause1 = linitial_node(TargetEntry, plan->targetlist)->expr; + clause2 = lsecond_node(TargetEntry, plan->targetlist)->expr; + + /* + * Because the clauses are in the SELECT list, preprocess_expression did + * not pass them through canonicalize_qual nor make_ands_implicit. + * + * We can't do canonicalize_qual here, since it's unclear whether the + * expressions ought to be treated as WHERE or CHECK clauses. Fortunately, + * useful test expressions wouldn't be affected by those transformations + * anyway. We should do make_ands_implicit, though. + * + * Another way in which this does not exactly duplicate the normal usage + * of the proof functions is that they are often given qual clauses + * containing RestrictInfo nodes. But since predtest.c just looks through + * those anyway, it seems OK to not worry about that point. + */ + clause1 = (Expr *) make_ands_implicit(clause1); + clause2 = (Expr *) make_ands_implicit(clause2); + + strong_implied_by = predicate_implied_by((List *) clause1, + (List *) clause2, + false); + + weak_implied_by = predicate_implied_by((List *) clause1, + (List *) clause2, + true); + + strong_refuted_by = predicate_refuted_by((List *) clause1, + (List *) clause2, + false); + + weak_refuted_by = predicate_refuted_by((List *) clause1, + (List *) clause2, + true); + + /* + * Issue warning if any proof is demonstrably incorrect. + */ + if (strong_implied_by && !s_i_holds) + elog(WARNING, "strong_implied_by result is incorrect"); + if (weak_implied_by && !w_i_holds) + elog(WARNING, "weak_implied_by result is incorrect"); + if (strong_refuted_by && !s_r_holds) + elog(WARNING, "strong_refuted_by result is incorrect"); + if (weak_refuted_by && !w_r_holds) + elog(WARNING, "weak_refuted_by result is incorrect"); + + /* + * Clean up and return a record of the results. + */ + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); + + tupdesc = CreateTemplateTupleDesc(8); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, + "strong_implied_by", BOOLOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 2, + "weak_implied_by", BOOLOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 3, + "strong_refuted_by", BOOLOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 4, + "weak_refuted_by", BOOLOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 5, + "s_i_holds", BOOLOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 6, + "w_i_holds", BOOLOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 7, + "s_r_holds", BOOLOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 8, + "w_r_holds", BOOLOID, -1, 0); + tupdesc = BlessTupleDesc(tupdesc); + + MemSet(nulls, 0, sizeof(nulls)); + values[0] = BoolGetDatum(strong_implied_by); + values[1] = BoolGetDatum(weak_implied_by); + values[2] = BoolGetDatum(strong_refuted_by); + values[3] = BoolGetDatum(weak_refuted_by); + values[4] = BoolGetDatum(s_i_holds); + values[5] = BoolGetDatum(w_i_holds); + values[6] = BoolGetDatum(s_r_holds); + values[7] = BoolGetDatum(w_r_holds); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} diff --git a/src/test/modules/test_predtest/test_predtest.control b/src/test/modules/test_predtest/test_predtest.control new file mode 100644 index 0000000..a899a9d --- /dev/null +++ b/src/test/modules/test_predtest/test_predtest.control @@ -0,0 +1,4 @@ +comment = 'Test code for optimizer/util/predtest.c' +default_version = '1.0' +module_pathname = '$libdir/test_predtest' +relocatable = true diff --git a/src/test/modules/test_rbtree/.gitignore b/src/test/modules/test_rbtree/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_rbtree/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_rbtree/Makefile b/src/test/modules/test_rbtree/Makefile new file mode 100644 index 0000000..faf376a --- /dev/null +++ b/src/test/modules/test_rbtree/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_rbtree/Makefile + +MODULE_big = test_rbtree +OBJS = \ + $(WIN32RES) \ + test_rbtree.o +PGFILEDESC = "test_rbtree - test code for red-black tree library" + +EXTENSION = test_rbtree +DATA = test_rbtree--1.0.sql + +REGRESS = test_rbtree + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_rbtree +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_rbtree/README b/src/test/modules/test_rbtree/README new file mode 100644 index 0000000..d69eb8d --- /dev/null +++ b/src/test/modules/test_rbtree/README @@ -0,0 +1,13 @@ +test_rbtree is a test module for checking the correctness of red-black +tree operations. + +These tests are performed on red-black trees that store integers. +Since the rbtree logic treats the comparison function as a black +box, it shouldn't be important exactly what the key type is. + +Checking the correctness of traversals is based on the fact that a red-black +tree is a binary search tree, so the elements should be visited in increasing +(for Left-Current-Right) or decreasing (for Right-Current-Left) order. + +Also, this module does some checks of the correctness of the find, delete +and leftmost operations. diff --git a/src/test/modules/test_rbtree/expected/test_rbtree.out b/src/test/modules/test_rbtree/expected/test_rbtree.out new file mode 100644 index 0000000..3e32956 --- /dev/null +++ b/src/test/modules/test_rbtree/expected/test_rbtree.out @@ -0,0 +1,12 @@ +CREATE EXTENSION test_rbtree; +-- +-- These tests don't produce any interesting output. We're checking that +-- the operations complete without crashing or hanging and that none of their +-- internal sanity tests fail. +-- +SELECT test_rb_tree(10000); + test_rb_tree +-------------- + +(1 row) + diff --git a/src/test/modules/test_rbtree/sql/test_rbtree.sql b/src/test/modules/test_rbtree/sql/test_rbtree.sql new file mode 100644 index 0000000..d8dc88e --- /dev/null +++ b/src/test/modules/test_rbtree/sql/test_rbtree.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION test_rbtree; + +-- +-- These tests don't produce any interesting output. We're checking that +-- the operations complete without crashing or hanging and that none of their +-- internal sanity tests fail. +-- +SELECT test_rb_tree(10000); diff --git a/src/test/modules/test_rbtree/test_rbtree--1.0.sql b/src/test/modules/test_rbtree/test_rbtree--1.0.sql new file mode 100644 index 0000000..04f2a3a --- /dev/null +++ b/src/test/modules/test_rbtree/test_rbtree--1.0.sql @@ -0,0 +1,8 @@ +/* src/test/modules/test_rbtree/test_rbtree--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_rbtree" to load this file. \quit + +CREATE FUNCTION test_rb_tree(size INTEGER) + RETURNS pg_catalog.void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_rbtree/test_rbtree.c b/src/test/modules/test_rbtree/test_rbtree.c new file mode 100644 index 0000000..7cb3875 --- /dev/null +++ b/src/test/modules/test_rbtree/test_rbtree.c @@ -0,0 +1,414 @@ +/*-------------------------------------------------------------------------- + * + * test_rbtree.c + * Test correctness of red-black tree operations. + * + * Copyright (c) 2009-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_rbtree/test_rbtree.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "common/pg_prng.h" +#include "fmgr.h" +#include "lib/rbtree.h" +#include "utils/memutils.h" + +PG_MODULE_MAGIC; + + +/* + * Our test trees store an integer key, and nothing else. + */ +typedef struct IntRBTreeNode +{ + RBTNode rbtnode; + int key; +} IntRBTreeNode; + + +/* + * Node comparator. We don't worry about overflow in the subtraction, + * since none of our test keys are negative. + */ +static int +irbt_cmp(const RBTNode *a, const RBTNode *b, void *arg) +{ + const IntRBTreeNode *ea = (const IntRBTreeNode *) a; + const IntRBTreeNode *eb = (const IntRBTreeNode *) b; + + return ea->key - eb->key; +} + +/* + * Node combiner. For testing purposes, just check that library doesn't + * try to combine unequal keys. + */ +static void +irbt_combine(RBTNode *existing, const RBTNode *newdata, void *arg) +{ + const IntRBTreeNode *eexist = (const IntRBTreeNode *) existing; + const IntRBTreeNode *enew = (const IntRBTreeNode *) newdata; + + if (eexist->key != enew->key) + elog(ERROR, "red-black tree combines %d into %d", + enew->key, eexist->key); +} + +/* Node allocator */ +static RBTNode * +irbt_alloc(void *arg) +{ + return (RBTNode *) palloc(sizeof(IntRBTreeNode)); +} + +/* Node freer */ +static void +irbt_free(RBTNode *node, void *arg) +{ + pfree(node); +} + +/* + * Create a red-black tree using our support functions + */ +static RBTree * +create_int_rbtree(void) +{ + return rbt_create(sizeof(IntRBTreeNode), + irbt_cmp, + irbt_combine, + irbt_alloc, + irbt_free, + NULL); +} + +/* + * Generate a random permutation of the integers 0..size-1 + */ +static int * +GetPermutation(int size) +{ + int *permutation; + int i; + + permutation = (int *) palloc(size * sizeof(int)); + + permutation[0] = 0; + + /* + * This is the "inside-out" variant of the Fisher-Yates shuffle algorithm. + * Notionally, we append each new value to the array and then swap it with + * a randomly-chosen array element (possibly including itself, else we + * fail to generate permutations with the last integer last). The swap + * step can be optimized by combining it with the insertion. + */ + for (i = 1; i < size; i++) + { + int j = pg_prng_uint64_range(&pg_global_prng_state, 0, i); + + if (j < i) /* avoid fetching undefined data if j=i */ + permutation[i] = permutation[j]; + permutation[j] = i; + } + + return permutation; +} + +/* + * Populate an empty RBTree with "size" integers having the values + * 0, step, 2*step, 3*step, ..., inserting them in random order + */ +static void +rbt_populate(RBTree *tree, int size, int step) +{ + int *permutation = GetPermutation(size); + IntRBTreeNode node; + bool isNew; + int i; + + /* Insert values. We don't expect any collisions. */ + for (i = 0; i < size; i++) + { + node.key = step * permutation[i]; + rbt_insert(tree, (RBTNode *) &node, &isNew); + if (!isNew) + elog(ERROR, "unexpected !isNew result from rbt_insert"); + } + + /* + * Re-insert the first value to make sure collisions work right. It's + * probably not useful to test that case over again for all the values. + */ + if (size > 0) + { + node.key = step * permutation[0]; + rbt_insert(tree, (RBTNode *) &node, &isNew); + if (isNew) + elog(ERROR, "unexpected isNew result from rbt_insert"); + } + + pfree(permutation); +} + +/* + * Check the correctness of left-right traversal. + * Left-right traversal is correct if all elements are + * visited in increasing order. + */ +static void +testleftright(int size) +{ + RBTree *tree = create_int_rbtree(); + IntRBTreeNode *node; + RBTreeIterator iter; + int lastKey = -1; + int count = 0; + + /* check iteration over empty tree */ + rbt_begin_iterate(tree, LeftRightWalk, &iter); + if (rbt_iterate(&iter) != NULL) + elog(ERROR, "left-right walk over empty tree produced an element"); + + /* fill tree with consecutive natural numbers */ + rbt_populate(tree, size, 1); + + /* iterate over the tree */ + rbt_begin_iterate(tree, LeftRightWalk, &iter); + + while ((node = (IntRBTreeNode *) rbt_iterate(&iter)) != NULL) + { + /* check that order is increasing */ + if (node->key <= lastKey) + elog(ERROR, "left-right walk gives elements not in sorted order"); + lastKey = node->key; + count++; + } + + if (lastKey != size - 1) + elog(ERROR, "left-right walk did not reach end"); + if (count != size) + elog(ERROR, "left-right walk missed some elements"); +} + +/* + * Check the correctness of right-left traversal. + * Right-left traversal is correct if all elements are + * visited in decreasing order. + */ +static void +testrightleft(int size) +{ + RBTree *tree = create_int_rbtree(); + IntRBTreeNode *node; + RBTreeIterator iter; + int lastKey = size; + int count = 0; + + /* check iteration over empty tree */ + rbt_begin_iterate(tree, RightLeftWalk, &iter); + if (rbt_iterate(&iter) != NULL) + elog(ERROR, "right-left walk over empty tree produced an element"); + + /* fill tree with consecutive natural numbers */ + rbt_populate(tree, size, 1); + + /* iterate over the tree */ + rbt_begin_iterate(tree, RightLeftWalk, &iter); + + while ((node = (IntRBTreeNode *) rbt_iterate(&iter)) != NULL) + { + /* check that order is decreasing */ + if (node->key >= lastKey) + elog(ERROR, "right-left walk gives elements not in sorted order"); + lastKey = node->key; + count++; + } + + if (lastKey != 0) + elog(ERROR, "right-left walk did not reach end"); + if (count != size) + elog(ERROR, "right-left walk missed some elements"); +} + +/* + * Check the correctness of the rbt_find operation by searching for + * both elements we inserted and elements we didn't. + */ +static void +testfind(int size) +{ + RBTree *tree = create_int_rbtree(); + int i; + + /* Insert even integers from 0 to 2 * (size-1) */ + rbt_populate(tree, size, 2); + + /* Check that all inserted elements can be found */ + for (i = 0; i < size; i++) + { + IntRBTreeNode node; + IntRBTreeNode *resultNode; + + node.key = 2 * i; + resultNode = (IntRBTreeNode *) rbt_find(tree, (RBTNode *) &node); + if (resultNode == NULL) + elog(ERROR, "inserted element was not found"); + if (node.key != resultNode->key) + elog(ERROR, "find operation in rbtree gave wrong result"); + } + + /* + * Check that not-inserted elements can not be found, being sure to try + * values before the first and after the last element. + */ + for (i = -1; i <= 2 * size; i += 2) + { + IntRBTreeNode node; + IntRBTreeNode *resultNode; + + node.key = i; + resultNode = (IntRBTreeNode *) rbt_find(tree, (RBTNode *) &node); + if (resultNode != NULL) + elog(ERROR, "not-inserted element was found"); + } +} + +/* + * Check the correctness of the rbt_leftmost operation. + * This operation should always return the smallest element of the tree. + */ +static void +testleftmost(int size) +{ + RBTree *tree = create_int_rbtree(); + IntRBTreeNode *result; + + /* Check that empty tree has no leftmost element */ + if (rbt_leftmost(tree) != NULL) + elog(ERROR, "leftmost node of empty tree is not NULL"); + + /* fill tree with consecutive natural numbers */ + rbt_populate(tree, size, 1); + + /* Check that leftmost element is the smallest one */ + result = (IntRBTreeNode *) rbt_leftmost(tree); + if (result == NULL || result->key != 0) + elog(ERROR, "rbt_leftmost gave wrong result"); +} + +/* + * Check the correctness of the rbt_delete operation. + */ +static void +testdelete(int size, int delsize) +{ + RBTree *tree = create_int_rbtree(); + int *deleteIds; + bool *chosen; + int i; + + /* fill tree with consecutive natural numbers */ + rbt_populate(tree, size, 1); + + /* Choose unique ids to delete */ + deleteIds = (int *) palloc(delsize * sizeof(int)); + chosen = (bool *) palloc0(size * sizeof(bool)); + + for (i = 0; i < delsize; i++) + { + int k = pg_prng_uint64_range(&pg_global_prng_state, 0, size - 1); + + while (chosen[k]) + k = (k + 1) % size; + deleteIds[i] = k; + chosen[k] = true; + } + + /* Delete elements */ + for (i = 0; i < delsize; i++) + { + IntRBTreeNode find; + IntRBTreeNode *node; + + find.key = deleteIds[i]; + /* Locate the node to be deleted */ + node = (IntRBTreeNode *) rbt_find(tree, (RBTNode *) &find); + if (node == NULL || node->key != deleteIds[i]) + elog(ERROR, "expected element was not found during deleting"); + /* Delete it */ + rbt_delete(tree, (RBTNode *) node); + } + + /* Check that deleted elements are deleted */ + for (i = 0; i < size; i++) + { + IntRBTreeNode node; + IntRBTreeNode *result; + + node.key = i; + result = (IntRBTreeNode *) rbt_find(tree, (RBTNode *) &node); + if (chosen[i]) + { + /* Deleted element should be absent */ + if (result != NULL) + elog(ERROR, "deleted element still present in the rbtree"); + } + else + { + /* Else it should be present */ + if (result == NULL || result->key != i) + elog(ERROR, "delete operation removed wrong rbtree value"); + } + } + + /* Delete remaining elements, so as to exercise reducing tree to empty */ + for (i = 0; i < size; i++) + { + IntRBTreeNode find; + IntRBTreeNode *node; + + if (chosen[i]) + continue; + find.key = i; + /* Locate the node to be deleted */ + node = (IntRBTreeNode *) rbt_find(tree, (RBTNode *) &find); + if (node == NULL || node->key != i) + elog(ERROR, "expected element was not found during deleting"); + /* Delete it */ + rbt_delete(tree, (RBTNode *) node); + } + + /* Tree should now be empty */ + if (rbt_leftmost(tree) != NULL) + elog(ERROR, "deleting all elements failed"); + + pfree(deleteIds); + pfree(chosen); +} + +/* + * SQL-callable entry point to perform all tests + * + * Argument is the number of entries to put in the trees + */ +PG_FUNCTION_INFO_V1(test_rb_tree); + +Datum +test_rb_tree(PG_FUNCTION_ARGS) +{ + int size = PG_GETARG_INT32(0); + + if (size <= 0 || size > MaxAllocSize / sizeof(int)) + elog(ERROR, "invalid size for test_rb_tree: %d", size); + testleftright(size); + testrightleft(size); + testfind(size); + testleftmost(size); + testdelete(size, Max(size / 10, 1)); + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_rbtree/test_rbtree.control b/src/test/modules/test_rbtree/test_rbtree.control new file mode 100644 index 0000000..17966a5 --- /dev/null +++ b/src/test/modules/test_rbtree/test_rbtree.control @@ -0,0 +1,4 @@ +comment = 'Test code for red-black tree library' +default_version = '1.0' +module_pathname = '$libdir/test_rbtree' +relocatable = true diff --git a/src/test/modules/test_regex/.gitignore b/src/test/modules/test_regex/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_regex/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_regex/Makefile b/src/test/modules/test_regex/Makefile new file mode 100644 index 0000000..dfbc5dc --- /dev/null +++ b/src/test/modules/test_regex/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_regex/Makefile + +MODULE_big = test_regex +OBJS = \ + $(WIN32RES) \ + test_regex.o +PGFILEDESC = "test_regex - test code for backend/regex/" + +EXTENSION = test_regex +DATA = test_regex--1.0.sql + +REGRESS = test_regex test_regex_utf8 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_regex +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_regex/README b/src/test/modules/test_regex/README new file mode 100644 index 0000000..3ef152d --- /dev/null +++ b/src/test/modules/test_regex/README @@ -0,0 +1,78 @@ +test_regex is a module for testing the regular expression package. +It is mostly meant to allow us to absorb Tcl's regex test suite. +Therefore, there are provisions to exercise regex features that +aren't currently exposed at the SQL level by PostgreSQL. + +Currently, one function is provided: + +test_regex(pattern text, string text, flags text) returns setof text[] + +Reports an error if the pattern is an invalid regex. Otherwise, +the first row of output contains the number of subexpressions, +followed by words reporting set bit(s) in the regex's re_info field. +If the pattern doesn't match the string, that's all. +If the pattern does match, the next row contains the whole match +as the first array element. If there are parenthesized subexpression(s), +following array elements contain the matches to those subexpressions. +If the "g" (glob) flag is set, then additional row(s) of output similarly +report any additional matches. + +The "flags" argument is a string of zero or more single-character +flags that modify the behavior of the regex package or the test +function. As described in Tcl's reg.test file: + +The flag characters are complex and a bit eclectic. Generally speaking, +lowercase letters are compile options, uppercase are expected re_info +bits, and nonalphabetics are match options, controls for how the test is +run, or testing options. The one small surprise is that AREs are the +default, and you must explicitly request lesser flavors of RE. The flags +are as follows. It is admitted that some are not very mnemonic. + + - no-op (placeholder) + 0 report indices not actual strings + (This substitutes for Tcl's -indices switch) + ! expect partial match, report start position anyway + % force small state-set cache in matcher (to test cache replace) + ^ beginning of string is not beginning of line + $ end of string is not end of line + * test is Unicode-specific, needs big character set + + provide fake xy equivalence class and ch collating element + (Note: the equivalence class is implemented, the + collating element is not; so references to [.ch.] fail) + , set REG_PROGRESS (only useful in REG_DEBUG builds) + . set REG_DUMP (only useful in REG_DEBUG builds) + : set REG_MTRACE (only useful in REG_DEBUG builds) + ; set REG_FTRACE (only useful in REG_DEBUG builds) + + & test as both ARE and BRE + (Not implemented in Postgres, we use separate tests) + b BRE + e ERE + a turn advanced-features bit on (error unless ERE already) + q literal string, no metacharacters at all + + g global match (find all matches) + i case-independent matching + o ("opaque") do not return match locations + p newlines are half-magic, excluded from . and [^ only + w newlines are half-magic, significant to ^ and $ only + n newlines are fully magic, both effects + x expanded RE syntax + t incomplete-match reporting + c canmatch (equivalent to "t0!", in Postgres implementation) + s match only at start (REG_BOSONLY) + + A backslash-_a_lphanumeric seen + B ERE/ARE literal-_b_race heuristic used + E backslash (_e_scape) seen within [] + H looka_h_ead constraint seen + I _i_mpossible to match + L _l_ocale-specific construct seen + M unportable (_m_achine-specific) construct seen + N RE can match empty (_n_ull) string + P non-_P_OSIX construct seen + Q {} _q_uantifier seen + R back _r_eference seen + S POSIX-un_s_pecified syntax seen + T prefers shortest (_t_iny) + U saw original-POSIX botch: unmatched right paren in ERE (_u_gh) diff --git a/src/test/modules/test_regex/expected/test_regex.out b/src/test/modules/test_regex/expected/test_regex.out new file mode 100644 index 0000000..731ba50 --- /dev/null +++ b/src/test/modules/test_regex/expected/test_regex.out @@ -0,0 +1,5084 @@ +-- This file is based on tests/reg.test from the Tcl distribution, +-- which is marked +-- # Copyright (c) 1998, 1999 Henry Spencer. All rights reserved. +-- The full copyright notice can be found in src/backend/regex/COPYRIGHT. +-- Most commented lines below are copied from reg.test. Each +-- test case is followed by an equivalent test using test_regex(). +create extension test_regex; +set standard_conforming_strings = on; +-- # support functions and preliminary misc. +-- # This is sensitive to changes in message wording, but we really have to +-- # test the code->message expansion at least once. +-- ::tcltest::test reg-0.1 "regexp error reporting" { +-- list [catch {regexp (*) ign} msg] $msg +-- } {1 {couldn't compile regular expression pattern: quantifier operand invalid}} +select * from test_regex('(*)', '', ''); +ERROR: invalid regular expression: quantifier operand invalid +-- doing 1 "basic sanity checks" +-- expectMatch 1.1 & abc abc abc +select * from test_regex('abc', 'abc', ''); + test_regex +------------ + {0} + {abc} +(2 rows) + +select * from test_regex('abc', 'abc', 'b'); + test_regex +------------ + {0} + {abc} +(2 rows) + +-- expectNomatch 1.2 & abc def +select * from test_regex('abc', 'def', ''); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('abc', 'def', 'b'); + test_regex +------------ + {0} +(1 row) + +-- expectMatch 1.3 & abc xyabxabce abc +select * from test_regex('abc', 'xyabxabce', ''); + test_regex +------------ + {0} + {abc} +(2 rows) + +select * from test_regex('abc', 'xyabxabce', 'b'); + test_regex +------------ + {0} + {abc} +(2 rows) + +-- doing 2 "invalid option combinations" +-- expectError 2.1 qe a INVARG +select * from test_regex('a', '', 'qe'); +ERROR: invalid regular expression: invalid argument to regex function +-- expectError 2.2 qa a INVARG +select * from test_regex('a', '', 'qa'); +ERROR: invalid regular expression: invalid argument to regex function +-- expectError 2.3 qx a INVARG +select * from test_regex('a', '', 'qx'); +ERROR: invalid regular expression: invalid argument to regex function +-- expectError 2.4 qn a INVARG +select * from test_regex('a', '', 'qn'); +ERROR: invalid regular expression: invalid argument to regex function +-- expectError 2.5 ba a INVARG +select * from test_regex('a', '', 'ba'); +ERROR: invalid regular expression: invalid argument to regex function +-- doing 3 "basic syntax" +-- expectIndices 3.1 &NS "" a {0 -1} +select * from test_regex('', 'a', '0NS'); + test_regex +--------------------------------- + {0,REG_UUNSPEC,REG_UEMPTYMATCH} + {"0 -1"} +(2 rows) + +select * from test_regex('', 'a', '0NSb'); + test_regex +--------------------------------- + {0,REG_UUNSPEC,REG_UEMPTYMATCH} + {"0 -1"} +(2 rows) + +-- expectMatch 3.2 NS a| a a +select * from test_regex('a|', 'a', 'NS'); + test_regex +--------------------------------- + {0,REG_UUNSPEC,REG_UEMPTYMATCH} + {a} +(2 rows) + +-- expectMatch 3.3 - a|b a a +select * from test_regex('a|b', 'a', '-'); + test_regex +------------ + {0} + {a} +(2 rows) + +-- expectMatch 3.4 - a|b b b +select * from test_regex('a|b', 'b', '-'); + test_regex +------------ + {0} + {b} +(2 rows) + +-- expectMatch 3.5 NS a||b b b +select * from test_regex('a||b', 'b', 'NS'); + test_regex +--------------------------------- + {0,REG_UUNSPEC,REG_UEMPTYMATCH} + {b} +(2 rows) + +-- expectMatch 3.6 & ab ab ab +select * from test_regex('ab', 'ab', ''); + test_regex +------------ + {0} + {ab} +(2 rows) + +select * from test_regex('ab', 'ab', 'b'); + test_regex +------------ + {0} + {ab} +(2 rows) + +-- doing 4 "parentheses" +-- expectMatch 4.1 - (a)e ae ae a +select * from test_regex('(a)e', 'ae', '-'); + test_regex +------------ + {1} + {ae,a} +(2 rows) + +-- expectMatch 4.2 oPR (.)\1e abeaae aae {} +select * from test_regex('(.)\1e', 'abeaae', 'oPR'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {aae,NULL} +(2 rows) + +-- expectMatch 4.3 b {\(a\)b} ab ab a +select * from test_regex('\(a\)b', 'ab', 'b'); + test_regex +------------ + {1} + {ab,a} +(2 rows) + +-- expectMatch 4.4 - a((b)c) abc abc bc b +select * from test_regex('a((b)c)', 'abc', '-'); + test_regex +------------ + {2} + {abc,bc,b} +(2 rows) + +-- expectMatch 4.5 - a(b)(c) abc abc b c +select * from test_regex('a(b)(c)', 'abc', '-'); + test_regex +------------ + {2} + {abc,b,c} +(2 rows) + +-- expectError 4.6 - a(b EPAREN +select * from test_regex('a(b', '', '-'); +ERROR: invalid regular expression: parentheses () not balanced +-- expectError 4.7 b {a\(b} EPAREN +select * from test_regex('a\(b', '', 'b'); +ERROR: invalid regular expression: parentheses () not balanced +-- # sigh, we blew it on the specs here... someday this will be fixed in POSIX, +-- # but meanwhile, it's fixed in AREs +-- expectMatch 4.8 eU a)b a)b a)b +select * from test_regex('a)b', 'a)b', 'eU'); + test_regex +----------------- + {0,REG_UPBOTCH} + {a)b} +(2 rows) + +-- expectError 4.9 - a)b EPAREN +select * from test_regex('a)b', '', '-'); +ERROR: invalid regular expression: parentheses () not balanced +-- expectError 4.10 b {a\)b} EPAREN +select * from test_regex('a\)b', '', 'b'); +ERROR: invalid regular expression: parentheses () not balanced +-- expectMatch 4.11 P a(?:b)c abc abc +select * from test_regex('a(?:b)c', 'abc', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {abc} +(2 rows) + +-- expectError 4.12 e a(?:b)c BADRPT +select * from test_regex('a(?:b)c', '', 'e'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectIndices 4.13 S a()b ab {0 1} {1 0} +select * from test_regex('a()b', 'ab', '0S'); + test_regex +----------------- + {1,REG_UUNSPEC} + {"0 1","1 0"} +(2 rows) + +-- expectMatch 4.14 SP a(?:)b ab ab +select * from test_regex('a(?:)b', 'ab', 'SP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNSPEC} + {ab} +(2 rows) + +-- expectIndices 4.15 S a(|b)c ac {0 1} {1 0} +select * from test_regex('a(|b)c', 'ac', '0S'); + test_regex +----------------- + {1,REG_UUNSPEC} + {"0 1","1 0"} +(2 rows) + +-- expectMatch 4.16 S a(b|)c abc abc b +select * from test_regex('a(b|)c', 'abc', 'S'); + test_regex +----------------- + {1,REG_UUNSPEC} + {abc,b} +(2 rows) + +-- doing 5 "simple one-char matching" +-- # general case of brackets done later +-- expectMatch 5.1 & a.b axb axb +select * from test_regex('a.b', 'axb', ''); + test_regex +------------ + {0} + {axb} +(2 rows) + +select * from test_regex('a.b', 'axb', 'b'); + test_regex +------------ + {0} + {axb} +(2 rows) + +-- expectNomatch 5.2 &n "a.b" "a\nb" +select * from test_regex('a.b', E'a\nb', 'n'); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('a.b', E'a\nb', 'nb'); + test_regex +------------ + {0} +(1 row) + +-- expectMatch 5.3 & {a[bc]d} abd abd +select * from test_regex('a[bc]d', 'abd', ''); + test_regex +------------ + {0} + {abd} +(2 rows) + +select * from test_regex('a[bc]d', 'abd', 'b'); + test_regex +------------ + {0} + {abd} +(2 rows) + +-- expectMatch 5.4 & {a[bc]d} acd acd +select * from test_regex('a[bc]d', 'acd', ''); + test_regex +------------ + {0} + {acd} +(2 rows) + +select * from test_regex('a[bc]d', 'acd', 'b'); + test_regex +------------ + {0} + {acd} +(2 rows) + +-- expectNomatch 5.5 & {a[bc]d} aed +select * from test_regex('a[bc]d', 'aed', ''); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('a[bc]d', 'aed', 'b'); + test_regex +------------ + {0} +(1 row) + +-- expectNomatch 5.6 & {a[^bc]d} abd +select * from test_regex('a[^bc]d', 'abd', ''); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('a[^bc]d', 'abd', 'b'); + test_regex +------------ + {0} +(1 row) + +-- expectMatch 5.7 & {a[^bc]d} aed aed +select * from test_regex('a[^bc]d', 'aed', ''); + test_regex +------------ + {0} + {aed} +(2 rows) + +select * from test_regex('a[^bc]d', 'aed', 'b'); + test_regex +------------ + {0} + {aed} +(2 rows) + +-- expectNomatch 5.8 &p "a\[^bc]d" "a\nd" +select * from test_regex('a[^bc]d', E'a\nd', 'p'); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('a[^bc]d', E'a\nd', 'pb'); + test_regex +------------ + {0} +(1 row) + +-- doing 6 "context-dependent syntax" +-- # plus odds and ends +-- expectError 6.1 - * BADRPT +select * from test_regex('*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 6.2 b * * * +select * from test_regex('*', '*', 'b'); + test_regex +------------ + {0} + {*} +(2 rows) + +-- expectMatch 6.3 b {\(*\)} * * * +select * from test_regex('\(*\)', '*', 'b'); + test_regex +------------ + {1} + {*,*} +(2 rows) + +-- expectError 6.4 - (*) BADRPT +select * from test_regex('(*)', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 6.5 b ^* * * +select * from test_regex('^*', '*', 'b'); + test_regex +------------ + {0} + {*} +(2 rows) + +-- expectError 6.6 - ^* BADRPT +select * from test_regex('^*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectNomatch 6.7 & ^b ^b +select * from test_regex('^b', '^b', ''); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('^b', '^b', 'b'); + test_regex +------------ + {0} +(1 row) + +-- expectMatch 6.8 b x^ x^ x^ +select * from test_regex('x^', 'x^', 'b'); + test_regex +------------ + {0} + {x^} +(2 rows) + +-- expectNomatch 6.9 I x^ x +select * from test_regex('x^', 'x', 'I'); + test_regex +--------------------- + {0,REG_UIMPOSSIBLE} +(1 row) + +-- expectMatch 6.10 n "\n^" "x\nb" "\n" +select * from test_regex(E'\n^', E'x\nb', 'n'); + test_regex +------------ + {0} + {" + + "} +(2 rows) + +-- expectNomatch 6.11 bS {\(^b\)} ^b +select * from test_regex('\(^b\)', '^b', 'bS'); + test_regex +----------------- + {1,REG_UUNSPEC} +(1 row) + +-- expectMatch 6.12 - (^b) b b b +select * from test_regex('(^b)', 'b', '-'); + test_regex +------------ + {1} + {b,b} +(2 rows) + +-- expectMatch 6.13 & {x$} x x +select * from test_regex('x$', 'x', ''); + test_regex +------------ + {0} + {x} +(2 rows) + +select * from test_regex('x$', 'x', 'b'); + test_regex +------------ + {0} + {x} +(2 rows) + +-- expectMatch 6.14 bS {\(x$\)} x x x +select * from test_regex('\(x$\)', 'x', 'bS'); + test_regex +----------------- + {1,REG_UUNSPEC} + {x,x} +(2 rows) + +-- expectMatch 6.15 - {(x$)} x x x +select * from test_regex('(x$)', 'x', '-'); + test_regex +------------ + {1} + {x,x} +(2 rows) + +-- expectMatch 6.16 b {x$y} "x\$y" "x\$y" +select * from test_regex('x$y', 'x$y', 'b'); + test_regex +------------ + {0} + {x$y} +(2 rows) + +-- expectNomatch 6.17 I {x$y} xy +select * from test_regex('x$y', 'xy', 'I'); + test_regex +--------------------- + {0,REG_UIMPOSSIBLE} +(1 row) + +-- expectMatch 6.18 n "x\$\n" "x\n" "x\n" +select * from test_regex(E'x$\n', E'x\n', 'n'); + test_regex +------------ + {0} + {"x + + "} +(2 rows) + +-- expectError 6.19 - + BADRPT +select * from test_regex('+', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 6.20 - ? BADRPT +select * from test_regex('?', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- These two are not yet incorporated in Tcl, cf +-- https://core.tcl-lang.org/tcl/tktview?name=5ea71fdcd3291c38 +-- expectError 6.21 - {x(\w)(?=(\1))} ESUBREG +select * from test_regex('x(\w)(?=(\1))', '', '-'); +ERROR: invalid regular expression: invalid backreference number +-- expectMatch 6.22 HP {x(?=((foo)))} xfoo x +select * from test_regex('x(?=((foo)))', 'xfoo', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {x} +(2 rows) + +-- doing 7 "simple quantifiers" +-- expectMatch 7.1 &N a* aa aa +select * from test_regex('a*', 'aa', 'N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {aa} +(2 rows) + +select * from test_regex('a*', 'aa', 'Nb'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {aa} +(2 rows) + +-- expectIndices 7.2 &N a* b {0 -1} +select * from test_regex('a*', 'b', '0N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"0 -1"} +(2 rows) + +select * from test_regex('a*', 'b', '0Nb'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"0 -1"} +(2 rows) + +-- expectMatch 7.3 - a+ aa aa +select * from test_regex('a+', 'aa', '-'); + test_regex +------------ + {0} + {aa} +(2 rows) + +-- expectMatch 7.4 - a?b ab ab +select * from test_regex('a?b', 'ab', '-'); + test_regex +------------ + {0} + {ab} +(2 rows) + +-- expectMatch 7.5 - a?b b b +select * from test_regex('a?b', 'b', '-'); + test_regex +------------ + {0} + {b} +(2 rows) + +-- expectError 7.6 - ** BADRPT +select * from test_regex('**', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 7.7 bN ** *** *** +select * from test_regex('**', '***', 'bN'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {***} +(2 rows) + +-- expectError 7.8 & a** BADRPT +select * from test_regex('a**', '', ''); +ERROR: invalid regular expression: quantifier operand invalid +select * from test_regex('a**', '', 'b'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 7.9 & a**b BADRPT +select * from test_regex('a**b', '', ''); +ERROR: invalid regular expression: quantifier operand invalid +select * from test_regex('a**b', '', 'b'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 7.10 & *** BADRPT +select * from test_regex('***', '', ''); +ERROR: invalid regular expression: quantifier operand invalid +select * from test_regex('***', '', 'b'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 7.11 - a++ BADRPT +select * from test_regex('a++', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 7.12 - a?+ BADRPT +select * from test_regex('a?+', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 7.13 - a?* BADRPT +select * from test_regex('a?*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 7.14 - a+* BADRPT +select * from test_regex('a+*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 7.15 - a*+ BADRPT +select * from test_regex('a*+', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- tests for ancient brenext() bugs; not currently in Tcl +select * from test_regex('.*b', 'aaabbb', 'b'); + test_regex +------------ + {0} + {aaabbb} +(2 rows) + +select * from test_regex('.\{1,10\}', 'abcdef', 'bQ'); + test_regex +----------------- + {0,REG_UBOUNDS} + {abcdef} +(2 rows) + +-- doing 8 "braces" +-- expectMatch 8.1 NQ "a{0,1}" "" "" +select * from test_regex('a{0,1}', '', 'NQ'); + test_regex +--------------------------------- + {0,REG_UBOUNDS,REG_UEMPTYMATCH} + {""} +(2 rows) + +-- expectMatch 8.2 NQ "a{0,1}" ac a +select * from test_regex('a{0,1}', 'ac', 'NQ'); + test_regex +--------------------------------- + {0,REG_UBOUNDS,REG_UEMPTYMATCH} + {a} +(2 rows) + +-- expectError 8.3 - "a{1,0}" BADBR +select * from test_regex('a{1,0}', '', '-'); +ERROR: invalid regular expression: invalid repetition count(s) +-- expectError 8.4 - "a{1,2,3}" BADBR +select * from test_regex('a{1,2,3}', '', '-'); +ERROR: invalid regular expression: invalid repetition count(s) +-- expectError 8.5 - "a{257}" BADBR +select * from test_regex('a{257}', '', '-'); +ERROR: invalid regular expression: invalid repetition count(s) +-- expectError 8.6 - "a{1000}" BADBR +select * from test_regex('a{1000}', '', '-'); +ERROR: invalid regular expression: invalid repetition count(s) +-- expectError 8.7 - "a{1" EBRACE +select * from test_regex('a{1', '', '-'); +ERROR: invalid regular expression: braces {} not balanced +-- expectError 8.8 - "a{1n}" BADBR +select * from test_regex('a{1n}', '', '-'); +ERROR: invalid regular expression: invalid repetition count(s) +-- expectMatch 8.9 BS "a{b" "a\{b" "a\{b" +select * from test_regex('a{b', 'a{b', 'BS'); + test_regex +----------------------------- + {0,REG_UBRACES,REG_UUNSPEC} + {"a{b"} +(2 rows) + +-- expectMatch 8.10 BS "a{" "a\{" "a\{" +select * from test_regex('a{', 'a{', 'BS'); + test_regex +----------------------------- + {0,REG_UBRACES,REG_UUNSPEC} + {"a{"} +(2 rows) + +-- expectMatch 8.11 bQ "a\\{0,1\\}b" cb b +select * from test_regex('a\{0,1\}b', 'cb', 'bQ'); + test_regex +----------------- + {0,REG_UBOUNDS} + {b} +(2 rows) + +-- expectError 8.12 b "a\\{0,1" EBRACE +select * from test_regex('a\{0,1', '', 'b'); +ERROR: invalid regular expression: braces {} not balanced +-- expectError 8.13 - "a{0,1\\" BADBR +select * from test_regex('a{0,1\', '', '-'); +ERROR: invalid regular expression: invalid repetition count(s) +-- expectMatch 8.14 Q "a{0}b" ab b +select * from test_regex('a{0}b', 'ab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {b} +(2 rows) + +-- expectMatch 8.15 Q "a{0,0}b" ab b +select * from test_regex('a{0,0}b', 'ab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {b} +(2 rows) + +-- expectMatch 8.16 Q "a{0,1}b" ab ab +select * from test_regex('a{0,1}b', 'ab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {ab} +(2 rows) + +-- expectMatch 8.17 Q "a{0,2}b" b b +select * from test_regex('a{0,2}b', 'b', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {b} +(2 rows) + +-- expectMatch 8.18 Q "a{0,2}b" aab aab +select * from test_regex('a{0,2}b', 'aab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {aab} +(2 rows) + +-- expectMatch 8.19 Q "a{0,}b" aab aab +select * from test_regex('a{0,}b', 'aab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {aab} +(2 rows) + +-- expectMatch 8.20 Q "a{1,1}b" aab ab +select * from test_regex('a{1,1}b', 'aab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {ab} +(2 rows) + +-- expectMatch 8.21 Q "a{1,3}b" aaaab aaab +select * from test_regex('a{1,3}b', 'aaaab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {aaab} +(2 rows) + +-- expectNomatch 8.22 Q "a{1,3}b" b +select * from test_regex('a{1,3}b', 'b', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} +(1 row) + +-- expectMatch 8.23 Q "a{1,}b" aab aab +select * from test_regex('a{1,}b', 'aab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {aab} +(2 rows) + +-- expectNomatch 8.24 Q "a{2,3}b" ab +select * from test_regex('a{2,3}b', 'ab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} +(1 row) + +-- expectMatch 8.25 Q "a{2,3}b" aaaab aaab +select * from test_regex('a{2,3}b', 'aaaab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {aaab} +(2 rows) + +-- expectNomatch 8.26 Q "a{2,}b" ab +select * from test_regex('a{2,}b', 'ab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} +(1 row) + +-- expectMatch 8.27 Q "a{2,}b" aaaab aaaab +select * from test_regex('a{2,}b', 'aaaab', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {aaaab} +(2 rows) + +-- doing 9 "brackets" +-- expectMatch 9.1 & {a[bc]} ac ac +select * from test_regex('a[bc]', 'ac', ''); + test_regex +------------ + {0} + {ac} +(2 rows) + +select * from test_regex('a[bc]', 'ac', 'b'); + test_regex +------------ + {0} + {ac} +(2 rows) + +-- expectMatch 9.2 & {a[-]} a- a- +select * from test_regex('a[-]', 'a-', ''); + test_regex +------------ + {0} + {a-} +(2 rows) + +select * from test_regex('a[-]', 'a-', 'b'); + test_regex +------------ + {0} + {a-} +(2 rows) + +-- expectMatch 9.3 & {a[[.-.]]} a- a- +select * from test_regex('a[[.-.]]', 'a-', ''); + test_regex +------------ + {0} + {a-} +(2 rows) + +select * from test_regex('a[[.-.]]', 'a-', 'b'); + test_regex +------------ + {0} + {a-} +(2 rows) + +-- expectMatch 9.4 &L {a[[.zero.]]} a0 a0 +select * from test_regex('a[[.zero.]]', 'a0', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {a0} +(2 rows) + +select * from test_regex('a[[.zero.]]', 'a0', 'Lb'); + test_regex +----------------- + {0,REG_ULOCALE} + {a0} +(2 rows) + +-- expectMatch 9.5 &LM {a[[.zero.]-9]} a2 a2 +select * from test_regex('a[[.zero.]-9]', 'a2', 'LM'); + test_regex +----------------------------- + {0,REG_UUNPORT,REG_ULOCALE} + {a2} +(2 rows) + +select * from test_regex('a[[.zero.]-9]', 'a2', 'LMb'); + test_regex +----------------------------- + {0,REG_UUNPORT,REG_ULOCALE} + {a2} +(2 rows) + +-- expectMatch 9.6 &M {a[0-[.9.]]} a2 a2 +select * from test_regex('a[0-[.9.]]', 'a2', 'M'); + test_regex +----------------- + {0,REG_UUNPORT} + {a2} +(2 rows) + +select * from test_regex('a[0-[.9.]]', 'a2', 'Mb'); + test_regex +----------------- + {0,REG_UUNPORT} + {a2} +(2 rows) + +-- expectMatch 9.7 &+L {a[[=x=]]} ax ax +select * from test_regex('a[[=x=]]', 'ax', '+L'); + test_regex +----------------- + {0,REG_ULOCALE} + {ax} +(2 rows) + +select * from test_regex('a[[=x=]]', 'ax', '+Lb'); + test_regex +----------------- + {0,REG_ULOCALE} + {ax} +(2 rows) + +-- expectMatch 9.8 &+L {a[[=x=]]} ay ay +select * from test_regex('a[[=x=]]', 'ay', '+L'); + test_regex +----------------- + {0,REG_ULOCALE} + {ay} +(2 rows) + +select * from test_regex('a[[=x=]]', 'ay', '+Lb'); + test_regex +----------------- + {0,REG_ULOCALE} + {ay} +(2 rows) + +-- expectNomatch 9.9 &+L {a[[=x=]]} az +select * from test_regex('a[[=x=]]', 'az', '+L'); + test_regex +----------------- + {0,REG_ULOCALE} +(1 row) + +select * from test_regex('a[[=x=]]', 'az', '+Lb'); + test_regex +----------------- + {0,REG_ULOCALE} +(1 row) + +-- expectMatch 9.9b &iL {a[[=Y=]]} ay ay +select * from test_regex('a[[=Y=]]', 'ay', 'iL'); + test_regex +----------------- + {0,REG_ULOCALE} + {ay} +(2 rows) + +select * from test_regex('a[[=Y=]]', 'ay', 'iLb'); + test_regex +----------------- + {0,REG_ULOCALE} + {ay} +(2 rows) + +-- expectNomatch 9.9c &L {a[[=Y=]]} ay +select * from test_regex('a[[=Y=]]', 'ay', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} +(1 row) + +select * from test_regex('a[[=Y=]]', 'ay', 'Lb'); + test_regex +----------------- + {0,REG_ULOCALE} +(1 row) + +-- expectError 9.10 & {a[0-[=x=]]} ERANGE +select * from test_regex('a[0-[=x=]]', '', ''); +ERROR: invalid regular expression: invalid character range +select * from test_regex('a[0-[=x=]]', '', 'b'); +ERROR: invalid regular expression: invalid character range +-- expectMatch 9.11 &L {a[[:digit:]]} a0 a0 +select * from test_regex('a[[:digit:]]', 'a0', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {a0} +(2 rows) + +select * from test_regex('a[[:digit:]]', 'a0', 'Lb'); + test_regex +----------------- + {0,REG_ULOCALE} + {a0} +(2 rows) + +-- expectError 9.12 & {a[[:woopsie:]]} ECTYPE +select * from test_regex('a[[:woopsie:]]', '', ''); +ERROR: invalid regular expression: invalid character class +select * from test_regex('a[[:woopsie:]]', '', 'b'); +ERROR: invalid regular expression: invalid character class +-- expectNomatch 9.13 &L {a[[:digit:]]} ab +select * from test_regex('a[[:digit:]]', 'ab', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} +(1 row) + +select * from test_regex('a[[:digit:]]', 'ab', 'Lb'); + test_regex +----------------- + {0,REG_ULOCALE} +(1 row) + +-- expectError 9.14 & {a[0-[:digit:]]} ERANGE +select * from test_regex('a[0-[:digit:]]', '', ''); +ERROR: invalid regular expression: invalid character range +select * from test_regex('a[0-[:digit:]]', '', 'b'); +ERROR: invalid regular expression: invalid character range +-- expectMatch 9.15 &LP {[[:<:]]a} a a +select * from test_regex('[[:<:]]a', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('[[:<:]]a', 'a', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectMatch 9.16 &LP {a[[:>:]]} a a +select * from test_regex('a[[:>:]]', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('a[[:>:]]', 'a', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectError 9.17 & {a[[..]]b} ECOLLATE +select * from test_regex('a[[..]]b', '', ''); +ERROR: invalid regular expression: invalid collating element +select * from test_regex('a[[..]]b', '', 'b'); +ERROR: invalid regular expression: invalid collating element +-- expectError 9.18 & {a[[==]]b} ECOLLATE +select * from test_regex('a[[==]]b', '', ''); +ERROR: invalid regular expression: invalid collating element +select * from test_regex('a[[==]]b', '', 'b'); +ERROR: invalid regular expression: invalid collating element +-- expectError 9.19 & {a[[::]]b} ECTYPE +select * from test_regex('a[[::]]b', '', ''); +ERROR: invalid regular expression: invalid character class +select * from test_regex('a[[::]]b', '', 'b'); +ERROR: invalid regular expression: invalid character class +-- expectError 9.20 & {a[[.a} EBRACK +select * from test_regex('a[[.a', '', ''); +ERROR: invalid regular expression: brackets [] not balanced +select * from test_regex('a[[.a', '', 'b'); +ERROR: invalid regular expression: brackets [] not balanced +-- expectError 9.21 & {a[[=a} EBRACK +select * from test_regex('a[[=a', '', ''); +ERROR: invalid regular expression: brackets [] not balanced +select * from test_regex('a[[=a', '', 'b'); +ERROR: invalid regular expression: brackets [] not balanced +-- expectError 9.22 & {a[[:a} EBRACK +select * from test_regex('a[[:a', '', ''); +ERROR: invalid regular expression: brackets [] not balanced +select * from test_regex('a[[:a', '', 'b'); +ERROR: invalid regular expression: brackets [] not balanced +-- expectError 9.23 & {a[} EBRACK +select * from test_regex('a[', '', ''); +ERROR: invalid regular expression: brackets [] not balanced +select * from test_regex('a[', '', 'b'); +ERROR: invalid regular expression: brackets [] not balanced +-- expectError 9.24 & {a[b} EBRACK +select * from test_regex('a[b', '', ''); +ERROR: invalid regular expression: brackets [] not balanced +select * from test_regex('a[b', '', 'b'); +ERROR: invalid regular expression: brackets [] not balanced +-- expectError 9.25 & {a[b-} EBRACK +select * from test_regex('a[b-', '', ''); +ERROR: invalid regular expression: brackets [] not balanced +select * from test_regex('a[b-', '', 'b'); +ERROR: invalid regular expression: brackets [] not balanced +-- expectError 9.26 & {a[b-c} EBRACK +select * from test_regex('a[b-c', '', ''); +ERROR: invalid regular expression: brackets [] not balanced +select * from test_regex('a[b-c', '', 'b'); +ERROR: invalid regular expression: brackets [] not balanced +-- expectMatch 9.27 &M {a[b-c]} ab ab +select * from test_regex('a[b-c]', 'ab', 'M'); + test_regex +----------------- + {0,REG_UUNPORT} + {ab} +(2 rows) + +select * from test_regex('a[b-c]', 'ab', 'Mb'); + test_regex +----------------- + {0,REG_UUNPORT} + {ab} +(2 rows) + +-- expectMatch 9.28 & {a[b-b]} ab ab +select * from test_regex('a[b-b]', 'ab', ''); + test_regex +------------ + {0} + {ab} +(2 rows) + +select * from test_regex('a[b-b]', 'ab', 'b'); + test_regex +------------ + {0} + {ab} +(2 rows) + +-- expectMatch 9.29 &M {a[1-2]} a2 a2 +select * from test_regex('a[1-2]', 'a2', 'M'); + test_regex +----------------- + {0,REG_UUNPORT} + {a2} +(2 rows) + +select * from test_regex('a[1-2]', 'a2', 'Mb'); + test_regex +----------------- + {0,REG_UUNPORT} + {a2} +(2 rows) + +-- expectError 9.30 & {a[c-b]} ERANGE +select * from test_regex('a[c-b]', '', ''); +ERROR: invalid regular expression: invalid character range +select * from test_regex('a[c-b]', '', 'b'); +ERROR: invalid regular expression: invalid character range +-- expectError 9.31 & {a[a-b-c]} ERANGE +select * from test_regex('a[a-b-c]', '', ''); +ERROR: invalid regular expression: invalid character range +select * from test_regex('a[a-b-c]', '', 'b'); +ERROR: invalid regular expression: invalid character range +-- expectMatch 9.32 &M {a[--?]b} a?b a?b +select * from test_regex('a[--?]b', 'a?b', 'M'); + test_regex +----------------- + {0,REG_UUNPORT} + {a?b} +(2 rows) + +select * from test_regex('a[--?]b', 'a?b', 'Mb'); + test_regex +----------------- + {0,REG_UUNPORT} + {a?b} +(2 rows) + +-- expectMatch 9.33 & {a[---]b} a-b a-b +select * from test_regex('a[---]b', 'a-b', ''); + test_regex +------------ + {0} + {a-b} +(2 rows) + +select * from test_regex('a[---]b', 'a-b', 'b'); + test_regex +------------ + {0} + {a-b} +(2 rows) + +-- expectMatch 9.34 & {a[]b]c} a]c a]c +select * from test_regex('a[]b]c', 'a]c', ''); + test_regex +------------ + {0} + {a]c} +(2 rows) + +select * from test_regex('a[]b]c', 'a]c', 'b'); + test_regex +------------ + {0} + {a]c} +(2 rows) + +-- expectMatch 9.35 EP {a[\]]b} a]b a]b +select * from test_regex('a[\]]b', 'a]b', 'EP'); + test_regex +---------------------------- + {0,REG_UBBS,REG_UNONPOSIX} + {a]b} +(2 rows) + +-- expectNomatch 9.36 bE {a[\]]b} a]b +select * from test_regex('a[\]]b', 'a]b', 'bE'); + test_regex +-------------- + {0,REG_UBBS} +(1 row) + +-- expectMatch 9.37 bE {a[\]]b} "a\\]b" "a\\]b" +select * from test_regex('a[\]]b', 'a\]b', 'bE'); + test_regex +-------------- + {0,REG_UBBS} + {"a\\]b"} +(2 rows) + +-- expectMatch 9.38 eE {a[\]]b} "a\\]b" "a\\]b" +select * from test_regex('a[\]]b', 'a\]b', 'eE'); + test_regex +-------------- + {0,REG_UBBS} + {"a\\]b"} +(2 rows) + +-- expectMatch 9.39 EP {a[\\]b} "a\\b" "a\\b" +select * from test_regex('a[\\]b', 'a\b', 'EP'); + test_regex +---------------------------- + {0,REG_UBBS,REG_UNONPOSIX} + {"a\\b"} +(2 rows) + +-- expectMatch 9.40 eE {a[\\]b} "a\\b" "a\\b" +select * from test_regex('a[\\]b', 'a\b', 'eE'); + test_regex +-------------- + {0,REG_UBBS} + {"a\\b"} +(2 rows) + +-- expectMatch 9.41 bE {a[\\]b} "a\\b" "a\\b" +select * from test_regex('a[\\]b', 'a\b', 'bE'); + test_regex +-------------- + {0,REG_UBBS} + {"a\\b"} +(2 rows) + +-- expectError 9.42 - {a[\Z]b} EESCAPE +select * from test_regex('a[\Z]b', '', '-'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 9.43 & {a[[b]c} "a\[c" "a\[c" +select * from test_regex('a[[b]c', 'a[c', ''); + test_regex +------------ + {0} + {a[c} +(2 rows) + +select * from test_regex('a[[b]c', 'a[c', 'b'); + test_regex +------------ + {0} + {a[c} +(2 rows) + +-- This only works in UTF8 encoding, so it's moved to test_regex_utf8.sql: +-- expectMatch 9.44 EMP* {a[\u00fe-\u0507][\u00ff-\u0300]b} \ +-- "a\u0102\u02ffb" "a\u0102\u02ffb" +-- doing 10 "anchors and newlines" +-- expectMatch 10.1 & ^a a a +select * from test_regex('^a', 'a', ''); + test_regex +------------ + {0} + {a} +(2 rows) + +select * from test_regex('^a', 'a', 'b'); + test_regex +------------ + {0} + {a} +(2 rows) + +-- expectNomatch 10.2 &^ ^a a +select * from test_regex('^a', 'a', '^'); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('^a', 'a', '^b'); + test_regex +------------ + {0} +(1 row) + +-- expectIndices 10.3 &N ^ a {0 -1} +select * from test_regex('^', 'a', '0N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"0 -1"} +(2 rows) + +select * from test_regex('^', 'a', '0Nb'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"0 -1"} +(2 rows) + +-- expectIndices 10.4 & {a$} aba {2 2} +select * from test_regex('a$', 'aba', '0'); + test_regex +------------ + {0} + {"2 2"} +(2 rows) + +select * from test_regex('a$', 'aba', '0b'); + test_regex +------------ + {0} + {"2 2"} +(2 rows) + +-- expectNomatch 10.5 {&$} {a$} a +select * from test_regex('a$', 'a', '$'); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('a$', 'a', '$b'); + test_regex +------------ + {0} +(1 row) + +-- expectIndices 10.6 &N {$} ab {2 1} +select * from test_regex('$', 'ab', '0N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"2 1"} +(2 rows) + +select * from test_regex('$', 'ab', '0Nb'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"2 1"} +(2 rows) + +-- expectMatch 10.7 &n ^a a a +select * from test_regex('^a', 'a', 'n'); + test_regex +------------ + {0} + {a} +(2 rows) + +select * from test_regex('^a', 'a', 'nb'); + test_regex +------------ + {0} + {a} +(2 rows) + +-- expectMatch 10.8 &n "^a" "b\na" "a" +select * from test_regex('^a', E'b\na', 'n'); + test_regex +------------ + {0} + {a} +(2 rows) + +select * from test_regex('^a', E'b\na', 'nb'); + test_regex +------------ + {0} + {a} +(2 rows) + +-- expectIndices 10.9 &w "^a" "a\na" {0 0} +select * from test_regex('^a', E'a\na', '0w'); + test_regex +------------ + {0} + {"0 0"} +(2 rows) + +select * from test_regex('^a', E'a\na', '0wb'); + test_regex +------------ + {0} + {"0 0"} +(2 rows) + +-- expectIndices 10.10 &n^ "^a" "a\na" {2 2} +select * from test_regex('^a', E'a\na', '0n^'); + test_regex +------------ + {0} + {"2 2"} +(2 rows) + +select * from test_regex('^a', E'a\na', '0n^b'); + test_regex +------------ + {0} + {"2 2"} +(2 rows) + +-- expectMatch 10.11 &n {a$} a a +select * from test_regex('a$', 'a', 'n'); + test_regex +------------ + {0} + {a} +(2 rows) + +select * from test_regex('a$', 'a', 'nb'); + test_regex +------------ + {0} + {a} +(2 rows) + +-- expectMatch 10.12 &n "a\$" "a\nb" "a" +select * from test_regex('a$', E'a\nb', 'n'); + test_regex +------------ + {0} + {a} +(2 rows) + +select * from test_regex('a$', E'a\nb', 'nb'); + test_regex +------------ + {0} + {a} +(2 rows) + +-- expectIndices 10.13 &n "a\$" "a\na" {0 0} +select * from test_regex('a$', E'a\na', '0n'); + test_regex +------------ + {0} + {"0 0"} +(2 rows) + +select * from test_regex('a$', E'a\na', '0nb'); + test_regex +------------ + {0} + {"0 0"} +(2 rows) + +-- expectIndices 10.14 N ^^ a {0 -1} +select * from test_regex('^^', 'a', '0N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"0 -1"} +(2 rows) + +-- expectMatch 10.15 b ^^ ^ ^ +select * from test_regex('^^', '^', 'b'); + test_regex +------------ + {0} + {^} +(2 rows) + +-- expectIndices 10.16 N {$$} a {1 0} +select * from test_regex('$$', 'a', '0N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"1 0"} +(2 rows) + +-- expectMatch 10.17 b {$$} "\$" "\$" +select * from test_regex('$$', '$', 'b'); + test_regex +------------ + {0} + {$} +(2 rows) + +-- expectMatch 10.18 &N {^$} "" "" +select * from test_regex('^$', '', 'N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {""} +(2 rows) + +select * from test_regex('^$', '', 'Nb'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {""} +(2 rows) + +-- expectNomatch 10.19 &N {^$} a +select * from test_regex('^$', 'a', 'N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} +(1 row) + +select * from test_regex('^$', 'a', 'Nb'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} +(1 row) + +-- expectIndices 10.20 &nN "^\$" a\n\nb {2 1} +select * from test_regex('^$', E'a\n\nb', '0nN'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"2 1"} +(2 rows) + +select * from test_regex('^$', E'a\n\nb', '0nNb'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {"2 1"} +(2 rows) + +-- expectMatch 10.21 N {$^} "" "" +select * from test_regex('$^', '', 'N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {""} +(2 rows) + +-- expectMatch 10.22 b {$^} "\$^" "\$^" +select * from test_regex('$^', '$^', 'b'); + test_regex +------------ + {0} + {$^} +(2 rows) + +-- expectMatch 10.23 P {\Aa} a a +select * from test_regex('\Aa', 'a', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectMatch 10.24 ^P {\Aa} a a +select * from test_regex('\Aa', 'a', '^P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectNomatch 10.25 ^nP {\Aa} "b\na" +select * from test_regex('\Aa', E'b\na', '^nP'); + test_regex +------------------- + {0,REG_UNONPOSIX} +(1 row) + +-- expectMatch 10.26 P {a\Z} a a +select * from test_regex('a\Z', 'a', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectMatch 10.27 \$P {a\Z} a a +select * from test_regex('a\Z', 'a', '$P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectNomatch 10.28 \$nP {a\Z} "a\nb" +select * from test_regex('a\Z', E'a\nb', '$nP'); + test_regex +------------------- + {0,REG_UNONPOSIX} +(1 row) + +-- expectError 10.29 - ^* BADRPT +select * from test_regex('^*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 10.30 - {$*} BADRPT +select * from test_regex('$*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 10.31 - {\A*} BADRPT +select * from test_regex('\A*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 10.32 - {\Z*} BADRPT +select * from test_regex('\Z*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- doing 11 "boundary constraints" +-- expectMatch 11.1 &LP {[[:<:]]a} a a +select * from test_regex('[[:<:]]a', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('[[:<:]]a', 'a', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectMatch 11.2 &LP {[[:<:]]a} -a a +select * from test_regex('[[:<:]]a', '-a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('[[:<:]]a', '-a', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.3 &LP {[[:<:]]a} ba +select * from test_regex('[[:<:]]a', 'ba', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +select * from test_regex('[[:<:]]a', 'ba', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.4 &LP {a[[:>:]]} a a +select * from test_regex('a[[:>:]]', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('a[[:>:]]', 'a', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectMatch 11.5 &LP {a[[:>:]]} a- a +select * from test_regex('a[[:>:]]', 'a-', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('a[[:>:]]', 'a-', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.6 &LP {a[[:>:]]} ab +select * from test_regex('a[[:>:]]', 'ab', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +select * from test_regex('a[[:>:]]', 'ab', 'LPb'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.7 bLP {\<a} a a +select * from test_regex('\<a', 'a', 'bLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.8 bLP {\<a} ba +select * from test_regex('\<a', 'ba', 'bLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.9 bLP {a\>} a a +select * from test_regex('a\>', 'a', 'bLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.10 bLP {a\>} ab +select * from test_regex('a\>', 'ab', 'bLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.11 LP {\ya} a a +select * from test_regex('\ya', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.12 LP {\ya} ba +select * from test_regex('\ya', 'ba', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.13 LP {a\y} a a +select * from test_regex('a\y', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.14 LP {a\y} ab +select * from test_regex('a\y', 'ab', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.15 LP {a\Y} ab a +select * from test_regex('a\Y', 'ab', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.16 LP {a\Y} a- +select * from test_regex('a\Y', 'a-', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectNomatch 11.17 LP {a\Y} a +select * from test_regex('a\Y', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectNomatch 11.18 LP {-\Y} -a +select * from test_regex('-\Y', '-a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.19 LP {-\Y} -% - +select * from test_regex('-\Y', '-%', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {-} +(2 rows) + +-- expectNomatch 11.20 LP {\Y-} a- +select * from test_regex('\Y-', 'a-', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectError 11.21 - {[[:<:]]*} BADRPT +select * from test_regex('[[:<:]]*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 11.22 - {[[:>:]]*} BADRPT +select * from test_regex('[[:>:]]*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 11.23 b {\<*} BADRPT +select * from test_regex('\<*', '', 'b'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 11.24 b {\>*} BADRPT +select * from test_regex('\>*', '', 'b'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 11.25 - {\y*} BADRPT +select * from test_regex('\y*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectError 11.26 - {\Y*} BADRPT +select * from test_regex('\Y*', '', '-'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 11.27 LP {\ma} a a +select * from test_regex('\ma', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.28 LP {\ma} ba +select * from test_regex('\ma', 'ba', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 11.29 LP {a\M} a a +select * from test_regex('a\M', 'a', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +-- expectNomatch 11.30 LP {a\M} ab +select * from test_regex('a\M', 'ab', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectNomatch 11.31 ILP {\Ma} a +select * from test_regex('\Ma', 'a', 'ILP'); + test_regex +----------------------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE,REG_UIMPOSSIBLE} +(1 row) + +-- expectNomatch 11.32 ILP {a\m} a +select * from test_regex('a\m', 'a', 'ILP'); + test_regex +----------------------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE,REG_UIMPOSSIBLE} +(1 row) + +-- doing 12 "character classes" +-- expectMatch 12.1 LP {a\db} a0b a0b +select * from test_regex('a\db', 'a0b', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a0b} +(2 rows) + +-- expectNomatch 12.2 LP {a\db} axb +select * from test_regex('a\db', 'axb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectNomatch 12.3 LP {a\Db} a0b +select * from test_regex('a\Db', 'a0b', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 12.4 LP {a\Db} axb axb +select * from test_regex('a\Db', 'axb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {axb} +(2 rows) + +-- expectMatch 12.5 LP "a\\sb" "a b" "a b" +select * from test_regex('a\sb', 'a b', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"a b"} +(2 rows) + +-- expectMatch 12.6 LP "a\\sb" "a\tb" "a\tb" +select * from test_regex('a\sb', E'a\tb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"a b"} +(2 rows) + +-- expectMatch 12.7 LP "a\\sb" "a\nb" "a\nb" +select * from test_regex('a\sb', E'a\nb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"a + + b"} +(2 rows) + +-- expectNomatch 12.8 LP {a\sb} axb +select * from test_regex('a\sb', 'axb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 12.9 LP {a\Sb} axb axb +select * from test_regex('a\Sb', 'axb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {axb} +(2 rows) + +-- expectNomatch 12.10 LP "a\\Sb" "a b" +select * from test_regex('a\Sb', 'a b', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 12.11 LP {a\wb} axb axb +select * from test_regex('a\wb', 'axb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {axb} +(2 rows) + +-- expectNomatch 12.12 LP {a\wb} a-b +select * from test_regex('a\wb', 'a-b', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectNomatch 12.13 LP {a\Wb} axb +select * from test_regex('a\Wb', 'axb', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 12.14 LP {a\Wb} a-b a-b +select * from test_regex('a\Wb', 'a-b', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a-b} +(2 rows) + +-- expectMatch 12.15 LP {\y\w+z\y} adze-guz guz +select * from test_regex('\y\w+z\y', 'adze-guz', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {guz} +(2 rows) + +-- expectMatch 12.16 LPE {a[\d]b} a1b a1b +select * from test_regex('a[\d]b', 'a1b', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {a1b} +(2 rows) + +-- expectMatch 12.17 LPE "a\[\\s]b" "a b" "a b" +select * from test_regex('a[\s]b', 'a b', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {"a b"} +(2 rows) + +-- expectMatch 12.18 LPE {a[\w]b} axb axb +select * from test_regex('a[\w]b', 'axb', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {axb} +(2 rows) + +-- these should be invalid +select * from test_regex('[\w-~]*', 'ab01_~-`**', 'LNPSE'); +ERROR: invalid regular expression: invalid character range +select * from test_regex('[~-\w]*', 'ab01_~-`**', 'LNPSE'); +ERROR: invalid regular expression: invalid character range +select * from test_regex('[[:alnum:]-~]*', 'ab01~-`**', 'LNS'); +ERROR: invalid regular expression: invalid character range +select * from test_regex('[~-[:alnum:]]*', 'ab01~-`**', 'LNS'); +ERROR: invalid regular expression: invalid character range +-- test complemented char classes within brackets +select * from test_regex('[\D]', '0123456789abc*', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('[^\D]', 'abc0123456789*', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {0} +(2 rows) + +select * from test_regex('[1\D7]', '0123456789abc*', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {1} +(2 rows) + +select * from test_regex('[7\D1]', '0123456789abc*', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {1} +(2 rows) + +select * from test_regex('[^0\D1]', 'abc0123456789*', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {2} +(2 rows) + +select * from test_regex('[^1\D0]', 'abc0123456789*', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {2} +(2 rows) + +select * from test_regex('\W', '0123456789abc_*', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {*} +(2 rows) + +select * from test_regex('[\W]', '0123456789abc_*', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {*} +(2 rows) + +select * from test_regex('[\s\S]*', '012 3456789abc_*', 'LNPE'); + test_regex +-------------------------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE,REG_UEMPTYMATCH} + {"012 3456789abc_*"} +(2 rows) + +-- check char classes' handling of newlines +select * from test_regex('\s+', E'abc \n def', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {" + + "} +(2 rows) + +select * from test_regex('\s+', E'abc \n def', 'nLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {" + + "} +(2 rows) + +select * from test_regex('[\s]+', E'abc \n def', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {" + + "} +(2 rows) + +select * from test_regex('[\s]+', E'abc \n def', 'nLPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {" + + "} +(2 rows) + +select * from test_regex('\S+', E'abc\ndef', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {abc} +(2 rows) + +select * from test_regex('\S+', E'abc\ndef', 'nLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {abc} +(2 rows) + +select * from test_regex('[\S]+', E'abc\ndef', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {abc} +(2 rows) + +select * from test_regex('[\S]+', E'abc\ndef', 'nLPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {abc} +(2 rows) + +select * from test_regex('\d+', E'012\n345', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {012} +(2 rows) + +select * from test_regex('\d+', E'012\n345', 'nLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {012} +(2 rows) + +select * from test_regex('[\d]+', E'012\n345', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {012} +(2 rows) + +select * from test_regex('[\d]+', E'012\n345', 'nLPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {012} +(2 rows) + +select * from test_regex('\D+', E'abc\ndef345', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"abc + + def"} +(2 rows) + +select * from test_regex('\D+', E'abc\ndef345', 'nLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"abc + + def"} +(2 rows) + +select * from test_regex('[\D]+', E'abc\ndef345', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {"abc + + def"} +(2 rows) + +select * from test_regex('[\D]+', E'abc\ndef345', 'nLPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {"abc + + def"} +(2 rows) + +select * from test_regex('\w+', E'abc_012\ndef', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {abc_012} +(2 rows) + +select * from test_regex('\w+', E'abc_012\ndef', 'nLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {abc_012} +(2 rows) + +select * from test_regex('[\w]+', E'abc_012\ndef', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {abc_012} +(2 rows) + +select * from test_regex('[\w]+', E'abc_012\ndef', 'nLPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {abc_012} +(2 rows) + +select * from test_regex('\W+', E'***\n@@@___', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"*** + + @@@"} +(2 rows) + +select * from test_regex('\W+', E'***\n@@@___', 'nLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"*** + + @@@"} +(2 rows) + +select * from test_regex('[\W]+', E'***\n@@@___', 'LPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {"*** + + @@@"} +(2 rows) + +select * from test_regex('[\W]+', E'***\n@@@___', 'nLPE'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_ULOCALE} + {"*** + + @@@"} +(2 rows) + +-- doing 13 "escapes" +-- expectError 13.1 & "a\\" EESCAPE +select * from test_regex('a\', '', ''); +ERROR: invalid regular expression: invalid escape \ sequence +select * from test_regex('a\', '', 'b'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.2 - {a\<b} a<b a<b +select * from test_regex('a\<b', 'a<b', '-'); + test_regex +------------ + {0} + {a<b} +(2 rows) + +-- expectMatch 13.3 e {a\<b} a<b a<b +select * from test_regex('a\<b', 'a<b', 'e'); + test_regex +------------ + {0} + {a<b} +(2 rows) + +-- expectMatch 13.4 bAS {a\wb} awb awb +select * from test_regex('a\wb', 'awb', 'bAS'); + test_regex +------------------------------ + {0,REG_UBSALNUM,REG_UUNSPEC} + {awb} +(2 rows) + +-- expectMatch 13.5 eAS {a\wb} awb awb +select * from test_regex('a\wb', 'awb', 'eAS'); + test_regex +------------------------------ + {0,REG_UBSALNUM,REG_UUNSPEC} + {awb} +(2 rows) + +-- expectMatch 13.6 PL "a\\ab" "a\007b" "a\007b" +select * from test_regex('a\ab', E'a\007b', 'PL'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {a\x07b} +(2 rows) + +-- expectMatch 13.7 P "a\\bb" "a\bb" "a\bb" +select * from test_regex('a\bb', E'a\bb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a\x08b} +(2 rows) + +-- expectMatch 13.8 P {a\Bb} "a\\b" "a\\b" +select * from test_regex('a\Bb', 'a\b', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a\\b"} +(2 rows) + +-- expectMatch 13.9 MP "a\\chb" "a\bb" "a\bb" +select * from test_regex('a\chb', E'a\bb', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x08b} +(2 rows) + +-- expectMatch 13.10 MP "a\\cHb" "a\bb" "a\bb" +select * from test_regex('a\cHb', E'a\bb', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x08b} +(2 rows) + +-- expectMatch 13.11 LMP "a\\e" "a\033" "a\033" +select * from test_regex('a\e', E'a\033', 'LMP'); + test_regex +------------------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT,REG_ULOCALE} + {a\x1B} +(2 rows) + +-- expectMatch 13.12 P "a\\fb" "a\fb" "a\fb" +select * from test_regex('a\fb', E'a\fb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a\x0Cb"} +(2 rows) + +-- expectMatch 13.13 P "a\\nb" "a\nb" "a\nb" +select * from test_regex('a\nb', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a + + b"} +(2 rows) + +-- expectMatch 13.14 P "a\\rb" "a\rb" "a\rb" +select * from test_regex('a\rb', E'a\rb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a\rb"} +(2 rows) + +-- expectMatch 13.15 P "a\\tb" "a\tb" "a\tb" +select * from test_regex('a\tb', E'a\tb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a b"} +(2 rows) + +-- expectMatch 13.16 P "a\\u0008x" "a\bx" "a\bx" +select * from test_regex('a\u0008x', E'a\bx', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a\x08x} +(2 rows) + +-- expectMatch 13.17 P {a\u008x} "a\bx" "a\bx" +-- Tcl has relaxed their code to allow 1-4 hex digits, but Postgres hasn't +select * from test_regex('a\u008x', E'a\bx', 'P'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.18 P "a\\u00088x" "a\b8x" "a\b8x" +select * from test_regex('a\u00088x', E'a\b8x', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a\x088x} +(2 rows) + +-- expectMatch 13.19 P "a\\U00000008x" "a\bx" "a\bx" +select * from test_regex('a\U00000008x', E'a\bx', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a\x08x} +(2 rows) + +-- expectMatch 13.20 P {a\U0000008x} "a\bx" "a\bx" +-- Tcl has relaxed their code to allow 1-8 hex digits, but Postgres hasn't +select * from test_regex('a\U0000008x', E'a\bx', 'P'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.21 P "a\\vb" "a\vb" "a\vb" +select * from test_regex('a\vb', E'a\013b', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a\x0Bb"} +(2 rows) + +-- expectMatch 13.22 MP "a\\x08x" "a\bx" "a\bx" +select * from test_regex('a\x08x', E'a\bx', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x08x} +(2 rows) + +-- expectError 13.23 - {a\xq} EESCAPE +select * from test_regex('a\xq', '', '-'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.24 MP "a\\x08x" "a\bx" "a\bx" +select * from test_regex('a\x08x', E'a\bx', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x08x} +(2 rows) + +-- expectError 13.25 - {a\z} EESCAPE +select * from test_regex('a\z', '', '-'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.26 MP "a\\010b" "a\bb" "a\bb" +select * from test_regex('a\010b', E'a\bb', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x08b} +(2 rows) + +-- These only work in UTF8 encoding, so they're moved to test_regex_utf8.sql: +-- expectMatch 13.27 P "a\\U00001234x" "a\u1234x" "a\u1234x" +-- expectMatch 13.28 P {a\U00001234x} "a\u1234x" "a\u1234x" +-- expectMatch 13.29 P "a\\U0001234x" "a\u1234x" "a\u1234x" +-- expectMatch 13.30 P {a\U0001234x} "a\u1234x" "a\u1234x" +-- expectMatch 13.31 P "a\\U000012345x" "a\u12345x" "a\u12345x" +-- expectMatch 13.32 P {a\U000012345x} "a\u12345x" "a\u12345x" +-- expectMatch 13.33 P "a\\U1000000x" "a\ufffd0x" "a\ufffd0x" +-- expectMatch 13.34 P {a\U1000000x} "a\ufffd0x" "a\ufffd0x" +-- doing 14 "back references" +-- # ugh +-- expectMatch 14.1 RP {a(b*)c\1} abbcbb abbcbb bb +select * from test_regex('a(b*)c\1', 'abbcbb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abbcbb,bb} +(2 rows) + +-- expectMatch 14.2 RP {a(b*)c\1} ac ac "" +select * from test_regex('a(b*)c\1', 'ac', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {ac,""} +(2 rows) + +-- expectNomatch 14.3 RP {a(b*)c\1} abbcb +select * from test_regex('a(b*)c\1', 'abbcb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectMatch 14.4 RP {a(b*)\1} abbcbb abb b +select * from test_regex('a(b*)\1', 'abbcbb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abb,b} +(2 rows) + +-- expectMatch 14.5 RP {a(b|bb)\1} abbcbb abb b +select * from test_regex('a(b|bb)\1', 'abbcbb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abb,b} +(2 rows) + +-- expectMatch 14.6 RP {a([bc])\1} abb abb b +select * from test_regex('a([bc])\1', 'abb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abb,b} +(2 rows) + +-- expectNomatch 14.7 RP {a([bc])\1} abc +select * from test_regex('a([bc])\1', 'abc', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectMatch 14.8 RP {a([bc])\1} abcabb abb b +select * from test_regex('a([bc])\1', 'abcabb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abb,b} +(2 rows) + +-- expectNomatch 14.9 RP {a([bc])*\1} abc +select * from test_regex('a([bc])*\1', 'abc', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 14.10 RP {a([bc])\1} abB +select * from test_regex('a([bc])\1', 'abB', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectMatch 14.11 iRP {a([bc])\1} abB abB b +select * from test_regex('a([bc])\1', 'abB', 'iRP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abB,b} +(2 rows) + +-- expectMatch 14.12 RP {a([bc])\1+} abbb abbb b +select * from test_regex('a([bc])\1+', 'abbb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abbb,b} +(2 rows) + +-- expectMatch 14.13 QRP "a(\[bc])\\1{3,4}" abbbb abbbb b +select * from test_regex('a([bc])\1{3,4}', 'abbbb', 'QRP'); + test_regex +-------------------------------------------- + {1,REG_UBACKREF,REG_UBOUNDS,REG_UNONPOSIX} + {abbbb,b} +(2 rows) + +-- expectNomatch 14.14 QRP "a(\[bc])\\1{3,4}" abbb +select * from test_regex('a([bc])\1{3,4}', 'abbb', 'QRP'); + test_regex +-------------------------------------------- + {1,REG_UBACKREF,REG_UBOUNDS,REG_UNONPOSIX} +(1 row) + +-- expectMatch 14.15 RP {a([bc])\1*} abbb abbb b +select * from test_regex('a([bc])\1*', 'abbb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abbb,b} +(2 rows) + +-- expectMatch 14.16 RP {a([bc])\1*} ab ab b +select * from test_regex('a([bc])\1*', 'ab', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {ab,b} +(2 rows) + +-- expectMatch 14.17 RP {a([bc])(\1*)} ab ab b "" +select * from test_regex('a([bc])(\1*)', 'ab', 'RP'); + test_regex +-------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX} + {ab,b,""} +(2 rows) + +-- expectError 14.18 - {a((b)\1)} ESUBREG +select * from test_regex('a((b)\1)', '', '-'); +ERROR: invalid regular expression: invalid backreference number +-- expectError 14.19 - {a(b)c\2} ESUBREG +select * from test_regex('a(b)c\2', '', '-'); +ERROR: invalid regular expression: invalid backreference number +-- expectMatch 14.20 bR {a\(b*\)c\1} abbcbb abbcbb bb +select * from test_regex('a\(b*\)c\1', 'abbcbb', 'bR'); + test_regex +------------------ + {1,REG_UBACKREF} + {abbcbb,bb} +(2 rows) + +-- expectMatch 14.21 RP {^([bc])\1*$} bbb bbb b +select * from test_regex('^([bc])\1*$', 'bbb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {bbb,b} +(2 rows) + +-- expectMatch 14.22 RP {^([bc])\1*$} ccc ccc c +select * from test_regex('^([bc])\1*$', 'ccc', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {ccc,c} +(2 rows) + +-- expectNomatch 14.23 RP {^([bc])\1*$} bcb +select * from test_regex('^([bc])\1*$', 'bcb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectMatch 14.24 LRP {^(\w+)( \1)+$} {abc abc abc} {abc abc abc} abc { abc} +select * from test_regex('^(\w+)( \1)+$', 'abc abc abc', 'LRP'); + test_regex +-------------------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX,REG_ULOCALE} + {"abc abc abc",abc," abc"} +(2 rows) + +-- expectNomatch 14.25 LRP {^(\w+)( \1)+$} {abc abd abc} +select * from test_regex('^(\w+)( \1)+$', 'abc abd abc', 'LRP'); + test_regex +-------------------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectNomatch 14.26 LRP {^(\w+)( \1)+$} {abc abc abd} +select * from test_regex('^(\w+)( \1)+$', 'abc abc abd', 'LRP'); + test_regex +-------------------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- expectMatch 14.27 RP {^(.+)( \1)+$} {abc abc abc} {abc abc abc} abc { abc} +select * from test_regex('^(.+)( \1)+$', 'abc abc abc', 'RP'); + test_regex +-------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX} + {"abc abc abc",abc," abc"} +(2 rows) + +-- expectNomatch 14.28 RP {^(.+)( \1)+$} {abc abd abc} +select * from test_regex('^(.+)( \1)+$', 'abc abd abc', 'RP'); + test_regex +-------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 14.29 RP {^(.+)( \1)+$} {abc abc abd} +select * from test_regex('^(.+)( \1)+$', 'abc abc abd', 'RP'); + test_regex +-------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 14.30 RP {^(.)\1|\1.} {abcdef} +select * from test_regex('^(.)\1|\1.', 'abcdef', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 14.31 RP {^((.)\2|..)\2} {abadef} +select * from test_regex('^((.)\2|..)\2', 'abadef', 'RP'); + test_regex +-------------------------------- + {2,REG_UBACKREF,REG_UNONPOSIX} +(1 row) + +-- back reference only matches the string, not any constraints +select * from test_regex('(^\w+).*\1', 'abc abc abc', 'LRP'); + test_regex +-------------------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX,REG_ULOCALE} + {"abc abc abc",abc} +(2 rows) + +select * from test_regex('(^\w+\M).*\1', 'abc abcd abd', 'LRP'); + test_regex +-------------------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX,REG_ULOCALE} + {"abc abc",abc} +(2 rows) + +select * from test_regex('(\w+(?= )).*\1', 'abc abcd abd', 'HLRP'); + test_regex +------------------------------------------------------------ + {1,REG_UBACKREF,REG_ULOOKAROUND,REG_UNONPOSIX,REG_ULOCALE} + {"abc abc",abc} +(2 rows) + +-- exercise oversize-regmatch_t-array paths in regexec() +-- (that case is not reachable via test_regex, sadly) +select substring('fffoooooooooooooooooooooooooooooooo', '^(.)\1(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'); + substring +----------- + f +(1 row) + +select regexp_split_to_array('abcxxxdefyyyghi', '((.))(\1\2)'); + regexp_split_to_array +----------------------- + {abc,def,ghi} +(1 row) + +-- doing 15 "octal escapes vs back references" +-- # initial zero is always octal +-- expectMatch 15.1 MP "a\\010b" "a\bb" "a\bb" +select * from test_regex('a\010b', E'a\bb', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x08b} +(2 rows) + +-- expectMatch 15.2 MP "a\\0070b" "a\0070b" "a\0070b" +select * from test_regex('a\0070b', E'a\0070b', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x070b} +(2 rows) + +-- expectMatch 15.3 MP "a\\07b" "a\007b" "a\007b" +select * from test_regex('a\07b', E'a\007b', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x07b} +(2 rows) + +-- expectMatch 15.4 MP "a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\\07c" \ +-- "abbbbbbbbbb\007c" abbbbbbbbbb\007c b b b b b b b b b b +select * from test_regex('a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\07c', E'abbbbbbbbbb\007c', 'MP'); + test_regex +---------------------------------------- + {10,REG_UNONPOSIX,REG_UUNPORT} + {abbbbbbbbbb\x07c,b,b,b,b,b,b,b,b,b,b} +(2 rows) + +-- # a single digit is always a backref +-- expectError 15.5 - {a\7b} ESUBREG +select * from test_regex('a\7b', '', '-'); +ERROR: invalid regular expression: invalid backreference number +-- # otherwise it's a backref only if within range (barf!) +-- expectMatch 15.6 MP "a\\10b" "a\bb" "a\bb" +select * from test_regex('a\10b', E'a\bb', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a\x08b} +(2 rows) + +-- expectMatch 15.7 MP {a\101b} aAb aAb +select * from test_regex('a\101b', 'aAb', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {aAb} +(2 rows) + +-- expectMatch 15.8 RP {a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\10c} \ +-- "abbbbbbbbbbbc" abbbbbbbbbbbc b b b b b b b b b b +select * from test_regex('a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\10c', 'abbbbbbbbbbbc', 'RP'); + test_regex +------------------------------------- + {10,REG_UBACKREF,REG_UNONPOSIX} + {abbbbbbbbbbbc,b,b,b,b,b,b,b,b,b,b} +(2 rows) + +-- # but we're fussy about border cases -- guys who want octal should use the zero +-- expectError 15.9 - {a((((((((((b\10))))))))))c} ESUBREG +select * from test_regex('a((((((((((b\10))))))))))c', '', '-'); +ERROR: invalid regular expression: invalid backreference number +-- # BREs don't have octal, EREs don't have backrefs +-- expectMatch 15.10 MP "a\\12b" "a\nb" "a\nb" +select * from test_regex('a\12b', E'a\nb', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {"a + + b"} +(2 rows) + +-- expectError 15.11 b {a\12b} ESUBREG +select * from test_regex('a\12b', '', 'b'); +ERROR: invalid regular expression: invalid backreference number +-- expectMatch 15.12 eAS {a\12b} a12b a12b +select * from test_regex('a\12b', 'a12b', 'eAS'); + test_regex +------------------------------ + {0,REG_UBSALNUM,REG_UUNSPEC} + {a12b} +(2 rows) + +-- expectMatch 15.13 MP {a\701b} a\u00381b a\u00381b +select * from test_regex('a\701b', 'a81b', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} + {a81b} +(2 rows) + +-- doing 16 "expanded syntax" +-- expectMatch 16.1 xP "a b c" "abc" "abc" +select * from test_regex('a b c', 'abc', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {abc} +(2 rows) + +-- expectMatch 16.2 xP "a b #oops\nc\td" "abcd" "abcd" +select * from test_regex(E'a b #oops\nc\td', 'abcd', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {abcd} +(2 rows) + +-- expectMatch 16.3 x "a\\ b\\\tc" "a b\tc" "a b\tc" +select * from test_regex(E'a\\ b\\\tc', E'a b\tc', 'x'); + test_regex +------------- + {0} + {"a b c"} +(2 rows) + +-- expectMatch 16.4 xP "a b\\#c" "ab#c" "ab#c" +select * from test_regex('a b\#c', 'ab#c', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {ab#c} +(2 rows) + +-- expectMatch 16.5 xP "a b\[c d]e" "ab e" "ab e" +select * from test_regex('a b[c d]e', 'ab e', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"ab e"} +(2 rows) + +-- expectMatch 16.6 xP "a b\[c#d]e" "ab#e" "ab#e" +select * from test_regex('a b[c#d]e', 'ab#e', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {ab#e} +(2 rows) + +-- expectMatch 16.7 xP "a b\[c#d]e" "abde" "abde" +select * from test_regex('a b[c#d]e', 'abde', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {abde} +(2 rows) + +-- expectMatch 16.8 xSPB "ab{ d" "ab\{d" "ab\{d" +select * from test_regex('ab{ d', 'ab{d', 'xSPB'); + test_regex +------------------------------------------- + {0,REG_UBRACES,REG_UNONPOSIX,REG_UUNSPEC} + {"ab{d"} +(2 rows) + +-- expectMatch 16.9 xPQ "ab{ 1 , 2 }c" "abc" "abc" +select * from test_regex('ab{ 1 , 2 }c', 'abc', 'xPQ'); + test_regex +------------------------------- + {0,REG_UBOUNDS,REG_UNONPOSIX} + {abc} +(2 rows) + +-- doing 17 "misc syntax" +-- expectMatch 17.1 P a(?#comment)b ab ab +select * from test_regex('a(?#comment)b', 'ab', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {ab} +(2 rows) + +-- doing 18 "unmatchable REs" +-- expectNomatch 18.1 I a^b ab +select * from test_regex('a^b', 'ab', 'I'); + test_regex +--------------------- + {0,REG_UIMPOSSIBLE} +(1 row) + +-- doing 19 "case independence" +-- expectMatch 19.1 &i ab Ab Ab +select * from test_regex('ab', 'Ab', 'i'); + test_regex +------------ + {0} + {Ab} +(2 rows) + +select * from test_regex('ab', 'Ab', 'ib'); + test_regex +------------ + {0} + {Ab} +(2 rows) + +-- expectMatch 19.2 &i {a[bc]} aC aC +select * from test_regex('a[bc]', 'aC', 'i'); + test_regex +------------ + {0} + {aC} +(2 rows) + +select * from test_regex('a[bc]', 'aC', 'ib'); + test_regex +------------ + {0} + {aC} +(2 rows) + +-- expectNomatch 19.3 &i {a[^bc]} aB +select * from test_regex('a[^bc]', 'aB', 'i'); + test_regex +------------ + {0} +(1 row) + +select * from test_regex('a[^bc]', 'aB', 'ib'); + test_regex +------------ + {0} +(1 row) + +-- expectMatch 19.4 &iM {a[b-d]} aC aC +select * from test_regex('a[b-d]', 'aC', 'iM'); + test_regex +----------------- + {0,REG_UUNPORT} + {aC} +(2 rows) + +select * from test_regex('a[b-d]', 'aC', 'iMb'); + test_regex +----------------- + {0,REG_UUNPORT} + {aC} +(2 rows) + +-- expectNomatch 19.5 &iM {a[^b-d]} aC +select * from test_regex('a[^b-d]', 'aC', 'iM'); + test_regex +----------------- + {0,REG_UUNPORT} +(1 row) + +select * from test_regex('a[^b-d]', 'aC', 'iMb'); + test_regex +----------------- + {0,REG_UUNPORT} +(1 row) + +-- expectMatch 19.6 &iM {a[B-Z]} aC aC +select * from test_regex('a[B-Z]', 'aC', 'iM'); + test_regex +----------------- + {0,REG_UUNPORT} + {aC} +(2 rows) + +select * from test_regex('a[B-Z]', 'aC', 'iMb'); + test_regex +----------------- + {0,REG_UUNPORT} + {aC} +(2 rows) + +-- expectNomatch 19.7 &iM {a[^B-Z]} aC +select * from test_regex('a[^B-Z]', 'aC', 'iM'); + test_regex +----------------- + {0,REG_UUNPORT} +(1 row) + +select * from test_regex('a[^B-Z]', 'aC', 'iMb'); + test_regex +----------------- + {0,REG_UUNPORT} +(1 row) + +-- doing 20 "directors and embedded options" +-- expectError 20.1 & ***? BADPAT +select * from test_regex('***?', '', ''); +ERROR: invalid regular expression: invalid regexp (reg version 0.8) +select * from test_regex('***?', '', 'b'); +ERROR: invalid regular expression: invalid regexp (reg version 0.8) +-- expectMatch 20.2 q ***? ***? ***? +select * from test_regex('***?', '***?', 'q'); + test_regex +------------ + {0} + {***?} +(2 rows) + +-- expectMatch 20.3 &P ***=a*b a*b a*b +select * from test_regex('***=a*b', 'a*b', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a*b} +(2 rows) + +select * from test_regex('***=a*b', 'a*b', 'Pb'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a*b} +(2 rows) + +-- expectMatch 20.4 q ***=a*b ***=a*b ***=a*b +select * from test_regex('***=a*b', '***=a*b', 'q'); + test_regex +------------ + {0} + {***=a*b} +(2 rows) + +-- expectMatch 20.5 bLP {***:\w+} ab ab +select * from test_regex('***:\w+', 'ab', 'bLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {ab} +(2 rows) + +-- expectMatch 20.6 eLP {***:\w+} ab ab +select * from test_regex('***:\w+', 'ab', 'eLP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {ab} +(2 rows) + +-- expectError 20.7 & ***:***=a*b BADRPT +select * from test_regex('***:***=a*b', '', ''); +ERROR: invalid regular expression: quantifier operand invalid +select * from test_regex('***:***=a*b', '', 'b'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 20.8 &P ***:(?b)a+b a+b a+b +select * from test_regex('***:(?b)a+b', 'a+b', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a+b} +(2 rows) + +select * from test_regex('***:(?b)a+b', 'a+b', 'Pb'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a+b} +(2 rows) + +-- expectMatch 20.9 P (?b)a+b a+b a+b +select * from test_regex('(?b)a+b', 'a+b', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a+b} +(2 rows) + +-- expectError 20.10 e {(?b)\w+} BADRPT +select * from test_regex('(?b)\w+', '', 'e'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 20.11 bAS {(?b)\w+} (?b)w+ (?b)w+ +select * from test_regex('(?b)\w+', '(?b)w+', 'bAS'); + test_regex +------------------------------ + {0,REG_UBSALNUM,REG_UUNSPEC} + {(?b)w+} +(2 rows) + +-- expectMatch 20.12 iP (?c)a a a +select * from test_regex('(?c)a', 'a', 'iP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectNomatch 20.13 iP (?c)a A +select * from test_regex('(?c)a', 'A', 'iP'); + test_regex +------------------- + {0,REG_UNONPOSIX} +(1 row) + +-- expectMatch 20.14 APS {(?e)\W+} WW WW +select * from test_regex('(?e)\W+', 'WW', 'APS'); + test_regex +-------------------------------------------- + {0,REG_UBSALNUM,REG_UNONPOSIX,REG_UUNSPEC} + {WW} +(2 rows) + +-- expectMatch 20.15 P (?i)a+ Aa Aa +select * from test_regex('(?i)a+', 'Aa', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {Aa} +(2 rows) + +-- expectNomatch 20.16 P "(?m)a.b" "a\nb" +select * from test_regex('(?m)a.b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} +(1 row) + +-- expectMatch 20.17 P "(?m)^b" "a\nb" "b" +select * from test_regex('(?m)^b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {b} +(2 rows) + +-- expectNomatch 20.18 P "(?n)a.b" "a\nb" +select * from test_regex('(?n)a.b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} +(1 row) + +-- expectMatch 20.19 P "(?n)^b" "a\nb" "b" +select * from test_regex('(?n)^b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {b} +(2 rows) + +-- expectNomatch 20.20 P "(?p)a.b" "a\nb" +select * from test_regex('(?p)a.b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 20.21 P "(?p)^b" "a\nb" +select * from test_regex('(?p)^b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} +(1 row) + +-- expectMatch 20.22 P (?q)a+b a+b a+b +select * from test_regex('(?q)a+b', 'a+b', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a+b} +(2 rows) + +-- expectMatch 20.23 nP "(?s)a.b" "a\nb" "a\nb" +select * from test_regex('(?s)a.b', E'a\nb', 'nP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a + + b"} +(2 rows) + +-- expectMatch 20.24 xP "(?t)a b" "a b" "a b" +select * from test_regex('(?t)a b', 'a b', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a b"} +(2 rows) + +-- expectMatch 20.25 P "(?w)a.b" "a\nb" "a\nb" +select * from test_regex('(?w)a.b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a + + b"} +(2 rows) + +-- expectMatch 20.26 P "(?w)^b" "a\nb" "b" +select * from test_regex('(?w)^b', E'a\nb', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {b} +(2 rows) + +-- expectMatch 20.27 P "(?x)a b" "ab" "ab" +select * from test_regex('(?x)a b', 'ab', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {ab} +(2 rows) + +-- expectError 20.28 - (?z)ab BADOPT +select * from test_regex('(?z)ab', '', '-'); +ERROR: invalid regular expression: invalid embedded option +-- expectMatch 20.29 P (?ici)a+ Aa Aa +select * from test_regex('(?ici)a+', 'Aa', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {Aa} +(2 rows) + +-- expectError 20.30 P (?i)(?q)a+ BADRPT +select * from test_regex('(?i)(?q)a+', '', 'P'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 20.31 P (?q)(?i)a+ (?i)a+ (?i)a+ +select * from test_regex('(?q)(?i)a+', '(?i)a+', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {(?i)a+} +(2 rows) + +-- expectMatch 20.32 P (?qe)a+ a a +select * from test_regex('(?qe)a+', 'a', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectMatch 20.33 xP "(?q)a b" "a b" "a b" +select * from test_regex('(?q)a b', 'a b', 'xP'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a b"} +(2 rows) + +-- expectMatch 20.34 P "(?qx)a b" "a b" "a b" +select * from test_regex('(?qx)a b', 'a b', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {"a b"} +(2 rows) + +-- expectMatch 20.35 P (?qi)ab Ab Ab +select * from test_regex('(?qi)ab', 'Ab', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {Ab} +(2 rows) + +-- doing 21 "capturing" +-- expectMatch 21.1 - a(b)c abc abc b +select * from test_regex('a(b)c', 'abc', '-'); + test_regex +------------ + {1} + {abc,b} +(2 rows) + +-- expectMatch 21.2 P a(?:b)c xabc abc +select * from test_regex('a(?:b)c', 'xabc', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {abc} +(2 rows) + +-- expectMatch 21.3 - a((b))c xabcy abc b b +select * from test_regex('a((b))c', 'xabcy', '-'); + test_regex +------------ + {2} + {abc,b,b} +(2 rows) + +-- expectMatch 21.4 P a(?:(b))c abcy abc b +select * from test_regex('a(?:(b))c', 'abcy', 'P'); + test_regex +------------------- + {1,REG_UNONPOSIX} + {abc,b} +(2 rows) + +-- expectMatch 21.5 P a((?:b))c abc abc b +select * from test_regex('a((?:b))c', 'abc', 'P'); + test_regex +------------------- + {1,REG_UNONPOSIX} + {abc,b} +(2 rows) + +-- expectMatch 21.6 P a(?:(?:b))c abc abc +select * from test_regex('a(?:(?:b))c', 'abc', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {abc} +(2 rows) + +-- expectIndices 21.7 Q "a(b){0}c" ac {0 1} {-1 -1} +select * from test_regex('a(b){0}c', 'ac', '0Q'); + test_regex +----------------- + {1,REG_UBOUNDS} + {"0 1","-1 -1"} +(2 rows) + +-- expectMatch 21.8 - a(b)c(d)e abcde abcde b d +select * from test_regex('a(b)c(d)e', 'abcde', '-'); + test_regex +------------- + {2} + {abcde,b,d} +(2 rows) + +-- expectMatch 21.9 - (b)c(d)e bcde bcde b d +select * from test_regex('(b)c(d)e', 'bcde', '-'); + test_regex +------------ + {2} + {bcde,b,d} +(2 rows) + +-- expectMatch 21.10 - a(b)(d)e abde abde b d +select * from test_regex('a(b)(d)e', 'abde', '-'); + test_regex +------------ + {2} + {abde,b,d} +(2 rows) + +-- expectMatch 21.11 - a(b)c(d) abcd abcd b d +select * from test_regex('a(b)c(d)', 'abcd', '-'); + test_regex +------------ + {2} + {abcd,b,d} +(2 rows) + +-- expectMatch 21.12 - (ab)(cd) xabcdy abcd ab cd +select * from test_regex('(ab)(cd)', 'xabcdy', '-'); + test_regex +-------------- + {2} + {abcd,ab,cd} +(2 rows) + +-- expectMatch 21.13 - a(b)?c xabcy abc b +select * from test_regex('a(b)?c', 'xabcy', '-'); + test_regex +------------ + {1} + {abc,b} +(2 rows) + +-- expectIndices 21.14 - a(b)?c xacy {1 2} {-1 -1} +select * from test_regex('a(b)?c', 'xacy', '0-'); + test_regex +----------------- + {1} + {"1 2","-1 -1"} +(2 rows) + +-- expectMatch 21.15 - a(b)?c(d)?e xabcdey abcde b d +select * from test_regex('a(b)?c(d)?e', 'xabcdey', '-'); + test_regex +------------- + {2} + {abcde,b,d} +(2 rows) + +-- expectIndices 21.16 - a(b)?c(d)?e xacdey {1 4} {-1 -1} {3 3} +select * from test_regex('a(b)?c(d)?e', 'xacdey', '0-'); + test_regex +----------------------- + {2} + {"1 4","-1 -1","3 3"} +(2 rows) + +-- expectIndices 21.17 - a(b)?c(d)?e xabcey {1 4} {2 2} {-1 -1} +select * from test_regex('a(b)?c(d)?e', 'xabcey', '0-'); + test_regex +----------------------- + {2} + {"1 4","2 2","-1 -1"} +(2 rows) + +-- expectIndices 21.18 - a(b)?c(d)?e xacey {1 3} {-1 -1} {-1 -1} +select * from test_regex('a(b)?c(d)?e', 'xacey', '0-'); + test_regex +------------------------- + {2} + {"1 3","-1 -1","-1 -1"} +(2 rows) + +-- expectMatch 21.19 - a(b)*c xabcy abc b +select * from test_regex('a(b)*c', 'xabcy', '-'); + test_regex +------------ + {1} + {abc,b} +(2 rows) + +-- expectIndices 21.20 - a(b)*c xabbbcy {1 5} {4 4} +select * from test_regex('a(b)*c', 'xabbbcy', '0-'); + test_regex +--------------- + {1} + {"1 5","4 4"} +(2 rows) + +-- expectIndices 21.21 - a(b)*c xacy {1 2} {-1 -1} +select * from test_regex('a(b)*c', 'xacy', '0-'); + test_regex +----------------- + {1} + {"1 2","-1 -1"} +(2 rows) + +-- expectMatch 21.22 - a(b*)c xabbbcy abbbc bbb +select * from test_regex('a(b*)c', 'xabbbcy', '-'); + test_regex +------------- + {1} + {abbbc,bbb} +(2 rows) + +-- expectMatch 21.23 - a(b*)c xacy ac "" +select * from test_regex('a(b*)c', 'xacy', '-'); + test_regex +------------ + {1} + {ac,""} +(2 rows) + +-- expectNomatch 21.24 - a(b)+c xacy +select * from test_regex('a(b)+c', 'xacy', '-'); + test_regex +------------ + {1} +(1 row) + +-- expectMatch 21.25 - a(b)+c xabcy abc b +select * from test_regex('a(b)+c', 'xabcy', '-'); + test_regex +------------ + {1} + {abc,b} +(2 rows) + +-- expectIndices 21.26 - a(b)+c xabbbcy {1 5} {4 4} +select * from test_regex('a(b)+c', 'xabbbcy', '0-'); + test_regex +--------------- + {1} + {"1 5","4 4"} +(2 rows) + +-- expectMatch 21.27 - a(b+)c xabbbcy abbbc bbb +select * from test_regex('a(b+)c', 'xabbbcy', '-'); + test_regex +------------- + {1} + {abbbc,bbb} +(2 rows) + +-- expectIndices 21.28 Q "a(b){2,3}c" xabbbcy {1 5} {4 4} +select * from test_regex('a(b){2,3}c', 'xabbbcy', '0Q'); + test_regex +----------------- + {1,REG_UBOUNDS} + {"1 5","4 4"} +(2 rows) + +-- expectIndices 21.29 Q "a(b){2,3}c" xabbcy {1 4} {3 3} +select * from test_regex('a(b){2,3}c', 'xabbcy', '0Q'); + test_regex +----------------- + {1,REG_UBOUNDS} + {"1 4","3 3"} +(2 rows) + +-- expectNomatch 21.30 Q "a(b){2,3}c" xabcy +select * from test_regex('a(b){2,3}c', 'xabcy', 'Q'); + test_regex +----------------- + {1,REG_UBOUNDS} +(1 row) + +-- expectMatch 21.31 LP "\\y(\\w+)\\y" "-- abc-" "abc" "abc" +select * from test_regex('\y(\w+)\y', '-- abc-', 'LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {abc,abc} +(2 rows) + +-- expectMatch 21.32 - a((b|c)d+)+ abacdbd acdbd bd b +select * from test_regex('a((b|c)d+)+', 'abacdbd', '-'); + test_regex +-------------- + {2} + {acdbd,bd,b} +(2 rows) + +-- expectMatch 21.33 N (.*).* abc abc abc +select * from test_regex('(.*).*', 'abc', 'N'); + test_regex +--------------------- + {1,REG_UEMPTYMATCH} + {abc,abc} +(2 rows) + +-- expectMatch 21.34 N (a*)* bc "" "" +select * from test_regex('(a*)*', 'bc', 'N'); + test_regex +--------------------- + {1,REG_UEMPTYMATCH} + {"",""} +(2 rows) + +-- expectMatch 21.35 M { TO (([a-z0-9._]+|"([^"]+|"")+")+)} {asd TO foo} { TO foo} foo o {} +select * from test_regex(' TO (([a-z0-9._]+|"([^"]+|"")+")+)', 'asd TO foo', 'M'); + test_regex +------------------------ + {3,REG_UUNPORT} + {" TO foo",foo,o,NULL} +(2 rows) + +-- expectMatch 21.36 RPQ ((.))(\2){0} xy x x x {} +select * from test_regex('((.))(\2){0}', 'xy', 'RPQ'); + test_regex +-------------------------------------------- + {3,REG_UBACKREF,REG_UBOUNDS,REG_UNONPOSIX} + {x,x,x,NULL} +(2 rows) + +-- expectMatch 21.37 RP ((.))(\2) xyy yy y y y +select * from test_regex('((.))(\2)', 'xyy', 'RP'); + test_regex +-------------------------------- + {3,REG_UBACKREF,REG_UNONPOSIX} + {yy,y,y,y} +(2 rows) + +-- expectMatch 21.38 oRP ((.))(\2) xyy yy {} {} {} +select * from test_regex('((.))(\2)', 'xyy', 'oRP'); + test_regex +-------------------------------- + {3,REG_UBACKREF,REG_UNONPOSIX} + {yy,NULL,NULL,NULL} +(2 rows) + +-- expectNomatch 21.39 PQR {(.){0}(\1)} xxx +select * from test_regex('(.){0}(\1)', 'xxx', 'PQR'); + test_regex +-------------------------------------------- + {2,REG_UBACKREF,REG_UBOUNDS,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 21.40 PQR {((.)){0}(\2)} xxx +select * from test_regex('((.)){0}(\2)', 'xxx', 'PQR'); + test_regex +-------------------------------------------- + {3,REG_UBACKREF,REG_UBOUNDS,REG_UNONPOSIX} +(1 row) + +-- expectMatch 21.41 NPQR {((.)){0}(\2){0}} xyz {} {} {} {} +select * from test_regex('((.)){0}(\2){0}', 'xyz', 'NPQR'); + test_regex +------------------------------------------------------------ + {3,REG_UBACKREF,REG_UBOUNDS,REG_UNONPOSIX,REG_UEMPTYMATCH} + {"",NULL,NULL,NULL} +(2 rows) + +-- doing 22 "multicharacter collating elements" +-- # again ugh +-- MCCEs are not implemented in Postgres, so we skip all these tests +-- expectMatch 22.1 &+L {a[c]e} ace ace +-- select * from test_regex('a[c]e', 'ace', '+L'); +-- select * from test_regex('a[c]e', 'ace', '+Lb'); +-- expectNomatch 22.2 &+IL {a[c]h} ach +-- select * from test_regex('a[c]h', 'ach', '+IL'); +-- select * from test_regex('a[c]h', 'ach', '+ILb'); +-- expectMatch 22.3 &+L {a[[.ch.]]} ach ach +-- select * from test_regex('a[[.ch.]]', 'ach', '+L'); +-- select * from test_regex('a[[.ch.]]', 'ach', '+Lb'); +-- expectNomatch 22.4 &+L {a[[.ch.]]} ace +-- select * from test_regex('a[[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[[.ch.]]', 'ace', '+Lb'); +-- expectMatch 22.5 &+L {a[c[.ch.]]} ac ac +-- select * from test_regex('a[c[.ch.]]', 'ac', '+L'); +-- select * from test_regex('a[c[.ch.]]', 'ac', '+Lb'); +-- expectMatch 22.6 &+L {a[c[.ch.]]} ace ac +-- select * from test_regex('a[c[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[c[.ch.]]', 'ace', '+Lb'); +-- expectMatch 22.7 &+L {a[c[.ch.]]} ache ach +-- select * from test_regex('a[c[.ch.]]', 'ache', '+L'); +-- select * from test_regex('a[c[.ch.]]', 'ache', '+Lb'); +-- expectNomatch 22.8 &+L {a[^c]e} ace +-- select * from test_regex('a[^c]e', 'ace', '+L'); +-- select * from test_regex('a[^c]e', 'ace', '+Lb'); +-- expectMatch 22.9 &+L {a[^c]e} abe abe +-- select * from test_regex('a[^c]e', 'abe', '+L'); +-- select * from test_regex('a[^c]e', 'abe', '+Lb'); +-- expectMatch 22.10 &+L {a[^c]e} ache ache +-- select * from test_regex('a[^c]e', 'ache', '+L'); +-- select * from test_regex('a[^c]e', 'ache', '+Lb'); +-- expectNomatch 22.11 &+L {a[^[.ch.]]} ach +-- select * from test_regex('a[^[.ch.]]', 'ach', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'ach', '+Lb'); +-- expectMatch 22.12 &+L {a[^[.ch.]]} ace ac +-- select * from test_regex('a[^[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'ace', '+Lb'); +-- expectMatch 22.13 &+L {a[^[.ch.]]} ac ac +-- select * from test_regex('a[^[.ch.]]', 'ac', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'ac', '+Lb'); +-- expectMatch 22.14 &+L {a[^[.ch.]]} abe ab +-- select * from test_regex('a[^[.ch.]]', 'abe', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'abe', '+Lb'); +-- expectNomatch 22.15 &+L {a[^c[.ch.]]} ach +-- select * from test_regex('a[^c[.ch.]]', 'ach', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'ach', '+Lb'); +-- expectNomatch 22.16 &+L {a[^c[.ch.]]} ace +-- select * from test_regex('a[^c[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'ace', '+Lb'); +-- expectNomatch 22.17 &+L {a[^c[.ch.]]} ac +-- select * from test_regex('a[^c[.ch.]]', 'ac', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'ac', '+Lb'); +-- expectMatch 22.18 &+L {a[^c[.ch.]]} abe ab +-- select * from test_regex('a[^c[.ch.]]', 'abe', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'abe', '+Lb'); +-- expectMatch 22.19 &+L {a[^b]} ac ac +-- select * from test_regex('a[^b]', 'ac', '+L'); +-- select * from test_regex('a[^b]', 'ac', '+Lb'); +-- expectMatch 22.20 &+L {a[^b]} ace ac +-- select * from test_regex('a[^b]', 'ace', '+L'); +-- select * from test_regex('a[^b]', 'ace', '+Lb'); +-- expectMatch 22.21 &+L {a[^b]} ach ach +-- select * from test_regex('a[^b]', 'ach', '+L'); +-- select * from test_regex('a[^b]', 'ach', '+Lb'); +-- expectNomatch 22.22 &+L {a[^b]} abe +-- select * from test_regex('a[^b]', 'abe', '+L'); +-- select * from test_regex('a[^b]', 'abe', '+Lb'); +-- doing 23 "lookahead constraints" +-- expectMatch 23.1 HP a(?=b)b* ab ab +select * from test_regex('a(?=b)b*', 'ab', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {ab} +(2 rows) + +-- expectNomatch 23.2 HP a(?=b)b* a +select * from test_regex('a(?=b)b*', 'a', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- expectMatch 23.3 HP a(?=b)b*(?=c)c* abc abc +select * from test_regex('a(?=b)b*(?=c)c*', 'abc', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {abc} +(2 rows) + +-- expectNomatch 23.4 HP a(?=b)b*(?=c)c* ab +select * from test_regex('a(?=b)b*(?=c)c*', 'ab', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 23.5 HP a(?!b)b* ab +select * from test_regex('a(?!b)b*', 'ab', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- expectMatch 23.6 HP a(?!b)b* a a +select * from test_regex('a(?!b)b*', 'a', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectMatch 23.7 HP (?=b)b b b +select * from test_regex('(?=b)b', 'b', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {b} +(2 rows) + +-- expectNomatch 23.8 HP (?=b)b a +select * from test_regex('(?=b)b', 'a', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- expectMatch 23.9 HP ...(?!.) abcde cde +select * from test_regex('...(?!.)', 'abcde', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {cde} +(2 rows) + +-- expectNomatch 23.10 HP ...(?=.) abc +select * from test_regex('...(?=.)', 'abc', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- Postgres addition: lookbehind constraints +-- expectMatch 23.11 HPN (?<=a)b* ab b +select * from test_regex('(?<=a)b*', 'ab', 'HPN'); + test_regex +--------------------------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX,REG_UEMPTYMATCH} + {b} +(2 rows) + +-- expectNomatch 23.12 HPN (?<=a)b* b +select * from test_regex('(?<=a)b*', 'b', 'HPN'); + test_regex +--------------------------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX,REG_UEMPTYMATCH} +(1 row) + +-- expectMatch 23.13 HP (?<=a)b*(?<=b)c* abc bc +select * from test_regex('(?<=a)b*(?<=b)c*', 'abc', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {bc} +(2 rows) + +-- expectNomatch 23.14 HP (?<=a)b*(?<=b)c* ac +select * from test_regex('(?<=a)b*(?<=b)c*', 'ac', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- expectNomatch 23.15 IHP a(?<!a)b* ab +select * from test_regex('a(?<!a)b*', 'ab', 'IHP'); + test_regex +--------------------------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX,REG_UIMPOSSIBLE} +(1 row) + +-- expectMatch 23.16 HP a(?<!b)b* a a +select * from test_regex('a(?<!b)b*', 'a', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectMatch 23.17 HP (?<=b)b bb b +select * from test_regex('(?<=b)b', 'bb', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {b} +(2 rows) + +-- expectNomatch 23.18 HP (?<=b)b b +select * from test_regex('(?<=b)b', 'b', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- expectMatch 23.19 HP (?<=.).. abcde bc +select * from test_regex('(?<=.)..', 'abcde', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {bc} +(2 rows) + +-- expectMatch 23.20 HP (?<=..)a* aaabb a +select * from test_regex('(?<=..)a*', 'aaabb', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {a} +(2 rows) + +-- expectMatch 23.21 HP (?<=..)b* aaabb {} +-- Note: empty match here is correct, it matches after the first 2 characters +select * from test_regex('(?<=..)b*', 'aaabb', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {""} +(2 rows) + +-- expectMatch 23.22 HP (?<=..)b+ aaabb bb +select * from test_regex('(?<=..)b+', 'aaabb', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {bb} +(2 rows) + +-- doing 24 "non-greedy quantifiers" +-- expectMatch 24.1 PT ab+? abb ab +select * from test_regex('ab+?', 'abb', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {ab} +(2 rows) + +-- expectMatch 24.2 PT ab+?c abbc abbc +select * from test_regex('ab+?c', 'abbc', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {abbc} +(2 rows) + +-- expectMatch 24.3 PT ab*? abb a +select * from test_regex('ab*?', 'abb', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {a} +(2 rows) + +-- expectMatch 24.4 PT ab*?c abbc abbc +select * from test_regex('ab*?c', 'abbc', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {abbc} +(2 rows) + +-- expectMatch 24.5 PT ab?? ab a +select * from test_regex('ab??', 'ab', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {a} +(2 rows) + +-- expectMatch 24.6 PT ab??c abc abc +select * from test_regex('ab??c', 'abc', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {abc} +(2 rows) + +-- expectMatch 24.7 PQT "ab{2,4}?" abbbb abb +select * from test_regex('ab{2,4}?', 'abbbb', 'PQT'); + test_regex +--------------------------------------------- + {0,REG_UBOUNDS,REG_UNONPOSIX,REG_USHORTEST} + {abb} +(2 rows) + +-- expectMatch 24.8 PQT "ab{2,4}?c" abbbbc abbbbc +select * from test_regex('ab{2,4}?c', 'abbbbc', 'PQT'); + test_regex +--------------------------------------------- + {0,REG_UBOUNDS,REG_UNONPOSIX,REG_USHORTEST} + {abbbbc} +(2 rows) + +-- expectMatch 24.9 - 3z* 123zzzz456 3zzzz +select * from test_regex('3z*', '123zzzz456', '-'); + test_regex +------------ + {0} + {3zzzz} +(2 rows) + +-- expectMatch 24.10 PT 3z*? 123zzzz456 3 +select * from test_regex('3z*?', '123zzzz456', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {3} +(2 rows) + +-- expectMatch 24.11 - z*4 123zzzz456 zzzz4 +select * from test_regex('z*4', '123zzzz456', '-'); + test_regex +------------ + {0} + {zzzz4} +(2 rows) + +-- expectMatch 24.12 PT z*?4 123zzzz456 zzzz4 +select * from test_regex('z*?4', '123zzzz456', 'PT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {zzzz4} +(2 rows) + +-- expectMatch 24.13 PT {^([^/]+?)(?:/([^/]+?))(?:/([^/]+?))?$} {foo/bar/baz} {foo/bar/baz} {foo} {bar} {baz} +select * from test_regex('^([^/]+?)(?:/([^/]+?))(?:/([^/]+?))?$', 'foo/bar/baz', 'PT'); + test_regex +--------------------------------- + {3,REG_UNONPOSIX,REG_USHORTEST} + {foo/bar/baz,foo,bar,baz} +(2 rows) + +-- expectMatch 24.14 PRT {^(.+?)(?:/(.+?))(?:/(.+?)\3)?$} {foo/bar/baz/quux} {foo/bar/baz/quux} {foo} {bar/baz/quux} {} +select * from test_regex('^(.+?)(?:/(.+?))(?:/(.+?)\3)?$', 'foo/bar/baz/quux', 'PRT'); + test_regex +---------------------------------------------- + {3,REG_UBACKREF,REG_UNONPOSIX,REG_USHORTEST} + {foo/bar/baz/quux,foo,bar/baz/quux,NULL} +(2 rows) + +-- doing 25 "mixed quantifiers" +-- # this is very incomplete as yet +-- # should include | +-- expectMatch 25.1 PNT {^(.*?)(a*)$} "xyza" xyza xyz a +select * from test_regex('^(.*?)(a*)$', 'xyza', 'PNT'); + test_regex +------------------------------------------------- + {2,REG_UNONPOSIX,REG_UEMPTYMATCH,REG_USHORTEST} + {xyza,xyz,a} +(2 rows) + +-- expectMatch 25.2 PNT {^(.*?)(a*)$} "xyzaa" xyzaa xyz aa +select * from test_regex('^(.*?)(a*)$', 'xyzaa', 'PNT'); + test_regex +------------------------------------------------- + {2,REG_UNONPOSIX,REG_UEMPTYMATCH,REG_USHORTEST} + {xyzaa,xyz,aa} +(2 rows) + +-- expectMatch 25.3 PNT {^(.*?)(a*)$} "xyz" xyz xyz "" +select * from test_regex('^(.*?)(a*)$', 'xyz', 'PNT'); + test_regex +------------------------------------------------- + {2,REG_UNONPOSIX,REG_UEMPTYMATCH,REG_USHORTEST} + {xyz,xyz,""} +(2 rows) + +-- doing 26 "tricky cases" +-- # attempts to trick the matcher into accepting a short match +-- expectMatch 26.1 - (week|wee)(night|knights) \ +-- "weeknights" weeknights wee knights +select * from test_regex('(week|wee)(night|knights)', 'weeknights', '-'); + test_regex +-------------------------- + {2} + {weeknights,wee,knights} +(2 rows) + +-- expectMatch 26.2 RP {a(bc*).*\1} abccbccb abccbccb b +select * from test_regex('a(bc*).*\1', 'abccbccb', 'RP'); + test_regex +-------------------------------- + {1,REG_UBACKREF,REG_UNONPOSIX} + {abccbccb,b} +(2 rows) + +-- expectMatch 26.3 - {a(b.[bc]*)+} abcbd abcbd bd +select * from test_regex('a(b.[bc]*)+', 'abcbd', '-'); + test_regex +------------ + {1} + {abcbd,bd} +(2 rows) + +-- doing 27 "implementation misc." +-- # duplicate arcs are suppressed +-- expectMatch 27.1 P a(?:b|b)c abc abc +select * from test_regex('a(?:b|b)c', 'abc', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {abc} +(2 rows) + +-- # make color/subcolor relationship go back and forth +-- expectMatch 27.2 & {[ab][ab][ab]} aba aba +select * from test_regex('[ab][ab][ab]', 'aba', ''); + test_regex +------------ + {0} + {aba} +(2 rows) + +select * from test_regex('[ab][ab][ab]', 'aba', 'b'); + test_regex +------------ + {0} + {aba} +(2 rows) + +-- expectMatch 27.3 & {[ab][ab][ab][ab][ab][ab][ab]} \ +-- "abababa" abababa +select * from test_regex('[ab][ab][ab][ab][ab][ab][ab]', 'abababa', ''); + test_regex +------------ + {0} + {abababa} +(2 rows) + +select * from test_regex('[ab][ab][ab][ab][ab][ab][ab]', 'abababa', 'b'); + test_regex +------------ + {0} + {abababa} +(2 rows) + +-- doing 28 "boundary busters etc." +-- # color-descriptor allocation changes at 10 +-- expectMatch 28.1 & abcdefghijkl "abcdefghijkl" abcdefghijkl +select * from test_regex('abcdefghijkl', 'abcdefghijkl', ''); + test_regex +---------------- + {0} + {abcdefghijkl} +(2 rows) + +select * from test_regex('abcdefghijkl', 'abcdefghijkl', 'b'); + test_regex +---------------- + {0} + {abcdefghijkl} +(2 rows) + +-- # so does arc allocation +-- expectMatch 28.2 P a(?:b|c|d|e|f|g|h|i|j|k|l|m)n "agn" agn +select * from test_regex('a(?:b|c|d|e|f|g|h|i|j|k|l|m)n', 'agn', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {agn} +(2 rows) + +-- # subexpression tracking also at 10 +-- expectMatch 28.3 - a(((((((((((((b)))))))))))))c \ +-- "abc" abc b b b b b b b b b b b b b +select * from test_regex('a(((((((((((((b)))))))))))))c', 'abc', '-'); + test_regex +--------------------------------- + {13} + {abc,b,b,b,b,b,b,b,b,b,b,b,b,b} +(2 rows) + +-- # state-set handling changes slightly at unsigned size (might be 64...) +-- # (also stresses arc allocation) +-- expectMatch 28.4 Q "ab{1,100}c" abbc abbc +select * from test_regex('ab{1,100}c', 'abbc', 'Q'); + test_regex +----------------- + {0,REG_UBOUNDS} + {abbc} +(2 rows) + +-- expectMatch 28.5 Q "ab{1,100}c" \ +-- "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc" \ +-- abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc +select * from test_regex('ab{1,100}c', 'abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc', 'Q'); + test_regex +--------------------------------------- + {0,REG_UBOUNDS} + {abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc} +(2 rows) + +-- expectMatch 28.6 Q "ab{1,100}c" \ +-- "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc"\ +-- abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc +select * from test_regex('ab{1,100}c', 'abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc', 'Q'); + test_regex +------------------------------------------------------------------------ + {0,REG_UBOUNDS} + {abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc} +(2 rows) + +-- # force small cache and bust it, several ways +-- expectMatch 28.7 LP {\w+abcdefgh} xyzabcdefgh xyzabcdefgh +select * from test_regex('\w+abcdefgh', 'xyzabcdefgh', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {xyzabcdefgh} +(2 rows) + +-- expectMatch 28.8 %LP {\w+abcdefgh} xyzabcdefgh xyzabcdefgh +select * from test_regex('\w+abcdefgh', 'xyzabcdefgh', '%LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {xyzabcdefgh} +(2 rows) + +-- expectMatch 28.9 %LP {\w+abcdefghijklmnopqrst} \ +-- "xyzabcdefghijklmnopqrst" xyzabcdefghijklmnopqrst +select * from test_regex('\w+abcdefghijklmnopqrst', 'xyzabcdefghijklmnopqrst', '%LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {xyzabcdefghijklmnopqrst} +(2 rows) + +-- expectIndices 28.10 %LP {\w+(abcdefgh)?} xyz {0 2} {-1 -1} +select * from test_regex('\w+(abcdefgh)?', 'xyz', '0%LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {"0 2","-1 -1"} +(2 rows) + +-- expectIndices 28.11 %LP {\w+(abcdefgh)?} xyzabcdefg {0 9} {-1 -1} +select * from test_regex('\w+(abcdefgh)?', 'xyzabcdefg', '0%LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {"0 9","-1 -1"} +(2 rows) + +-- expectIndices 28.12 %LP {\w+(abcdefghijklmnopqrst)?} \ +-- "xyzabcdefghijklmnopqrs" {0 21} {-1 -1} +select * from test_regex('\w+(abcdefghijklmnopqrst)?', 'xyzabcdefghijklmnopqrs', '0%LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {"0 21","-1 -1"} +(2 rows) + +-- doing 29 "incomplete matches" +-- expectPartial 29.1 t def abc {3 2} "" +select * from test_regex('def', 'abc', '0!t'); + test_regex +--------------- + {0} + {"3 2","3 2"} +(2 rows) + +-- expectPartial 29.2 t bcd abc {1 2} "" +select * from test_regex('bcd', 'abc', '0!t'); + test_regex +--------------- + {0} + {"1 2","1 2"} +(2 rows) + +-- expectPartial 29.3 t abc abab {0 3} "" +select * from test_regex('abc', 'abab', '0!t'); + test_regex +--------------- + {0} + {"0 3","0 3"} +(2 rows) + +-- expectPartial 29.4 t abc abdab {3 4} "" +select * from test_regex('abc', 'abdab', '0!t'); + test_regex +--------------- + {0} + {"3 4","3 4"} +(2 rows) + +-- expectIndices 29.5 t abc abc {0 2} {0 2} +select * from test_regex('abc', 'abc', '0t'); + test_regex +--------------- + {0} + {"0 2","0 2"} +(2 rows) + +-- expectIndices 29.6 t abc xyabc {2 4} {2 4} +select * from test_regex('abc', 'xyabc', '0t'); + test_regex +--------------- + {0} + {"2 4","2 4"} +(2 rows) + +-- expectPartial 29.7 t abc+ xyab {2 3} "" +select * from test_regex('abc+', 'xyab', '0!t'); + test_regex +--------------- + {0} + {"2 3","2 3"} +(2 rows) + +-- expectIndices 29.8 t abc+ xyabc {2 4} {2 4} +select * from test_regex('abc+', 'xyabc', '0t'); + test_regex +--------------- + {0} + {"2 4","2 4"} +(2 rows) + +-- knownBug expectIndices 29.9 t abc+ xyabcd {2 4} {6 5} +select * from test_regex('abc+', 'xyabcd', '0t'); + test_regex +--------------- + {0} + {"2 4","2 5"} +(2 rows) + +-- expectIndices 29.10 t abc+ xyabcdd {2 4} {7 6} +select * from test_regex('abc+', 'xyabcdd', '0t'); + test_regex +--------------- + {0} + {"2 4","7 6"} +(2 rows) + +-- expectPartial 29.11 tPT abc+? xyab {2 3} "" +select * from test_regex('abc+?', 'xyab', '0!tPT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {"2 3","2 3"} +(2 rows) + +-- # the retain numbers in these two may look wrong, but they aren't +-- expectIndices 29.12 tPT abc+? xyabc {2 4} {5 4} +select * from test_regex('abc+?', 'xyabc', '0tPT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {"2 4","5 4"} +(2 rows) + +-- expectIndices 29.13 tPT abc+? xyabcc {2 4} {6 5} +select * from test_regex('abc+?', 'xyabcc', '0tPT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {"2 4","6 5"} +(2 rows) + +-- expectIndices 29.14 tPT abc+? xyabcd {2 4} {6 5} +select * from test_regex('abc+?', 'xyabcd', '0tPT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {"2 4","6 5"} +(2 rows) + +-- expectIndices 29.15 tPT abc+? xyabcdd {2 4} {7 6} +select * from test_regex('abc+?', 'xyabcdd', '0tPT'); + test_regex +--------------------------------- + {0,REG_UNONPOSIX,REG_USHORTEST} + {"2 4","7 6"} +(2 rows) + +-- expectIndices 29.16 t abcd|bc xyabc {3 4} {2 4} +select * from test_regex('abcd|bc', 'xyabc', '0t'); + test_regex +--------------- + {0} + {"3 4","2 4"} +(2 rows) + +-- expectPartial 29.17 tn .*k "xx\nyyy" {3 5} "" +select * from test_regex('.*k', E'xx\nyyy', '0!tn'); + test_regex +--------------- + {0} + {"3 5","3 5"} +(2 rows) + +-- doing 30 "misc. oddities and old bugs" +-- expectError 30.1 & *** BADRPT +select * from test_regex('***', '', ''); +ERROR: invalid regular expression: quantifier operand invalid +select * from test_regex('***', '', 'b'); +ERROR: invalid regular expression: quantifier operand invalid +-- expectMatch 30.2 N a?b* abb abb +select * from test_regex('a?b*', 'abb', 'N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {abb} +(2 rows) + +-- expectMatch 30.3 N a?b* bb bb +select * from test_regex('a?b*', 'bb', 'N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {bb} +(2 rows) + +-- expectMatch 30.4 & a*b aab aab +select * from test_regex('a*b', 'aab', ''); + test_regex +------------ + {0} + {aab} +(2 rows) + +select * from test_regex('a*b', 'aab', 'b'); + test_regex +------------ + {0} + {aab} +(2 rows) + +-- expectMatch 30.5 & ^a*b aaaab aaaab +select * from test_regex('^a*b', 'aaaab', ''); + test_regex +------------ + {0} + {aaaab} +(2 rows) + +select * from test_regex('^a*b', 'aaaab', 'b'); + test_regex +------------ + {0} + {aaaab} +(2 rows) + +-- expectMatch 30.6 &M {[0-6][1-2][0-3][0-6][1-6][0-6]} \ +-- "010010" 010010 +select * from test_regex('[0-6][1-2][0-3][0-6][1-6][0-6]', '010010', 'M'); + test_regex +----------------- + {0,REG_UUNPORT} + {010010} +(2 rows) + +select * from test_regex('[0-6][1-2][0-3][0-6][1-6][0-6]', '010010', 'Mb'); + test_regex +----------------- + {0,REG_UUNPORT} + {010010} +(2 rows) + +-- # temporary REG_BOSONLY kludge +-- expectMatch 30.7 s abc abcd abc +select * from test_regex('abc', 'abcd', 's'); + test_regex +------------ + {0} + {abc} +(2 rows) + +-- expectNomatch 30.8 s abc xabcd +select * from test_regex('abc', 'xabcd', 's'); + test_regex +------------ + {0} +(1 row) + +-- # back to normal stuff +-- expectMatch 30.9 HLP {(?n)^(?![t#])\S+} \ +-- "tk\n\n#\n#\nit0" it0 +select * from test_regex('(?n)^(?![t#])\S+', E'tk\n\n#\n#\nit0', 'HLP'); + test_regex +----------------------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX,REG_ULOCALE} + {it0} +(2 rows) + +-- # Now for tests *not* written by Henry Spencer +-- # Tests resulting from bugs reported by users +-- test reg-31.1 {[[:xdigit:]] behaves correctly when followed by [[:space:]]} { +-- set str {2:::DebugWin32} +-- set re {([[:xdigit:]])([[:space:]]*)} +-- list [regexp $re $str match xdigit spaces] $match $xdigit $spaces +-- # Code used to produce {1 2:::DebugWin32 2 :::DebugWin32} !!! +-- } {1 2 2 {}} +select * from test_regex('([[:xdigit:]])([[:space:]]*)', '2:::DebugWin32', 'L'); + test_regex +----------------- + {2,REG_ULOCALE} + {2,2,""} +(2 rows) + +-- test reg-32.1 {canmatch functionality -- at end} testregexp { +-- set pat {blah} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 7} +select * from test_regex('blah', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"7 6","7 6"} +(2 rows) + +-- test reg-32.2 {canmatch functionality -- at end} testregexp { +-- set pat {s%$} +-- set line "asd asd" +-- # can only match after the end of the string +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 7} +select * from test_regex('s%$', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"7 6","7 6"} +(2 rows) + +-- test reg-32.3 {canmatch functionality -- not last char} testregexp { +-- set pat {[^d]%$} +-- set line "asd asd" +-- # can only match after the end of the string +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 7} +select * from test_regex('[^d]%$', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"7 6","7 6"} +(2 rows) + +-- test reg-32.3.1 {canmatch functionality -- no match} testregexp { +-- set pat {\Zx} +-- set line "asd asd" +-- # can match the last char, if followed by x +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 -1} +select * from test_regex('\Zx', 'asd asd', 'cIP'); + test_regex +----------------------------------- + {0,REG_UNONPOSIX,REG_UIMPOSSIBLE} + {"-1 -1","-1 -1"} +(2 rows) + +-- test reg-32.4 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {.x} +-- set line "asd asd" +-- # can match the last char, if followed by x +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('.x', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"0 6","0 6"} +(2 rows) + +-- test reg-32.4.1 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {.x$} +-- set line "asd asd" +-- # can match the last char, if followed by x +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('.x$', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"0 6","0 6"} +(2 rows) + +-- test reg-32.5 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {.[^d]x$} +-- set line "asd asd" +-- # can match the last char, if followed by not-d and x. +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('.[^d]x$', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"0 6","0 6"} +(2 rows) + +-- test reg-32.6 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {[^a]%[^\r\n]*$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('[^a]%[^\r\n]*$', 'asd asd', 'cEP'); + test_regex +---------------------------- + {0,REG_UBBS,REG_UNONPOSIX} + {"5 6","5 6"} +(2 rows) + +-- test reg-32.7 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {[^a]%$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('[^a]%$', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"5 6","5 6"} +(2 rows) + +-- test reg-32.8 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {[^x]%$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('[^x]%$', 'asd asd', 'c'); + test_regex +--------------- + {0} + {"0 6","0 6"} +(2 rows) + +-- test reg-32.9 {canmatch functionality -- more complex case} {knownBug testregexp} { +-- set pat {((\B\B|\Bh+line)[ \t]*|[^\B]%[^\r\n]*)$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('((\B\B|\Bh+line)[ \t]*|[^\B]%[^\r\n]*)$', 'asd asd', 'cEP'); + test_regex +------------------------------- + {2,REG_UBBS,REG_UNONPOSIX} + {"0 6","-1 -1","-1 -1","0 6"} +(2 rows) + +-- # Tests reg-33.*: Checks for bug fixes +-- test reg-33.1 {Bug 230589} { +-- regexp {[ ]*(^|[^%])%V} "*%V2" m s +-- } 1 +select * from test_regex('[ ]*(^|[^%])%V', '*%V2', '-'); + test_regex +------------ + {1} + {*%V,*} +(2 rows) + +-- test reg-33.2 {Bug 504785} { +-- regexp -inline {([^_.]*)([^.]*)\.(..)(.).*} bbcos_001_c01.q1la +-- } {bbcos_001_c01.q1la bbcos _001_c01 q1 l} +select * from test_regex('([^_.]*)([^.]*)\.(..)(.).*', 'bbcos_001_c01.q1la', '-'); + test_regex +------------------------------------------ + {4} + {bbcos_001_c01.q1la,bbcos,_001_c01,q1,l} +(2 rows) + +-- test reg-33.3 {Bug 505048} { +-- regexp {\A\s*[^<]*\s*<([^>]+)>} a<a> +-- } 1 +select * from test_regex('\A\s*[^<]*\s*<([^>]+)>', 'a<a>', 'LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {a<a>,a} +(2 rows) + +-- test reg-33.4 {Bug 505048} { +-- regexp {\A\s*([^b]*)b} ab +-- } 1 +select * from test_regex('\A\s*([^b]*)b', 'ab', 'LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {ab,a} +(2 rows) + +-- test reg-33.5 {Bug 505048} { +-- regexp {\A\s*[^b]*(b)} ab +-- } 1 +select * from test_regex('\A\s*[^b]*(b)', 'ab', 'LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {ab,b} +(2 rows) + +-- test reg-33.6 {Bug 505048} { +-- regexp {\A(\s*)[^b]*(b)} ab +-- } 1 +select * from test_regex('\A(\s*)[^b]*(b)', 'ab', 'LP'); + test_regex +------------------------------- + {2,REG_UNONPOSIX,REG_ULOCALE} + {ab,"",b} +(2 rows) + +-- test reg-33.7 {Bug 505048} { +-- regexp {\A\s*[^b]*b} ab +-- } 1 +select * from test_regex('\A\s*[^b]*b', 'ab', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {ab} +(2 rows) + +-- test reg-33.8 {Bug 505048} { +-- regexp -inline {\A\s*[^b]*b} ab +-- } ab +select * from test_regex('\A\s*[^b]*b', 'ab', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {ab} +(2 rows) + +-- test reg-33.9 {Bug 505048} { +-- regexp -indices -inline {\A\s*[^b]*b} ab +-- } {{0 1}} +select * from test_regex('\A\s*[^b]*b', 'ab', '0LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {"0 1"} +(2 rows) + +-- test reg-33.10 {Bug 840258} -body { +-- regsub {(^|\n)+\.*b} \n.b {} tmp +-- } -cleanup { +-- unset tmp +-- } -result 1 +select * from test_regex('(^|\n)+\.*b', E'\n.b', 'P'); + test_regex +------------------- + {1,REG_UNONPOSIX} + {" + + .b"," + + "} +(2 rows) + +-- test reg-33.11 {Bug 840258} -body { +-- regsub {(^|[\n\r]+)\.*\?<.*?(\n|\r)+} \ +-- "TQ\r\n.?<5000267>Test already stopped\r\n" {} tmp +-- } -cleanup { +-- unset tmp +-- } -result 1 +select * from test_regex('(^|[\n\r]+)\.*\?<.*?(\n|\r)+', E'TQ\r\n.?<5000267>Test already stopped\r\n', 'EP'); + test_regex +----------------------------------- + {2,REG_UBBS,REG_UNONPOSIX} + {"\r + + .?<5000267>Test already stopped\r+ + ","\r + + "," + + "} +(2 rows) + +-- test reg-33.12 {Bug 1810264 - bad read} { +-- regexp {\3161573148} {\3161573148} +-- } 0 +select * from test_regex('\3161573148', '\3161573148', 'MP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_UUNPORT} +(1 row) + +-- test reg-33.13 {Bug 1810264 - infinite loop} { +-- regexp {($|^)*} {x} +-- } 1 +select * from test_regex('($|^)*', 'x', 'N'); + test_regex +--------------------- + {1,REG_UEMPTYMATCH} + {"",""} +(2 rows) + +-- # Some environments have small default stack sizes. [Bug 1905562] +-- test reg-33.14 {Bug 1810264 - super-expensive expression} nonPortable { +-- regexp {(x{200}){200}$y} {x} +-- } 0 +-- This might or might not work depending on platform, so skip it +-- select * from test_regex('(x{200}){200}$y', 'x', 'IQ'); +-- test reg-33.15.1 {Bug 3603557 - an "in the wild" RE} { +-- lindex [regexp -expanded -about { +-- ^TETRA_MODE_CMD # Message Type +-- ([[:blank:]]+) # Pad +-- (ETS_1_1|ETS_1_2|ETS_2_2) # SystemCode +-- ([[:blank:]]+) # Pad +-- (CONTINUOUS|CARRIER|MCCH|TRAFFIC) # SharingMode +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # ColourCode +-- ([[:blank:]]+) # Pad +-- (1|2|3|4|6|9|12|18) # TSReservedFrames +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # UPlaneDTX +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # Frame18Extension +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,4}) # MCC +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,5}) # MNC +-- ([[:blank:]]+) # Pad +-- (BOTH|BCAST|ENQRY|NONE) # NbrCellBcast +-- ([[:blank:]]+) # Pad +-- (UNKNOWN|LOW|MEDIUM|HIGH) # CellServiceLevel +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # LateEntryInfo +-- ([[:blank:]]+) # Pad +-- (300|400) # FrequencyBand +-- ([[:blank:]]+) # Pad +-- (NORMAL|REVERSE) # ReverseOperation +-- ([[:blank:]]+) # Pad +-- (NONE|\+6\.25|\-6\.25|\+12\.5) # Offset +-- ([[:blank:]]+) # Pad +-- (10) # DuplexSpacing +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,4}) # MainCarrierNr +-- ([[:blank:]]+) # Pad +-- (0|1|2|3) # NrCSCCH +-- ([[:blank:]]+) # Pad +-- (15|20|25|30|35|40|45) # MSTxPwrMax +-- ([[:blank:]]+) # Pad +-- (\-125|\-120|\-115|\-110|\-105|\-100|\-95|\-90|\-85|\-80|\-75|\-70|\-65|\-60|\-55|\-50) +-- # RxLevAccessMin +-- ([[:blank:]]+) # Pad +-- (\-53|\-51|\-49|\-47|\-45|\-43|\-41|\-39|\-37|\-35|\-33|\-31|\-29|\-27|\-25|\-23) +-- # AccessParameter +-- ([[:blank:]]+) # Pad +-- (DISABLE|[[:digit:]]{3,4}) # RadioDLTimeout +-- ([[:blank:]]+) # Pad +-- (\-[[:digit:]]{2,3}) # RSSIThreshold +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,5}) # CCKIdSCKVerNr +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,5}) # LocationArea +-- ([[:blank:]]+) # Pad +-- ([(1|0)]{16}) # SubscriberClass +-- ([[:blank:]]+) # Pad +-- ([(1|0)]{12}) # BSServiceDetails +-- ([[:blank:]]+) # Pad +-- (RANDOMIZE|IMMEDIATE|[[:digit:]]{1,2}) # IMM +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # WT +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # Nu +-- ([[:blank:]]+) # Pad +-- ([0-1]) # FrameLngFctr +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # TSPtr +-- ([[:blank:]]+) # Pad +-- ([0-7]) # MinPriority +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # ExtdSrvcsEnabled +-- ([[:blank:]]+) # Pad +-- (.*) # ConditionalFields +-- }] 0 +-- } 68 +select * from test_regex($$ + ^TETRA_MODE_CMD # Message Type + ([[:blank:]]+) # Pad + (ETS_1_1|ETS_1_2|ETS_2_2) # SystemCode + ([[:blank:]]+) # Pad + (CONTINUOUS|CARRIER|MCCH|TRAFFIC) # SharingMode + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # ColourCode + ([[:blank:]]+) # Pad + (1|2|3|4|6|9|12|18) # TSReservedFrames + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # UPlaneDTX + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # Frame18Extension + ([[:blank:]]+) # Pad + ([[:digit:]]{1,4}) # MCC + ([[:blank:]]+) # Pad + ([[:digit:]]{1,5}) # MNC + ([[:blank:]]+) # Pad + (BOTH|BCAST|ENQRY|NONE) # NbrCellBcast + ([[:blank:]]+) # Pad + (UNKNOWN|LOW|MEDIUM|HIGH) # CellServiceLevel + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # LateEntryInfo + ([[:blank:]]+) # Pad + (300|400) # FrequencyBand + ([[:blank:]]+) # Pad + (NORMAL|REVERSE) # ReverseOperation + ([[:blank:]]+) # Pad + (NONE|\+6\.25|\-6\.25|\+12\.5) # Offset + ([[:blank:]]+) # Pad + (10) # DuplexSpacing + ([[:blank:]]+) # Pad + ([[:digit:]]{1,4}) # MainCarrierNr + ([[:blank:]]+) # Pad + (0|1|2|3) # NrCSCCH + ([[:blank:]]+) # Pad + (15|20|25|30|35|40|45) # MSTxPwrMax + ([[:blank:]]+) # Pad + (\-125|\-120|\-115|\-110|\-105|\-100|\-95|\-90|\-85|\-80|\-75|\-70|\-65|\-60|\-55|\-50) + # RxLevAccessMin + ([[:blank:]]+) # Pad + (\-53|\-51|\-49|\-47|\-45|\-43|\-41|\-39|\-37|\-35|\-33|\-31|\-29|\-27|\-25|\-23) + # AccessParameter + ([[:blank:]]+) # Pad + (DISABLE|[[:digit:]]{3,4}) # RadioDLTimeout + ([[:blank:]]+) # Pad + (\-[[:digit:]]{2,3}) # RSSIThreshold + ([[:blank:]]+) # Pad + ([[:digit:]]{1,5}) # CCKIdSCKVerNr + ([[:blank:]]+) # Pad + ([[:digit:]]{1,5}) # LocationArea + ([[:blank:]]+) # Pad + ([(1|0)]{16}) # SubscriberClass + ([[:blank:]]+) # Pad + ([(1|0)]{12}) # BSServiceDetails + ([[:blank:]]+) # Pad + (RANDOMIZE|IMMEDIATE|[[:digit:]]{1,2}) # IMM + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # WT + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # Nu + ([[:blank:]]+) # Pad + ([0-1]) # FrameLngFctr + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # TSPtr + ([[:blank:]]+) # Pad + ([0-7]) # MinPriority + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # ExtdSrvcsEnabled + ([[:blank:]]+) # Pad + (.*) # ConditionalFields + $$, '', 'xLMPQ'); + test_regex +-------------------------------------------------------- + {68,REG_UBOUNDS,REG_UNONPOSIX,REG_UUNPORT,REG_ULOCALE} +(1 row) + +-- test reg-33.16.1 {Bug [8d2c0da36d]- another "in the wild" RE} { +-- lindex [regexp -about "^MRK:client1: =1339 14HKelly Talisman 10011000 (\[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]*) \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 8 0 8 0 0 0 77 77 1 1 2 0 11 { 1 3 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 13HC6 My Creator 2 3 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 31HC7 Slightly offensive name, huh 3 8 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 23HE-mail:kelly@hotbox.com 4 9 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 17Hcompface must die 5 10 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 0 3HAir 6 12 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 14HPGP public key 7 13 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 16Hkelly@hotbox.com 8 30 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 0 12H2 text/plain 9 30 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 0 13H2 x-kom/basic 10 33 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 1H0 11 14 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 1H3 }\r?"] 0 +-- } 1 +select * from test_regex(E'^MRK:client1: =1339 14HKelly Talisman 10011000 ([0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]*) [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 8 0 8 0 0 0 77 77 1 1 2 0 11 { 1 3 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 13HC6 My Creator 2 3 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 31HC7 Slightly offensive name, huh 3 8 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 23HE-mail:kelly@hotbox.com 4 9 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 17Hcompface must die 5 10 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 0 3HAir 6 12 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 14HPGP public key 7 13 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 16Hkelly@hotbox.com 8 30 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 0 12H2 text/plain 9 30 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 0 13H2 x-kom/basic 10 33 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 1H0 11 14 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 1H3 }\r?', '', 'BMS'); + test_regex +----------------------------------------- + {1,REG_UBRACES,REG_UUNSPEC,REG_UUNPORT} +(1 row) + +-- test reg-33.15 {constraint fixes} { +-- regexp {(^)+^} x +-- } 1 +select * from test_regex('(^)+^', 'x', 'N'); + test_regex +--------------------- + {1,REG_UEMPTYMATCH} + {"",""} +(2 rows) + +-- test reg-33.16 {constraint fixes} { +-- regexp {($^)+} x +-- } 0 +select * from test_regex('($^)+', 'x', 'N'); + test_regex +--------------------- + {1,REG_UEMPTYMATCH} +(1 row) + +-- test reg-33.17 {constraint fixes} { +-- regexp {(^$)*} x +-- } 1 +select * from test_regex('(^$)*', 'x', 'N'); + test_regex +--------------------- + {1,REG_UEMPTYMATCH} + {"",NULL} +(2 rows) + +-- test reg-33.18 {constraint fixes} { +-- regexp {(^(?!aa))+} {aa bb cc} +-- } 0 +select * from test_regex('(^(?!aa))+', 'aa bb cc', 'HP'); + test_regex +----------------------------------- + {1,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- test reg-33.19 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {aa x} +-- } 0 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'aa x', 'HP'); + test_regex +----------------------------------- + {1,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- test reg-33.20 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {bb x} +-- } 0 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'bb x', 'HP'); + test_regex +----------------------------------- + {1,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- test reg-33.21 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {cc x} +-- } 0 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'cc x', 'HP'); + test_regex +----------------------------------- + {1,REG_ULOOKAROUND,REG_UNONPOSIX} +(1 row) + +-- test reg-33.22 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {dd x} +-- } 1 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'dd x', 'HP'); + test_regex +----------------------------------- + {1,REG_ULOOKAROUND,REG_UNONPOSIX} + {"",""} +(2 rows) + +-- test reg-33.23 {} { +-- regexp {abcd(\m)+xyz} x +-- } 0 +select * from test_regex('abcd(\m)+xyz', 'x', 'ILP'); + test_regex +----------------------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE,REG_UIMPOSSIBLE} +(1 row) + +-- test reg-33.24 {} { +-- regexp {abcd(\m)+xyz} a +-- } 0 +select * from test_regex('abcd(\m)+xyz', 'a', 'ILP'); + test_regex +----------------------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE,REG_UIMPOSSIBLE} +(1 row) + +-- test reg-33.25 {} { +-- regexp {^abcd*(((((^(a c(e?d)a+|)+|)+|)+|)+|a)+|)} x +-- } 0 +select * from test_regex('^abcd*(((((^(a c(e?d)a+|)+|)+|)+|)+|a)+|)', 'x', 'S'); + test_regex +----------------- + {7,REG_UUNSPEC} +(1 row) + +-- test reg-33.26 {} { +-- regexp {a^(^)bcd*xy(((((($a+|)+|)+|)+$|)+|)+|)^$} x +-- } 0 +select * from test_regex('a^(^)bcd*xy(((((($a+|)+|)+|)+$|)+|)+|)^$', 'x', 'IS'); + test_regex +--------------------------------- + {7,REG_UUNSPEC,REG_UIMPOSSIBLE} +(1 row) + +-- test reg-33.27 {} { +-- regexp {xyz(\Y\Y)+} x +-- } 0 +select * from test_regex('xyz(\Y\Y)+', 'x', 'LP'); + test_regex +------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} +(1 row) + +-- test reg-33.28 {} { +-- regexp {x|(?:\M)+} x +-- } 1 +select * from test_regex('x|(?:\M)+', 'x', 'LNP'); + test_regex +----------------------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE,REG_UEMPTYMATCH} + {x} +(2 rows) + +-- test reg-33.29 {} { +-- # This is near the limits of the RE engine +-- regexp [string repeat x*y*z* 480] x +-- } 1 +-- The runtime cost of this seems out of proportion to the value, +-- so for Postgres purposes reduce the repeat to 200x +select * from test_regex(repeat('x*y*z*', 200), 'x', 'N'); + test_regex +--------------------- + {0,REG_UEMPTYMATCH} + {x} +(2 rows) + +-- test reg-33.30 {Bug 1080042} { +-- regexp {(\Y)+} foo +-- } 1 +select * from test_regex('(\Y)+', 'foo', 'LNP'); + test_regex +----------------------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE,REG_UEMPTYMATCH} + {"",""} +(2 rows) + +-- and now, tests not from either Spencer or the Tcl project +-- These cases exercise additional code paths in pushfwd()/push()/combine() +select * from test_regex('a\Y(?=45)', 'a45', 'HLP'); + test_regex +----------------------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX,REG_ULOCALE} + {a} +(2 rows) + +select * from test_regex('a(?=.)c', 'ac', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {ac} +(2 rows) + +select * from test_regex('a(?=.).*(?=3)3*', 'azz33', 'HP'); + test_regex +----------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX} + {azz33} +(2 rows) + +select * from test_regex('a(?=\w)\w*(?=.).*', 'az3%', 'HLP'); + test_regex +----------------------------------------------- + {0,REG_ULOOKAROUND,REG_UNONPOSIX,REG_ULOCALE} + {az3%} +(2 rows) + +-- These exercise the bulk-arc-movement paths in moveins() and moveouts(); +-- you may need to make them longer if you change BULK_ARC_OP_USE_SORT() +select * from test_regex('ABCDEFGHIJKLMNOPQRSTUVWXYZ(?:\w|a|b|c|d|e|f|0|1|2|3|4|5|6|Q)', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ3', 'LP'); + test_regex +------------------------------- + {0,REG_UNONPOSIX,REG_ULOCALE} + {ABCDEFGHIJKLMNOPQRSTUVWXYZ3} +(2 rows) + +select * from test_regex('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(\Y\Y)+', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789Z', 'LP'); + test_regex +------------------------------------------- + {1,REG_UNONPOSIX,REG_ULOCALE} + {ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,""} +(2 rows) + +select * from test_regex('((x|xabcdefghijklmnopqrstuvwxyz0123456789)x*|[^y]z)$', + 'az', ''); + test_regex +-------------- + {2} + {az,az,NULL} +(2 rows) + diff --git a/src/test/modules/test_regex/expected/test_regex_utf8.out b/src/test/modules/test_regex/expected/test_regex_utf8.out new file mode 100644 index 0000000..befd75e --- /dev/null +++ b/src/test/modules/test_regex/expected/test_regex_utf8.out @@ -0,0 +1,206 @@ +/* + * This test must be run in a database with UTF-8 encoding, + * because other encodings don't support all the characters used. + */ +SELECT getdatabaseencoding() <> 'UTF8' + AS skip_test \gset +\if :skip_test +\quit +\endif +set client_encoding = utf8; +set standard_conforming_strings = on; +-- Run the Tcl test cases that require Unicode +-- expectMatch 9.44 EMP* {a[\u00fe-\u0507][\u00ff-\u0300]b} \ +-- "a\u0102\u02ffb" "a\u0102\u02ffb" +select * from test_regex('a[\u00fe-\u0507][\u00ff-\u0300]b', E'a\u0102\u02ffb', 'EMP*'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_UUNPORT} + {aÄ‚Ë¿b} +(2 rows) + +-- expectMatch 13.27 P "a\\U00001234x" "a\u1234x" "a\u1234x" +select * from test_regex('a\U00001234x', E'a\u1234x', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {aሴx} +(2 rows) + +-- expectMatch 13.28 P {a\U00001234x} "a\u1234x" "a\u1234x" +select * from test_regex('a\U00001234x', E'a\u1234x', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {aሴx} +(2 rows) + +-- expectMatch 13.29 P "a\\U0001234x" "a\u1234x" "a\u1234x" +-- Tcl has relaxed their code to allow 1-8 hex digits, but Postgres hasn't +select * from test_regex('a\U0001234x', E'a\u1234x', 'P'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.30 P {a\U0001234x} "a\u1234x" "a\u1234x" +-- Tcl has relaxed their code to allow 1-8 hex digits, but Postgres hasn't +select * from test_regex('a\U0001234x', E'a\u1234x', 'P'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.31 P "a\\U000012345x" "a\u12345x" "a\u12345x" +select * from test_regex('a\U000012345x', E'a\u12345x', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {aሴ5x} +(2 rows) + +-- expectMatch 13.32 P {a\U000012345x} "a\u12345x" "a\u12345x" +select * from test_regex('a\U000012345x', E'a\u12345x', 'P'); + test_regex +------------------- + {0,REG_UNONPOSIX} + {aሴ5x} +(2 rows) + +-- expectMatch 13.33 P "a\\U1000000x" "a\ufffd0x" "a\ufffd0x" +-- Tcl allows this as a standalone character, but Postgres doesn't +select * from test_regex('a\U1000000x', E'a\ufffd0x', 'P'); +ERROR: invalid regular expression: invalid escape \ sequence +-- expectMatch 13.34 P {a\U1000000x} "a\ufffd0x" "a\ufffd0x" +-- Tcl allows this as a standalone character, but Postgres doesn't +select * from test_regex('a\U1000000x', E'a\ufffd0x', 'P'); +ERROR: invalid regular expression: invalid escape \ sequence +-- Additional tests, not derived from Tcl +-- Exercise logic around high character ranges a bit more +select * from test_regex('a + [\u1000-\u1100]* + [\u3000-\u3100]* + [\u1234-\u25ff]+ + [\u2000-\u35ff]* + [\u2600-\u2f00]* + \u1236\u1236x', + E'a\u1234\u1236\u1236x', 'xEMP'); + test_regex +---------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_UUNPORT} + {aሴሶሶx} +(2 rows) + +select * from test_regex('[[:alnum:]]*[[:upper:]]*[\u1000-\u2000]*\u1237', + E'\u1500\u1237', 'ELMP'); + test_regex +---------------------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_UUNPORT,REG_ULOCALE} + {ᔀሷ} +(2 rows) + +select * from test_regex('[[:alnum:]]*[[:upper:]]*[\u1000-\u2000]*\u1237', + E'A\u1239', 'ELMP'); + test_regex +---------------------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_UUNPORT,REG_ULOCALE} +(1 row) + +select * from test_regex('[[:alnum:]]*[[:upper:]]*[\u1000-\u2000]*\u1237', + E'\u1500\u1237', 'iELMP'); + test_regex +---------------------------------------------------- + {0,REG_UBBS,REG_UNONPOSIX,REG_UUNPORT,REG_ULOCALE} + {ᔀሷ} +(2 rows) + +-- systematically test char classes +select * from test_regex('[[:alnum:]]+', E'x*\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {x} +(2 rows) + +select * from test_regex('[[:alpha:]]+', E'x*\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {x} +(2 rows) + +select * from test_regex('[[:ascii:]]+', E'x\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {x} +(2 rows) + +select * from test_regex('[[:blank:]]+', E'x \t\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {" "} +(2 rows) + +select * from test_regex('[[:cntrl:]]+', E'x\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} +(1 row) + +select * from test_regex('[[:digit:]]+', E'x9\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {9} +(2 rows) + +select * from test_regex('[[:graph:]]+', E'x\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {xᔀሷ} +(2 rows) + +select * from test_regex('[[:lower:]]+', E'x\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {x} +(2 rows) + +select * from test_regex('[[:print:]]+', E'x\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {xᔀሷ} +(2 rows) + +select * from test_regex('[[:punct:]]+', E'x.\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {.} +(2 rows) + +select * from test_regex('[[:space:]]+', E'x \t\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {" "} +(2 rows) + +select * from test_regex('[[:upper:]]+', E'xX\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {X} +(2 rows) + +select * from test_regex('[[:xdigit:]]+', E'xa9\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {a9} +(2 rows) + +select * from test_regex('[[:word:]]+', E'x_*\u1500\u1237', 'L'); + test_regex +----------------- + {0,REG_ULOCALE} + {x_} +(2 rows) + diff --git a/src/test/modules/test_regex/expected/test_regex_utf8_1.out b/src/test/modules/test_regex/expected/test_regex_utf8_1.out new file mode 100644 index 0000000..37aead8 --- /dev/null +++ b/src/test/modules/test_regex/expected/test_regex_utf8_1.out @@ -0,0 +1,8 @@ +/* + * This test must be run in a database with UTF-8 encoding, + * because other encodings don't support all the characters used. + */ +SELECT getdatabaseencoding() <> 'UTF8' + AS skip_test \gset +\if :skip_test +\quit diff --git a/src/test/modules/test_regex/sql/test_regex.sql b/src/test/modules/test_regex/sql/test_regex.sql new file mode 100644 index 0000000..478fa2c --- /dev/null +++ b/src/test/modules/test_regex/sql/test_regex.sql @@ -0,0 +1,1785 @@ +-- This file is based on tests/reg.test from the Tcl distribution, +-- which is marked +-- # Copyright (c) 1998, 1999 Henry Spencer. All rights reserved. +-- The full copyright notice can be found in src/backend/regex/COPYRIGHT. +-- Most commented lines below are copied from reg.test. Each +-- test case is followed by an equivalent test using test_regex(). + +create extension test_regex; + +set standard_conforming_strings = on; + +-- # support functions and preliminary misc. +-- # This is sensitive to changes in message wording, but we really have to +-- # test the code->message expansion at least once. +-- ::tcltest::test reg-0.1 "regexp error reporting" { +-- list [catch {regexp (*) ign} msg] $msg +-- } {1 {couldn't compile regular expression pattern: quantifier operand invalid}} +select * from test_regex('(*)', '', ''); + +-- doing 1 "basic sanity checks" + +-- expectMatch 1.1 & abc abc abc +select * from test_regex('abc', 'abc', ''); +select * from test_regex('abc', 'abc', 'b'); +-- expectNomatch 1.2 & abc def +select * from test_regex('abc', 'def', ''); +select * from test_regex('abc', 'def', 'b'); +-- expectMatch 1.3 & abc xyabxabce abc +select * from test_regex('abc', 'xyabxabce', ''); +select * from test_regex('abc', 'xyabxabce', 'b'); + +-- doing 2 "invalid option combinations" + +-- expectError 2.1 qe a INVARG +select * from test_regex('a', '', 'qe'); +-- expectError 2.2 qa a INVARG +select * from test_regex('a', '', 'qa'); +-- expectError 2.3 qx a INVARG +select * from test_regex('a', '', 'qx'); +-- expectError 2.4 qn a INVARG +select * from test_regex('a', '', 'qn'); +-- expectError 2.5 ba a INVARG +select * from test_regex('a', '', 'ba'); + +-- doing 3 "basic syntax" + +-- expectIndices 3.1 &NS "" a {0 -1} +select * from test_regex('', 'a', '0NS'); +select * from test_regex('', 'a', '0NSb'); +-- expectMatch 3.2 NS a| a a +select * from test_regex('a|', 'a', 'NS'); +-- expectMatch 3.3 - a|b a a +select * from test_regex('a|b', 'a', '-'); +-- expectMatch 3.4 - a|b b b +select * from test_regex('a|b', 'b', '-'); +-- expectMatch 3.5 NS a||b b b +select * from test_regex('a||b', 'b', 'NS'); +-- expectMatch 3.6 & ab ab ab +select * from test_regex('ab', 'ab', ''); +select * from test_regex('ab', 'ab', 'b'); + +-- doing 4 "parentheses" + +-- expectMatch 4.1 - (a)e ae ae a +select * from test_regex('(a)e', 'ae', '-'); +-- expectMatch 4.2 oPR (.)\1e abeaae aae {} +select * from test_regex('(.)\1e', 'abeaae', 'oPR'); +-- expectMatch 4.3 b {\(a\)b} ab ab a +select * from test_regex('\(a\)b', 'ab', 'b'); +-- expectMatch 4.4 - a((b)c) abc abc bc b +select * from test_regex('a((b)c)', 'abc', '-'); +-- expectMatch 4.5 - a(b)(c) abc abc b c +select * from test_regex('a(b)(c)', 'abc', '-'); +-- expectError 4.6 - a(b EPAREN +select * from test_regex('a(b', '', '-'); +-- expectError 4.7 b {a\(b} EPAREN +select * from test_regex('a\(b', '', 'b'); +-- # sigh, we blew it on the specs here... someday this will be fixed in POSIX, +-- # but meanwhile, it's fixed in AREs +-- expectMatch 4.8 eU a)b a)b a)b +select * from test_regex('a)b', 'a)b', 'eU'); +-- expectError 4.9 - a)b EPAREN +select * from test_regex('a)b', '', '-'); +-- expectError 4.10 b {a\)b} EPAREN +select * from test_regex('a\)b', '', 'b'); +-- expectMatch 4.11 P a(?:b)c abc abc +select * from test_regex('a(?:b)c', 'abc', 'P'); +-- expectError 4.12 e a(?:b)c BADRPT +select * from test_regex('a(?:b)c', '', 'e'); +-- expectIndices 4.13 S a()b ab {0 1} {1 0} +select * from test_regex('a()b', 'ab', '0S'); +-- expectMatch 4.14 SP a(?:)b ab ab +select * from test_regex('a(?:)b', 'ab', 'SP'); +-- expectIndices 4.15 S a(|b)c ac {0 1} {1 0} +select * from test_regex('a(|b)c', 'ac', '0S'); +-- expectMatch 4.16 S a(b|)c abc abc b +select * from test_regex('a(b|)c', 'abc', 'S'); + +-- doing 5 "simple one-char matching" +-- # general case of brackets done later + +-- expectMatch 5.1 & a.b axb axb +select * from test_regex('a.b', 'axb', ''); +select * from test_regex('a.b', 'axb', 'b'); +-- expectNomatch 5.2 &n "a.b" "a\nb" +select * from test_regex('a.b', E'a\nb', 'n'); +select * from test_regex('a.b', E'a\nb', 'nb'); +-- expectMatch 5.3 & {a[bc]d} abd abd +select * from test_regex('a[bc]d', 'abd', ''); +select * from test_regex('a[bc]d', 'abd', 'b'); +-- expectMatch 5.4 & {a[bc]d} acd acd +select * from test_regex('a[bc]d', 'acd', ''); +select * from test_regex('a[bc]d', 'acd', 'b'); +-- expectNomatch 5.5 & {a[bc]d} aed +select * from test_regex('a[bc]d', 'aed', ''); +select * from test_regex('a[bc]d', 'aed', 'b'); +-- expectNomatch 5.6 & {a[^bc]d} abd +select * from test_regex('a[^bc]d', 'abd', ''); +select * from test_regex('a[^bc]d', 'abd', 'b'); +-- expectMatch 5.7 & {a[^bc]d} aed aed +select * from test_regex('a[^bc]d', 'aed', ''); +select * from test_regex('a[^bc]d', 'aed', 'b'); +-- expectNomatch 5.8 &p "a\[^bc]d" "a\nd" +select * from test_regex('a[^bc]d', E'a\nd', 'p'); +select * from test_regex('a[^bc]d', E'a\nd', 'pb'); + +-- doing 6 "context-dependent syntax" +-- # plus odds and ends + +-- expectError 6.1 - * BADRPT +select * from test_regex('*', '', '-'); +-- expectMatch 6.2 b * * * +select * from test_regex('*', '*', 'b'); +-- expectMatch 6.3 b {\(*\)} * * * +select * from test_regex('\(*\)', '*', 'b'); +-- expectError 6.4 - (*) BADRPT +select * from test_regex('(*)', '', '-'); +-- expectMatch 6.5 b ^* * * +select * from test_regex('^*', '*', 'b'); +-- expectError 6.6 - ^* BADRPT +select * from test_regex('^*', '', '-'); +-- expectNomatch 6.7 & ^b ^b +select * from test_regex('^b', '^b', ''); +select * from test_regex('^b', '^b', 'b'); +-- expectMatch 6.8 b x^ x^ x^ +select * from test_regex('x^', 'x^', 'b'); +-- expectNomatch 6.9 I x^ x +select * from test_regex('x^', 'x', 'I'); +-- expectMatch 6.10 n "\n^" "x\nb" "\n" +select * from test_regex(E'\n^', E'x\nb', 'n'); +-- expectNomatch 6.11 bS {\(^b\)} ^b +select * from test_regex('\(^b\)', '^b', 'bS'); +-- expectMatch 6.12 - (^b) b b b +select * from test_regex('(^b)', 'b', '-'); +-- expectMatch 6.13 & {x$} x x +select * from test_regex('x$', 'x', ''); +select * from test_regex('x$', 'x', 'b'); +-- expectMatch 6.14 bS {\(x$\)} x x x +select * from test_regex('\(x$\)', 'x', 'bS'); +-- expectMatch 6.15 - {(x$)} x x x +select * from test_regex('(x$)', 'x', '-'); +-- expectMatch 6.16 b {x$y} "x\$y" "x\$y" +select * from test_regex('x$y', 'x$y', 'b'); +-- expectNomatch 6.17 I {x$y} xy +select * from test_regex('x$y', 'xy', 'I'); +-- expectMatch 6.18 n "x\$\n" "x\n" "x\n" +select * from test_regex(E'x$\n', E'x\n', 'n'); +-- expectError 6.19 - + BADRPT +select * from test_regex('+', '', '-'); +-- expectError 6.20 - ? BADRPT +select * from test_regex('?', '', '-'); + +-- These two are not yet incorporated in Tcl, cf +-- https://core.tcl-lang.org/tcl/tktview?name=5ea71fdcd3291c38 +-- expectError 6.21 - {x(\w)(?=(\1))} ESUBREG +select * from test_regex('x(\w)(?=(\1))', '', '-'); +-- expectMatch 6.22 HP {x(?=((foo)))} xfoo x +select * from test_regex('x(?=((foo)))', 'xfoo', 'HP'); + +-- doing 7 "simple quantifiers" + +-- expectMatch 7.1 &N a* aa aa +select * from test_regex('a*', 'aa', 'N'); +select * from test_regex('a*', 'aa', 'Nb'); +-- expectIndices 7.2 &N a* b {0 -1} +select * from test_regex('a*', 'b', '0N'); +select * from test_regex('a*', 'b', '0Nb'); +-- expectMatch 7.3 - a+ aa aa +select * from test_regex('a+', 'aa', '-'); +-- expectMatch 7.4 - a?b ab ab +select * from test_regex('a?b', 'ab', '-'); +-- expectMatch 7.5 - a?b b b +select * from test_regex('a?b', 'b', '-'); +-- expectError 7.6 - ** BADRPT +select * from test_regex('**', '', '-'); +-- expectMatch 7.7 bN ** *** *** +select * from test_regex('**', '***', 'bN'); +-- expectError 7.8 & a** BADRPT +select * from test_regex('a**', '', ''); +select * from test_regex('a**', '', 'b'); +-- expectError 7.9 & a**b BADRPT +select * from test_regex('a**b', '', ''); +select * from test_regex('a**b', '', 'b'); +-- expectError 7.10 & *** BADRPT +select * from test_regex('***', '', ''); +select * from test_regex('***', '', 'b'); +-- expectError 7.11 - a++ BADRPT +select * from test_regex('a++', '', '-'); +-- expectError 7.12 - a?+ BADRPT +select * from test_regex('a?+', '', '-'); +-- expectError 7.13 - a?* BADRPT +select * from test_regex('a?*', '', '-'); +-- expectError 7.14 - a+* BADRPT +select * from test_regex('a+*', '', '-'); +-- expectError 7.15 - a*+ BADRPT +select * from test_regex('a*+', '', '-'); +-- tests for ancient brenext() bugs; not currently in Tcl +select * from test_regex('.*b', 'aaabbb', 'b'); +select * from test_regex('.\{1,10\}', 'abcdef', 'bQ'); + +-- doing 8 "braces" + +-- expectMatch 8.1 NQ "a{0,1}" "" "" +select * from test_regex('a{0,1}', '', 'NQ'); +-- expectMatch 8.2 NQ "a{0,1}" ac a +select * from test_regex('a{0,1}', 'ac', 'NQ'); +-- expectError 8.3 - "a{1,0}" BADBR +select * from test_regex('a{1,0}', '', '-'); +-- expectError 8.4 - "a{1,2,3}" BADBR +select * from test_regex('a{1,2,3}', '', '-'); +-- expectError 8.5 - "a{257}" BADBR +select * from test_regex('a{257}', '', '-'); +-- expectError 8.6 - "a{1000}" BADBR +select * from test_regex('a{1000}', '', '-'); +-- expectError 8.7 - "a{1" EBRACE +select * from test_regex('a{1', '', '-'); +-- expectError 8.8 - "a{1n}" BADBR +select * from test_regex('a{1n}', '', '-'); +-- expectMatch 8.9 BS "a{b" "a\{b" "a\{b" +select * from test_regex('a{b', 'a{b', 'BS'); +-- expectMatch 8.10 BS "a{" "a\{" "a\{" +select * from test_regex('a{', 'a{', 'BS'); +-- expectMatch 8.11 bQ "a\\{0,1\\}b" cb b +select * from test_regex('a\{0,1\}b', 'cb', 'bQ'); +-- expectError 8.12 b "a\\{0,1" EBRACE +select * from test_regex('a\{0,1', '', 'b'); +-- expectError 8.13 - "a{0,1\\" BADBR +select * from test_regex('a{0,1\', '', '-'); +-- expectMatch 8.14 Q "a{0}b" ab b +select * from test_regex('a{0}b', 'ab', 'Q'); +-- expectMatch 8.15 Q "a{0,0}b" ab b +select * from test_regex('a{0,0}b', 'ab', 'Q'); +-- expectMatch 8.16 Q "a{0,1}b" ab ab +select * from test_regex('a{0,1}b', 'ab', 'Q'); +-- expectMatch 8.17 Q "a{0,2}b" b b +select * from test_regex('a{0,2}b', 'b', 'Q'); +-- expectMatch 8.18 Q "a{0,2}b" aab aab +select * from test_regex('a{0,2}b', 'aab', 'Q'); +-- expectMatch 8.19 Q "a{0,}b" aab aab +select * from test_regex('a{0,}b', 'aab', 'Q'); +-- expectMatch 8.20 Q "a{1,1}b" aab ab +select * from test_regex('a{1,1}b', 'aab', 'Q'); +-- expectMatch 8.21 Q "a{1,3}b" aaaab aaab +select * from test_regex('a{1,3}b', 'aaaab', 'Q'); +-- expectNomatch 8.22 Q "a{1,3}b" b +select * from test_regex('a{1,3}b', 'b', 'Q'); +-- expectMatch 8.23 Q "a{1,}b" aab aab +select * from test_regex('a{1,}b', 'aab', 'Q'); +-- expectNomatch 8.24 Q "a{2,3}b" ab +select * from test_regex('a{2,3}b', 'ab', 'Q'); +-- expectMatch 8.25 Q "a{2,3}b" aaaab aaab +select * from test_regex('a{2,3}b', 'aaaab', 'Q'); +-- expectNomatch 8.26 Q "a{2,}b" ab +select * from test_regex('a{2,}b', 'ab', 'Q'); +-- expectMatch 8.27 Q "a{2,}b" aaaab aaaab +select * from test_regex('a{2,}b', 'aaaab', 'Q'); + +-- doing 9 "brackets" + +-- expectMatch 9.1 & {a[bc]} ac ac +select * from test_regex('a[bc]', 'ac', ''); +select * from test_regex('a[bc]', 'ac', 'b'); +-- expectMatch 9.2 & {a[-]} a- a- +select * from test_regex('a[-]', 'a-', ''); +select * from test_regex('a[-]', 'a-', 'b'); +-- expectMatch 9.3 & {a[[.-.]]} a- a- +select * from test_regex('a[[.-.]]', 'a-', ''); +select * from test_regex('a[[.-.]]', 'a-', 'b'); +-- expectMatch 9.4 &L {a[[.zero.]]} a0 a0 +select * from test_regex('a[[.zero.]]', 'a0', 'L'); +select * from test_regex('a[[.zero.]]', 'a0', 'Lb'); +-- expectMatch 9.5 &LM {a[[.zero.]-9]} a2 a2 +select * from test_regex('a[[.zero.]-9]', 'a2', 'LM'); +select * from test_regex('a[[.zero.]-9]', 'a2', 'LMb'); +-- expectMatch 9.6 &M {a[0-[.9.]]} a2 a2 +select * from test_regex('a[0-[.9.]]', 'a2', 'M'); +select * from test_regex('a[0-[.9.]]', 'a2', 'Mb'); +-- expectMatch 9.7 &+L {a[[=x=]]} ax ax +select * from test_regex('a[[=x=]]', 'ax', '+L'); +select * from test_regex('a[[=x=]]', 'ax', '+Lb'); +-- expectMatch 9.8 &+L {a[[=x=]]} ay ay +select * from test_regex('a[[=x=]]', 'ay', '+L'); +select * from test_regex('a[[=x=]]', 'ay', '+Lb'); +-- expectNomatch 9.9 &+L {a[[=x=]]} az +select * from test_regex('a[[=x=]]', 'az', '+L'); +select * from test_regex('a[[=x=]]', 'az', '+Lb'); +-- expectMatch 9.9b &iL {a[[=Y=]]} ay ay +select * from test_regex('a[[=Y=]]', 'ay', 'iL'); +select * from test_regex('a[[=Y=]]', 'ay', 'iLb'); +-- expectNomatch 9.9c &L {a[[=Y=]]} ay +select * from test_regex('a[[=Y=]]', 'ay', 'L'); +select * from test_regex('a[[=Y=]]', 'ay', 'Lb'); +-- expectError 9.10 & {a[0-[=x=]]} ERANGE +select * from test_regex('a[0-[=x=]]', '', ''); +select * from test_regex('a[0-[=x=]]', '', 'b'); +-- expectMatch 9.11 &L {a[[:digit:]]} a0 a0 +select * from test_regex('a[[:digit:]]', 'a0', 'L'); +select * from test_regex('a[[:digit:]]', 'a0', 'Lb'); +-- expectError 9.12 & {a[[:woopsie:]]} ECTYPE +select * from test_regex('a[[:woopsie:]]', '', ''); +select * from test_regex('a[[:woopsie:]]', '', 'b'); +-- expectNomatch 9.13 &L {a[[:digit:]]} ab +select * from test_regex('a[[:digit:]]', 'ab', 'L'); +select * from test_regex('a[[:digit:]]', 'ab', 'Lb'); +-- expectError 9.14 & {a[0-[:digit:]]} ERANGE +select * from test_regex('a[0-[:digit:]]', '', ''); +select * from test_regex('a[0-[:digit:]]', '', 'b'); +-- expectMatch 9.15 &LP {[[:<:]]a} a a +select * from test_regex('[[:<:]]a', 'a', 'LP'); +select * from test_regex('[[:<:]]a', 'a', 'LPb'); +-- expectMatch 9.16 &LP {a[[:>:]]} a a +select * from test_regex('a[[:>:]]', 'a', 'LP'); +select * from test_regex('a[[:>:]]', 'a', 'LPb'); +-- expectError 9.17 & {a[[..]]b} ECOLLATE +select * from test_regex('a[[..]]b', '', ''); +select * from test_regex('a[[..]]b', '', 'b'); +-- expectError 9.18 & {a[[==]]b} ECOLLATE +select * from test_regex('a[[==]]b', '', ''); +select * from test_regex('a[[==]]b', '', 'b'); +-- expectError 9.19 & {a[[::]]b} ECTYPE +select * from test_regex('a[[::]]b', '', ''); +select * from test_regex('a[[::]]b', '', 'b'); +-- expectError 9.20 & {a[[.a} EBRACK +select * from test_regex('a[[.a', '', ''); +select * from test_regex('a[[.a', '', 'b'); +-- expectError 9.21 & {a[[=a} EBRACK +select * from test_regex('a[[=a', '', ''); +select * from test_regex('a[[=a', '', 'b'); +-- expectError 9.22 & {a[[:a} EBRACK +select * from test_regex('a[[:a', '', ''); +select * from test_regex('a[[:a', '', 'b'); +-- expectError 9.23 & {a[} EBRACK +select * from test_regex('a[', '', ''); +select * from test_regex('a[', '', 'b'); +-- expectError 9.24 & {a[b} EBRACK +select * from test_regex('a[b', '', ''); +select * from test_regex('a[b', '', 'b'); +-- expectError 9.25 & {a[b-} EBRACK +select * from test_regex('a[b-', '', ''); +select * from test_regex('a[b-', '', 'b'); +-- expectError 9.26 & {a[b-c} EBRACK +select * from test_regex('a[b-c', '', ''); +select * from test_regex('a[b-c', '', 'b'); +-- expectMatch 9.27 &M {a[b-c]} ab ab +select * from test_regex('a[b-c]', 'ab', 'M'); +select * from test_regex('a[b-c]', 'ab', 'Mb'); +-- expectMatch 9.28 & {a[b-b]} ab ab +select * from test_regex('a[b-b]', 'ab', ''); +select * from test_regex('a[b-b]', 'ab', 'b'); +-- expectMatch 9.29 &M {a[1-2]} a2 a2 +select * from test_regex('a[1-2]', 'a2', 'M'); +select * from test_regex('a[1-2]', 'a2', 'Mb'); +-- expectError 9.30 & {a[c-b]} ERANGE +select * from test_regex('a[c-b]', '', ''); +select * from test_regex('a[c-b]', '', 'b'); +-- expectError 9.31 & {a[a-b-c]} ERANGE +select * from test_regex('a[a-b-c]', '', ''); +select * from test_regex('a[a-b-c]', '', 'b'); +-- expectMatch 9.32 &M {a[--?]b} a?b a?b +select * from test_regex('a[--?]b', 'a?b', 'M'); +select * from test_regex('a[--?]b', 'a?b', 'Mb'); +-- expectMatch 9.33 & {a[---]b} a-b a-b +select * from test_regex('a[---]b', 'a-b', ''); +select * from test_regex('a[---]b', 'a-b', 'b'); +-- expectMatch 9.34 & {a[]b]c} a]c a]c +select * from test_regex('a[]b]c', 'a]c', ''); +select * from test_regex('a[]b]c', 'a]c', 'b'); +-- expectMatch 9.35 EP {a[\]]b} a]b a]b +select * from test_regex('a[\]]b', 'a]b', 'EP'); +-- expectNomatch 9.36 bE {a[\]]b} a]b +select * from test_regex('a[\]]b', 'a]b', 'bE'); +-- expectMatch 9.37 bE {a[\]]b} "a\\]b" "a\\]b" +select * from test_regex('a[\]]b', 'a\]b', 'bE'); +-- expectMatch 9.38 eE {a[\]]b} "a\\]b" "a\\]b" +select * from test_regex('a[\]]b', 'a\]b', 'eE'); +-- expectMatch 9.39 EP {a[\\]b} "a\\b" "a\\b" +select * from test_regex('a[\\]b', 'a\b', 'EP'); +-- expectMatch 9.40 eE {a[\\]b} "a\\b" "a\\b" +select * from test_regex('a[\\]b', 'a\b', 'eE'); +-- expectMatch 9.41 bE {a[\\]b} "a\\b" "a\\b" +select * from test_regex('a[\\]b', 'a\b', 'bE'); +-- expectError 9.42 - {a[\Z]b} EESCAPE +select * from test_regex('a[\Z]b', '', '-'); +-- expectMatch 9.43 & {a[[b]c} "a\[c" "a\[c" +select * from test_regex('a[[b]c', 'a[c', ''); +select * from test_regex('a[[b]c', 'a[c', 'b'); +-- This only works in UTF8 encoding, so it's moved to test_regex_utf8.sql: +-- expectMatch 9.44 EMP* {a[\u00fe-\u0507][\u00ff-\u0300]b} \ +-- "a\u0102\u02ffb" "a\u0102\u02ffb" + +-- doing 10 "anchors and newlines" + +-- expectMatch 10.1 & ^a a a +select * from test_regex('^a', 'a', ''); +select * from test_regex('^a', 'a', 'b'); +-- expectNomatch 10.2 &^ ^a a +select * from test_regex('^a', 'a', '^'); +select * from test_regex('^a', 'a', '^b'); +-- expectIndices 10.3 &N ^ a {0 -1} +select * from test_regex('^', 'a', '0N'); +select * from test_regex('^', 'a', '0Nb'); +-- expectIndices 10.4 & {a$} aba {2 2} +select * from test_regex('a$', 'aba', '0'); +select * from test_regex('a$', 'aba', '0b'); +-- expectNomatch 10.5 {&$} {a$} a +select * from test_regex('a$', 'a', '$'); +select * from test_regex('a$', 'a', '$b'); +-- expectIndices 10.6 &N {$} ab {2 1} +select * from test_regex('$', 'ab', '0N'); +select * from test_regex('$', 'ab', '0Nb'); +-- expectMatch 10.7 &n ^a a a +select * from test_regex('^a', 'a', 'n'); +select * from test_regex('^a', 'a', 'nb'); +-- expectMatch 10.8 &n "^a" "b\na" "a" +select * from test_regex('^a', E'b\na', 'n'); +select * from test_regex('^a', E'b\na', 'nb'); +-- expectIndices 10.9 &w "^a" "a\na" {0 0} +select * from test_regex('^a', E'a\na', '0w'); +select * from test_regex('^a', E'a\na', '0wb'); +-- expectIndices 10.10 &n^ "^a" "a\na" {2 2} +select * from test_regex('^a', E'a\na', '0n^'); +select * from test_regex('^a', E'a\na', '0n^b'); +-- expectMatch 10.11 &n {a$} a a +select * from test_regex('a$', 'a', 'n'); +select * from test_regex('a$', 'a', 'nb'); +-- expectMatch 10.12 &n "a\$" "a\nb" "a" +select * from test_regex('a$', E'a\nb', 'n'); +select * from test_regex('a$', E'a\nb', 'nb'); +-- expectIndices 10.13 &n "a\$" "a\na" {0 0} +select * from test_regex('a$', E'a\na', '0n'); +select * from test_regex('a$', E'a\na', '0nb'); +-- expectIndices 10.14 N ^^ a {0 -1} +select * from test_regex('^^', 'a', '0N'); +-- expectMatch 10.15 b ^^ ^ ^ +select * from test_regex('^^', '^', 'b'); +-- expectIndices 10.16 N {$$} a {1 0} +select * from test_regex('$$', 'a', '0N'); +-- expectMatch 10.17 b {$$} "\$" "\$" +select * from test_regex('$$', '$', 'b'); +-- expectMatch 10.18 &N {^$} "" "" +select * from test_regex('^$', '', 'N'); +select * from test_regex('^$', '', 'Nb'); +-- expectNomatch 10.19 &N {^$} a +select * from test_regex('^$', 'a', 'N'); +select * from test_regex('^$', 'a', 'Nb'); +-- expectIndices 10.20 &nN "^\$" a\n\nb {2 1} +select * from test_regex('^$', E'a\n\nb', '0nN'); +select * from test_regex('^$', E'a\n\nb', '0nNb'); +-- expectMatch 10.21 N {$^} "" "" +select * from test_regex('$^', '', 'N'); +-- expectMatch 10.22 b {$^} "\$^" "\$^" +select * from test_regex('$^', '$^', 'b'); +-- expectMatch 10.23 P {\Aa} a a +select * from test_regex('\Aa', 'a', 'P'); +-- expectMatch 10.24 ^P {\Aa} a a +select * from test_regex('\Aa', 'a', '^P'); +-- expectNomatch 10.25 ^nP {\Aa} "b\na" +select * from test_regex('\Aa', E'b\na', '^nP'); +-- expectMatch 10.26 P {a\Z} a a +select * from test_regex('a\Z', 'a', 'P'); +-- expectMatch 10.27 \$P {a\Z} a a +select * from test_regex('a\Z', 'a', '$P'); +-- expectNomatch 10.28 \$nP {a\Z} "a\nb" +select * from test_regex('a\Z', E'a\nb', '$nP'); +-- expectError 10.29 - ^* BADRPT +select * from test_regex('^*', '', '-'); +-- expectError 10.30 - {$*} BADRPT +select * from test_regex('$*', '', '-'); +-- expectError 10.31 - {\A*} BADRPT +select * from test_regex('\A*', '', '-'); +-- expectError 10.32 - {\Z*} BADRPT +select * from test_regex('\Z*', '', '-'); + +-- doing 11 "boundary constraints" + +-- expectMatch 11.1 &LP {[[:<:]]a} a a +select * from test_regex('[[:<:]]a', 'a', 'LP'); +select * from test_regex('[[:<:]]a', 'a', 'LPb'); +-- expectMatch 11.2 &LP {[[:<:]]a} -a a +select * from test_regex('[[:<:]]a', '-a', 'LP'); +select * from test_regex('[[:<:]]a', '-a', 'LPb'); +-- expectNomatch 11.3 &LP {[[:<:]]a} ba +select * from test_regex('[[:<:]]a', 'ba', 'LP'); +select * from test_regex('[[:<:]]a', 'ba', 'LPb'); +-- expectMatch 11.4 &LP {a[[:>:]]} a a +select * from test_regex('a[[:>:]]', 'a', 'LP'); +select * from test_regex('a[[:>:]]', 'a', 'LPb'); +-- expectMatch 11.5 &LP {a[[:>:]]} a- a +select * from test_regex('a[[:>:]]', 'a-', 'LP'); +select * from test_regex('a[[:>:]]', 'a-', 'LPb'); +-- expectNomatch 11.6 &LP {a[[:>:]]} ab +select * from test_regex('a[[:>:]]', 'ab', 'LP'); +select * from test_regex('a[[:>:]]', 'ab', 'LPb'); +-- expectMatch 11.7 bLP {\<a} a a +select * from test_regex('\<a', 'a', 'bLP'); +-- expectNomatch 11.8 bLP {\<a} ba +select * from test_regex('\<a', 'ba', 'bLP'); +-- expectMatch 11.9 bLP {a\>} a a +select * from test_regex('a\>', 'a', 'bLP'); +-- expectNomatch 11.10 bLP {a\>} ab +select * from test_regex('a\>', 'ab', 'bLP'); +-- expectMatch 11.11 LP {\ya} a a +select * from test_regex('\ya', 'a', 'LP'); +-- expectNomatch 11.12 LP {\ya} ba +select * from test_regex('\ya', 'ba', 'LP'); +-- expectMatch 11.13 LP {a\y} a a +select * from test_regex('a\y', 'a', 'LP'); +-- expectNomatch 11.14 LP {a\y} ab +select * from test_regex('a\y', 'ab', 'LP'); +-- expectMatch 11.15 LP {a\Y} ab a +select * from test_regex('a\Y', 'ab', 'LP'); +-- expectNomatch 11.16 LP {a\Y} a- +select * from test_regex('a\Y', 'a-', 'LP'); +-- expectNomatch 11.17 LP {a\Y} a +select * from test_regex('a\Y', 'a', 'LP'); +-- expectNomatch 11.18 LP {-\Y} -a +select * from test_regex('-\Y', '-a', 'LP'); +-- expectMatch 11.19 LP {-\Y} -% - +select * from test_regex('-\Y', '-%', 'LP'); +-- expectNomatch 11.20 LP {\Y-} a- +select * from test_regex('\Y-', 'a-', 'LP'); +-- expectError 11.21 - {[[:<:]]*} BADRPT +select * from test_regex('[[:<:]]*', '', '-'); +-- expectError 11.22 - {[[:>:]]*} BADRPT +select * from test_regex('[[:>:]]*', '', '-'); +-- expectError 11.23 b {\<*} BADRPT +select * from test_regex('\<*', '', 'b'); +-- expectError 11.24 b {\>*} BADRPT +select * from test_regex('\>*', '', 'b'); +-- expectError 11.25 - {\y*} BADRPT +select * from test_regex('\y*', '', '-'); +-- expectError 11.26 - {\Y*} BADRPT +select * from test_regex('\Y*', '', '-'); +-- expectMatch 11.27 LP {\ma} a a +select * from test_regex('\ma', 'a', 'LP'); +-- expectNomatch 11.28 LP {\ma} ba +select * from test_regex('\ma', 'ba', 'LP'); +-- expectMatch 11.29 LP {a\M} a a +select * from test_regex('a\M', 'a', 'LP'); +-- expectNomatch 11.30 LP {a\M} ab +select * from test_regex('a\M', 'ab', 'LP'); +-- expectNomatch 11.31 ILP {\Ma} a +select * from test_regex('\Ma', 'a', 'ILP'); +-- expectNomatch 11.32 ILP {a\m} a +select * from test_regex('a\m', 'a', 'ILP'); + +-- doing 12 "character classes" + +-- expectMatch 12.1 LP {a\db} a0b a0b +select * from test_regex('a\db', 'a0b', 'LP'); +-- expectNomatch 12.2 LP {a\db} axb +select * from test_regex('a\db', 'axb', 'LP'); +-- expectNomatch 12.3 LP {a\Db} a0b +select * from test_regex('a\Db', 'a0b', 'LP'); +-- expectMatch 12.4 LP {a\Db} axb axb +select * from test_regex('a\Db', 'axb', 'LP'); +-- expectMatch 12.5 LP "a\\sb" "a b" "a b" +select * from test_regex('a\sb', 'a b', 'LP'); +-- expectMatch 12.6 LP "a\\sb" "a\tb" "a\tb" +select * from test_regex('a\sb', E'a\tb', 'LP'); +-- expectMatch 12.7 LP "a\\sb" "a\nb" "a\nb" +select * from test_regex('a\sb', E'a\nb', 'LP'); +-- expectNomatch 12.8 LP {a\sb} axb +select * from test_regex('a\sb', 'axb', 'LP'); +-- expectMatch 12.9 LP {a\Sb} axb axb +select * from test_regex('a\Sb', 'axb', 'LP'); +-- expectNomatch 12.10 LP "a\\Sb" "a b" +select * from test_regex('a\Sb', 'a b', 'LP'); +-- expectMatch 12.11 LP {a\wb} axb axb +select * from test_regex('a\wb', 'axb', 'LP'); +-- expectNomatch 12.12 LP {a\wb} a-b +select * from test_regex('a\wb', 'a-b', 'LP'); +-- expectNomatch 12.13 LP {a\Wb} axb +select * from test_regex('a\Wb', 'axb', 'LP'); +-- expectMatch 12.14 LP {a\Wb} a-b a-b +select * from test_regex('a\Wb', 'a-b', 'LP'); +-- expectMatch 12.15 LP {\y\w+z\y} adze-guz guz +select * from test_regex('\y\w+z\y', 'adze-guz', 'LP'); +-- expectMatch 12.16 LPE {a[\d]b} a1b a1b +select * from test_regex('a[\d]b', 'a1b', 'LPE'); +-- expectMatch 12.17 LPE "a\[\\s]b" "a b" "a b" +select * from test_regex('a[\s]b', 'a b', 'LPE'); +-- expectMatch 12.18 LPE {a[\w]b} axb axb +select * from test_regex('a[\w]b', 'axb', 'LPE'); + +-- these should be invalid +select * from test_regex('[\w-~]*', 'ab01_~-`**', 'LNPSE'); +select * from test_regex('[~-\w]*', 'ab01_~-`**', 'LNPSE'); +select * from test_regex('[[:alnum:]-~]*', 'ab01~-`**', 'LNS'); +select * from test_regex('[~-[:alnum:]]*', 'ab01~-`**', 'LNS'); + +-- test complemented char classes within brackets +select * from test_regex('[\D]', '0123456789abc*', 'LPE'); +select * from test_regex('[^\D]', 'abc0123456789*', 'LPE'); +select * from test_regex('[1\D7]', '0123456789abc*', 'LPE'); +select * from test_regex('[7\D1]', '0123456789abc*', 'LPE'); +select * from test_regex('[^0\D1]', 'abc0123456789*', 'LPE'); +select * from test_regex('[^1\D0]', 'abc0123456789*', 'LPE'); +select * from test_regex('\W', '0123456789abc_*', 'LP'); +select * from test_regex('[\W]', '0123456789abc_*', 'LPE'); +select * from test_regex('[\s\S]*', '012 3456789abc_*', 'LNPE'); + +-- check char classes' handling of newlines +select * from test_regex('\s+', E'abc \n def', 'LP'); +select * from test_regex('\s+', E'abc \n def', 'nLP'); +select * from test_regex('[\s]+', E'abc \n def', 'LPE'); +select * from test_regex('[\s]+', E'abc \n def', 'nLPE'); +select * from test_regex('\S+', E'abc\ndef', 'LP'); +select * from test_regex('\S+', E'abc\ndef', 'nLP'); +select * from test_regex('[\S]+', E'abc\ndef', 'LPE'); +select * from test_regex('[\S]+', E'abc\ndef', 'nLPE'); +select * from test_regex('\d+', E'012\n345', 'LP'); +select * from test_regex('\d+', E'012\n345', 'nLP'); +select * from test_regex('[\d]+', E'012\n345', 'LPE'); +select * from test_regex('[\d]+', E'012\n345', 'nLPE'); +select * from test_regex('\D+', E'abc\ndef345', 'LP'); +select * from test_regex('\D+', E'abc\ndef345', 'nLP'); +select * from test_regex('[\D]+', E'abc\ndef345', 'LPE'); +select * from test_regex('[\D]+', E'abc\ndef345', 'nLPE'); +select * from test_regex('\w+', E'abc_012\ndef', 'LP'); +select * from test_regex('\w+', E'abc_012\ndef', 'nLP'); +select * from test_regex('[\w]+', E'abc_012\ndef', 'LPE'); +select * from test_regex('[\w]+', E'abc_012\ndef', 'nLPE'); +select * from test_regex('\W+', E'***\n@@@___', 'LP'); +select * from test_regex('\W+', E'***\n@@@___', 'nLP'); +select * from test_regex('[\W]+', E'***\n@@@___', 'LPE'); +select * from test_regex('[\W]+', E'***\n@@@___', 'nLPE'); + + +-- doing 13 "escapes" + +-- expectError 13.1 & "a\\" EESCAPE +select * from test_regex('a\', '', ''); +select * from test_regex('a\', '', 'b'); +-- expectMatch 13.2 - {a\<b} a<b a<b +select * from test_regex('a\<b', 'a<b', '-'); +-- expectMatch 13.3 e {a\<b} a<b a<b +select * from test_regex('a\<b', 'a<b', 'e'); +-- expectMatch 13.4 bAS {a\wb} awb awb +select * from test_regex('a\wb', 'awb', 'bAS'); +-- expectMatch 13.5 eAS {a\wb} awb awb +select * from test_regex('a\wb', 'awb', 'eAS'); +-- expectMatch 13.6 PL "a\\ab" "a\007b" "a\007b" +select * from test_regex('a\ab', E'a\007b', 'PL'); +-- expectMatch 13.7 P "a\\bb" "a\bb" "a\bb" +select * from test_regex('a\bb', E'a\bb', 'P'); +-- expectMatch 13.8 P {a\Bb} "a\\b" "a\\b" +select * from test_regex('a\Bb', 'a\b', 'P'); +-- expectMatch 13.9 MP "a\\chb" "a\bb" "a\bb" +select * from test_regex('a\chb', E'a\bb', 'MP'); +-- expectMatch 13.10 MP "a\\cHb" "a\bb" "a\bb" +select * from test_regex('a\cHb', E'a\bb', 'MP'); +-- expectMatch 13.11 LMP "a\\e" "a\033" "a\033" +select * from test_regex('a\e', E'a\033', 'LMP'); +-- expectMatch 13.12 P "a\\fb" "a\fb" "a\fb" +select * from test_regex('a\fb', E'a\fb', 'P'); +-- expectMatch 13.13 P "a\\nb" "a\nb" "a\nb" +select * from test_regex('a\nb', E'a\nb', 'P'); +-- expectMatch 13.14 P "a\\rb" "a\rb" "a\rb" +select * from test_regex('a\rb', E'a\rb', 'P'); +-- expectMatch 13.15 P "a\\tb" "a\tb" "a\tb" +select * from test_regex('a\tb', E'a\tb', 'P'); +-- expectMatch 13.16 P "a\\u0008x" "a\bx" "a\bx" +select * from test_regex('a\u0008x', E'a\bx', 'P'); +-- expectMatch 13.17 P {a\u008x} "a\bx" "a\bx" +-- Tcl has relaxed their code to allow 1-4 hex digits, but Postgres hasn't +select * from test_regex('a\u008x', E'a\bx', 'P'); +-- expectMatch 13.18 P "a\\u00088x" "a\b8x" "a\b8x" +select * from test_regex('a\u00088x', E'a\b8x', 'P'); +-- expectMatch 13.19 P "a\\U00000008x" "a\bx" "a\bx" +select * from test_regex('a\U00000008x', E'a\bx', 'P'); +-- expectMatch 13.20 P {a\U0000008x} "a\bx" "a\bx" +-- Tcl has relaxed their code to allow 1-8 hex digits, but Postgres hasn't +select * from test_regex('a\U0000008x', E'a\bx', 'P'); +-- expectMatch 13.21 P "a\\vb" "a\vb" "a\vb" +select * from test_regex('a\vb', E'a\013b', 'P'); +-- expectMatch 13.22 MP "a\\x08x" "a\bx" "a\bx" +select * from test_regex('a\x08x', E'a\bx', 'MP'); +-- expectError 13.23 - {a\xq} EESCAPE +select * from test_regex('a\xq', '', '-'); +-- expectMatch 13.24 MP "a\\x08x" "a\bx" "a\bx" +select * from test_regex('a\x08x', E'a\bx', 'MP'); +-- expectError 13.25 - {a\z} EESCAPE +select * from test_regex('a\z', '', '-'); +-- expectMatch 13.26 MP "a\\010b" "a\bb" "a\bb" +select * from test_regex('a\010b', E'a\bb', 'MP'); +-- These only work in UTF8 encoding, so they're moved to test_regex_utf8.sql: +-- expectMatch 13.27 P "a\\U00001234x" "a\u1234x" "a\u1234x" +-- expectMatch 13.28 P {a\U00001234x} "a\u1234x" "a\u1234x" +-- expectMatch 13.29 P "a\\U0001234x" "a\u1234x" "a\u1234x" +-- expectMatch 13.30 P {a\U0001234x} "a\u1234x" "a\u1234x" +-- expectMatch 13.31 P "a\\U000012345x" "a\u12345x" "a\u12345x" +-- expectMatch 13.32 P {a\U000012345x} "a\u12345x" "a\u12345x" +-- expectMatch 13.33 P "a\\U1000000x" "a\ufffd0x" "a\ufffd0x" +-- expectMatch 13.34 P {a\U1000000x} "a\ufffd0x" "a\ufffd0x" + +-- doing 14 "back references" +-- # ugh + +-- expectMatch 14.1 RP {a(b*)c\1} abbcbb abbcbb bb +select * from test_regex('a(b*)c\1', 'abbcbb', 'RP'); +-- expectMatch 14.2 RP {a(b*)c\1} ac ac "" +select * from test_regex('a(b*)c\1', 'ac', 'RP'); +-- expectNomatch 14.3 RP {a(b*)c\1} abbcb +select * from test_regex('a(b*)c\1', 'abbcb', 'RP'); +-- expectMatch 14.4 RP {a(b*)\1} abbcbb abb b +select * from test_regex('a(b*)\1', 'abbcbb', 'RP'); +-- expectMatch 14.5 RP {a(b|bb)\1} abbcbb abb b +select * from test_regex('a(b|bb)\1', 'abbcbb', 'RP'); +-- expectMatch 14.6 RP {a([bc])\1} abb abb b +select * from test_regex('a([bc])\1', 'abb', 'RP'); +-- expectNomatch 14.7 RP {a([bc])\1} abc +select * from test_regex('a([bc])\1', 'abc', 'RP'); +-- expectMatch 14.8 RP {a([bc])\1} abcabb abb b +select * from test_regex('a([bc])\1', 'abcabb', 'RP'); +-- expectNomatch 14.9 RP {a([bc])*\1} abc +select * from test_regex('a([bc])*\1', 'abc', 'RP'); +-- expectNomatch 14.10 RP {a([bc])\1} abB +select * from test_regex('a([bc])\1', 'abB', 'RP'); +-- expectMatch 14.11 iRP {a([bc])\1} abB abB b +select * from test_regex('a([bc])\1', 'abB', 'iRP'); +-- expectMatch 14.12 RP {a([bc])\1+} abbb abbb b +select * from test_regex('a([bc])\1+', 'abbb', 'RP'); +-- expectMatch 14.13 QRP "a(\[bc])\\1{3,4}" abbbb abbbb b +select * from test_regex('a([bc])\1{3,4}', 'abbbb', 'QRP'); +-- expectNomatch 14.14 QRP "a(\[bc])\\1{3,4}" abbb +select * from test_regex('a([bc])\1{3,4}', 'abbb', 'QRP'); +-- expectMatch 14.15 RP {a([bc])\1*} abbb abbb b +select * from test_regex('a([bc])\1*', 'abbb', 'RP'); +-- expectMatch 14.16 RP {a([bc])\1*} ab ab b +select * from test_regex('a([bc])\1*', 'ab', 'RP'); +-- expectMatch 14.17 RP {a([bc])(\1*)} ab ab b "" +select * from test_regex('a([bc])(\1*)', 'ab', 'RP'); +-- expectError 14.18 - {a((b)\1)} ESUBREG +select * from test_regex('a((b)\1)', '', '-'); +-- expectError 14.19 - {a(b)c\2} ESUBREG +select * from test_regex('a(b)c\2', '', '-'); +-- expectMatch 14.20 bR {a\(b*\)c\1} abbcbb abbcbb bb +select * from test_regex('a\(b*\)c\1', 'abbcbb', 'bR'); +-- expectMatch 14.21 RP {^([bc])\1*$} bbb bbb b +select * from test_regex('^([bc])\1*$', 'bbb', 'RP'); +-- expectMatch 14.22 RP {^([bc])\1*$} ccc ccc c +select * from test_regex('^([bc])\1*$', 'ccc', 'RP'); +-- expectNomatch 14.23 RP {^([bc])\1*$} bcb +select * from test_regex('^([bc])\1*$', 'bcb', 'RP'); +-- expectMatch 14.24 LRP {^(\w+)( \1)+$} {abc abc abc} {abc abc abc} abc { abc} +select * from test_regex('^(\w+)( \1)+$', 'abc abc abc', 'LRP'); +-- expectNomatch 14.25 LRP {^(\w+)( \1)+$} {abc abd abc} +select * from test_regex('^(\w+)( \1)+$', 'abc abd abc', 'LRP'); +-- expectNomatch 14.26 LRP {^(\w+)( \1)+$} {abc abc abd} +select * from test_regex('^(\w+)( \1)+$', 'abc abc abd', 'LRP'); +-- expectMatch 14.27 RP {^(.+)( \1)+$} {abc abc abc} {abc abc abc} abc { abc} +select * from test_regex('^(.+)( \1)+$', 'abc abc abc', 'RP'); +-- expectNomatch 14.28 RP {^(.+)( \1)+$} {abc abd abc} +select * from test_regex('^(.+)( \1)+$', 'abc abd abc', 'RP'); +-- expectNomatch 14.29 RP {^(.+)( \1)+$} {abc abc abd} +select * from test_regex('^(.+)( \1)+$', 'abc abc abd', 'RP'); +-- expectNomatch 14.30 RP {^(.)\1|\1.} {abcdef} +select * from test_regex('^(.)\1|\1.', 'abcdef', 'RP'); +-- expectNomatch 14.31 RP {^((.)\2|..)\2} {abadef} +select * from test_regex('^((.)\2|..)\2', 'abadef', 'RP'); + +-- back reference only matches the string, not any constraints +select * from test_regex('(^\w+).*\1', 'abc abc abc', 'LRP'); +select * from test_regex('(^\w+\M).*\1', 'abc abcd abd', 'LRP'); +select * from test_regex('(\w+(?= )).*\1', 'abc abcd abd', 'HLRP'); + +-- exercise oversize-regmatch_t-array paths in regexec() +-- (that case is not reachable via test_regex, sadly) +select substring('fffoooooooooooooooooooooooooooooooo', '^(.)\1(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'); +select regexp_split_to_array('abcxxxdefyyyghi', '((.))(\1\2)'); + +-- doing 15 "octal escapes vs back references" + +-- # initial zero is always octal +-- expectMatch 15.1 MP "a\\010b" "a\bb" "a\bb" +select * from test_regex('a\010b', E'a\bb', 'MP'); +-- expectMatch 15.2 MP "a\\0070b" "a\0070b" "a\0070b" +select * from test_regex('a\0070b', E'a\0070b', 'MP'); +-- expectMatch 15.3 MP "a\\07b" "a\007b" "a\007b" +select * from test_regex('a\07b', E'a\007b', 'MP'); +-- expectMatch 15.4 MP "a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\\07c" \ +-- "abbbbbbbbbb\007c" abbbbbbbbbb\007c b b b b b b b b b b +select * from test_regex('a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\07c', E'abbbbbbbbbb\007c', 'MP'); +-- # a single digit is always a backref +-- expectError 15.5 - {a\7b} ESUBREG +select * from test_regex('a\7b', '', '-'); +-- # otherwise it's a backref only if within range (barf!) +-- expectMatch 15.6 MP "a\\10b" "a\bb" "a\bb" +select * from test_regex('a\10b', E'a\bb', 'MP'); +-- expectMatch 15.7 MP {a\101b} aAb aAb +select * from test_regex('a\101b', 'aAb', 'MP'); +-- expectMatch 15.8 RP {a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\10c} \ +-- "abbbbbbbbbbbc" abbbbbbbbbbbc b b b b b b b b b b +select * from test_regex('a(b)(b)(b)(b)(b)(b)(b)(b)(b)(b)\10c', 'abbbbbbbbbbbc', 'RP'); +-- # but we're fussy about border cases -- guys who want octal should use the zero +-- expectError 15.9 - {a((((((((((b\10))))))))))c} ESUBREG +select * from test_regex('a((((((((((b\10))))))))))c', '', '-'); +-- # BREs don't have octal, EREs don't have backrefs +-- expectMatch 15.10 MP "a\\12b" "a\nb" "a\nb" +select * from test_regex('a\12b', E'a\nb', 'MP'); +-- expectError 15.11 b {a\12b} ESUBREG +select * from test_regex('a\12b', '', 'b'); +-- expectMatch 15.12 eAS {a\12b} a12b a12b +select * from test_regex('a\12b', 'a12b', 'eAS'); +-- expectMatch 15.13 MP {a\701b} a\u00381b a\u00381b +select * from test_regex('a\701b', 'a81b', 'MP'); + +-- doing 16 "expanded syntax" + +-- expectMatch 16.1 xP "a b c" "abc" "abc" +select * from test_regex('a b c', 'abc', 'xP'); +-- expectMatch 16.2 xP "a b #oops\nc\td" "abcd" "abcd" +select * from test_regex(E'a b #oops\nc\td', 'abcd', 'xP'); +-- expectMatch 16.3 x "a\\ b\\\tc" "a b\tc" "a b\tc" +select * from test_regex(E'a\\ b\\\tc', E'a b\tc', 'x'); +-- expectMatch 16.4 xP "a b\\#c" "ab#c" "ab#c" +select * from test_regex('a b\#c', 'ab#c', 'xP'); +-- expectMatch 16.5 xP "a b\[c d]e" "ab e" "ab e" +select * from test_regex('a b[c d]e', 'ab e', 'xP'); +-- expectMatch 16.6 xP "a b\[c#d]e" "ab#e" "ab#e" +select * from test_regex('a b[c#d]e', 'ab#e', 'xP'); +-- expectMatch 16.7 xP "a b\[c#d]e" "abde" "abde" +select * from test_regex('a b[c#d]e', 'abde', 'xP'); +-- expectMatch 16.8 xSPB "ab{ d" "ab\{d" "ab\{d" +select * from test_regex('ab{ d', 'ab{d', 'xSPB'); +-- expectMatch 16.9 xPQ "ab{ 1 , 2 }c" "abc" "abc" +select * from test_regex('ab{ 1 , 2 }c', 'abc', 'xPQ'); + +-- doing 17 "misc syntax" + +-- expectMatch 17.1 P a(?#comment)b ab ab +select * from test_regex('a(?#comment)b', 'ab', 'P'); + +-- doing 18 "unmatchable REs" + +-- expectNomatch 18.1 I a^b ab +select * from test_regex('a^b', 'ab', 'I'); + +-- doing 19 "case independence" + +-- expectMatch 19.1 &i ab Ab Ab +select * from test_regex('ab', 'Ab', 'i'); +select * from test_regex('ab', 'Ab', 'ib'); +-- expectMatch 19.2 &i {a[bc]} aC aC +select * from test_regex('a[bc]', 'aC', 'i'); +select * from test_regex('a[bc]', 'aC', 'ib'); +-- expectNomatch 19.3 &i {a[^bc]} aB +select * from test_regex('a[^bc]', 'aB', 'i'); +select * from test_regex('a[^bc]', 'aB', 'ib'); +-- expectMatch 19.4 &iM {a[b-d]} aC aC +select * from test_regex('a[b-d]', 'aC', 'iM'); +select * from test_regex('a[b-d]', 'aC', 'iMb'); +-- expectNomatch 19.5 &iM {a[^b-d]} aC +select * from test_regex('a[^b-d]', 'aC', 'iM'); +select * from test_regex('a[^b-d]', 'aC', 'iMb'); +-- expectMatch 19.6 &iM {a[B-Z]} aC aC +select * from test_regex('a[B-Z]', 'aC', 'iM'); +select * from test_regex('a[B-Z]', 'aC', 'iMb'); +-- expectNomatch 19.7 &iM {a[^B-Z]} aC +select * from test_regex('a[^B-Z]', 'aC', 'iM'); +select * from test_regex('a[^B-Z]', 'aC', 'iMb'); + +-- doing 20 "directors and embedded options" + +-- expectError 20.1 & ***? BADPAT +select * from test_regex('***?', '', ''); +select * from test_regex('***?', '', 'b'); +-- expectMatch 20.2 q ***? ***? ***? +select * from test_regex('***?', '***?', 'q'); +-- expectMatch 20.3 &P ***=a*b a*b a*b +select * from test_regex('***=a*b', 'a*b', 'P'); +select * from test_regex('***=a*b', 'a*b', 'Pb'); +-- expectMatch 20.4 q ***=a*b ***=a*b ***=a*b +select * from test_regex('***=a*b', '***=a*b', 'q'); +-- expectMatch 20.5 bLP {***:\w+} ab ab +select * from test_regex('***:\w+', 'ab', 'bLP'); +-- expectMatch 20.6 eLP {***:\w+} ab ab +select * from test_regex('***:\w+', 'ab', 'eLP'); +-- expectError 20.7 & ***:***=a*b BADRPT +select * from test_regex('***:***=a*b', '', ''); +select * from test_regex('***:***=a*b', '', 'b'); +-- expectMatch 20.8 &P ***:(?b)a+b a+b a+b +select * from test_regex('***:(?b)a+b', 'a+b', 'P'); +select * from test_regex('***:(?b)a+b', 'a+b', 'Pb'); +-- expectMatch 20.9 P (?b)a+b a+b a+b +select * from test_regex('(?b)a+b', 'a+b', 'P'); +-- expectError 20.10 e {(?b)\w+} BADRPT +select * from test_regex('(?b)\w+', '', 'e'); +-- expectMatch 20.11 bAS {(?b)\w+} (?b)w+ (?b)w+ +select * from test_regex('(?b)\w+', '(?b)w+', 'bAS'); +-- expectMatch 20.12 iP (?c)a a a +select * from test_regex('(?c)a', 'a', 'iP'); +-- expectNomatch 20.13 iP (?c)a A +select * from test_regex('(?c)a', 'A', 'iP'); +-- expectMatch 20.14 APS {(?e)\W+} WW WW +select * from test_regex('(?e)\W+', 'WW', 'APS'); +-- expectMatch 20.15 P (?i)a+ Aa Aa +select * from test_regex('(?i)a+', 'Aa', 'P'); +-- expectNomatch 20.16 P "(?m)a.b" "a\nb" +select * from test_regex('(?m)a.b', E'a\nb', 'P'); +-- expectMatch 20.17 P "(?m)^b" "a\nb" "b" +select * from test_regex('(?m)^b', E'a\nb', 'P'); +-- expectNomatch 20.18 P "(?n)a.b" "a\nb" +select * from test_regex('(?n)a.b', E'a\nb', 'P'); +-- expectMatch 20.19 P "(?n)^b" "a\nb" "b" +select * from test_regex('(?n)^b', E'a\nb', 'P'); +-- expectNomatch 20.20 P "(?p)a.b" "a\nb" +select * from test_regex('(?p)a.b', E'a\nb', 'P'); +-- expectNomatch 20.21 P "(?p)^b" "a\nb" +select * from test_regex('(?p)^b', E'a\nb', 'P'); +-- expectMatch 20.22 P (?q)a+b a+b a+b +select * from test_regex('(?q)a+b', 'a+b', 'P'); +-- expectMatch 20.23 nP "(?s)a.b" "a\nb" "a\nb" +select * from test_regex('(?s)a.b', E'a\nb', 'nP'); +-- expectMatch 20.24 xP "(?t)a b" "a b" "a b" +select * from test_regex('(?t)a b', 'a b', 'xP'); +-- expectMatch 20.25 P "(?w)a.b" "a\nb" "a\nb" +select * from test_regex('(?w)a.b', E'a\nb', 'P'); +-- expectMatch 20.26 P "(?w)^b" "a\nb" "b" +select * from test_regex('(?w)^b', E'a\nb', 'P'); +-- expectMatch 20.27 P "(?x)a b" "ab" "ab" +select * from test_regex('(?x)a b', 'ab', 'P'); +-- expectError 20.28 - (?z)ab BADOPT +select * from test_regex('(?z)ab', '', '-'); +-- expectMatch 20.29 P (?ici)a+ Aa Aa +select * from test_regex('(?ici)a+', 'Aa', 'P'); +-- expectError 20.30 P (?i)(?q)a+ BADRPT +select * from test_regex('(?i)(?q)a+', '', 'P'); +-- expectMatch 20.31 P (?q)(?i)a+ (?i)a+ (?i)a+ +select * from test_regex('(?q)(?i)a+', '(?i)a+', 'P'); +-- expectMatch 20.32 P (?qe)a+ a a +select * from test_regex('(?qe)a+', 'a', 'P'); +-- expectMatch 20.33 xP "(?q)a b" "a b" "a b" +select * from test_regex('(?q)a b', 'a b', 'xP'); +-- expectMatch 20.34 P "(?qx)a b" "a b" "a b" +select * from test_regex('(?qx)a b', 'a b', 'P'); +-- expectMatch 20.35 P (?qi)ab Ab Ab +select * from test_regex('(?qi)ab', 'Ab', 'P'); + +-- doing 21 "capturing" + +-- expectMatch 21.1 - a(b)c abc abc b +select * from test_regex('a(b)c', 'abc', '-'); +-- expectMatch 21.2 P a(?:b)c xabc abc +select * from test_regex('a(?:b)c', 'xabc', 'P'); +-- expectMatch 21.3 - a((b))c xabcy abc b b +select * from test_regex('a((b))c', 'xabcy', '-'); +-- expectMatch 21.4 P a(?:(b))c abcy abc b +select * from test_regex('a(?:(b))c', 'abcy', 'P'); +-- expectMatch 21.5 P a((?:b))c abc abc b +select * from test_regex('a((?:b))c', 'abc', 'P'); +-- expectMatch 21.6 P a(?:(?:b))c abc abc +select * from test_regex('a(?:(?:b))c', 'abc', 'P'); +-- expectIndices 21.7 Q "a(b){0}c" ac {0 1} {-1 -1} +select * from test_regex('a(b){0}c', 'ac', '0Q'); +-- expectMatch 21.8 - a(b)c(d)e abcde abcde b d +select * from test_regex('a(b)c(d)e', 'abcde', '-'); +-- expectMatch 21.9 - (b)c(d)e bcde bcde b d +select * from test_regex('(b)c(d)e', 'bcde', '-'); +-- expectMatch 21.10 - a(b)(d)e abde abde b d +select * from test_regex('a(b)(d)e', 'abde', '-'); +-- expectMatch 21.11 - a(b)c(d) abcd abcd b d +select * from test_regex('a(b)c(d)', 'abcd', '-'); +-- expectMatch 21.12 - (ab)(cd) xabcdy abcd ab cd +select * from test_regex('(ab)(cd)', 'xabcdy', '-'); +-- expectMatch 21.13 - a(b)?c xabcy abc b +select * from test_regex('a(b)?c', 'xabcy', '-'); +-- expectIndices 21.14 - a(b)?c xacy {1 2} {-1 -1} +select * from test_regex('a(b)?c', 'xacy', '0-'); +-- expectMatch 21.15 - a(b)?c(d)?e xabcdey abcde b d +select * from test_regex('a(b)?c(d)?e', 'xabcdey', '-'); +-- expectIndices 21.16 - a(b)?c(d)?e xacdey {1 4} {-1 -1} {3 3} +select * from test_regex('a(b)?c(d)?e', 'xacdey', '0-'); +-- expectIndices 21.17 - a(b)?c(d)?e xabcey {1 4} {2 2} {-1 -1} +select * from test_regex('a(b)?c(d)?e', 'xabcey', '0-'); +-- expectIndices 21.18 - a(b)?c(d)?e xacey {1 3} {-1 -1} {-1 -1} +select * from test_regex('a(b)?c(d)?e', 'xacey', '0-'); +-- expectMatch 21.19 - a(b)*c xabcy abc b +select * from test_regex('a(b)*c', 'xabcy', '-'); +-- expectIndices 21.20 - a(b)*c xabbbcy {1 5} {4 4} +select * from test_regex('a(b)*c', 'xabbbcy', '0-'); +-- expectIndices 21.21 - a(b)*c xacy {1 2} {-1 -1} +select * from test_regex('a(b)*c', 'xacy', '0-'); +-- expectMatch 21.22 - a(b*)c xabbbcy abbbc bbb +select * from test_regex('a(b*)c', 'xabbbcy', '-'); +-- expectMatch 21.23 - a(b*)c xacy ac "" +select * from test_regex('a(b*)c', 'xacy', '-'); +-- expectNomatch 21.24 - a(b)+c xacy +select * from test_regex('a(b)+c', 'xacy', '-'); +-- expectMatch 21.25 - a(b)+c xabcy abc b +select * from test_regex('a(b)+c', 'xabcy', '-'); +-- expectIndices 21.26 - a(b)+c xabbbcy {1 5} {4 4} +select * from test_regex('a(b)+c', 'xabbbcy', '0-'); +-- expectMatch 21.27 - a(b+)c xabbbcy abbbc bbb +select * from test_regex('a(b+)c', 'xabbbcy', '-'); +-- expectIndices 21.28 Q "a(b){2,3}c" xabbbcy {1 5} {4 4} +select * from test_regex('a(b){2,3}c', 'xabbbcy', '0Q'); +-- expectIndices 21.29 Q "a(b){2,3}c" xabbcy {1 4} {3 3} +select * from test_regex('a(b){2,3}c', 'xabbcy', '0Q'); +-- expectNomatch 21.30 Q "a(b){2,3}c" xabcy +select * from test_regex('a(b){2,3}c', 'xabcy', 'Q'); +-- expectMatch 21.31 LP "\\y(\\w+)\\y" "-- abc-" "abc" "abc" +select * from test_regex('\y(\w+)\y', '-- abc-', 'LP'); +-- expectMatch 21.32 - a((b|c)d+)+ abacdbd acdbd bd b +select * from test_regex('a((b|c)d+)+', 'abacdbd', '-'); +-- expectMatch 21.33 N (.*).* abc abc abc +select * from test_regex('(.*).*', 'abc', 'N'); +-- expectMatch 21.34 N (a*)* bc "" "" +select * from test_regex('(a*)*', 'bc', 'N'); +-- expectMatch 21.35 M { TO (([a-z0-9._]+|"([^"]+|"")+")+)} {asd TO foo} { TO foo} foo o {} +select * from test_regex(' TO (([a-z0-9._]+|"([^"]+|"")+")+)', 'asd TO foo', 'M'); +-- expectMatch 21.36 RPQ ((.))(\2){0} xy x x x {} +select * from test_regex('((.))(\2){0}', 'xy', 'RPQ'); +-- expectMatch 21.37 RP ((.))(\2) xyy yy y y y +select * from test_regex('((.))(\2)', 'xyy', 'RP'); +-- expectMatch 21.38 oRP ((.))(\2) xyy yy {} {} {} +select * from test_regex('((.))(\2)', 'xyy', 'oRP'); +-- expectNomatch 21.39 PQR {(.){0}(\1)} xxx +select * from test_regex('(.){0}(\1)', 'xxx', 'PQR'); +-- expectNomatch 21.40 PQR {((.)){0}(\2)} xxx +select * from test_regex('((.)){0}(\2)', 'xxx', 'PQR'); +-- expectMatch 21.41 NPQR {((.)){0}(\2){0}} xyz {} {} {} {} +select * from test_regex('((.)){0}(\2){0}', 'xyz', 'NPQR'); + +-- doing 22 "multicharacter collating elements" +-- # again ugh + +-- MCCEs are not implemented in Postgres, so we skip all these tests +-- expectMatch 22.1 &+L {a[c]e} ace ace +-- select * from test_regex('a[c]e', 'ace', '+L'); +-- select * from test_regex('a[c]e', 'ace', '+Lb'); +-- expectNomatch 22.2 &+IL {a[c]h} ach +-- select * from test_regex('a[c]h', 'ach', '+IL'); +-- select * from test_regex('a[c]h', 'ach', '+ILb'); +-- expectMatch 22.3 &+L {a[[.ch.]]} ach ach +-- select * from test_regex('a[[.ch.]]', 'ach', '+L'); +-- select * from test_regex('a[[.ch.]]', 'ach', '+Lb'); +-- expectNomatch 22.4 &+L {a[[.ch.]]} ace +-- select * from test_regex('a[[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[[.ch.]]', 'ace', '+Lb'); +-- expectMatch 22.5 &+L {a[c[.ch.]]} ac ac +-- select * from test_regex('a[c[.ch.]]', 'ac', '+L'); +-- select * from test_regex('a[c[.ch.]]', 'ac', '+Lb'); +-- expectMatch 22.6 &+L {a[c[.ch.]]} ace ac +-- select * from test_regex('a[c[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[c[.ch.]]', 'ace', '+Lb'); +-- expectMatch 22.7 &+L {a[c[.ch.]]} ache ach +-- select * from test_regex('a[c[.ch.]]', 'ache', '+L'); +-- select * from test_regex('a[c[.ch.]]', 'ache', '+Lb'); +-- expectNomatch 22.8 &+L {a[^c]e} ace +-- select * from test_regex('a[^c]e', 'ace', '+L'); +-- select * from test_regex('a[^c]e', 'ace', '+Lb'); +-- expectMatch 22.9 &+L {a[^c]e} abe abe +-- select * from test_regex('a[^c]e', 'abe', '+L'); +-- select * from test_regex('a[^c]e', 'abe', '+Lb'); +-- expectMatch 22.10 &+L {a[^c]e} ache ache +-- select * from test_regex('a[^c]e', 'ache', '+L'); +-- select * from test_regex('a[^c]e', 'ache', '+Lb'); +-- expectNomatch 22.11 &+L {a[^[.ch.]]} ach +-- select * from test_regex('a[^[.ch.]]', 'ach', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'ach', '+Lb'); +-- expectMatch 22.12 &+L {a[^[.ch.]]} ace ac +-- select * from test_regex('a[^[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'ace', '+Lb'); +-- expectMatch 22.13 &+L {a[^[.ch.]]} ac ac +-- select * from test_regex('a[^[.ch.]]', 'ac', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'ac', '+Lb'); +-- expectMatch 22.14 &+L {a[^[.ch.]]} abe ab +-- select * from test_regex('a[^[.ch.]]', 'abe', '+L'); +-- select * from test_regex('a[^[.ch.]]', 'abe', '+Lb'); +-- expectNomatch 22.15 &+L {a[^c[.ch.]]} ach +-- select * from test_regex('a[^c[.ch.]]', 'ach', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'ach', '+Lb'); +-- expectNomatch 22.16 &+L {a[^c[.ch.]]} ace +-- select * from test_regex('a[^c[.ch.]]', 'ace', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'ace', '+Lb'); +-- expectNomatch 22.17 &+L {a[^c[.ch.]]} ac +-- select * from test_regex('a[^c[.ch.]]', 'ac', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'ac', '+Lb'); +-- expectMatch 22.18 &+L {a[^c[.ch.]]} abe ab +-- select * from test_regex('a[^c[.ch.]]', 'abe', '+L'); +-- select * from test_regex('a[^c[.ch.]]', 'abe', '+Lb'); +-- expectMatch 22.19 &+L {a[^b]} ac ac +-- select * from test_regex('a[^b]', 'ac', '+L'); +-- select * from test_regex('a[^b]', 'ac', '+Lb'); +-- expectMatch 22.20 &+L {a[^b]} ace ac +-- select * from test_regex('a[^b]', 'ace', '+L'); +-- select * from test_regex('a[^b]', 'ace', '+Lb'); +-- expectMatch 22.21 &+L {a[^b]} ach ach +-- select * from test_regex('a[^b]', 'ach', '+L'); +-- select * from test_regex('a[^b]', 'ach', '+Lb'); +-- expectNomatch 22.22 &+L {a[^b]} abe +-- select * from test_regex('a[^b]', 'abe', '+L'); +-- select * from test_regex('a[^b]', 'abe', '+Lb'); + +-- doing 23 "lookahead constraints" + +-- expectMatch 23.1 HP a(?=b)b* ab ab +select * from test_regex('a(?=b)b*', 'ab', 'HP'); +-- expectNomatch 23.2 HP a(?=b)b* a +select * from test_regex('a(?=b)b*', 'a', 'HP'); +-- expectMatch 23.3 HP a(?=b)b*(?=c)c* abc abc +select * from test_regex('a(?=b)b*(?=c)c*', 'abc', 'HP'); +-- expectNomatch 23.4 HP a(?=b)b*(?=c)c* ab +select * from test_regex('a(?=b)b*(?=c)c*', 'ab', 'HP'); +-- expectNomatch 23.5 HP a(?!b)b* ab +select * from test_regex('a(?!b)b*', 'ab', 'HP'); +-- expectMatch 23.6 HP a(?!b)b* a a +select * from test_regex('a(?!b)b*', 'a', 'HP'); +-- expectMatch 23.7 HP (?=b)b b b +select * from test_regex('(?=b)b', 'b', 'HP'); +-- expectNomatch 23.8 HP (?=b)b a +select * from test_regex('(?=b)b', 'a', 'HP'); +-- expectMatch 23.9 HP ...(?!.) abcde cde +select * from test_regex('...(?!.)', 'abcde', 'HP'); +-- expectNomatch 23.10 HP ...(?=.) abc +select * from test_regex('...(?=.)', 'abc', 'HP'); + +-- Postgres addition: lookbehind constraints + +-- expectMatch 23.11 HPN (?<=a)b* ab b +select * from test_regex('(?<=a)b*', 'ab', 'HPN'); +-- expectNomatch 23.12 HPN (?<=a)b* b +select * from test_regex('(?<=a)b*', 'b', 'HPN'); +-- expectMatch 23.13 HP (?<=a)b*(?<=b)c* abc bc +select * from test_regex('(?<=a)b*(?<=b)c*', 'abc', 'HP'); +-- expectNomatch 23.14 HP (?<=a)b*(?<=b)c* ac +select * from test_regex('(?<=a)b*(?<=b)c*', 'ac', 'HP'); +-- expectNomatch 23.15 IHP a(?<!a)b* ab +select * from test_regex('a(?<!a)b*', 'ab', 'IHP'); +-- expectMatch 23.16 HP a(?<!b)b* a a +select * from test_regex('a(?<!b)b*', 'a', 'HP'); +-- expectMatch 23.17 HP (?<=b)b bb b +select * from test_regex('(?<=b)b', 'bb', 'HP'); +-- expectNomatch 23.18 HP (?<=b)b b +select * from test_regex('(?<=b)b', 'b', 'HP'); +-- expectMatch 23.19 HP (?<=.).. abcde bc +select * from test_regex('(?<=.)..', 'abcde', 'HP'); +-- expectMatch 23.20 HP (?<=..)a* aaabb a +select * from test_regex('(?<=..)a*', 'aaabb', 'HP'); +-- expectMatch 23.21 HP (?<=..)b* aaabb {} +-- Note: empty match here is correct, it matches after the first 2 characters +select * from test_regex('(?<=..)b*', 'aaabb', 'HP'); +-- expectMatch 23.22 HP (?<=..)b+ aaabb bb +select * from test_regex('(?<=..)b+', 'aaabb', 'HP'); + +-- doing 24 "non-greedy quantifiers" + +-- expectMatch 24.1 PT ab+? abb ab +select * from test_regex('ab+?', 'abb', 'PT'); +-- expectMatch 24.2 PT ab+?c abbc abbc +select * from test_regex('ab+?c', 'abbc', 'PT'); +-- expectMatch 24.3 PT ab*? abb a +select * from test_regex('ab*?', 'abb', 'PT'); +-- expectMatch 24.4 PT ab*?c abbc abbc +select * from test_regex('ab*?c', 'abbc', 'PT'); +-- expectMatch 24.5 PT ab?? ab a +select * from test_regex('ab??', 'ab', 'PT'); +-- expectMatch 24.6 PT ab??c abc abc +select * from test_regex('ab??c', 'abc', 'PT'); +-- expectMatch 24.7 PQT "ab{2,4}?" abbbb abb +select * from test_regex('ab{2,4}?', 'abbbb', 'PQT'); +-- expectMatch 24.8 PQT "ab{2,4}?c" abbbbc abbbbc +select * from test_regex('ab{2,4}?c', 'abbbbc', 'PQT'); +-- expectMatch 24.9 - 3z* 123zzzz456 3zzzz +select * from test_regex('3z*', '123zzzz456', '-'); +-- expectMatch 24.10 PT 3z*? 123zzzz456 3 +select * from test_regex('3z*?', '123zzzz456', 'PT'); +-- expectMatch 24.11 - z*4 123zzzz456 zzzz4 +select * from test_regex('z*4', '123zzzz456', '-'); +-- expectMatch 24.12 PT z*?4 123zzzz456 zzzz4 +select * from test_regex('z*?4', '123zzzz456', 'PT'); +-- expectMatch 24.13 PT {^([^/]+?)(?:/([^/]+?))(?:/([^/]+?))?$} {foo/bar/baz} {foo/bar/baz} {foo} {bar} {baz} +select * from test_regex('^([^/]+?)(?:/([^/]+?))(?:/([^/]+?))?$', 'foo/bar/baz', 'PT'); +-- expectMatch 24.14 PRT {^(.+?)(?:/(.+?))(?:/(.+?)\3)?$} {foo/bar/baz/quux} {foo/bar/baz/quux} {foo} {bar/baz/quux} {} +select * from test_regex('^(.+?)(?:/(.+?))(?:/(.+?)\3)?$', 'foo/bar/baz/quux', 'PRT'); + +-- doing 25 "mixed quantifiers" +-- # this is very incomplete as yet +-- # should include | + +-- expectMatch 25.1 PNT {^(.*?)(a*)$} "xyza" xyza xyz a +select * from test_regex('^(.*?)(a*)$', 'xyza', 'PNT'); +-- expectMatch 25.2 PNT {^(.*?)(a*)$} "xyzaa" xyzaa xyz aa +select * from test_regex('^(.*?)(a*)$', 'xyzaa', 'PNT'); +-- expectMatch 25.3 PNT {^(.*?)(a*)$} "xyz" xyz xyz "" +select * from test_regex('^(.*?)(a*)$', 'xyz', 'PNT'); + +-- doing 26 "tricky cases" + +-- # attempts to trick the matcher into accepting a short match +-- expectMatch 26.1 - (week|wee)(night|knights) \ +-- "weeknights" weeknights wee knights +select * from test_regex('(week|wee)(night|knights)', 'weeknights', '-'); +-- expectMatch 26.2 RP {a(bc*).*\1} abccbccb abccbccb b +select * from test_regex('a(bc*).*\1', 'abccbccb', 'RP'); +-- expectMatch 26.3 - {a(b.[bc]*)+} abcbd abcbd bd +select * from test_regex('a(b.[bc]*)+', 'abcbd', '-'); + +-- doing 27 "implementation misc." + +-- # duplicate arcs are suppressed +-- expectMatch 27.1 P a(?:b|b)c abc abc +select * from test_regex('a(?:b|b)c', 'abc', 'P'); +-- # make color/subcolor relationship go back and forth +-- expectMatch 27.2 & {[ab][ab][ab]} aba aba +select * from test_regex('[ab][ab][ab]', 'aba', ''); +select * from test_regex('[ab][ab][ab]', 'aba', 'b'); +-- expectMatch 27.3 & {[ab][ab][ab][ab][ab][ab][ab]} \ +-- "abababa" abababa +select * from test_regex('[ab][ab][ab][ab][ab][ab][ab]', 'abababa', ''); +select * from test_regex('[ab][ab][ab][ab][ab][ab][ab]', 'abababa', 'b'); + +-- doing 28 "boundary busters etc." + +-- # color-descriptor allocation changes at 10 +-- expectMatch 28.1 & abcdefghijkl "abcdefghijkl" abcdefghijkl +select * from test_regex('abcdefghijkl', 'abcdefghijkl', ''); +select * from test_regex('abcdefghijkl', 'abcdefghijkl', 'b'); +-- # so does arc allocation +-- expectMatch 28.2 P a(?:b|c|d|e|f|g|h|i|j|k|l|m)n "agn" agn +select * from test_regex('a(?:b|c|d|e|f|g|h|i|j|k|l|m)n', 'agn', 'P'); +-- # subexpression tracking also at 10 +-- expectMatch 28.3 - a(((((((((((((b)))))))))))))c \ +-- "abc" abc b b b b b b b b b b b b b +select * from test_regex('a(((((((((((((b)))))))))))))c', 'abc', '-'); +-- # state-set handling changes slightly at unsigned size (might be 64...) +-- # (also stresses arc allocation) +-- expectMatch 28.4 Q "ab{1,100}c" abbc abbc +select * from test_regex('ab{1,100}c', 'abbc', 'Q'); +-- expectMatch 28.5 Q "ab{1,100}c" \ +-- "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc" \ +-- abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc +select * from test_regex('ab{1,100}c', 'abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc', 'Q'); +-- expectMatch 28.6 Q "ab{1,100}c" \ +-- "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc"\ +-- abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc +select * from test_regex('ab{1,100}c', 'abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc', 'Q'); +-- # force small cache and bust it, several ways +-- expectMatch 28.7 LP {\w+abcdefgh} xyzabcdefgh xyzabcdefgh +select * from test_regex('\w+abcdefgh', 'xyzabcdefgh', 'LP'); +-- expectMatch 28.8 %LP {\w+abcdefgh} xyzabcdefgh xyzabcdefgh +select * from test_regex('\w+abcdefgh', 'xyzabcdefgh', '%LP'); +-- expectMatch 28.9 %LP {\w+abcdefghijklmnopqrst} \ +-- "xyzabcdefghijklmnopqrst" xyzabcdefghijklmnopqrst +select * from test_regex('\w+abcdefghijklmnopqrst', 'xyzabcdefghijklmnopqrst', '%LP'); +-- expectIndices 28.10 %LP {\w+(abcdefgh)?} xyz {0 2} {-1 -1} +select * from test_regex('\w+(abcdefgh)?', 'xyz', '0%LP'); +-- expectIndices 28.11 %LP {\w+(abcdefgh)?} xyzabcdefg {0 9} {-1 -1} +select * from test_regex('\w+(abcdefgh)?', 'xyzabcdefg', '0%LP'); +-- expectIndices 28.12 %LP {\w+(abcdefghijklmnopqrst)?} \ +-- "xyzabcdefghijklmnopqrs" {0 21} {-1 -1} +select * from test_regex('\w+(abcdefghijklmnopqrst)?', 'xyzabcdefghijklmnopqrs', '0%LP'); + +-- doing 29 "incomplete matches" + +-- expectPartial 29.1 t def abc {3 2} "" +select * from test_regex('def', 'abc', '0!t'); +-- expectPartial 29.2 t bcd abc {1 2} "" +select * from test_regex('bcd', 'abc', '0!t'); +-- expectPartial 29.3 t abc abab {0 3} "" +select * from test_regex('abc', 'abab', '0!t'); +-- expectPartial 29.4 t abc abdab {3 4} "" +select * from test_regex('abc', 'abdab', '0!t'); +-- expectIndices 29.5 t abc abc {0 2} {0 2} +select * from test_regex('abc', 'abc', '0t'); +-- expectIndices 29.6 t abc xyabc {2 4} {2 4} +select * from test_regex('abc', 'xyabc', '0t'); +-- expectPartial 29.7 t abc+ xyab {2 3} "" +select * from test_regex('abc+', 'xyab', '0!t'); +-- expectIndices 29.8 t abc+ xyabc {2 4} {2 4} +select * from test_regex('abc+', 'xyabc', '0t'); +-- knownBug expectIndices 29.9 t abc+ xyabcd {2 4} {6 5} +select * from test_regex('abc+', 'xyabcd', '0t'); +-- expectIndices 29.10 t abc+ xyabcdd {2 4} {7 6} +select * from test_regex('abc+', 'xyabcdd', '0t'); +-- expectPartial 29.11 tPT abc+? xyab {2 3} "" +select * from test_regex('abc+?', 'xyab', '0!tPT'); +-- # the retain numbers in these two may look wrong, but they aren't +-- expectIndices 29.12 tPT abc+? xyabc {2 4} {5 4} +select * from test_regex('abc+?', 'xyabc', '0tPT'); +-- expectIndices 29.13 tPT abc+? xyabcc {2 4} {6 5} +select * from test_regex('abc+?', 'xyabcc', '0tPT'); +-- expectIndices 29.14 tPT abc+? xyabcd {2 4} {6 5} +select * from test_regex('abc+?', 'xyabcd', '0tPT'); +-- expectIndices 29.15 tPT abc+? xyabcdd {2 4} {7 6} +select * from test_regex('abc+?', 'xyabcdd', '0tPT'); +-- expectIndices 29.16 t abcd|bc xyabc {3 4} {2 4} +select * from test_regex('abcd|bc', 'xyabc', '0t'); +-- expectPartial 29.17 tn .*k "xx\nyyy" {3 5} "" +select * from test_regex('.*k', E'xx\nyyy', '0!tn'); + +-- doing 30 "misc. oddities and old bugs" + +-- expectError 30.1 & *** BADRPT +select * from test_regex('***', '', ''); +select * from test_regex('***', '', 'b'); +-- expectMatch 30.2 N a?b* abb abb +select * from test_regex('a?b*', 'abb', 'N'); +-- expectMatch 30.3 N a?b* bb bb +select * from test_regex('a?b*', 'bb', 'N'); +-- expectMatch 30.4 & a*b aab aab +select * from test_regex('a*b', 'aab', ''); +select * from test_regex('a*b', 'aab', 'b'); +-- expectMatch 30.5 & ^a*b aaaab aaaab +select * from test_regex('^a*b', 'aaaab', ''); +select * from test_regex('^a*b', 'aaaab', 'b'); +-- expectMatch 30.6 &M {[0-6][1-2][0-3][0-6][1-6][0-6]} \ +-- "010010" 010010 +select * from test_regex('[0-6][1-2][0-3][0-6][1-6][0-6]', '010010', 'M'); +select * from test_regex('[0-6][1-2][0-3][0-6][1-6][0-6]', '010010', 'Mb'); +-- # temporary REG_BOSONLY kludge +-- expectMatch 30.7 s abc abcd abc +select * from test_regex('abc', 'abcd', 's'); +-- expectNomatch 30.8 s abc xabcd +select * from test_regex('abc', 'xabcd', 's'); +-- # back to normal stuff +-- expectMatch 30.9 HLP {(?n)^(?![t#])\S+} \ +-- "tk\n\n#\n#\nit0" it0 +select * from test_regex('(?n)^(?![t#])\S+', E'tk\n\n#\n#\nit0', 'HLP'); + +-- # Now for tests *not* written by Henry Spencer + +-- # Tests resulting from bugs reported by users +-- test reg-31.1 {[[:xdigit:]] behaves correctly when followed by [[:space:]]} { +-- set str {2:::DebugWin32} +-- set re {([[:xdigit:]])([[:space:]]*)} +-- list [regexp $re $str match xdigit spaces] $match $xdigit $spaces +-- # Code used to produce {1 2:::DebugWin32 2 :::DebugWin32} !!! +-- } {1 2 2 {}} +select * from test_regex('([[:xdigit:]])([[:space:]]*)', '2:::DebugWin32', 'L'); + +-- test reg-32.1 {canmatch functionality -- at end} testregexp { +-- set pat {blah} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 7} +select * from test_regex('blah', 'asd asd', 'c'); + +-- test reg-32.2 {canmatch functionality -- at end} testregexp { +-- set pat {s%$} +-- set line "asd asd" +-- # can only match after the end of the string +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 7} +select * from test_regex('s%$', 'asd asd', 'c'); + +-- test reg-32.3 {canmatch functionality -- not last char} testregexp { +-- set pat {[^d]%$} +-- set line "asd asd" +-- # can only match after the end of the string +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 7} +select * from test_regex('[^d]%$', 'asd asd', 'c'); + +-- test reg-32.3.1 {canmatch functionality -- no match} testregexp { +-- set pat {\Zx} +-- set line "asd asd" +-- # can match the last char, if followed by x +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 -1} +select * from test_regex('\Zx', 'asd asd', 'cIP'); + +-- test reg-32.4 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {.x} +-- set line "asd asd" +-- # can match the last char, if followed by x +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('.x', 'asd asd', 'c'); + +-- test reg-32.4.1 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {.x$} +-- set line "asd asd" +-- # can match the last char, if followed by x +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('.x$', 'asd asd', 'c'); + +-- test reg-32.5 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {.[^d]x$} +-- set line "asd asd" +-- # can match the last char, if followed by not-d and x. +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('.[^d]x$', 'asd asd', 'c'); + +-- test reg-32.6 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {[^a]%[^\r\n]*$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('[^a]%[^\r\n]*$', 'asd asd', 'cEP'); + +-- test reg-32.7 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {[^a]%$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('[^a]%$', 'asd asd', 'c'); + +-- test reg-32.8 {canmatch functionality -- last char} {knownBug testregexp} { +-- set pat {[^x]%$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('[^x]%$', 'asd asd', 'c'); + +-- test reg-32.9 {canmatch functionality -- more complex case} {knownBug testregexp} { +-- set pat {((\B\B|\Bh+line)[ \t]*|[^\B]%[^\r\n]*)$} +-- set line "asd asd" +-- # can match at the final d, if '%' follows +-- set res [testregexp -xflags -- c $pat $line resvar] +-- lappend res $resvar +-- } {0 6} +select * from test_regex('((\B\B|\Bh+line)[ \t]*|[^\B]%[^\r\n]*)$', 'asd asd', 'cEP'); + +-- # Tests reg-33.*: Checks for bug fixes + +-- test reg-33.1 {Bug 230589} { +-- regexp {[ ]*(^|[^%])%V} "*%V2" m s +-- } 1 +select * from test_regex('[ ]*(^|[^%])%V', '*%V2', '-'); + +-- test reg-33.2 {Bug 504785} { +-- regexp -inline {([^_.]*)([^.]*)\.(..)(.).*} bbcos_001_c01.q1la +-- } {bbcos_001_c01.q1la bbcos _001_c01 q1 l} +select * from test_regex('([^_.]*)([^.]*)\.(..)(.).*', 'bbcos_001_c01.q1la', '-'); + +-- test reg-33.3 {Bug 505048} { +-- regexp {\A\s*[^<]*\s*<([^>]+)>} a<a> +-- } 1 +select * from test_regex('\A\s*[^<]*\s*<([^>]+)>', 'a<a>', 'LP'); + +-- test reg-33.4 {Bug 505048} { +-- regexp {\A\s*([^b]*)b} ab +-- } 1 +select * from test_regex('\A\s*([^b]*)b', 'ab', 'LP'); + +-- test reg-33.5 {Bug 505048} { +-- regexp {\A\s*[^b]*(b)} ab +-- } 1 +select * from test_regex('\A\s*[^b]*(b)', 'ab', 'LP'); + +-- test reg-33.6 {Bug 505048} { +-- regexp {\A(\s*)[^b]*(b)} ab +-- } 1 +select * from test_regex('\A(\s*)[^b]*(b)', 'ab', 'LP'); + +-- test reg-33.7 {Bug 505048} { +-- regexp {\A\s*[^b]*b} ab +-- } 1 +select * from test_regex('\A\s*[^b]*b', 'ab', 'LP'); + +-- test reg-33.8 {Bug 505048} { +-- regexp -inline {\A\s*[^b]*b} ab +-- } ab +select * from test_regex('\A\s*[^b]*b', 'ab', 'LP'); + +-- test reg-33.9 {Bug 505048} { +-- regexp -indices -inline {\A\s*[^b]*b} ab +-- } {{0 1}} +select * from test_regex('\A\s*[^b]*b', 'ab', '0LP'); + +-- test reg-33.10 {Bug 840258} -body { +-- regsub {(^|\n)+\.*b} \n.b {} tmp +-- } -cleanup { +-- unset tmp +-- } -result 1 +select * from test_regex('(^|\n)+\.*b', E'\n.b', 'P'); + +-- test reg-33.11 {Bug 840258} -body { +-- regsub {(^|[\n\r]+)\.*\?<.*?(\n|\r)+} \ +-- "TQ\r\n.?<5000267>Test already stopped\r\n" {} tmp +-- } -cleanup { +-- unset tmp +-- } -result 1 +select * from test_regex('(^|[\n\r]+)\.*\?<.*?(\n|\r)+', E'TQ\r\n.?<5000267>Test already stopped\r\n', 'EP'); + +-- test reg-33.12 {Bug 1810264 - bad read} { +-- regexp {\3161573148} {\3161573148} +-- } 0 +select * from test_regex('\3161573148', '\3161573148', 'MP'); + +-- test reg-33.13 {Bug 1810264 - infinite loop} { +-- regexp {($|^)*} {x} +-- } 1 +select * from test_regex('($|^)*', 'x', 'N'); + +-- # Some environments have small default stack sizes. [Bug 1905562] +-- test reg-33.14 {Bug 1810264 - super-expensive expression} nonPortable { +-- regexp {(x{200}){200}$y} {x} +-- } 0 +-- This might or might not work depending on platform, so skip it +-- select * from test_regex('(x{200}){200}$y', 'x', 'IQ'); + +-- test reg-33.15.1 {Bug 3603557 - an "in the wild" RE} { +-- lindex [regexp -expanded -about { +-- ^TETRA_MODE_CMD # Message Type +-- ([[:blank:]]+) # Pad +-- (ETS_1_1|ETS_1_2|ETS_2_2) # SystemCode +-- ([[:blank:]]+) # Pad +-- (CONTINUOUS|CARRIER|MCCH|TRAFFIC) # SharingMode +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # ColourCode +-- ([[:blank:]]+) # Pad +-- (1|2|3|4|6|9|12|18) # TSReservedFrames +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # UPlaneDTX +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # Frame18Extension +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,4}) # MCC +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,5}) # MNC +-- ([[:blank:]]+) # Pad +-- (BOTH|BCAST|ENQRY|NONE) # NbrCellBcast +-- ([[:blank:]]+) # Pad +-- (UNKNOWN|LOW|MEDIUM|HIGH) # CellServiceLevel +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # LateEntryInfo +-- ([[:blank:]]+) # Pad +-- (300|400) # FrequencyBand +-- ([[:blank:]]+) # Pad +-- (NORMAL|REVERSE) # ReverseOperation +-- ([[:blank:]]+) # Pad +-- (NONE|\+6\.25|\-6\.25|\+12\.5) # Offset +-- ([[:blank:]]+) # Pad +-- (10) # DuplexSpacing +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,4}) # MainCarrierNr +-- ([[:blank:]]+) # Pad +-- (0|1|2|3) # NrCSCCH +-- ([[:blank:]]+) # Pad +-- (15|20|25|30|35|40|45) # MSTxPwrMax +-- ([[:blank:]]+) # Pad +-- (\-125|\-120|\-115|\-110|\-105|\-100|\-95|\-90|\-85|\-80|\-75|\-70|\-65|\-60|\-55|\-50) +-- # RxLevAccessMin +-- ([[:blank:]]+) # Pad +-- (\-53|\-51|\-49|\-47|\-45|\-43|\-41|\-39|\-37|\-35|\-33|\-31|\-29|\-27|\-25|\-23) +-- # AccessParameter +-- ([[:blank:]]+) # Pad +-- (DISABLE|[[:digit:]]{3,4}) # RadioDLTimeout +-- ([[:blank:]]+) # Pad +-- (\-[[:digit:]]{2,3}) # RSSIThreshold +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,5}) # CCKIdSCKVerNr +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,5}) # LocationArea +-- ([[:blank:]]+) # Pad +-- ([(1|0)]{16}) # SubscriberClass +-- ([[:blank:]]+) # Pad +-- ([(1|0)]{12}) # BSServiceDetails +-- ([[:blank:]]+) # Pad +-- (RANDOMIZE|IMMEDIATE|[[:digit:]]{1,2}) # IMM +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # WT +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # Nu +-- ([[:blank:]]+) # Pad +-- ([0-1]) # FrameLngFctr +-- ([[:blank:]]+) # Pad +-- ([[:digit:]]{1,2}) # TSPtr +-- ([[:blank:]]+) # Pad +-- ([0-7]) # MinPriority +-- ([[:blank:]]+) # Pad +-- (PASS|TRUE|FAIL|FALSE) # ExtdSrvcsEnabled +-- ([[:blank:]]+) # Pad +-- (.*) # ConditionalFields +-- }] 0 +-- } 68 +select * from test_regex($$ + ^TETRA_MODE_CMD # Message Type + ([[:blank:]]+) # Pad + (ETS_1_1|ETS_1_2|ETS_2_2) # SystemCode + ([[:blank:]]+) # Pad + (CONTINUOUS|CARRIER|MCCH|TRAFFIC) # SharingMode + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # ColourCode + ([[:blank:]]+) # Pad + (1|2|3|4|6|9|12|18) # TSReservedFrames + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # UPlaneDTX + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # Frame18Extension + ([[:blank:]]+) # Pad + ([[:digit:]]{1,4}) # MCC + ([[:blank:]]+) # Pad + ([[:digit:]]{1,5}) # MNC + ([[:blank:]]+) # Pad + (BOTH|BCAST|ENQRY|NONE) # NbrCellBcast + ([[:blank:]]+) # Pad + (UNKNOWN|LOW|MEDIUM|HIGH) # CellServiceLevel + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # LateEntryInfo + ([[:blank:]]+) # Pad + (300|400) # FrequencyBand + ([[:blank:]]+) # Pad + (NORMAL|REVERSE) # ReverseOperation + ([[:blank:]]+) # Pad + (NONE|\+6\.25|\-6\.25|\+12\.5) # Offset + ([[:blank:]]+) # Pad + (10) # DuplexSpacing + ([[:blank:]]+) # Pad + ([[:digit:]]{1,4}) # MainCarrierNr + ([[:blank:]]+) # Pad + (0|1|2|3) # NrCSCCH + ([[:blank:]]+) # Pad + (15|20|25|30|35|40|45) # MSTxPwrMax + ([[:blank:]]+) # Pad + (\-125|\-120|\-115|\-110|\-105|\-100|\-95|\-90|\-85|\-80|\-75|\-70|\-65|\-60|\-55|\-50) + # RxLevAccessMin + ([[:blank:]]+) # Pad + (\-53|\-51|\-49|\-47|\-45|\-43|\-41|\-39|\-37|\-35|\-33|\-31|\-29|\-27|\-25|\-23) + # AccessParameter + ([[:blank:]]+) # Pad + (DISABLE|[[:digit:]]{3,4}) # RadioDLTimeout + ([[:blank:]]+) # Pad + (\-[[:digit:]]{2,3}) # RSSIThreshold + ([[:blank:]]+) # Pad + ([[:digit:]]{1,5}) # CCKIdSCKVerNr + ([[:blank:]]+) # Pad + ([[:digit:]]{1,5}) # LocationArea + ([[:blank:]]+) # Pad + ([(1|0)]{16}) # SubscriberClass + ([[:blank:]]+) # Pad + ([(1|0)]{12}) # BSServiceDetails + ([[:blank:]]+) # Pad + (RANDOMIZE|IMMEDIATE|[[:digit:]]{1,2}) # IMM + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # WT + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # Nu + ([[:blank:]]+) # Pad + ([0-1]) # FrameLngFctr + ([[:blank:]]+) # Pad + ([[:digit:]]{1,2}) # TSPtr + ([[:blank:]]+) # Pad + ([0-7]) # MinPriority + ([[:blank:]]+) # Pad + (PASS|TRUE|FAIL|FALSE) # ExtdSrvcsEnabled + ([[:blank:]]+) # Pad + (.*) # ConditionalFields + $$, '', 'xLMPQ'); + +-- test reg-33.16.1 {Bug [8d2c0da36d]- another "in the wild" RE} { +-- lindex [regexp -about "^MRK:client1: =1339 14HKelly Talisman 10011000 (\[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]*) \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 8 0 8 0 0 0 77 77 1 1 2 0 11 { 1 3 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 13HC6 My Creator 2 3 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 31HC7 Slightly offensive name, huh 3 8 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 23HE-mail:kelly@hotbox.com 4 9 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 17Hcompface must die 5 10 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 0 3HAir 6 12 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 14HPGP public key 7 13 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 16Hkelly@hotbox.com 8 30 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 0 12H2 text/plain 9 30 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 0 13H2 x-kom/basic 10 33 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 1H0 11 14 8 \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* \[0-9\]* 00000000 1 1H3 }\r?"] 0 +-- } 1 +select * from test_regex(E'^MRK:client1: =1339 14HKelly Talisman 10011000 ([0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]*) [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 8 0 8 0 0 0 77 77 1 1 2 0 11 { 1 3 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 13HC6 My Creator 2 3 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 31HC7 Slightly offensive name, huh 3 8 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 23HE-mail:kelly@hotbox.com 4 9 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 17Hcompface must die 5 10 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 0 3HAir 6 12 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 14HPGP public key 7 13 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 16Hkelly@hotbox.com 8 30 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 0 12H2 text/plain 9 30 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 0 13H2 x-kom/basic 10 33 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 1H0 11 14 8 [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* [0-9]* 00000000 1 1H3 }\r?', '', 'BMS'); + +-- test reg-33.15 {constraint fixes} { +-- regexp {(^)+^} x +-- } 1 +select * from test_regex('(^)+^', 'x', 'N'); + +-- test reg-33.16 {constraint fixes} { +-- regexp {($^)+} x +-- } 0 +select * from test_regex('($^)+', 'x', 'N'); + +-- test reg-33.17 {constraint fixes} { +-- regexp {(^$)*} x +-- } 1 +select * from test_regex('(^$)*', 'x', 'N'); + +-- test reg-33.18 {constraint fixes} { +-- regexp {(^(?!aa))+} {aa bb cc} +-- } 0 +select * from test_regex('(^(?!aa))+', 'aa bb cc', 'HP'); + +-- test reg-33.19 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {aa x} +-- } 0 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'aa x', 'HP'); + +-- test reg-33.20 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {bb x} +-- } 0 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'bb x', 'HP'); + +-- test reg-33.21 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {cc x} +-- } 0 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'cc x', 'HP'); + +-- test reg-33.22 {constraint fixes} { +-- regexp {(^(?!aa)(?!bb)(?!cc))+} {dd x} +-- } 1 +select * from test_regex('(^(?!aa)(?!bb)(?!cc))+', 'dd x', 'HP'); + +-- test reg-33.23 {} { +-- regexp {abcd(\m)+xyz} x +-- } 0 +select * from test_regex('abcd(\m)+xyz', 'x', 'ILP'); + +-- test reg-33.24 {} { +-- regexp {abcd(\m)+xyz} a +-- } 0 +select * from test_regex('abcd(\m)+xyz', 'a', 'ILP'); + +-- test reg-33.25 {} { +-- regexp {^abcd*(((((^(a c(e?d)a+|)+|)+|)+|)+|a)+|)} x +-- } 0 +select * from test_regex('^abcd*(((((^(a c(e?d)a+|)+|)+|)+|)+|a)+|)', 'x', 'S'); + +-- test reg-33.26 {} { +-- regexp {a^(^)bcd*xy(((((($a+|)+|)+|)+$|)+|)+|)^$} x +-- } 0 +select * from test_regex('a^(^)bcd*xy(((((($a+|)+|)+|)+$|)+|)+|)^$', 'x', 'IS'); + +-- test reg-33.27 {} { +-- regexp {xyz(\Y\Y)+} x +-- } 0 +select * from test_regex('xyz(\Y\Y)+', 'x', 'LP'); + +-- test reg-33.28 {} { +-- regexp {x|(?:\M)+} x +-- } 1 +select * from test_regex('x|(?:\M)+', 'x', 'LNP'); + +-- test reg-33.29 {} { +-- # This is near the limits of the RE engine +-- regexp [string repeat x*y*z* 480] x +-- } 1 +-- The runtime cost of this seems out of proportion to the value, +-- so for Postgres purposes reduce the repeat to 200x +select * from test_regex(repeat('x*y*z*', 200), 'x', 'N'); + +-- test reg-33.30 {Bug 1080042} { +-- regexp {(\Y)+} foo +-- } 1 +select * from test_regex('(\Y)+', 'foo', 'LNP'); + + +-- and now, tests not from either Spencer or the Tcl project + +-- These cases exercise additional code paths in pushfwd()/push()/combine() +select * from test_regex('a\Y(?=45)', 'a45', 'HLP'); +select * from test_regex('a(?=.)c', 'ac', 'HP'); +select * from test_regex('a(?=.).*(?=3)3*', 'azz33', 'HP'); +select * from test_regex('a(?=\w)\w*(?=.).*', 'az3%', 'HLP'); + +-- These exercise the bulk-arc-movement paths in moveins() and moveouts(); +-- you may need to make them longer if you change BULK_ARC_OP_USE_SORT() +select * from test_regex('ABCDEFGHIJKLMNOPQRSTUVWXYZ(?:\w|a|b|c|d|e|f|0|1|2|3|4|5|6|Q)', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ3', 'LP'); +select * from test_regex('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(\Y\Y)+', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789Z', 'LP'); +select * from test_regex('((x|xabcdefghijklmnopqrstuvwxyz0123456789)x*|[^y]z)$', + 'az', ''); diff --git a/src/test/modules/test_regex/sql/test_regex_utf8.sql b/src/test/modules/test_regex/sql/test_regex_utf8.sql new file mode 100644 index 0000000..2aa3e0f --- /dev/null +++ b/src/test/modules/test_regex/sql/test_regex_utf8.sql @@ -0,0 +1,78 @@ +/* + * This test must be run in a database with UTF-8 encoding, + * because other encodings don't support all the characters used. + */ + +SELECT getdatabaseencoding() <> 'UTF8' + AS skip_test \gset +\if :skip_test +\quit +\endif + +set client_encoding = utf8; + +set standard_conforming_strings = on; + + +-- Run the Tcl test cases that require Unicode + +-- expectMatch 9.44 EMP* {a[\u00fe-\u0507][\u00ff-\u0300]b} \ +-- "a\u0102\u02ffb" "a\u0102\u02ffb" +select * from test_regex('a[\u00fe-\u0507][\u00ff-\u0300]b', E'a\u0102\u02ffb', 'EMP*'); + +-- expectMatch 13.27 P "a\\U00001234x" "a\u1234x" "a\u1234x" +select * from test_regex('a\U00001234x', E'a\u1234x', 'P'); +-- expectMatch 13.28 P {a\U00001234x} "a\u1234x" "a\u1234x" +select * from test_regex('a\U00001234x', E'a\u1234x', 'P'); +-- expectMatch 13.29 P "a\\U0001234x" "a\u1234x" "a\u1234x" +-- Tcl has relaxed their code to allow 1-8 hex digits, but Postgres hasn't +select * from test_regex('a\U0001234x', E'a\u1234x', 'P'); +-- expectMatch 13.30 P {a\U0001234x} "a\u1234x" "a\u1234x" +-- Tcl has relaxed their code to allow 1-8 hex digits, but Postgres hasn't +select * from test_regex('a\U0001234x', E'a\u1234x', 'P'); +-- expectMatch 13.31 P "a\\U000012345x" "a\u12345x" "a\u12345x" +select * from test_regex('a\U000012345x', E'a\u12345x', 'P'); +-- expectMatch 13.32 P {a\U000012345x} "a\u12345x" "a\u12345x" +select * from test_regex('a\U000012345x', E'a\u12345x', 'P'); +-- expectMatch 13.33 P "a\\U1000000x" "a\ufffd0x" "a\ufffd0x" +-- Tcl allows this as a standalone character, but Postgres doesn't +select * from test_regex('a\U1000000x', E'a\ufffd0x', 'P'); +-- expectMatch 13.34 P {a\U1000000x} "a\ufffd0x" "a\ufffd0x" +-- Tcl allows this as a standalone character, but Postgres doesn't +select * from test_regex('a\U1000000x', E'a\ufffd0x', 'P'); + + +-- Additional tests, not derived from Tcl + +-- Exercise logic around high character ranges a bit more +select * from test_regex('a + [\u1000-\u1100]* + [\u3000-\u3100]* + [\u1234-\u25ff]+ + [\u2000-\u35ff]* + [\u2600-\u2f00]* + \u1236\u1236x', + E'a\u1234\u1236\u1236x', 'xEMP'); + +select * from test_regex('[[:alnum:]]*[[:upper:]]*[\u1000-\u2000]*\u1237', + E'\u1500\u1237', 'ELMP'); +select * from test_regex('[[:alnum:]]*[[:upper:]]*[\u1000-\u2000]*\u1237', + E'A\u1239', 'ELMP'); +select * from test_regex('[[:alnum:]]*[[:upper:]]*[\u1000-\u2000]*\u1237', + E'\u1500\u1237', 'iELMP'); + +-- systematically test char classes +select * from test_regex('[[:alnum:]]+', E'x*\u1500\u1237', 'L'); +select * from test_regex('[[:alpha:]]+', E'x*\u1500\u1237', 'L'); +select * from test_regex('[[:ascii:]]+', E'x\u1500\u1237', 'L'); +select * from test_regex('[[:blank:]]+', E'x \t\u1500\u1237', 'L'); +select * from test_regex('[[:cntrl:]]+', E'x\u1500\u1237', 'L'); +select * from test_regex('[[:digit:]]+', E'x9\u1500\u1237', 'L'); +select * from test_regex('[[:graph:]]+', E'x\u1500\u1237', 'L'); +select * from test_regex('[[:lower:]]+', E'x\u1500\u1237', 'L'); +select * from test_regex('[[:print:]]+', E'x\u1500\u1237', 'L'); +select * from test_regex('[[:punct:]]+', E'x.\u1500\u1237', 'L'); +select * from test_regex('[[:space:]]+', E'x \t\u1500\u1237', 'L'); +select * from test_regex('[[:upper:]]+', E'xX\u1500\u1237', 'L'); +select * from test_regex('[[:xdigit:]]+', E'xa9\u1500\u1237', 'L'); +select * from test_regex('[[:word:]]+', E'x_*\u1500\u1237', 'L'); diff --git a/src/test/modules/test_regex/test_regex--1.0.sql b/src/test/modules/test_regex/test_regex--1.0.sql new file mode 100644 index 0000000..7d99153 --- /dev/null +++ b/src/test/modules/test_regex/test_regex--1.0.sql @@ -0,0 +1,9 @@ +/* src/test/modules/test_regex/test_regex--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_regex" to load this file. \quit + +CREATE FUNCTION test_regex(pattern text, string text, flags text) +RETURNS SETOF text[] +STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_regex/test_regex.c b/src/test/modules/test_regex/test_regex.c new file mode 100644 index 0000000..e23a0bd --- /dev/null +++ b/src/test/modules/test_regex/test_regex.c @@ -0,0 +1,773 @@ +/*-------------------------------------------------------------------------- + * + * test_regex.c + * Test harness for the regular expression package. + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/test/modules/test_regex/test_regex.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "funcapi.h" +#include "miscadmin.h" +#include "regex/regex.h" +#include "utils/array.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + + +/* all the options of interest for regex functions */ +typedef struct test_re_flags +{ + int cflags; /* compile flags for Spencer's regex code */ + int eflags; /* execute flags for Spencer's regex code */ + long info; /* expected re_info bits */ + bool glob; /* do it globally (for each occurrence) */ + bool indices; /* report indices not actual strings */ + bool partial; /* expect partial match */ +} test_re_flags; + +/* cross-call state for test_regex() */ +typedef struct test_regex_ctx +{ + test_re_flags re_flags; /* flags */ + rm_detail_t details; /* "details" from execution */ + text *orig_str; /* data string in original TEXT form */ + int nmatches; /* number of places where pattern matched */ + int npatterns; /* number of capturing subpatterns */ + /* We store start char index and end+1 char index for each match */ + /* so the number of entries in match_locs is nmatches * npatterns * 2 */ + int *match_locs; /* 0-based character indexes */ + int next_match; /* 0-based index of next match to process */ + /* workspace for build_test_match_result() */ + Datum *elems; /* has npatterns+1 elements */ + bool *nulls; /* has npatterns+1 elements */ + pg_wchar *wide_str; /* wide-char version of original string */ + char *conv_buf; /* conversion buffer, if needed */ + int conv_bufsiz; /* size thereof */ +} test_regex_ctx; + +/* Local functions */ +static void test_re_compile(text *text_re, int cflags, Oid collation, + regex_t *result_re); +static void parse_test_flags(test_re_flags *flags, text *opts); +static test_regex_ctx *setup_test_matches(text *orig_str, + regex_t *cpattern, + test_re_flags *flags, + Oid collation, + bool use_subpatterns); +static ArrayType *build_test_info_result(regex_t *cpattern, + test_re_flags *flags); +static ArrayType *build_test_match_result(test_regex_ctx *matchctx); + + +/* + * test_regex(pattern text, string text, flags text) returns setof text[] + * + * This is largely based on regexp.c's regexp_matches, with additions + * for debugging purposes. + */ +PG_FUNCTION_INFO_V1(test_regex); + +Datum +test_regex(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + test_regex_ctx *matchctx; + ArrayType *result_ary; + + if (SRF_IS_FIRSTCALL()) + { + text *pattern = PG_GETARG_TEXT_PP(0); + text *flags = PG_GETARG_TEXT_PP(2); + Oid collation = PG_GET_COLLATION(); + test_re_flags re_flags; + regex_t cpattern; + MemoryContext oldcontext; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + /* Determine options */ + parse_test_flags(&re_flags, flags); + + /* set up the compiled pattern */ + test_re_compile(pattern, re_flags.cflags, collation, &cpattern); + + /* be sure to copy the input string into the multi-call ctx */ + matchctx = setup_test_matches(PG_GETARG_TEXT_P_COPY(1), &cpattern, + &re_flags, + collation, + true); + + /* Pre-create workspace that build_test_match_result needs */ + matchctx->elems = (Datum *) palloc(sizeof(Datum) * + (matchctx->npatterns + 1)); + matchctx->nulls = (bool *) palloc(sizeof(bool) * + (matchctx->npatterns + 1)); + + MemoryContextSwitchTo(oldcontext); + funcctx->user_fctx = (void *) matchctx; + + /* + * Return the first result row, which is info equivalent to Tcl's + * "regexp -about" output + */ + result_ary = build_test_info_result(&cpattern, &re_flags); + + pg_regfree(&cpattern); + + SRF_RETURN_NEXT(funcctx, PointerGetDatum(result_ary)); + } + else + { + /* Each subsequent row describes one match */ + funcctx = SRF_PERCALL_SETUP(); + matchctx = (test_regex_ctx *) funcctx->user_fctx; + + if (matchctx->next_match < matchctx->nmatches) + { + result_ary = build_test_match_result(matchctx); + matchctx->next_match++; + SRF_RETURN_NEXT(funcctx, PointerGetDatum(result_ary)); + } + } + + SRF_RETURN_DONE(funcctx); +} + + +/* + * test_re_compile - compile a RE + * + * text_re --- the pattern, expressed as a TEXT object + * cflags --- compile options for the pattern + * collation --- collation to use for LC_CTYPE-dependent behavior + * result_re --- output, compiled RE is stored here + * + * Pattern is given in the database encoding. We internally convert to + * an array of pg_wchar, which is what Spencer's regex package wants. + * + * Caller must eventually pg_regfree the resulting RE to avoid memory leaks. + */ +static void +test_re_compile(text *text_re, int cflags, Oid collation, + regex_t *result_re) +{ + int text_re_len = VARSIZE_ANY_EXHDR(text_re); + char *text_re_val = VARDATA_ANY(text_re); + pg_wchar *pattern; + int pattern_len; + int regcomp_result; + char errMsg[100]; + + /* Convert pattern string to wide characters */ + pattern = (pg_wchar *) palloc((text_re_len + 1) * sizeof(pg_wchar)); + pattern_len = pg_mb2wchar_with_len(text_re_val, + pattern, + text_re_len); + + regcomp_result = pg_regcomp(result_re, + pattern, + pattern_len, + cflags, + collation); + + pfree(pattern); + + if (regcomp_result != REG_OKAY) + { + /* re didn't compile (no need for pg_regfree, if so) */ + + /* + * Here and in other places in this file, do CHECK_FOR_INTERRUPTS + * before reporting a regex error. This is so that if the regex + * library aborts and returns REG_CANCEL, we don't print an error + * message that implies the regex was invalid. + */ + CHECK_FOR_INTERRUPTS(); + + pg_regerror(regcomp_result, result_re, errMsg, sizeof(errMsg)); + ereport(ERROR, + (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION), + errmsg("invalid regular expression: %s", errMsg))); + } +} + +/* + * test_re_execute - execute a RE on pg_wchar data + * + * Returns true on match, false on no match + * Arguments are as for pg_regexec + */ +static bool +test_re_execute(regex_t *re, pg_wchar *data, int data_len, + int start_search, + rm_detail_t *details, + int nmatch, regmatch_t *pmatch, + int eflags) +{ + int regexec_result; + char errMsg[100]; + + /* Initialize match locations in case engine doesn't */ + details->rm_extend.rm_so = -1; + details->rm_extend.rm_eo = -1; + for (int i = 0; i < nmatch; i++) + { + pmatch[i].rm_so = -1; + pmatch[i].rm_eo = -1; + } + + /* Perform RE match and return result */ + regexec_result = pg_regexec(re, + data, + data_len, + start_search, + details, + nmatch, + pmatch, + eflags); + + if (regexec_result != REG_OKAY && regexec_result != REG_NOMATCH) + { + /* re failed??? */ + CHECK_FOR_INTERRUPTS(); + pg_regerror(regexec_result, re, errMsg, sizeof(errMsg)); + ereport(ERROR, + (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION), + errmsg("regular expression failed: %s", errMsg))); + } + + return (regexec_result == REG_OKAY); +} + + +/* + * parse_test_flags - parse the flags argument + * + * flags --- output argument, filled with desired options + * opts --- TEXT object, or NULL for defaults + */ +static void +parse_test_flags(test_re_flags *flags, text *opts) +{ + /* these defaults must match Tcl's */ + int cflags = REG_ADVANCED; + int eflags = 0; + long info = 0; + + flags->glob = false; + flags->indices = false; + flags->partial = false; + + if (opts) + { + char *opt_p = VARDATA_ANY(opts); + int opt_len = VARSIZE_ANY_EXHDR(opts); + int i; + + for (i = 0; i < opt_len; i++) + { + switch (opt_p[i]) + { + case '-': + /* allowed, no-op */ + break; + case '!': + flags->partial = true; + break; + case '*': + /* test requires Unicode --- ignored here */ + break; + case '0': + flags->indices = true; + break; + + /* These flags correspond to user-exposed RE options: */ + case 'g': /* global match */ + flags->glob = true; + break; + case 'i': /* case insensitive */ + cflags |= REG_ICASE; + break; + case 'n': /* \n affects ^ $ . [^ */ + cflags |= REG_NEWLINE; + break; + case 'p': /* ~Perl, \n affects . [^ */ + cflags |= REG_NLSTOP; + cflags &= ~REG_NLANCH; + break; + case 'w': /* weird, \n affects ^ $ only */ + cflags &= ~REG_NLSTOP; + cflags |= REG_NLANCH; + break; + case 'x': /* expanded syntax */ + cflags |= REG_EXPANDED; + break; + + /* These flags correspond to Tcl's -xflags options: */ + case 'a': + cflags |= REG_ADVF; + break; + case 'b': + cflags &= ~REG_ADVANCED; + break; + case 'c': + + /* + * Tcl calls this TCL_REG_CANMATCH, but it's really + * REG_EXPECT. In this implementation we must also set + * the partial and indices flags, so that + * setup_test_matches and build_test_match_result will + * emit the desired data. (They'll emit more fields than + * Tcl would, but that's fine.) + */ + cflags |= REG_EXPECT; + flags->partial = true; + flags->indices = true; + break; + case 'e': + cflags &= ~REG_ADVANCED; + cflags |= REG_EXTENDED; + break; + case 'q': + cflags &= ~REG_ADVANCED; + cflags |= REG_QUOTE; + break; + case 'o': /* o for opaque */ + cflags |= REG_NOSUB; + break; + case 's': /* s for start */ + cflags |= REG_BOSONLY; + break; + case '+': + cflags |= REG_FAKE; + break; + case ',': + cflags |= REG_PROGRESS; + break; + case '.': + cflags |= REG_DUMP; + break; + case ':': + eflags |= REG_MTRACE; + break; + case ';': + eflags |= REG_FTRACE; + break; + case '^': + eflags |= REG_NOTBOL; + break; + case '$': + eflags |= REG_NOTEOL; + break; + case 't': + cflags |= REG_EXPECT; + break; + case '%': + eflags |= REG_SMALL; + break; + + /* These flags define expected info bits: */ + case 'A': + info |= REG_UBSALNUM; + break; + case 'B': + info |= REG_UBRACES; + break; + case 'E': + info |= REG_UBBS; + break; + case 'H': + info |= REG_ULOOKAROUND; + break; + case 'I': + info |= REG_UIMPOSSIBLE; + break; + case 'L': + info |= REG_ULOCALE; + break; + case 'M': + info |= REG_UUNPORT; + break; + case 'N': + info |= REG_UEMPTYMATCH; + break; + case 'P': + info |= REG_UNONPOSIX; + break; + case 'Q': + info |= REG_UBOUNDS; + break; + case 'R': + info |= REG_UBACKREF; + break; + case 'S': + info |= REG_UUNSPEC; + break; + case 'T': + info |= REG_USHORTEST; + break; + case 'U': + info |= REG_UPBOTCH; + break; + + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid regular expression test option: \"%.*s\"", + pg_mblen(opt_p + i), opt_p + i))); + break; + } + } + } + flags->cflags = cflags; + flags->eflags = eflags; + flags->info = info; +} + +/* + * setup_test_matches --- do the initial matching + * + * To simplify memory management, we do all the matching in one swoop. + * The returned test_regex_ctx contains the locations of all the substrings + * matching the pattern. + */ +static test_regex_ctx * +setup_test_matches(text *orig_str, + regex_t *cpattern, test_re_flags *re_flags, + Oid collation, + bool use_subpatterns) +{ + test_regex_ctx *matchctx = palloc0(sizeof(test_regex_ctx)); + int eml = pg_database_encoding_max_length(); + int orig_len; + pg_wchar *wide_str; + int wide_len; + regmatch_t *pmatch; + int pmatch_len; + int array_len; + int array_idx; + int prev_match_end; + int start_search; + int maxlen = 0; /* largest fetch length in characters */ + + /* save flags */ + matchctx->re_flags = *re_flags; + + /* save original string --- we'll extract result substrings from it */ + matchctx->orig_str = orig_str; + + /* convert string to pg_wchar form for matching */ + orig_len = VARSIZE_ANY_EXHDR(orig_str); + wide_str = (pg_wchar *) palloc(sizeof(pg_wchar) * (orig_len + 1)); + wide_len = pg_mb2wchar_with_len(VARDATA_ANY(orig_str), wide_str, orig_len); + + /* do we want to remember subpatterns? */ + if (use_subpatterns && cpattern->re_nsub > 0) + { + matchctx->npatterns = cpattern->re_nsub + 1; + pmatch_len = cpattern->re_nsub + 1; + } + else + { + use_subpatterns = false; + matchctx->npatterns = 1; + pmatch_len = 1; + } + + /* temporary output space for RE package */ + pmatch = palloc(sizeof(regmatch_t) * pmatch_len); + + /* + * the real output space (grown dynamically if needed) + * + * use values 2^n-1, not 2^n, so that we hit the limit at 2^28-1 rather + * than at 2^27 + */ + array_len = re_flags->glob ? 255 : 31; + matchctx->match_locs = (int *) palloc(sizeof(int) * array_len); + array_idx = 0; + + /* search for the pattern, perhaps repeatedly */ + prev_match_end = 0; + start_search = 0; + while (test_re_execute(cpattern, wide_str, wide_len, + start_search, + &matchctx->details, + pmatch_len, pmatch, + re_flags->eflags)) + { + /* enlarge output space if needed */ + while (array_idx + matchctx->npatterns * 2 + 1 > array_len) + { + array_len += array_len + 1; /* 2^n-1 => 2^(n+1)-1 */ + if (array_len > MaxAllocSize / sizeof(int)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many regular expression matches"))); + matchctx->match_locs = (int *) repalloc(matchctx->match_locs, + sizeof(int) * array_len); + } + + /* save this match's locations */ + for (int i = 0; i < matchctx->npatterns; i++) + { + int so = pmatch[i].rm_so; + int eo = pmatch[i].rm_eo; + + matchctx->match_locs[array_idx++] = so; + matchctx->match_locs[array_idx++] = eo; + if (so >= 0 && eo >= 0 && (eo - so) > maxlen) + maxlen = (eo - so); + } + matchctx->nmatches++; + prev_match_end = pmatch[0].rm_eo; + + /* if not glob, stop after one match */ + if (!re_flags->glob) + break; + + /* + * Advance search position. Normally we start the next search at the + * end of the previous match; but if the match was of zero length, we + * have to advance by one character, or we'd just find the same match + * again. + */ + start_search = prev_match_end; + if (pmatch[0].rm_so == pmatch[0].rm_eo) + start_search++; + if (start_search > wide_len) + break; + } + + /* + * If we had no match, but "partial" and "indices" are set, emit the + * details. + */ + if (matchctx->nmatches == 0 && re_flags->partial && re_flags->indices) + { + /* enlarge output space if needed */ + while (array_idx + matchctx->npatterns * 2 + 1 > array_len) + { + array_len += array_len + 1; /* 2^n-1 => 2^(n+1)-1 */ + if (array_len > MaxAllocSize / sizeof(int)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many regular expression matches"))); + matchctx->match_locs = (int *) repalloc(matchctx->match_locs, + sizeof(int) * array_len); + } + + matchctx->match_locs[array_idx++] = matchctx->details.rm_extend.rm_so; + matchctx->match_locs[array_idx++] = matchctx->details.rm_extend.rm_eo; + /* we don't have pmatch data, so emit -1 */ + for (int i = 1; i < matchctx->npatterns; i++) + { + matchctx->match_locs[array_idx++] = -1; + matchctx->match_locs[array_idx++] = -1; + } + matchctx->nmatches++; + } + + Assert(array_idx <= array_len); + + if (eml > 1) + { + int64 maxsiz = eml * (int64) maxlen; + int conv_bufsiz; + + /* + * Make the conversion buffer large enough for any substring of + * interest. + * + * Worst case: assume we need the maximum size (maxlen*eml), but take + * advantage of the fact that the original string length in bytes is + * an upper bound on the byte length of any fetched substring (and we + * know that len+1 is safe to allocate because the varlena header is + * longer than 1 byte). + */ + if (maxsiz > orig_len) + conv_bufsiz = orig_len + 1; + else + conv_bufsiz = maxsiz + 1; /* safe since maxsiz < 2^30 */ + + matchctx->conv_buf = palloc(conv_bufsiz); + matchctx->conv_bufsiz = conv_bufsiz; + matchctx->wide_str = wide_str; + } + else + { + /* No need to keep the wide string if we're in a single-byte charset. */ + pfree(wide_str); + matchctx->wide_str = NULL; + matchctx->conv_buf = NULL; + matchctx->conv_bufsiz = 0; + } + + /* Clean up temp storage */ + pfree(pmatch); + + return matchctx; +} + +/* + * build_test_info_result - build output array describing compiled regexp + * + * This borrows some code from Tcl's TclRegAbout(). + */ +static ArrayType * +build_test_info_result(regex_t *cpattern, test_re_flags *flags) +{ + /* Translation data for flag bits in regex_t.re_info */ + struct infoname + { + int bit; + const char *text; + }; + static const struct infoname infonames[] = { + {REG_UBACKREF, "REG_UBACKREF"}, + {REG_ULOOKAROUND, "REG_ULOOKAROUND"}, + {REG_UBOUNDS, "REG_UBOUNDS"}, + {REG_UBRACES, "REG_UBRACES"}, + {REG_UBSALNUM, "REG_UBSALNUM"}, + {REG_UPBOTCH, "REG_UPBOTCH"}, + {REG_UBBS, "REG_UBBS"}, + {REG_UNONPOSIX, "REG_UNONPOSIX"}, + {REG_UUNSPEC, "REG_UUNSPEC"}, + {REG_UUNPORT, "REG_UUNPORT"}, + {REG_ULOCALE, "REG_ULOCALE"}, + {REG_UEMPTYMATCH, "REG_UEMPTYMATCH"}, + {REG_UIMPOSSIBLE, "REG_UIMPOSSIBLE"}, + {REG_USHORTEST, "REG_USHORTEST"}, + {0, NULL} + }; + const struct infoname *inf; + Datum elems[lengthof(infonames) + 1]; + int nresults = 0; + char buf[80]; + int dims[1]; + int lbs[1]; + + /* Set up results: first, the number of subexpressions */ + snprintf(buf, sizeof(buf), "%d", (int) cpattern->re_nsub); + elems[nresults++] = PointerGetDatum(cstring_to_text(buf)); + + /* Report individual info bit states */ + for (inf = infonames; inf->bit != 0; inf++) + { + if (cpattern->re_info & inf->bit) + { + if (flags->info & inf->bit) + elems[nresults++] = PointerGetDatum(cstring_to_text(inf->text)); + else + { + snprintf(buf, sizeof(buf), "unexpected %s!", inf->text); + elems[nresults++] = PointerGetDatum(cstring_to_text(buf)); + } + } + else + { + if (flags->info & inf->bit) + { + snprintf(buf, sizeof(buf), "missing %s!", inf->text); + elems[nresults++] = PointerGetDatum(cstring_to_text(buf)); + } + } + } + + /* And form an array */ + dims[0] = nresults; + lbs[0] = 1; + /* XXX: this hardcodes assumptions about the text type */ + return construct_md_array(elems, NULL, 1, dims, lbs, + TEXTOID, -1, false, TYPALIGN_INT); +} + +/* + * build_test_match_result - build output array for current match + * + * Note that if the indices flag is set, we don't need any strings, + * just the location data. + */ +static ArrayType * +build_test_match_result(test_regex_ctx *matchctx) +{ + char *buf = matchctx->conv_buf; + Datum *elems = matchctx->elems; + bool *nulls = matchctx->nulls; + bool indices = matchctx->re_flags.indices; + char bufstr[80]; + int dims[1]; + int lbs[1]; + int loc; + int i; + + /* Extract matching substrings from the original string */ + loc = matchctx->next_match * matchctx->npatterns * 2; + for (i = 0; i < matchctx->npatterns; i++) + { + int so = matchctx->match_locs[loc++]; + int eo = matchctx->match_locs[loc++]; + + if (indices) + { + /* Report eo this way for consistency with Tcl */ + snprintf(bufstr, sizeof(bufstr), "%d %d", + so, so < 0 ? eo : eo - 1); + elems[i] = PointerGetDatum(cstring_to_text(bufstr)); + nulls[i] = false; + } + else if (so < 0 || eo < 0) + { + elems[i] = (Datum) 0; + nulls[i] = true; + } + else if (buf) + { + int len = pg_wchar2mb_with_len(matchctx->wide_str + so, + buf, + eo - so); + + Assert(len < matchctx->conv_bufsiz); + elems[i] = PointerGetDatum(cstring_to_text_with_len(buf, len)); + nulls[i] = false; + } + else + { + elems[i] = DirectFunctionCall3(text_substr, + PointerGetDatum(matchctx->orig_str), + Int32GetDatum(so + 1), + Int32GetDatum(eo - so)); + nulls[i] = false; + } + } + + /* In EXPECT indices mode, also report the "details" */ + if (indices && (matchctx->re_flags.cflags & REG_EXPECT)) + { + int so = matchctx->details.rm_extend.rm_so; + int eo = matchctx->details.rm_extend.rm_eo; + + snprintf(bufstr, sizeof(bufstr), "%d %d", + so, so < 0 ? eo : eo - 1); + elems[i] = PointerGetDatum(cstring_to_text(bufstr)); + nulls[i] = false; + i++; + } + + /* And form an array */ + dims[0] = i; + lbs[0] = 1; + /* XXX: this hardcodes assumptions about the text type */ + return construct_md_array(elems, nulls, 1, dims, lbs, + TEXTOID, -1, false, TYPALIGN_INT); +} diff --git a/src/test/modules/test_regex/test_regex.control b/src/test/modules/test_regex/test_regex.control new file mode 100644 index 0000000..bfce100 --- /dev/null +++ b/src/test/modules/test_regex/test_regex.control @@ -0,0 +1,4 @@ +comment = 'Test code for backend/regex/' +default_version = '1.0' +module_pathname = '$libdir/test_regex' +relocatable = true diff --git a/src/test/modules/test_rls_hooks/.gitignore b/src/test/modules/test_rls_hooks/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_rls_hooks/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_rls_hooks/Makefile b/src/test/modules/test_rls_hooks/Makefile new file mode 100644 index 0000000..14ccc35 --- /dev/null +++ b/src/test/modules/test_rls_hooks/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/test_rls_hooks/Makefile + +MODULE_big = test_rls_hooks +OBJS = \ + $(WIN32RES) \ + test_rls_hooks.o +PGFILEDESC = "test_rls_hooks - example use of RLS hooks" + +REGRESS = test_rls_hooks + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_rls_hooks +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_rls_hooks/README b/src/test/modules/test_rls_hooks/README new file mode 100644 index 0000000..c22e0d3 --- /dev/null +++ b/src/test/modules/test_rls_hooks/README @@ -0,0 +1,16 @@ +test_rls_hooks is an example of how to use the hooks provided for RLS to +define additional policies to be used. + +Functions +========= +test_rls_hooks_permissive(CmdType cmdtype, Relation relation) + RETURNS List* + +Returns a list of policies which should be added to any existing +policies on the relation, combined with OR. + +test_rls_hooks_restrictive(CmdType cmdtype, Relation relation) + RETURNS List* + +Returns a list of policies which should be added to any existing +policies on the relation, combined with AND. diff --git a/src/test/modules/test_rls_hooks/expected/test_rls_hooks.out b/src/test/modules/test_rls_hooks/expected/test_rls_hooks.out new file mode 100644 index 0000000..b8c6d38 --- /dev/null +++ b/src/test/modules/test_rls_hooks/expected/test_rls_hooks.out @@ -0,0 +1,201 @@ +LOAD 'test_rls_hooks'; +CREATE TABLE rls_test_permissive ( + username name, + supervisor name, + data integer +); +-- initial test data +INSERT INTO rls_test_permissive VALUES ('regress_r1','regress_s1',4); +INSERT INTO rls_test_permissive VALUES ('regress_r2','regress_s2',5); +INSERT INTO rls_test_permissive VALUES ('regress_r3','regress_s3',6); +CREATE TABLE rls_test_restrictive ( + username name, + supervisor name, + data integer +); +-- At least one permissive policy must exist, otherwise +-- the default deny policy will be applied. For +-- testing the only-restrictive-policies from the hook, +-- create a simple 'allow all' policy. +CREATE POLICY p1 ON rls_test_restrictive USING (true); +-- initial test data +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',1); +INSERT INTO rls_test_restrictive VALUES ('regress_r2','regress_s2',2); +INSERT INTO rls_test_restrictive VALUES ('regress_r3','regress_s3',3); +CREATE TABLE rls_test_both ( + username name, + supervisor name, + data integer +); +-- initial test data +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',7); +INSERT INTO rls_test_both VALUES ('regress_r2','regress_s2',8); +INSERT INTO rls_test_both VALUES ('regress_r3','regress_s3',9); +ALTER TABLE rls_test_permissive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_restrictive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_both ENABLE ROW LEVEL SECURITY; +CREATE ROLE regress_r1; +CREATE ROLE regress_s1; +GRANT SELECT,INSERT ON rls_test_permissive TO regress_r1; +GRANT SELECT,INSERT ON rls_test_restrictive TO regress_r1; +GRANT SELECT,INSERT ON rls_test_both TO regress_r1; +GRANT SELECT,INSERT ON rls_test_permissive TO regress_s1; +GRANT SELECT,INSERT ON rls_test_restrictive TO regress_s1; +GRANT SELECT,INSERT ON rls_test_both TO regress_s1; +SET ROLE regress_r1; +-- With only the hook's policies, permissive +-- hook's policy is current_user = username +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + QUERY PLAN +----------------------------------------- + Seq Scan on rls_test_permissive + Filter: ("current_user"() = username) +(2 rows) + +SELECT * FROM rls_test_permissive; + username | supervisor | data +------------+------------+------ + regress_r1 | regress_s1 | 4 +(1 row) + +-- success +INSERT INTO rls_test_permissive VALUES ('regress_r1','regress_s1',10); +-- failure +INSERT INTO rls_test_permissive VALUES ('regress_r4','regress_s4',10); +ERROR: new row violates row-level security policy for table "rls_test_permissive" +SET ROLE regress_s1; +-- With only the hook's policies, restrictive +-- hook's policy is current_user = supervisor +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + QUERY PLAN +------------------------------------------- + Seq Scan on rls_test_restrictive + Filter: ("current_user"() = supervisor) +(2 rows) + +SELECT * FROM rls_test_restrictive; + username | supervisor | data +------------+------------+------ + regress_r1 | regress_s1 | 1 +(1 row) + +-- success +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',10); +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r4','regress_s4',10); +ERROR: new row violates row-level security policy "extension policy" for table "rls_test_restrictive" +SET ROLE regress_s1; +-- With only the hook's policies, both +-- permissive hook's policy is current_user = username +-- restrictive hook's policy is current_user = superuser +-- combined with AND, results in nothing being allowed +EXPLAIN (costs off) SELECT * FROM rls_test_both; + QUERY PLAN +------------------------------------------------------------------------------- + Seq Scan on rls_test_both + Filter: ((supervisor = "current_user"()) AND (username = "current_user"())) +(2 rows) + +SELECT * FROM rls_test_both; + username | supervisor | data +----------+------------+------ +(0 rows) + +-- failure +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',10); +ERROR: new row violates row-level security policy for table "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('regress_r4','regress_s1',10); +ERROR: new row violates row-level security policy for table "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('regress_r4','regress_s4',10); +ERROR: new row violates row-level security policy for table "rls_test_both" +RESET ROLE; +-- Create "internal" policies, to check that the policies from +-- the hooks are combined correctly. +CREATE POLICY p1 ON rls_test_permissive USING (data % 2 = 0); +-- Remove the original allow-all policy +DROP POLICY p1 ON rls_test_restrictive; +CREATE POLICY p1 ON rls_test_restrictive USING (data % 2 = 0); +CREATE POLICY p1 ON rls_test_both USING (data % 2 = 0); +SET ROLE regress_r1; +-- With both internal and hook policies, permissive +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + QUERY PLAN +--------------------------------------------------------------- + Seq Scan on rls_test_permissive + Filter: (((data % 2) = 0) OR ("current_user"() = username)) +(2 rows) + +SELECT * FROM rls_test_permissive; + username | supervisor | data +------------+------------+------ + regress_r1 | regress_s1 | 4 + regress_r3 | regress_s3 | 6 + regress_r1 | regress_s1 | 10 +(3 rows) + +-- success +INSERT INTO rls_test_permissive VALUES ('regress_r1','regress_s1',7); +-- success +INSERT INTO rls_test_permissive VALUES ('regress_r3','regress_s3',10); +-- failure +INSERT INTO rls_test_permissive VALUES ('regress_r4','regress_s4',7); +ERROR: new row violates row-level security policy for table "rls_test_permissive" +SET ROLE regress_s1; +-- With both internal and hook policies, restrictive +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + QUERY PLAN +------------------------------------------------------------------ + Seq Scan on rls_test_restrictive + Filter: (("current_user"() = supervisor) AND ((data % 2) = 0)) +(2 rows) + +SELECT * FROM rls_test_restrictive; + username | supervisor | data +------------+------------+------ + regress_r1 | regress_s1 | 10 +(1 row) + +-- success +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',8); +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r3','regress_s3',10); +ERROR: new row violates row-level security policy "extension policy" for table "rls_test_restrictive" +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',7); +ERROR: new row violates row-level security policy for table "rls_test_restrictive" +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r4','regress_s4',7); +ERROR: new row violates row-level security policy for table "rls_test_restrictive" +-- With both internal and hook policies, both permissive +-- and restrictive hook policies +EXPLAIN (costs off) SELECT * FROM rls_test_both; + QUERY PLAN +----------------------------------------------------------------------------------------------------- + Seq Scan on rls_test_both + Filter: (("current_user"() = supervisor) AND (((data % 2) = 0) OR ("current_user"() = username))) +(2 rows) + +SELECT * FROM rls_test_both; + username | supervisor | data +----------+------------+------ +(0 rows) + +-- success +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',8); +-- failure +INSERT INTO rls_test_both VALUES ('regress_r3','regress_s3',10); +ERROR: new row violates row-level security policy "extension policy" for table "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',7); +ERROR: new row violates row-level security policy for table "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('regress_r4','regress_s4',7); +ERROR: new row violates row-level security policy for table "rls_test_both" +RESET ROLE; +DROP TABLE rls_test_restrictive; +DROP TABLE rls_test_permissive; +DROP TABLE rls_test_both; +DROP ROLE regress_r1; +DROP ROLE regress_s1; diff --git a/src/test/modules/test_rls_hooks/sql/test_rls_hooks.sql b/src/test/modules/test_rls_hooks/sql/test_rls_hooks.sql new file mode 100644 index 0000000..746f6dd --- /dev/null +++ b/src/test/modules/test_rls_hooks/sql/test_rls_hooks.sql @@ -0,0 +1,176 @@ +LOAD 'test_rls_hooks'; + +CREATE TABLE rls_test_permissive ( + username name, + supervisor name, + data integer +); + +-- initial test data +INSERT INTO rls_test_permissive VALUES ('regress_r1','regress_s1',4); +INSERT INTO rls_test_permissive VALUES ('regress_r2','regress_s2',5); +INSERT INTO rls_test_permissive VALUES ('regress_r3','regress_s3',6); + +CREATE TABLE rls_test_restrictive ( + username name, + supervisor name, + data integer +); + +-- At least one permissive policy must exist, otherwise +-- the default deny policy will be applied. For +-- testing the only-restrictive-policies from the hook, +-- create a simple 'allow all' policy. +CREATE POLICY p1 ON rls_test_restrictive USING (true); + +-- initial test data +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',1); +INSERT INTO rls_test_restrictive VALUES ('regress_r2','regress_s2',2); +INSERT INTO rls_test_restrictive VALUES ('regress_r3','regress_s3',3); + +CREATE TABLE rls_test_both ( + username name, + supervisor name, + data integer +); + +-- initial test data +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',7); +INSERT INTO rls_test_both VALUES ('regress_r2','regress_s2',8); +INSERT INTO rls_test_both VALUES ('regress_r3','regress_s3',9); + +ALTER TABLE rls_test_permissive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_restrictive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_both ENABLE ROW LEVEL SECURITY; + +CREATE ROLE regress_r1; +CREATE ROLE regress_s1; + +GRANT SELECT,INSERT ON rls_test_permissive TO regress_r1; +GRANT SELECT,INSERT ON rls_test_restrictive TO regress_r1; +GRANT SELECT,INSERT ON rls_test_both TO regress_r1; + +GRANT SELECT,INSERT ON rls_test_permissive TO regress_s1; +GRANT SELECT,INSERT ON rls_test_restrictive TO regress_s1; +GRANT SELECT,INSERT ON rls_test_both TO regress_s1; + +SET ROLE regress_r1; + +-- With only the hook's policies, permissive +-- hook's policy is current_user = username +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + +SELECT * FROM rls_test_permissive; + +-- success +INSERT INTO rls_test_permissive VALUES ('regress_r1','regress_s1',10); + +-- failure +INSERT INTO rls_test_permissive VALUES ('regress_r4','regress_s4',10); + +SET ROLE regress_s1; + +-- With only the hook's policies, restrictive +-- hook's policy is current_user = supervisor +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + +SELECT * FROM rls_test_restrictive; + +-- success +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',10); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r4','regress_s4',10); + +SET ROLE regress_s1; + +-- With only the hook's policies, both +-- permissive hook's policy is current_user = username +-- restrictive hook's policy is current_user = superuser +-- combined with AND, results in nothing being allowed +EXPLAIN (costs off) SELECT * FROM rls_test_both; + +SELECT * FROM rls_test_both; + +-- failure +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',10); + +-- failure +INSERT INTO rls_test_both VALUES ('regress_r4','regress_s1',10); + +-- failure +INSERT INTO rls_test_both VALUES ('regress_r4','regress_s4',10); + +RESET ROLE; + +-- Create "internal" policies, to check that the policies from +-- the hooks are combined correctly. +CREATE POLICY p1 ON rls_test_permissive USING (data % 2 = 0); + +-- Remove the original allow-all policy +DROP POLICY p1 ON rls_test_restrictive; +CREATE POLICY p1 ON rls_test_restrictive USING (data % 2 = 0); + +CREATE POLICY p1 ON rls_test_both USING (data % 2 = 0); + +SET ROLE regress_r1; + +-- With both internal and hook policies, permissive +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + +SELECT * FROM rls_test_permissive; + +-- success +INSERT INTO rls_test_permissive VALUES ('regress_r1','regress_s1',7); + +-- success +INSERT INTO rls_test_permissive VALUES ('regress_r3','regress_s3',10); + +-- failure +INSERT INTO rls_test_permissive VALUES ('regress_r4','regress_s4',7); + +SET ROLE regress_s1; + +-- With both internal and hook policies, restrictive +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + +SELECT * FROM rls_test_restrictive; + +-- success +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',8); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r3','regress_s3',10); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r1','regress_s1',7); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('regress_r4','regress_s4',7); + +-- With both internal and hook policies, both permissive +-- and restrictive hook policies +EXPLAIN (costs off) SELECT * FROM rls_test_both; + +SELECT * FROM rls_test_both; + +-- success +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',8); + +-- failure +INSERT INTO rls_test_both VALUES ('regress_r3','regress_s3',10); + +-- failure +INSERT INTO rls_test_both VALUES ('regress_r1','regress_s1',7); + +-- failure +INSERT INTO rls_test_both VALUES ('regress_r4','regress_s4',7); + +RESET ROLE; + +DROP TABLE rls_test_restrictive; +DROP TABLE rls_test_permissive; +DROP TABLE rls_test_both; + +DROP ROLE regress_r1; +DROP ROLE regress_s1; diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.c b/src/test/modules/test_rls_hooks/test_rls_hooks.c new file mode 100644 index 0000000..b8e0aa2 --- /dev/null +++ b/src/test/modules/test_rls_hooks/test_rls_hooks.c @@ -0,0 +1,167 @@ +/*-------------------------------------------------------------------------- + * + * test_rls_hooks.c + * Code for testing RLS hooks. + * + * Copyright (c) 2015-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_rls_hooks/test_rls_hooks.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_type.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_node.h" +#include "parser/parse_relation.h" +#include "rewrite/rowsecurity.h" +#include "test_rls_hooks.h" +#include "utils/acl.h" +#include "utils/rel.h" +#include "utils/relcache.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +/* Install hooks */ +void +_PG_init(void) +{ + /* Set our hooks */ + row_security_policy_hook_permissive = test_rls_hooks_permissive; + row_security_policy_hook_restrictive = test_rls_hooks_restrictive; +} + +/* + * Return permissive policies to be added + */ +List * +test_rls_hooks_permissive(CmdType cmdtype, Relation relation) +{ + List *policies = NIL; + RowSecurityPolicy *policy = palloc0(sizeof(RowSecurityPolicy)); + Datum role; + FuncCall *n; + Node *e; + ColumnRef *c; + ParseState *qual_pstate; + ParseNamespaceItem *nsitem; + + if (strcmp(RelationGetRelationName(relation), "rls_test_permissive") != 0 && + strcmp(RelationGetRelationName(relation), "rls_test_both") != 0) + return NIL; + + qual_pstate = make_parsestate(NULL); + + nsitem = addRangeTableEntryForRelation(qual_pstate, + relation, AccessShareLock, + NULL, false, false); + addNSItemToQuery(qual_pstate, nsitem, false, true, true); + + role = ObjectIdGetDatum(ACL_ID_PUBLIC); + + policy->policy_name = pstrdup("extension policy"); + policy->polcmd = '*'; + policy->roles = construct_array(&role, 1, OIDOID, sizeof(Oid), true, TYPALIGN_INT); + + /* + * policy->qual = (Expr *) makeConst(BOOLOID, -1, InvalidOid, + * sizeof(bool), BoolGetDatum(true), false, true); + */ + + n = makeFuncCall(list_make2(makeString("pg_catalog"), + makeString("current_user")), + NIL, + COERCE_EXPLICIT_CALL, + -1); + + c = makeNode(ColumnRef); + c->fields = list_make1(makeString("username")); + c->location = 0; + + e = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", (Node *) n, (Node *) c, 0); + + policy->qual = (Expr *) transformWhereClause(qual_pstate, copyObject(e), + EXPR_KIND_POLICY, + "POLICY"); + /* Fix up collation information */ + assign_expr_collations(qual_pstate, (Node *) policy->qual); + + policy->with_check_qual = copyObject(policy->qual); + policy->hassublinks = false; + + policies = list_make1(policy); + + return policies; +} + +/* + * Return restrictive policies to be added + * + * Note that a permissive policy must exist or the default-deny policy + * will be included and nothing will be visible. If no filtering should + * be done except for the restrictive policy, then a single "USING (true)" + * permissive policy can be used; see the regression tests. + */ +List * +test_rls_hooks_restrictive(CmdType cmdtype, Relation relation) +{ + List *policies = NIL; + RowSecurityPolicy *policy = palloc0(sizeof(RowSecurityPolicy)); + Datum role; + FuncCall *n; + Node *e; + ColumnRef *c; + ParseState *qual_pstate; + ParseNamespaceItem *nsitem; + + if (strcmp(RelationGetRelationName(relation), "rls_test_restrictive") != 0 && + strcmp(RelationGetRelationName(relation), "rls_test_both") != 0) + return NIL; + + qual_pstate = make_parsestate(NULL); + + nsitem = addRangeTableEntryForRelation(qual_pstate, + relation, AccessShareLock, + NULL, false, false); + addNSItemToQuery(qual_pstate, nsitem, false, true, true); + + role = ObjectIdGetDatum(ACL_ID_PUBLIC); + + policy->policy_name = pstrdup("extension policy"); + policy->polcmd = '*'; + policy->roles = construct_array(&role, 1, OIDOID, sizeof(Oid), true, TYPALIGN_INT); + + n = makeFuncCall(list_make2(makeString("pg_catalog"), + makeString("current_user")), + NIL, + COERCE_EXPLICIT_CALL, + -1); + + c = makeNode(ColumnRef); + c->fields = list_make1(makeString("supervisor")); + c->location = 0; + + e = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", (Node *) n, (Node *) c, 0); + + policy->qual = (Expr *) transformWhereClause(qual_pstate, copyObject(e), + EXPR_KIND_POLICY, + "POLICY"); + /* Fix up collation information */ + assign_expr_collations(qual_pstate, (Node *) policy->qual); + + policy->with_check_qual = copyObject(policy->qual); + policy->hassublinks = false; + + policies = list_make1(policy); + + return policies; +} diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.h b/src/test/modules/test_rls_hooks/test_rls_hooks.h new file mode 100644 index 0000000..d4dd107 --- /dev/null +++ b/src/test/modules/test_rls_hooks/test_rls_hooks.h @@ -0,0 +1,25 @@ +/*-------------------------------------------------------------------------- + * + * test_rls_hooks.h + * Definitions for RLS hooks + * + * Copyright (c) 2015-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_rls_hooks/test_rls_hooks.h + * + * ------------------------------------------------------------------------- + */ + +#ifndef TEST_RLS_HOOKS_H +#define TEST_RLS_HOOKS_H + +#include <rewrite/rowsecurity.h> + +/* Return set of permissive hooks based on CmdType and Relation */ +extern List *test_rls_hooks_permissive(CmdType cmdtype, Relation relation); + +/* Return set of restrictive hooks based on CmdType and Relation */ +extern List *test_rls_hooks_restrictive(CmdType cmdtype, Relation relation); + +#endif diff --git a/src/test/modules/test_shm_mq/.gitignore b/src/test/modules/test_shm_mq/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/test_shm_mq/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_shm_mq/Makefile b/src/test/modules/test_shm_mq/Makefile new file mode 100644 index 0000000..1171ced --- /dev/null +++ b/src/test/modules/test_shm_mq/Makefile @@ -0,0 +1,25 @@ +# src/test/modules/test_shm_mq/Makefile + +MODULE_big = test_shm_mq +OBJS = \ + $(WIN32RES) \ + setup.o \ + test.o \ + worker.o +PGFILEDESC = "test_shm_mq - example use of shared memory message queue" + +EXTENSION = test_shm_mq +DATA = test_shm_mq--1.0.sql + +REGRESS = test_shm_mq + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_shm_mq +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_shm_mq/README b/src/test/modules/test_shm_mq/README new file mode 100644 index 0000000..641407b --- /dev/null +++ b/src/test/modules/test_shm_mq/README @@ -0,0 +1,49 @@ +test_shm_mq is an example of how to use dynamic shared memory +and the shared memory message queue facilities to coordinate a user backend +with the efforts of one or more background workers. It is not intended to +do anything useful on its own; rather, it is a demonstration of how these +facilities can be used, and a unit test of those facilities. + +The function is this extension send the same message repeatedly through +a loop of processes. The message payload, the size of the message queue +through which it is sent, and the number of processes in the loop are +configurable. At the end, the message may be verified to ensure that it +has not been corrupted in transmission. + +Functions +========= + + +test_shm_mq(queue_size int8, message text, + repeat_count int4 default 1, num_workers int4 default 1) + RETURNS void + +This function sends and receives messages synchronously. The user +backend sends the provided message to the first background worker using +a message queue of the given size. The first background worker sends +the message to the second background worker, if the number of workers +is greater than one, and so forth. Eventually, the last background +worker sends the message back to the user backend. If the repeat count +is greater than one, the user backend then sends the message back to +the first worker. Once the message has been sent and received by all +the coordinating processes a number of times equal to the repeat count, +the user backend verifies that the message finally received matches the +one originally sent and throws an error if not. + + +test_shm_mq_pipelined(queue_size int8, message text, + repeat_count int4 default 1, num_workers int4 default 1, + verify bool default true) + RETURNS void + +This function sends the same message multiple times, as specified by the +repeat count, to the first background worker using a queue of the given +size. These messages are then forwarded to each background worker in +turn, in each case using a queue of the given size. Finally, the last +background worker sends the messages back to the user backend. The user +backend uses non-blocking sends and receives, so that it may begin receiving +copies of the message before it has finished sending all copies of the +message. The 'verify' argument controls whether or not the +received copies are checked against the message that was sent. (This +takes nontrivial time so it may be useful to disable it for benchmarking +purposes.) diff --git a/src/test/modules/test_shm_mq/expected/test_shm_mq.out b/src/test/modules/test_shm_mq/expected/test_shm_mq.out new file mode 100644 index 0000000..c4858b0 --- /dev/null +++ b/src/test/modules/test_shm_mq/expected/test_shm_mq.out @@ -0,0 +1,36 @@ +CREATE EXTENSION test_shm_mq; +-- +-- These tests don't produce any interesting output. We're checking that +-- the operations complete without crashing or hanging and that none of their +-- internal sanity tests fail. +-- +SELECT test_shm_mq(1024, '', 2000, 1); + test_shm_mq +------------- + +(1 row) + +SELECT test_shm_mq(1024, 'a', 2001, 1); + test_shm_mq +------------- + +(1 row) + +SELECT test_shm_mq(32768, (select string_agg(chr(32+(random()*95)::int), '') from generate_series(1,(100+900*random())::int)), 10000, 1); + test_shm_mq +------------- + +(1 row) + +SELECT test_shm_mq(100, (select string_agg(chr(32+(random()*95)::int), '') from generate_series(1,(100+200*random())::int)), 10000, 1); + test_shm_mq +------------- + +(1 row) + +SELECT test_shm_mq_pipelined(16384, (select string_agg(chr(32+(random()*95)::int), '') from generate_series(1,270000)), 200, 3); + test_shm_mq_pipelined +----------------------- + +(1 row) + diff --git a/src/test/modules/test_shm_mq/setup.c b/src/test/modules/test_shm_mq/setup.c new file mode 100644 index 0000000..6f1f4cf --- /dev/null +++ b/src/test/modules/test_shm_mq/setup.c @@ -0,0 +1,316 @@ +/*-------------------------------------------------------------------------- + * + * setup.c + * Code to set up a dynamic shared memory segments and a specified + * number of background workers for shared memory message queue + * testing. + * + * Copyright (c) 2013-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_shm_mq/setup.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "miscadmin.h" +#include "pgstat.h" +#include "postmaster/bgworker.h" +#include "storage/procsignal.h" +#include "storage/shm_toc.h" +#include "test_shm_mq.h" +#include "utils/memutils.h" + +typedef struct +{ + int nworkers; + BackgroundWorkerHandle *handle[FLEXIBLE_ARRAY_MEMBER]; +} worker_state; + +static void setup_dynamic_shared_memory(int64 queue_size, int nworkers, + dsm_segment **segp, + test_shm_mq_header **hdrp, + shm_mq **outp, shm_mq **inp); +static worker_state *setup_background_workers(int nworkers, + dsm_segment *seg); +static void cleanup_background_workers(dsm_segment *seg, Datum arg); +static void wait_for_workers_to_become_ready(worker_state *wstate, + volatile test_shm_mq_header *hdr); +static bool check_worker_status(worker_state *wstate); + +/* + * Set up a dynamic shared memory segment and zero or more background workers + * for a test run. + */ +void +test_shm_mq_setup(int64 queue_size, int32 nworkers, dsm_segment **segp, + shm_mq_handle **output, shm_mq_handle **input) +{ + dsm_segment *seg; + test_shm_mq_header *hdr; + shm_mq *outq = NULL; /* placate compiler */ + shm_mq *inq = NULL; /* placate compiler */ + worker_state *wstate; + + /* Set up a dynamic shared memory segment. */ + setup_dynamic_shared_memory(queue_size, nworkers, &seg, &hdr, &outq, &inq); + *segp = seg; + + /* Register background workers. */ + wstate = setup_background_workers(nworkers, seg); + + /* Attach the queues. */ + *output = shm_mq_attach(outq, seg, wstate->handle[0]); + *input = shm_mq_attach(inq, seg, wstate->handle[nworkers - 1]); + + /* Wait for workers to become ready. */ + wait_for_workers_to_become_ready(wstate, hdr); + + /* + * Once we reach this point, all workers are ready. We no longer need to + * kill them if we die; they'll die on their own as the message queues + * shut down. + */ + cancel_on_dsm_detach(seg, cleanup_background_workers, + PointerGetDatum(wstate)); + pfree(wstate); +} + +/* + * Set up a dynamic shared memory segment. + * + * We set up a small control region that contains only a test_shm_mq_header, + * plus one region per message queue. There are as many message queues as + * the number of workers, plus one. + */ +static void +setup_dynamic_shared_memory(int64 queue_size, int nworkers, + dsm_segment **segp, test_shm_mq_header **hdrp, + shm_mq **outp, shm_mq **inp) +{ + shm_toc_estimator e; + int i; + Size segsize; + dsm_segment *seg; + shm_toc *toc; + test_shm_mq_header *hdr; + + /* Ensure a valid queue size. */ + if (queue_size < 0 || ((uint64) queue_size) < shm_mq_minimum_size) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("queue size must be at least %zu bytes", + shm_mq_minimum_size))); + if (queue_size != ((Size) queue_size)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("queue size overflows size_t"))); + + /* + * Estimate how much shared memory we need. + * + * Because the TOC machinery may choose to insert padding of oddly-sized + * requests, we must estimate each chunk separately. + * + * We need one key to register the location of the header, and we need + * nworkers + 1 keys to track the locations of the message queues. + */ + shm_toc_initialize_estimator(&e); + shm_toc_estimate_chunk(&e, sizeof(test_shm_mq_header)); + for (i = 0; i <= nworkers; ++i) + shm_toc_estimate_chunk(&e, (Size) queue_size); + shm_toc_estimate_keys(&e, 2 + nworkers); + segsize = shm_toc_estimate(&e); + + /* Create the shared memory segment and establish a table of contents. */ + seg = dsm_create(shm_toc_estimate(&e), 0); + toc = shm_toc_create(PG_TEST_SHM_MQ_MAGIC, dsm_segment_address(seg), + segsize); + + /* Set up the header region. */ + hdr = shm_toc_allocate(toc, sizeof(test_shm_mq_header)); + SpinLockInit(&hdr->mutex); + hdr->workers_total = nworkers; + hdr->workers_attached = 0; + hdr->workers_ready = 0; + shm_toc_insert(toc, 0, hdr); + + /* Set up one message queue per worker, plus one. */ + for (i = 0; i <= nworkers; ++i) + { + shm_mq *mq; + + mq = shm_mq_create(shm_toc_allocate(toc, (Size) queue_size), + (Size) queue_size); + shm_toc_insert(toc, i + 1, mq); + + if (i == 0) + { + /* We send messages to the first queue. */ + shm_mq_set_sender(mq, MyProc); + *outp = mq; + } + if (i == nworkers) + { + /* We receive messages from the last queue. */ + shm_mq_set_receiver(mq, MyProc); + *inp = mq; + } + } + + /* Return results to caller. */ + *segp = seg; + *hdrp = hdr; +} + +/* + * Register background workers. + */ +static worker_state * +setup_background_workers(int nworkers, dsm_segment *seg) +{ + MemoryContext oldcontext; + BackgroundWorker worker; + worker_state *wstate; + int i; + + /* + * We need the worker_state object and the background worker handles to + * which it points to be allocated in CurTransactionContext rather than + * ExprContext; otherwise, they'll be destroyed before the on_dsm_detach + * hooks run. + */ + oldcontext = MemoryContextSwitchTo(CurTransactionContext); + + /* Create worker state object. */ + wstate = MemoryContextAlloc(TopTransactionContext, + offsetof(worker_state, handle) + + sizeof(BackgroundWorkerHandle *) * nworkers); + wstate->nworkers = 0; + + /* + * Arrange to kill all the workers if we abort before all workers are + * finished hooking themselves up to the dynamic shared memory segment. + * + * If we die after all the workers have finished hooking themselves up to + * the dynamic shared memory segment, we'll mark the two queues to which + * we're directly connected as detached, and the worker(s) connected to + * those queues will exit, marking any other queues to which they are + * connected as detached. This will cause any as-yet-unaware workers + * connected to those queues to exit in their turn, and so on, until + * everybody exits. + * + * But suppose the workers which are supposed to connect to the queues to + * which we're directly attached exit due to some error before they + * actually attach the queues. The remaining workers will have no way of + * knowing this. From their perspective, they're still waiting for those + * workers to start, when in fact they've already died. + */ + on_dsm_detach(seg, cleanup_background_workers, + PointerGetDatum(wstate)); + + /* Configure a worker. */ + memset(&worker, 0, sizeof(worker)); + worker.bgw_flags = BGWORKER_SHMEM_ACCESS; + worker.bgw_start_time = BgWorkerStart_ConsistentState; + worker.bgw_restart_time = BGW_NEVER_RESTART; + sprintf(worker.bgw_library_name, "test_shm_mq"); + sprintf(worker.bgw_function_name, "test_shm_mq_main"); + snprintf(worker.bgw_type, BGW_MAXLEN, "test_shm_mq"); + worker.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg)); + /* set bgw_notify_pid, so we can detect if the worker stops */ + worker.bgw_notify_pid = MyProcPid; + + /* Register the workers. */ + for (i = 0; i < nworkers; ++i) + { + if (!RegisterDynamicBackgroundWorker(&worker, &wstate->handle[i])) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("could not register background process"), + errhint("You may need to increase max_worker_processes."))); + ++wstate->nworkers; + } + + /* All done. */ + MemoryContextSwitchTo(oldcontext); + return wstate; +} + +static void +cleanup_background_workers(dsm_segment *seg, Datum arg) +{ + worker_state *wstate = (worker_state *) DatumGetPointer(arg); + + while (wstate->nworkers > 0) + { + --wstate->nworkers; + TerminateBackgroundWorker(wstate->handle[wstate->nworkers]); + } +} + +static void +wait_for_workers_to_become_ready(worker_state *wstate, + volatile test_shm_mq_header *hdr) +{ + bool result = false; + + for (;;) + { + int workers_ready; + + /* If all the workers are ready, we have succeeded. */ + SpinLockAcquire(&hdr->mutex); + workers_ready = hdr->workers_ready; + SpinLockRelease(&hdr->mutex); + if (workers_ready >= wstate->nworkers) + { + result = true; + break; + } + + /* If any workers (or the postmaster) have died, we have failed. */ + if (!check_worker_status(wstate)) + { + result = false; + break; + } + + /* Wait to be signaled. */ + (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, + PG_WAIT_EXTENSION); + + /* Reset the latch so we don't spin. */ + ResetLatch(MyLatch); + + /* An interrupt may have occurred while we were waiting. */ + CHECK_FOR_INTERRUPTS(); + } + + if (!result) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("one or more background workers failed to start"))); +} + +static bool +check_worker_status(worker_state *wstate) +{ + int n; + + /* If any workers (or the postmaster) have died, we have failed. */ + for (n = 0; n < wstate->nworkers; ++n) + { + BgwHandleStatus status; + pid_t pid; + + status = GetBackgroundWorkerPid(wstate->handle[n], &pid); + if (status == BGWH_STOPPED || status == BGWH_POSTMASTER_DIED) + return false; + } + + /* Otherwise, things still look OK. */ + return true; +} diff --git a/src/test/modules/test_shm_mq/sql/test_shm_mq.sql b/src/test/modules/test_shm_mq/sql/test_shm_mq.sql new file mode 100644 index 0000000..9de19d3 --- /dev/null +++ b/src/test/modules/test_shm_mq/sql/test_shm_mq.sql @@ -0,0 +1,12 @@ +CREATE EXTENSION test_shm_mq; + +-- +-- These tests don't produce any interesting output. We're checking that +-- the operations complete without crashing or hanging and that none of their +-- internal sanity tests fail. +-- +SELECT test_shm_mq(1024, '', 2000, 1); +SELECT test_shm_mq(1024, 'a', 2001, 1); +SELECT test_shm_mq(32768, (select string_agg(chr(32+(random()*95)::int), '') from generate_series(1,(100+900*random())::int)), 10000, 1); +SELECT test_shm_mq(100, (select string_agg(chr(32+(random()*95)::int), '') from generate_series(1,(100+200*random())::int)), 10000, 1); +SELECT test_shm_mq_pipelined(16384, (select string_agg(chr(32+(random()*95)::int), '') from generate_series(1,270000)), 200, 3); diff --git a/src/test/modules/test_shm_mq/test.c b/src/test/modules/test_shm_mq/test.c new file mode 100644 index 0000000..5a78869 --- /dev/null +++ b/src/test/modules/test_shm_mq/test.c @@ -0,0 +1,267 @@ +/*-------------------------------------------------------------------------- + * + * test.c + * Test harness code for shared memory message queues. + * + * Copyright (c) 2013-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_shm_mq/test.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "miscadmin.h" +#include "pgstat.h" + +#include "test_shm_mq.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_shm_mq); +PG_FUNCTION_INFO_V1(test_shm_mq_pipelined); + +void _PG_init(void); + +static void verify_message(Size origlen, char *origdata, Size newlen, + char *newdata); + +/* + * Simple test of the shared memory message queue infrastructure. + * + * We set up a ring of message queues passing through 1 or more background + * processes and eventually looping back to ourselves. We then send a message + * through the ring a number of times indicated by the loop count. At the end, + * we check whether the final message matches the one we started with. + */ +Datum +test_shm_mq(PG_FUNCTION_ARGS) +{ + int64 queue_size = PG_GETARG_INT64(0); + text *message = PG_GETARG_TEXT_PP(1); + char *message_contents = VARDATA_ANY(message); + int message_size = VARSIZE_ANY_EXHDR(message); + int32 loop_count = PG_GETARG_INT32(2); + int32 nworkers = PG_GETARG_INT32(3); + dsm_segment *seg; + shm_mq_handle *outqh; + shm_mq_handle *inqh; + shm_mq_result res; + Size len; + void *data; + + /* A negative loopcount is nonsensical. */ + if (loop_count < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("repeat count size must be an integer value greater than or equal to zero"))); + + /* + * Since this test sends data using the blocking interfaces, it cannot + * send data to itself. Therefore, a minimum of 1 worker is required. Of + * course, a negative worker count is nonsensical. + */ + if (nworkers <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("number of workers must be an integer value greater than zero"))); + + /* Set up dynamic shared memory segment and background workers. */ + test_shm_mq_setup(queue_size, nworkers, &seg, &outqh, &inqh); + + /* Send the initial message. */ + res = shm_mq_send(outqh, message_size, message_contents, false, true); + if (res != SHM_MQ_SUCCESS) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not send message"))); + + /* + * Receive a message and send it back out again. Do this a number of + * times equal to the loop count. + */ + for (;;) + { + /* Receive a message. */ + res = shm_mq_receive(inqh, &len, &data, false); + if (res != SHM_MQ_SUCCESS) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not receive message"))); + + /* If this is supposed to be the last iteration, stop here. */ + if (--loop_count <= 0) + break; + + /* Send it back out. */ + res = shm_mq_send(outqh, len, data, false, true); + if (res != SHM_MQ_SUCCESS) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not send message"))); + } + + /* + * Finally, check that we got back the same message from the last + * iteration that we originally sent. + */ + verify_message(message_size, message_contents, len, data); + + /* Clean up. */ + dsm_detach(seg); + + PG_RETURN_VOID(); +} + +/* + * Pipelined test of the shared memory message queue infrastructure. + * + * As in the basic test, we set up a ring of message queues passing through + * 1 or more background processes and eventually looping back to ourselves. + * Then, we send N copies of the user-specified message through the ring and + * receive them all back. Since this might fill up all message queues in the + * ring and then stall, we must be prepared to begin receiving the messages + * back before we've finished sending them. + */ +Datum +test_shm_mq_pipelined(PG_FUNCTION_ARGS) +{ + int64 queue_size = PG_GETARG_INT64(0); + text *message = PG_GETARG_TEXT_PP(1); + char *message_contents = VARDATA_ANY(message); + int message_size = VARSIZE_ANY_EXHDR(message); + int32 loop_count = PG_GETARG_INT32(2); + int32 nworkers = PG_GETARG_INT32(3); + bool verify = PG_GETARG_BOOL(4); + int32 send_count = 0; + int32 receive_count = 0; + dsm_segment *seg; + shm_mq_handle *outqh; + shm_mq_handle *inqh; + shm_mq_result res; + Size len; + void *data; + + /* A negative loopcount is nonsensical. */ + if (loop_count < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("repeat count size must be an integer value greater than or equal to zero"))); + + /* + * Using the nonblocking interfaces, we can even send data to ourselves, + * so the minimum number of workers for this test is zero. + */ + if (nworkers < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("number of workers must be an integer value greater than or equal to zero"))); + + /* Set up dynamic shared memory segment and background workers. */ + test_shm_mq_setup(queue_size, nworkers, &seg, &outqh, &inqh); + + /* Main loop. */ + for (;;) + { + bool wait = true; + + /* + * If we haven't yet sent the message the requisite number of times, + * try again to send it now. Note that when shm_mq_send() returns + * SHM_MQ_WOULD_BLOCK, the next call to that function must pass the + * same message size and contents; that's not an issue here because + * we're sending the same message every time. + */ + if (send_count < loop_count) + { + res = shm_mq_send(outqh, message_size, message_contents, true, + true); + if (res == SHM_MQ_SUCCESS) + { + ++send_count; + wait = false; + } + else if (res == SHM_MQ_DETACHED) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not send message"))); + } + + /* + * If we haven't yet received the message the requisite number of + * times, try to receive it again now. + */ + if (receive_count < loop_count) + { + res = shm_mq_receive(inqh, &len, &data, true); + if (res == SHM_MQ_SUCCESS) + { + ++receive_count; + /* Verifying every time is slow, so it's optional. */ + if (verify) + verify_message(message_size, message_contents, len, data); + wait = false; + } + else if (res == SHM_MQ_DETACHED) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not receive message"))); + } + else + { + /* + * Otherwise, we've received the message enough times. This + * shouldn't happen unless we've also sent it enough times. + */ + if (send_count != receive_count) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("message sent %d times, but received %d times", + send_count, receive_count))); + break; + } + + if (wait) + { + /* + * If we made no progress, wait for one of the other processes to + * which we are connected to set our latch, indicating that they + * have read or written data and therefore there may now be work + * for us to do. + */ + (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, + PG_WAIT_EXTENSION); + ResetLatch(MyLatch); + CHECK_FOR_INTERRUPTS(); + } + } + + /* Clean up. */ + dsm_detach(seg); + + PG_RETURN_VOID(); +} + +/* + * Verify that two messages are the same. + */ +static void +verify_message(Size origlen, char *origdata, Size newlen, char *newdata) +{ + Size i; + + if (origlen != newlen) + ereport(ERROR, + (errmsg("message corrupted"), + errdetail("The original message was %zu bytes but the final message is %zu bytes.", + origlen, newlen))); + + for (i = 0; i < origlen; ++i) + if (origdata[i] != newdata[i]) + ereport(ERROR, + (errmsg("message corrupted"), + errdetail("The new and original messages differ at byte %zu of %zu.", i, origlen))); +} diff --git a/src/test/modules/test_shm_mq/test_shm_mq--1.0.sql b/src/test/modules/test_shm_mq/test_shm_mq--1.0.sql new file mode 100644 index 0000000..56db05d --- /dev/null +++ b/src/test/modules/test_shm_mq/test_shm_mq--1.0.sql @@ -0,0 +1,19 @@ +/* src/test/modules/test_shm_mq/test_shm_mq--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_shm_mq" to load this file. \quit + +CREATE FUNCTION test_shm_mq(queue_size pg_catalog.int8, + message pg_catalog.text, + repeat_count pg_catalog.int4 default 1, + num_workers pg_catalog.int4 default 1) + RETURNS pg_catalog.void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_shm_mq_pipelined(queue_size pg_catalog.int8, + message pg_catalog.text, + repeat_count pg_catalog.int4 default 1, + num_workers pg_catalog.int4 default 1, + verify pg_catalog.bool default true) + RETURNS pg_catalog.void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_shm_mq/test_shm_mq.control b/src/test/modules/test_shm_mq/test_shm_mq.control new file mode 100644 index 0000000..d9a74c7 --- /dev/null +++ b/src/test/modules/test_shm_mq/test_shm_mq.control @@ -0,0 +1,4 @@ +comment = 'Test code for shared memory message queues' +default_version = '1.0' +module_pathname = '$libdir/test_shm_mq' +relocatable = true diff --git a/src/test/modules/test_shm_mq/test_shm_mq.h b/src/test/modules/test_shm_mq/test_shm_mq.h new file mode 100644 index 0000000..0310caf --- /dev/null +++ b/src/test/modules/test_shm_mq/test_shm_mq.h @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + * + * test_shm_mq.h + * Definitions for shared memory message queues + * + * Copyright (c) 2013-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_shm_mq/test_shm_mq.h + * + * ------------------------------------------------------------------------- + */ + +#ifndef TEST_SHM_MQ_H +#define TEST_SHM_MQ_H + +#include "storage/dsm.h" +#include "storage/shm_mq.h" +#include "storage/spin.h" + +/* Identifier for shared memory segments used by this extension. */ +#define PG_TEST_SHM_MQ_MAGIC 0x79fb2447 + +/* + * This structure is stored in the dynamic shared memory segment. We use + * it to determine whether all workers started up OK and successfully + * attached to their respective shared message queues. + */ +typedef struct +{ + slock_t mutex; + int workers_total; + int workers_attached; + int workers_ready; +} test_shm_mq_header; + +/* Set up dynamic shared memory and background workers for test run. */ +extern void test_shm_mq_setup(int64 queue_size, int32 nworkers, + dsm_segment **seg, shm_mq_handle **output, + shm_mq_handle **input); + +/* Main entrypoint for a worker. */ +extern void test_shm_mq_main(Datum) pg_attribute_noreturn(); + +#endif diff --git a/src/test/modules/test_shm_mq/worker.c b/src/test/modules/test_shm_mq/worker.c new file mode 100644 index 0000000..9128912 --- /dev/null +++ b/src/test/modules/test_shm_mq/worker.c @@ -0,0 +1,197 @@ +/*-------------------------------------------------------------------------- + * + * worker.c + * Code for sample worker making use of shared memory message queues. + * Our test worker simply reads messages from one message queue and + * writes them back out to another message queue. In a real + * application, you'd presumably want the worker to do some more + * complex calculation rather than simply returning the input, + * but it should be possible to use much of the control logic just + * as presented here. + * + * Copyright (c) 2013-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_shm_mq/worker.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "miscadmin.h" +#include "storage/ipc.h" +#include "storage/procarray.h" +#include "storage/shm_mq.h" +#include "storage/shm_toc.h" +#include "tcop/tcopprot.h" + +#include "test_shm_mq.h" + +static void attach_to_queues(dsm_segment *seg, shm_toc *toc, + int myworkernumber, shm_mq_handle **inqhp, + shm_mq_handle **outqhp); +static void copy_messages(shm_mq_handle *inqh, shm_mq_handle *outqh); + +/* + * Background worker entrypoint. + * + * This is intended to demonstrate how a background worker can be used to + * facilitate a parallel computation. Most of the logic here is fairly + * boilerplate stuff, designed to attach to the shared memory segment, + * notify the user backend that we're alive, and so on. The + * application-specific bits of logic that you'd replace for your own worker + * are attach_to_queues() and copy_messages(). + */ +void +test_shm_mq_main(Datum main_arg) +{ + dsm_segment *seg; + shm_toc *toc; + shm_mq_handle *inqh; + shm_mq_handle *outqh; + volatile test_shm_mq_header *hdr; + int myworkernumber; + PGPROC *registrant; + + /* + * Establish signal handlers. + * + * We want CHECK_FOR_INTERRUPTS() to kill off this worker process just as + * it would a normal user backend. To make that happen, we use die(). + */ + pqsignal(SIGTERM, die); + BackgroundWorkerUnblockSignals(); + + /* + * Connect to the dynamic shared memory segment. + * + * The backend that registered this worker passed us the ID of a shared + * memory segment to which we must attach for further instructions. Once + * we've mapped the segment in our address space, attach to the table of + * contents so we can locate the various data structures we'll need to + * find within the segment. + * + * Note: at this point, we have not created any ResourceOwner in this + * process. This will result in our DSM mapping surviving until process + * exit, which is fine. If there were a ResourceOwner, it would acquire + * ownership of the mapping, but we have no need for that. + */ + seg = dsm_attach(DatumGetInt32(main_arg)); + if (seg == NULL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("unable to map dynamic shared memory segment"))); + toc = shm_toc_attach(PG_TEST_SHM_MQ_MAGIC, dsm_segment_address(seg)); + if (toc == NULL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("bad magic number in dynamic shared memory segment"))); + + /* + * Acquire a worker number. + * + * By convention, the process registering this background worker should + * have stored the control structure at key 0. We look up that key to + * find it. Our worker number gives our identity: there may be just one + * worker involved in this parallel operation, or there may be many. + */ + hdr = shm_toc_lookup(toc, 0, false); + SpinLockAcquire(&hdr->mutex); + myworkernumber = ++hdr->workers_attached; + SpinLockRelease(&hdr->mutex); + if (myworkernumber > hdr->workers_total) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("too many message queue testing workers already"))); + + /* + * Attach to the appropriate message queues. + */ + attach_to_queues(seg, toc, myworkernumber, &inqh, &outqh); + + /* + * Indicate that we're fully initialized and ready to begin the main part + * of the parallel operation. + * + * Once we signal that we're ready, the user backend is entitled to assume + * that our on_dsm_detach callbacks will fire before we disconnect from + * the shared memory segment and exit. Generally, that means we must have + * attached to all relevant dynamic shared memory data structures by now. + */ + SpinLockAcquire(&hdr->mutex); + ++hdr->workers_ready; + SpinLockRelease(&hdr->mutex); + registrant = BackendPidGetProc(MyBgworkerEntry->bgw_notify_pid); + if (registrant == NULL) + { + elog(DEBUG1, "registrant backend has exited prematurely"); + proc_exit(1); + } + SetLatch(®istrant->procLatch); + + /* Do the work. */ + copy_messages(inqh, outqh); + + /* + * We're done. For cleanliness, explicitly detach from the shared memory + * segment (that would happen anyway during process exit, though). + */ + dsm_detach(seg); + proc_exit(1); +} + +/* + * Attach to shared memory message queues. + * + * We use our worker number to determine to which queue we should attach. + * The queues are registered at keys 1..<number-of-workers>. The user backend + * writes to queue #1 and reads from queue #<number-of-workers>; each worker + * reads from the queue whose number is equal to its worker number and writes + * to the next higher-numbered queue. + */ +static void +attach_to_queues(dsm_segment *seg, shm_toc *toc, int myworkernumber, + shm_mq_handle **inqhp, shm_mq_handle **outqhp) +{ + shm_mq *inq; + shm_mq *outq; + + inq = shm_toc_lookup(toc, myworkernumber, false); + shm_mq_set_receiver(inq, MyProc); + *inqhp = shm_mq_attach(inq, seg, NULL); + outq = shm_toc_lookup(toc, myworkernumber + 1, false); + shm_mq_set_sender(outq, MyProc); + *outqhp = shm_mq_attach(outq, seg, NULL); +} + +/* + * Loop, receiving and sending messages, until the connection is broken. + * + * This is the "real work" performed by this worker process. Everything that + * happens before this is initialization of one form or another, and everything + * after this point is cleanup. + */ +static void +copy_messages(shm_mq_handle *inqh, shm_mq_handle *outqh) +{ + Size len; + void *data; + shm_mq_result res; + + for (;;) + { + /* Notice any interrupts that have occurred. */ + CHECK_FOR_INTERRUPTS(); + + /* Receive a message. */ + res = shm_mq_receive(inqh, &len, &data, false); + if (res != SHM_MQ_SUCCESS) + break; + + /* Send it back out. */ + res = shm_mq_send(outqh, len, data, false, true); + if (res != SHM_MQ_SUCCESS) + break; + } +} diff --git a/src/test/modules/unsafe_tests/.gitignore b/src/test/modules/unsafe_tests/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/unsafe_tests/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/unsafe_tests/Makefile b/src/test/modules/unsafe_tests/Makefile new file mode 100644 index 0000000..90d1979 --- /dev/null +++ b/src/test/modules/unsafe_tests/Makefile @@ -0,0 +1,17 @@ +# src/test/modules/unsafe_tests/Makefile + +REGRESS = rolenames alter_system_table guc_privs + +# the whole point of these tests is to not run installcheck +NO_INSTALLCHECK = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/unsafe_tests +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/unsafe_tests/README b/src/test/modules/unsafe_tests/README new file mode 100644 index 0000000..d9dbd03 --- /dev/null +++ b/src/test/modules/unsafe_tests/README @@ -0,0 +1,8 @@ +This directory doesn't actually contain any extension module. + +Instead it is a home for regression tests that we don't want to run +during "make installcheck" because they could have side-effects that +seem undesirable for a production installation. + +An example is that rolenames.sql tests ALTER USER ALL and so could +have effects on pre-existing roles. diff --git a/src/test/modules/unsafe_tests/expected/alter_system_table.out b/src/test/modules/unsafe_tests/expected/alter_system_table.out new file mode 100644 index 0000000..be05595 --- /dev/null +++ b/src/test/modules/unsafe_tests/expected/alter_system_table.out @@ -0,0 +1,179 @@ +-- +-- Tests for things affected by allow_system_table_mods +-- +-- We run the same set of commands once with allow_system_table_mods +-- off and then again with on. +-- +-- The "on" tests should where possible be wrapped in BEGIN/ROLLBACK +-- blocks so as to not leave a mess around. +CREATE USER regress_user_ast; +SET allow_system_table_mods = off; +-- create new table in pg_catalog +CREATE TABLE pg_catalog.test (a int); +ERROR: permission denied to create "pg_catalog.test" +DETAIL: System catalog modifications are currently disallowed. +-- anyarray column +CREATE TABLE t1x (a int, b anyarray); +ERROR: column "b" has pseudo-type anyarray +-- index on system catalog +ALTER TABLE pg_namespace ADD CONSTRAINT foo UNIQUE USING INDEX pg_namespace_nspname_index; +ERROR: permission denied: "pg_namespace" is a system catalog +-- write to system catalog table as superuser +-- (allowed even without allow_system_table_mods) +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 0, 'foo'); +-- write to system catalog table as normal user +GRANT INSERT ON pg_description TO regress_user_ast; +SET ROLE regress_user_ast; +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 1, 'foo'); +ERROR: permission denied for table pg_description +RESET ROLE; +-- policy on system catalog +CREATE POLICY foo ON pg_description FOR SELECT USING (description NOT LIKE 'secret%'); +ERROR: permission denied: "pg_description" is a system catalog +-- reserved schema name +CREATE SCHEMA pg_foo; +ERROR: unacceptable schema name "pg_foo" +DETAIL: The prefix "pg_" is reserved for system schemas. +-- drop system table +DROP TABLE pg_description; +ERROR: permission denied: "pg_description" is a system catalog +-- truncate of system table +TRUNCATE pg_description; +ERROR: permission denied: "pg_description" is a system catalog +-- rename column of system table +ALTER TABLE pg_description RENAME COLUMN description TO comment; +ERROR: permission denied: "pg_description" is a system catalog +-- ATSimplePermissions() +ALTER TABLE pg_description ALTER COLUMN description SET NOT NULL; +ERROR: permission denied: "pg_description" is a system catalog +-- SET STATISTICS +ALTER TABLE pg_description ALTER COLUMN description SET STATISTICS -1; +ERROR: permission denied: "pg_description" is a system catalog +-- foreign key referencing catalog +CREATE TABLE foo (a oid, b oid, c int, FOREIGN KEY (a, b, c) REFERENCES pg_description); +ERROR: permission denied: "pg_description" is a system catalog +-- RangeVarCallbackOwnsRelation() +CREATE INDEX pg_description_test_index ON pg_description (description); +ERROR: permission denied: "pg_description" is a system catalog +-- RangeVarCallbackForAlterRelation() +ALTER TABLE pg_description RENAME TO pg_comment; +ERROR: permission denied: "pg_description" is a system catalog +ALTER TABLE pg_description SET SCHEMA public; +ERROR: permission denied: "pg_description" is a system catalog +-- reserved tablespace name +CREATE TABLESPACE pg_foo LOCATION '/no/such/location'; +ERROR: unacceptable tablespace name "pg_foo" +DETAIL: The prefix "pg_" is reserved for system tablespaces. +-- triggers +CREATE FUNCTION tf1() RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN NULL; +END $$; +CREATE TRIGGER t1 BEFORE INSERT ON pg_description EXECUTE FUNCTION tf1(); +ERROR: permission denied: "pg_description" is a system catalog +ALTER TRIGGER t1 ON pg_description RENAME TO t2; +ERROR: permission denied: "pg_description" is a system catalog +--DROP TRIGGER t2 ON pg_description; +-- rules +CREATE RULE r1 AS ON INSERT TO pg_description DO INSTEAD NOTHING; +ERROR: permission denied: "pg_description" is a system catalog +ALTER RULE r1 ON pg_description RENAME TO r2; +ERROR: permission denied: "pg_description" is a system catalog +-- now make one to test dropping: +SET allow_system_table_mods TO on; +CREATE RULE r2 AS ON INSERT TO pg_description DO INSTEAD NOTHING; +RESET allow_system_table_mods; +DROP RULE r2 ON pg_description; +ERROR: permission denied: "pg_description" is a system catalog +-- cleanup: +SET allow_system_table_mods TO on; +DROP RULE r2 ON pg_description; +RESET allow_system_table_mods; +SET allow_system_table_mods = on; +-- create new table in pg_catalog +BEGIN; +CREATE TABLE pg_catalog.test (a int); +ROLLBACK; +-- anyarray column +BEGIN; +CREATE TABLE t1 (a int, b anyarray); +ROLLBACK; +-- index on system catalog +BEGIN; +ALTER TABLE pg_namespace ADD CONSTRAINT foo UNIQUE USING INDEX pg_namespace_nspname_index; +NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "pg_namespace_nspname_index" to "foo" +ROLLBACK; +-- write to system catalog table as superuser +BEGIN; +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 2, 'foo'); +ROLLBACK; +-- write to system catalog table as normal user +-- (not allowed) +SET ROLE regress_user_ast; +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 3, 'foo'); +ERROR: permission denied for table pg_description +RESET ROLE; +-- policy on system catalog +BEGIN; +CREATE POLICY foo ON pg_description FOR SELECT USING (description NOT LIKE 'secret%'); +ROLLBACK; +-- reserved schema name +BEGIN; +CREATE SCHEMA pg_foo; +ROLLBACK; +-- drop system table +-- (This will fail anyway because it's pinned.) +BEGIN; +DROP TABLE pg_description; +ERROR: cannot drop table pg_description because it is required by the database system +ROLLBACK; +-- truncate of system table +BEGIN; +TRUNCATE pg_description; +ROLLBACK; +-- rename column of system table +BEGIN; +ALTER TABLE pg_description RENAME COLUMN description TO comment; +ROLLBACK; +-- ATSimplePermissions() +BEGIN; +ALTER TABLE pg_description ALTER COLUMN description SET NOT NULL; +ROLLBACK; +-- SET STATISTICS +BEGIN; +ALTER TABLE pg_description ALTER COLUMN description SET STATISTICS -1; +ROLLBACK; +-- foreign key referencing catalog +BEGIN; +CREATE TABLE foo (a oid, b oid, c int, FOREIGN KEY (a, b, c) REFERENCES pg_description); +ROLLBACK; +-- RangeVarCallbackOwnsRelation() +BEGIN; +CREATE INDEX pg_description_test_index ON pg_description (description); +ROLLBACK; +-- RangeVarCallbackForAlterRelation() +BEGIN; +ALTER TABLE pg_description RENAME TO pg_comment; +ROLLBACK; +BEGIN; +ALTER TABLE pg_description SET SCHEMA public; +ROLLBACK; +-- reserved tablespace name +SET client_min_messages = error; -- disable ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS warning +CREATE TABLESPACE pg_foo LOCATION '/no/such/location'; +ERROR: directory "/no/such/location" does not exist +RESET client_min_messages; +-- triggers +CREATE TRIGGER t1 BEFORE INSERT ON pg_description EXECUTE FUNCTION tf1(); +ALTER TRIGGER t1 ON pg_description RENAME TO t2; +DROP TRIGGER t2 ON pg_description; +-- rules +CREATE RULE r1 AS ON INSERT TO pg_description DO INSTEAD NOTHING; +ALTER RULE r1 ON pg_description RENAME TO r2; +DROP RULE r2 ON pg_description; +-- cleanup +REVOKE ALL ON pg_description FROM regress_user_ast; +DROP USER regress_user_ast; +DROP FUNCTION tf1; diff --git a/src/test/modules/unsafe_tests/expected/guc_privs.out b/src/test/modules/unsafe_tests/expected/guc_privs.out new file mode 100644 index 0000000..54f95b2 --- /dev/null +++ b/src/test/modules/unsafe_tests/expected/guc_privs.out @@ -0,0 +1,562 @@ +-- +-- Tests for privileges on GUCs. +-- This is unsafe because changes will affect other databases in the cluster. +-- +-- Test with a superuser role. +CREATE ROLE regress_admin SUPERUSER; +-- Perform operations as user 'regress_admin'. +SET SESSION AUTHORIZATION regress_admin; +-- PGC_BACKEND +SET ignore_system_indexes = OFF; -- fail, cannot be set after connection start +ERROR: parameter "ignore_system_indexes" cannot be set after connection start +RESET ignore_system_indexes; -- fail, cannot be set after connection start +ERROR: parameter "ignore_system_indexes" cannot be set after connection start +ALTER SYSTEM SET ignore_system_indexes = OFF; -- ok +ALTER SYSTEM RESET ignore_system_indexes; -- ok +-- PGC_INTERNAL +SET block_size = 50; -- fail, cannot be changed +ERROR: parameter "block_size" cannot be changed +RESET block_size; -- fail, cannot be changed +ERROR: parameter "block_size" cannot be changed +ALTER SYSTEM SET block_size = 50; -- fail, cannot be changed +ERROR: parameter "block_size" cannot be changed +ALTER SYSTEM RESET block_size; -- fail, cannot be changed +ERROR: parameter "block_size" cannot be changed +-- PGC_POSTMASTER +SET autovacuum_freeze_max_age = 1000050000; -- fail, requires restart +ERROR: parameter "autovacuum_freeze_max_age" cannot be changed without restarting the server +RESET autovacuum_freeze_max_age; -- fail, requires restart +ERROR: parameter "autovacuum_freeze_max_age" cannot be changed without restarting the server +ALTER SYSTEM SET autovacuum_freeze_max_age = 1000050000; -- ok +ALTER SYSTEM RESET autovacuum_freeze_max_age; -- ok +ALTER SYSTEM SET config_file = '/usr/local/data/postgresql.conf'; -- fail, cannot be changed +ERROR: parameter "config_file" cannot be changed +ALTER SYSTEM RESET config_file; -- fail, cannot be changed +ERROR: parameter "config_file" cannot be changed +-- PGC_SIGHUP +SET autovacuum = OFF; -- fail, requires reload +ERROR: parameter "autovacuum" cannot be changed now +RESET autovacuum; -- fail, requires reload +ERROR: parameter "autovacuum" cannot be changed now +ALTER SYSTEM SET autovacuum = OFF; -- ok +ALTER SYSTEM RESET autovacuum; -- ok +-- PGC_SUSET +SET lc_messages = 'C'; -- ok +RESET lc_messages; -- ok +ALTER SYSTEM SET lc_messages = 'C'; -- ok +ALTER SYSTEM RESET lc_messages; -- ok +-- PGC_SU_BACKEND +SET jit_debugging_support = OFF; -- fail, cannot be set after connection start +ERROR: parameter "jit_debugging_support" cannot be set after connection start +RESET jit_debugging_support; -- fail, cannot be set after connection start +ERROR: parameter "jit_debugging_support" cannot be set after connection start +ALTER SYSTEM SET jit_debugging_support = OFF; -- ok +ALTER SYSTEM RESET jit_debugging_support; -- ok +-- PGC_USERSET +SET DateStyle = 'ISO, MDY'; -- ok +RESET DateStyle; -- ok +ALTER SYSTEM SET DateStyle = 'ISO, MDY'; -- ok +ALTER SYSTEM RESET DateStyle; -- ok +ALTER SYSTEM SET ssl_renegotiation_limit = 0; -- fail, cannot be changed +ERROR: parameter "ssl_renegotiation_limit" cannot be changed +ALTER SYSTEM RESET ssl_renegotiation_limit; -- fail, cannot be changed +ERROR: parameter "ssl_renegotiation_limit" cannot be changed +-- Finished testing superuser +-- Create non-superuser with privileges to configure host resource usage +CREATE ROLE regress_host_resource_admin NOSUPERUSER; +-- Revoke privileges not yet granted +REVOKE SET, ALTER SYSTEM ON PARAMETER work_mem FROM regress_host_resource_admin; +REVOKE SET, ALTER SYSTEM ON PARAMETER zero_damaged_pages FROM regress_host_resource_admin; +-- Check the new role does not yet have privileges on parameters +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET, ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +-- Check inappropriate and nonsense privilege types +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SELECT, UPDATE, CREATE'); +ERROR: unrecognized privilege type: "SELECT" +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'USAGE'); +ERROR: unrecognized privilege type: "USAGE" +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'WHATEVER'); +ERROR: unrecognized privilege type: "WHATEVER" +-- Revoke, grant, and revoke again a SUSET parameter not yet granted +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +REVOKE SET ON PARAMETER zero_damaged_pages FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +GRANT SET ON PARAMETER zero_damaged_pages TO regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +REVOKE SET ON PARAMETER zero_damaged_pages FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +-- Revoke, grant, and revoke again a USERSET parameter not yet granted +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +REVOKE SET ON PARAMETER work_mem FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +GRANT SET ON PARAMETER work_mem TO regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +REVOKE SET ON PARAMETER work_mem FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +-- Revoke privileges from a non-existent custom GUC. This should not create +-- entries in the catalog. +REVOKE ALL ON PARAMETER "none.such" FROM regress_host_resource_admin; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such'; + ?column? +---------- +(0 rows) + +-- Grant and then revoke privileges on the non-existent custom GUC. Check that +-- a do-nothing entry is not left in the catalogs after the revoke. +GRANT ALL ON PARAMETER none.such TO regress_host_resource_admin; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such'; + ?column? +---------- + 1 +(1 row) + +REVOKE ALL ON PARAMETER "None.Such" FROM regress_host_resource_admin; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such'; + ?column? +---------- +(0 rows) + +-- Can't grant on a non-existent core GUC. +GRANT ALL ON PARAMETER no_such_guc TO regress_host_resource_admin; -- fail +ERROR: invalid parameter name "no_such_guc" +-- Initially there are no privileges and no catalog entry for this GUC. +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; + ?column? +---------- +(0 rows) + +-- GRANT SET creates an entry: +GRANT SET ON PARAMETER enable_material TO PUBLIC; +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; + ?column? +---------- + 1 +(1 row) + +-- Now grant ALTER SYSTEM: +GRANT ALL ON PARAMETER enable_material TO PUBLIC; +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; + ?column? +---------- + 1 +(1 row) + +-- REVOKE ALTER SYSTEM brings us back to just the SET privilege: +REVOKE ALTER SYSTEM ON PARAMETER enable_material FROM PUBLIC; +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + f +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; + ?column? +---------- + 1 +(1 row) + +-- And this should remove the entry altogether: +REVOKE SET ON PARAMETER enable_material FROM PUBLIC; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; + ?column? +---------- +(0 rows) + +-- Grant privileges on parameters to the new non-superuser role +GRANT SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +TO regress_host_resource_admin; +-- Check the new role now has privilges on parameters +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET, ALTER SYSTEM'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET WITH GRANT OPTION, ALTER SYSTEM WITH GRANT OPTION'); + has_parameter_privilege +------------------------- + f +(1 row) + +-- Check again the inappropriate and nonsense privilege types. The prior +-- similar check was performed before any entry for work_mem existed. +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SELECT, UPDATE, CREATE'); +ERROR: unrecognized privilege type: "SELECT" +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'USAGE'); +ERROR: unrecognized privilege type: "USAGE" +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'WHATEVER'); +ERROR: unrecognized privilege type: "WHATEVER" +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'WHATEVER WITH GRANT OPTION'); +ERROR: unrecognized privilege type: "WHATEVER WITH GRANT OPTION" +-- Check other function signatures +SELECT has_parameter_privilege((SELECT oid FROM pg_catalog.pg_authid WHERE rolname = 'regress_host_resource_admin'), + 'max_stack_depth', + 'SET'); + has_parameter_privilege +------------------------- + t +(1 row) + +SELECT has_parameter_privilege('hash_mem_multiplier', 'set'); + has_parameter_privilege +------------------------- + t +(1 row) + +-- Check object identity functions +SELECT pg_describe_object(tableoid, oid, 0) +FROM pg_parameter_acl WHERE parname = 'work_mem'; + pg_describe_object +-------------------- + parameter work_mem +(1 row) + +SELECT pg_identify_object(tableoid, oid, 0) +FROM pg_parameter_acl WHERE parname = 'work_mem'; + pg_identify_object +------------------------------ + ("parameter ACL",,,work_mem) +(1 row) + +SELECT pg_identify_object_as_address(tableoid, oid, 0) +FROM pg_parameter_acl WHERE parname = 'work_mem'; + pg_identify_object_as_address +--------------------------------- + ("parameter ACL",{work_mem},{}) +(1 row) + +SELECT classid::regclass, + (SELECT parname FROM pg_parameter_acl WHERE oid = goa.objid) AS parname, + objsubid +FROM pg_get_object_address('parameter ACL', '{work_mem}', '{}') goa; + classid | parname | objsubid +------------------+----------+---------- + pg_parameter_acl | work_mem | 0 +(1 row) + +-- Make a per-role setting that regress_host_resource_admin can't change +ALTER ROLE regress_host_resource_admin SET lc_messages = 'C'; +-- Perform some operations as user 'regress_host_resource_admin' +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- ok, privileges have been granted +ALTER SYSTEM SET ignore_system_indexes = OFF; -- fail, insufficient privileges +ERROR: permission denied to set parameter "ignore_system_indexes" +ALTER SYSTEM RESET autovacuum_multixact_freeze_max_age; -- fail, insufficient privileges +ERROR: permission denied to set parameter "autovacuum_multixact_freeze_max_age" +SET jit_provider = 'llvmjit'; -- fail, insufficient privileges +ERROR: parameter "jit_provider" cannot be changed without restarting the server +SELECT set_config ('jit_provider', 'llvmjit', true); -- fail, insufficient privileges +ERROR: parameter "jit_provider" cannot be changed without restarting the server +ALTER SYSTEM SET shared_buffers = 50; -- ok +ALTER SYSTEM RESET shared_buffers; -- ok +SET autovacuum_work_mem = 50; -- cannot be changed now +ERROR: parameter "autovacuum_work_mem" cannot be changed now +ALTER SYSTEM RESET temp_file_limit; -- ok +SET TimeZone = 'Europe/Helsinki'; -- ok +RESET TimeZone; -- ok +SET max_stack_depth = '100kB'; -- ok, privileges have been granted +RESET max_stack_depth; -- ok, privileges have been granted +ALTER SYSTEM SET max_stack_depth = '100kB'; -- ok, privileges have been granted +ALTER SYSTEM RESET max_stack_depth; -- ok, privileges have been granted +SET lc_messages = 'C'; -- fail, insufficient privileges +ERROR: permission denied to set parameter "lc_messages" +RESET lc_messages; -- fail, insufficient privileges +ERROR: permission denied to set parameter "lc_messages" +ALTER SYSTEM SET lc_messages = 'C'; -- fail, insufficient privileges +ERROR: permission denied to set parameter "lc_messages" +ALTER SYSTEM RESET lc_messages; -- fail, insufficient privileges +ERROR: permission denied to set parameter "lc_messages" +SELECT set_config ('temp_buffers', '8192', false); -- ok + set_config +------------ + 64MB +(1 row) + +ALTER SYSTEM RESET autovacuum_work_mem; -- ok, privileges have been granted +ALTER SYSTEM RESET ALL; -- fail, insufficient privileges +ERROR: permission denied to perform ALTER SYSTEM RESET ALL +ALTER ROLE regress_host_resource_admin SET lc_messages = 'POSIX'; -- fail +ERROR: permission denied to set parameter "lc_messages" +ALTER ROLE regress_host_resource_admin SET max_stack_depth = '1MB'; -- ok +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; + setconfig +------------------------------------- + {lc_messages=C,max_stack_depth=1MB} +(1 row) + +ALTER ROLE regress_host_resource_admin RESET max_stack_depth; -- ok +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; + setconfig +----------------- + {lc_messages=C} +(1 row) + +ALTER ROLE regress_host_resource_admin SET max_stack_depth = '1MB'; -- ok +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; + setconfig +------------------------------------- + {lc_messages=C,max_stack_depth=1MB} +(1 row) + +ALTER ROLE regress_host_resource_admin RESET ALL; -- doesn't reset lc_messages +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; + setconfig +----------------- + {lc_messages=C} +(1 row) + +-- Check dropping/revoking behavior +SET SESSION AUTHORIZATION regress_admin; +DROP ROLE regress_host_resource_admin; -- fail, privileges remain +ERROR: role "regress_host_resource_admin" cannot be dropped because some objects depend on it +DETAIL: privileges for parameter autovacuum_work_mem +privileges for parameter hash_mem_multiplier +privileges for parameter max_stack_depth +privileges for parameter shared_buffers +privileges for parameter temp_file_limit +privileges for parameter work_mem +-- Use "revoke" to remove the privileges and allow the role to be dropped +REVOKE SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +FROM regress_host_resource_admin; +DROP ROLE regress_host_resource_admin; -- ok +-- Try that again, but use "drop owned by" instead of "revoke" +CREATE ROLE regress_host_resource_admin NOSUPERUSER; +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- fail, privileges not yet granted +ERROR: permission denied to set parameter "autovacuum_work_mem" +SET SESSION AUTHORIZATION regress_admin; +GRANT SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +TO regress_host_resource_admin; +DROP ROLE regress_host_resource_admin; -- fail, privileges remain +ERROR: role "regress_host_resource_admin" cannot be dropped because some objects depend on it +DETAIL: privileges for parameter autovacuum_work_mem +privileges for parameter hash_mem_multiplier +privileges for parameter max_stack_depth +privileges for parameter shared_buffers +privileges for parameter temp_file_limit +privileges for parameter work_mem +DROP OWNED BY regress_host_resource_admin RESTRICT; -- cascade should not be needed +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- fail, "drop owned" has dropped privileges +ERROR: permission denied to set parameter "autovacuum_work_mem" +SET SESSION AUTHORIZATION regress_admin; +DROP ROLE regress_host_resource_admin; -- ok +-- Check that "reassign owned" doesn't affect privileges +CREATE ROLE regress_host_resource_admin NOSUPERUSER; +CREATE ROLE regress_host_resource_newadmin NOSUPERUSER; +GRANT SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +TO regress_host_resource_admin; +REASSIGN OWNED BY regress_host_resource_admin TO regress_host_resource_newadmin; +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- ok, "reassign owned" did not change privileges +ALTER SYSTEM RESET autovacuum_work_mem; -- ok +SET SESSION AUTHORIZATION regress_admin; +DROP ROLE regress_host_resource_admin; -- fail, privileges remain +ERROR: role "regress_host_resource_admin" cannot be dropped because some objects depend on it +DETAIL: privileges for parameter autovacuum_work_mem +privileges for parameter hash_mem_multiplier +privileges for parameter max_stack_depth +privileges for parameter shared_buffers +privileges for parameter temp_file_limit +privileges for parameter work_mem +DROP ROLE regress_host_resource_newadmin; -- ok, nothing was transferred +-- Use "drop owned by" so we can drop the role +DROP OWNED BY regress_host_resource_admin; -- ok +DROP ROLE regress_host_resource_admin; -- ok +-- Clean up +RESET SESSION AUTHORIZATION; +DROP ROLE regress_admin; -- ok diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out new file mode 100644 index 0000000..88b1ff8 --- /dev/null +++ b/src/test/modules/unsafe_tests/expected/rolenames.out @@ -0,0 +1,1091 @@ +CREATE FUNCTION chkrolattr() + RETURNS TABLE ("role" name, rolekeyword text, canlogin bool, replication bool) + AS $$ +SELECT r.rolname, v.keyword, r.rolcanlogin, r.rolreplication + FROM pg_roles r + JOIN (VALUES(CURRENT_ROLE, 'current_role'), + (CURRENT_USER, 'current_user'), + (SESSION_USER, 'session_user'), + ('current_role', '-'), + ('current_user', '-'), + ('session_user', '-'), + ('Public', '-'), + ('None', '-')) + AS v(uname, keyword) + ON (r.rolname = v.uname) + ORDER BY 1, 2; +$$ LANGUAGE SQL; +CREATE FUNCTION chksetconfig() + RETURNS TABLE (db name, "role" name, rolkeyword text, setconfig text[]) + AS $$ +SELECT COALESCE(d.datname, 'ALL'), COALESCE(r.rolname, 'ALL'), + COALESCE(v.keyword, '-'), s.setconfig + FROM pg_db_role_setting s + LEFT JOIN pg_roles r ON (r.oid = s.setrole) + LEFT JOIN pg_database d ON (d.oid = s.setdatabase) + LEFT JOIN (VALUES(CURRENT_ROLE, 'current_role'), + (CURRENT_USER, 'current_user'), + (SESSION_USER, 'session_user')) + AS v(uname, keyword) + ON (r.rolname = v.uname) + WHERE (r.rolname) IN ('Public', 'current_user', 'regress_testrol1', 'regress_testrol2') +ORDER BY 1, 2, 3; +$$ LANGUAGE SQL; +CREATE FUNCTION chkumapping() + RETURNS TABLE (umname name, umserver name, umoptions text[]) + AS $$ +SELECT r.rolname, s.srvname, m.umoptions + FROM pg_user_mapping m + LEFT JOIN pg_roles r ON (r.oid = m.umuser) + JOIN pg_foreign_server s ON (s.oid = m.umserver) + ORDER BY 2, 1; +$$ LANGUAGE SQL; +-- +-- We test creation and use of these role names to ensure that the server +-- correctly distinguishes role keywords from quoted names that look like +-- those keywords. In a test environment, creation of these roles may +-- provoke warnings, so hide the warnings by raising client_min_messages. +-- +SET client_min_messages = ERROR; +CREATE ROLE "Public"; +CREATE ROLE "None"; +CREATE ROLE "current_role"; +CREATE ROLE "current_user"; +CREATE ROLE "session_user"; +CREATE ROLE "user"; +RESET client_min_messages; +CREATE ROLE current_user; -- error +ERROR: CURRENT_USER cannot be used as a role name here +LINE 1: CREATE ROLE current_user; + ^ +CREATE ROLE current_role; -- error +ERROR: CURRENT_ROLE cannot be used as a role name here +LINE 1: CREATE ROLE current_role; + ^ +CREATE ROLE session_user; -- error +ERROR: SESSION_USER cannot be used as a role name here +LINE 1: CREATE ROLE session_user; + ^ +CREATE ROLE user; -- error +ERROR: syntax error at or near "user" +LINE 1: CREATE ROLE user; + ^ +CREATE ROLE all; -- error +ERROR: syntax error at or near "all" +LINE 1: CREATE ROLE all; + ^ +CREATE ROLE public; -- error +ERROR: role name "public" is reserved +LINE 1: CREATE ROLE public; + ^ +CREATE ROLE "public"; -- error +ERROR: role name "public" is reserved +LINE 1: CREATE ROLE "public"; + ^ +CREATE ROLE none; -- error +ERROR: role name "none" is reserved +LINE 1: CREATE ROLE none; + ^ +CREATE ROLE "none"; -- error +ERROR: role name "none" is reserved +LINE 1: CREATE ROLE "none"; + ^ +CREATE ROLE pg_abc; -- error +ERROR: role name "pg_abc" is reserved +DETAIL: Role names starting with "pg_" are reserved. +CREATE ROLE "pg_abc"; -- error +ERROR: role name "pg_abc" is reserved +DETAIL: Role names starting with "pg_" are reserved. +CREATE ROLE pg_abcdef; -- error +ERROR: role name "pg_abcdef" is reserved +DETAIL: Role names starting with "pg_" are reserved. +CREATE ROLE "pg_abcdef"; -- error +ERROR: role name "pg_abcdef" is reserved +DETAIL: Role names starting with "pg_" are reserved. +CREATE ROLE regress_testrol0 SUPERUSER LOGIN; +CREATE ROLE regress_testrolx SUPERUSER LOGIN; +CREATE ROLE regress_testrol2 SUPERUSER; +CREATE ROLE regress_testrol1 SUPERUSER LOGIN IN ROLE regress_testrol2; +\c - +SET SESSION AUTHORIZATION regress_testrol1; +SET ROLE regress_testrol2; +-- ALTER ROLE +BEGIN; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | f + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | f + regress_testrol2 | current_user | f | f + session_user | - | f | f +(8 rows) + +ALTER ROLE CURRENT_ROLE WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | f + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER ROLE "current_role" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER ROLE CURRENT_ROLE WITH NOREPLICATION; +ALTER ROLE CURRENT_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER ROLE "current_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER ROLE SESSION_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | t + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER ROLE "session_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | t + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | t +(8 rows) + +ALTER USER "Public" WITH REPLICATION; +ALTER USER "None" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | t + Public | - | f | t + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | t + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | t +(8 rows) + +ALTER USER regress_testrol1 WITH NOREPLICATION; +ALTER USER regress_testrol2 WITH NOREPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | t + Public | - | f | t + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | f + regress_testrol2 | current_user | f | f + session_user | - | f | t +(8 rows) + +ROLLBACK; +ALTER ROLE USER WITH LOGIN; -- error +ERROR: syntax error at or near "USER" +LINE 1: ALTER ROLE USER WITH LOGIN; + ^ +ALTER ROLE ALL WITH REPLICATION; -- error +ERROR: syntax error at or near "WITH" +LINE 1: ALTER ROLE ALL WITH REPLICATION; + ^ +ALTER ROLE SESSION_ROLE WITH NOREPLICATION; -- error +ERROR: role "session_role" does not exist +ALTER ROLE PUBLIC WITH NOREPLICATION; -- error +ERROR: role "public" does not exist +ALTER ROLE "public" WITH NOREPLICATION; -- error +ERROR: role "public" does not exist +ALTER ROLE NONE WITH NOREPLICATION; -- error +ERROR: role name "none" is reserved +LINE 1: ALTER ROLE NONE WITH NOREPLICATION; + ^ +ALTER ROLE "none" WITH NOREPLICATION; -- error +ERROR: role name "none" is reserved +LINE 1: ALTER ROLE "none" WITH NOREPLICATION; + ^ +ALTER ROLE nonexistent WITH NOREPLICATION; -- error +ERROR: role "nonexistent" does not exist +-- ALTER USER +BEGIN; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | f + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | f + regress_testrol2 | current_user | f | f + session_user | - | f | f +(8 rows) + +ALTER USER CURRENT_ROLE WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | f + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER USER "current_role" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER USER CURRENT_ROLE WITH NOREPLICATION; +ALTER USER CURRENT_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | f + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER USER "current_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER USER SESSION_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | t + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | f +(8 rows) + +ALTER USER "session_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | f + Public | - | f | f + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | t + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | t +(8 rows) + +ALTER USER "Public" WITH REPLICATION; +ALTER USER "None" WITH REPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | t + Public | - | f | t + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | t + regress_testrol2 | current_role | f | t + regress_testrol2 | current_user | f | t + session_user | - | f | t +(8 rows) + +ALTER USER regress_testrol1 WITH NOREPLICATION; +ALTER USER regress_testrol2 WITH NOREPLICATION; +SELECT * FROM chkrolattr(); + role | rolekeyword | canlogin | replication +------------------+--------------+----------+------------- + None | - | f | t + Public | - | f | t + current_role | - | f | t + current_user | - | f | t + regress_testrol1 | session_user | t | f + regress_testrol2 | current_role | f | f + regress_testrol2 | current_user | f | f + session_user | - | f | t +(8 rows) + +ROLLBACK; +ALTER USER USER WITH LOGIN; -- error +ERROR: syntax error at or near "USER" +LINE 1: ALTER USER USER WITH LOGIN; + ^ +ALTER USER ALL WITH REPLICATION; -- error +ERROR: syntax error at or near "WITH" +LINE 1: ALTER USER ALL WITH REPLICATION; + ^ +ALTER USER SESSION_ROLE WITH NOREPLICATION; -- error +ERROR: role "session_role" does not exist +ALTER USER PUBLIC WITH NOREPLICATION; -- error +ERROR: role "public" does not exist +ALTER USER "public" WITH NOREPLICATION; -- error +ERROR: role "public" does not exist +ALTER USER NONE WITH NOREPLICATION; -- error +ERROR: role name "none" is reserved +LINE 1: ALTER USER NONE WITH NOREPLICATION; + ^ +ALTER USER "none" WITH NOREPLICATION; -- error +ERROR: role name "none" is reserved +LINE 1: ALTER USER "none" WITH NOREPLICATION; + ^ +ALTER USER nonexistent WITH NOREPLICATION; -- error +ERROR: role "nonexistent" does not exist +-- ALTER ROLE SET/RESET +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +----+------+------------+----------- +(0 rows) + +ALTER ROLE CURRENT_ROLE SET application_name to 'BAZ'; +ALTER ROLE CURRENT_USER SET application_name to 'FOO'; +ALTER ROLE SESSION_USER SET application_name to 'BAR'; +ALTER ROLE "current_user" SET application_name to 'FOOFOO'; +ALTER ROLE "Public" SET application_name to 'BARBAR'; +ALTER ROLE ALL SET application_name to 'SLAP'; +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +-----+------------------+--------------+--------------------------- + ALL | Public | - | {application_name=BARBAR} + ALL | current_user | - | {application_name=FOOFOO} + ALL | regress_testrol1 | session_user | {application_name=BAR} + ALL | regress_testrol2 | current_role | {application_name=FOO} + ALL | regress_testrol2 | current_user | {application_name=FOO} +(5 rows) + +ALTER ROLE regress_testrol1 SET application_name to 'SLAM'; +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +-----+------------------+--------------+--------------------------- + ALL | Public | - | {application_name=BARBAR} + ALL | current_user | - | {application_name=FOOFOO} + ALL | regress_testrol1 | session_user | {application_name=SLAM} + ALL | regress_testrol2 | current_role | {application_name=FOO} + ALL | regress_testrol2 | current_user | {application_name=FOO} +(5 rows) + +ALTER ROLE CURRENT_ROLE RESET application_name; +ALTER ROLE CURRENT_USER RESET application_name; +ALTER ROLE SESSION_USER RESET application_name; +ALTER ROLE "current_user" RESET application_name; +ALTER ROLE "Public" RESET application_name; +ALTER ROLE ALL RESET application_name; +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +----+------+------------+----------- +(0 rows) + +ALTER ROLE USER SET application_name to 'BOOM'; -- error +ERROR: syntax error at or near "USER" +LINE 1: ALTER ROLE USER SET application_name to 'BOOM'; + ^ +ALTER ROLE PUBLIC SET application_name to 'BOMB'; -- error +ERROR: role "public" does not exist +ALTER ROLE nonexistent SET application_name to 'BOMB'; -- error +ERROR: role "nonexistent" does not exist +-- ALTER USER SET/RESET +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +----+------+------------+----------- +(0 rows) + +ALTER USER CURRENT_ROLE SET application_name to 'BAZ'; +ALTER USER CURRENT_USER SET application_name to 'FOO'; +ALTER USER SESSION_USER SET application_name to 'BAR'; +ALTER USER "current_user" SET application_name to 'FOOFOO'; +ALTER USER "Public" SET application_name to 'BARBAR'; +ALTER USER ALL SET application_name to 'SLAP'; +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +-----+------------------+--------------+--------------------------- + ALL | Public | - | {application_name=BARBAR} + ALL | current_user | - | {application_name=FOOFOO} + ALL | regress_testrol1 | session_user | {application_name=BAR} + ALL | regress_testrol2 | current_role | {application_name=FOO} + ALL | regress_testrol2 | current_user | {application_name=FOO} +(5 rows) + +ALTER USER regress_testrol1 SET application_name to 'SLAM'; +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +-----+------------------+--------------+--------------------------- + ALL | Public | - | {application_name=BARBAR} + ALL | current_user | - | {application_name=FOOFOO} + ALL | regress_testrol1 | session_user | {application_name=SLAM} + ALL | regress_testrol2 | current_role | {application_name=FOO} + ALL | regress_testrol2 | current_user | {application_name=FOO} +(5 rows) + +ALTER USER CURRENT_ROLE RESET application_name; +ALTER USER CURRENT_USER RESET application_name; +ALTER USER SESSION_USER RESET application_name; +ALTER USER "current_user" RESET application_name; +ALTER USER "Public" RESET application_name; +ALTER USER ALL RESET application_name; +SELECT * FROM chksetconfig(); + db | role | rolkeyword | setconfig +----+------+------------+----------- +(0 rows) + +ALTER USER USER SET application_name to 'BOOM'; -- error +ERROR: syntax error at or near "USER" +LINE 1: ALTER USER USER SET application_name to 'BOOM'; + ^ +ALTER USER PUBLIC SET application_name to 'BOMB'; -- error +ERROR: role "public" does not exist +ALTER USER NONE SET application_name to 'BOMB'; -- error +ERROR: role name "none" is reserved +LINE 1: ALTER USER NONE SET application_name to 'BOMB'; + ^ +ALTER USER nonexistent SET application_name to 'BOMB'; -- error +ERROR: role "nonexistent" does not exist +-- CREATE SCHEMA +CREATE SCHEMA newschema1 AUTHORIZATION CURRENT_USER; +CREATE SCHEMA newschema2 AUTHORIZATION "current_user"; +CREATE SCHEMA newschema3 AUTHORIZATION CURRENT_ROLE; +CREATE SCHEMA newschema4 AUTHORIZATION SESSION_USER; +CREATE SCHEMA newschema5 AUTHORIZATION regress_testrolx; +CREATE SCHEMA newschema6 AUTHORIZATION "Public"; +CREATE SCHEMA newschemax AUTHORIZATION USER; -- error +ERROR: syntax error at or near "USER" +LINE 1: CREATE SCHEMA newschemax AUTHORIZATION USER; + ^ +CREATE SCHEMA newschemax AUTHORIZATION PUBLIC; -- error +ERROR: role "public" does not exist +CREATE SCHEMA newschemax AUTHORIZATION "public"; -- error +ERROR: role "public" does not exist +CREATE SCHEMA newschemax AUTHORIZATION NONE; -- error +ERROR: role name "none" is reserved +LINE 1: CREATE SCHEMA newschemax AUTHORIZATION NONE; + ^ +CREATE SCHEMA newschemax AUTHORIZATION nonexistent; -- error +ERROR: role "nonexistent" does not exist +SELECT n.nspname, r.rolname FROM pg_namespace n + JOIN pg_roles r ON (r.oid = n.nspowner) + WHERE n.nspname LIKE 'newschema_' ORDER BY 1; + nspname | rolname +------------+------------------ + newschema1 | regress_testrol2 + newschema2 | current_user + newschema3 | regress_testrol2 + newschema4 | regress_testrol1 + newschema5 | regress_testrolx + newschema6 | Public +(6 rows) + +CREATE SCHEMA IF NOT EXISTS newschema1 AUTHORIZATION CURRENT_USER; +NOTICE: schema "newschema1" already exists, skipping +CREATE SCHEMA IF NOT EXISTS newschema2 AUTHORIZATION "current_user"; +NOTICE: schema "newschema2" already exists, skipping +CREATE SCHEMA IF NOT EXISTS newschema3 AUTHORIZATION CURRENT_ROLE; +NOTICE: schema "newschema3" already exists, skipping +CREATE SCHEMA IF NOT EXISTS newschema4 AUTHORIZATION SESSION_USER; +NOTICE: schema "newschema4" already exists, skipping +CREATE SCHEMA IF NOT EXISTS newschema5 AUTHORIZATION regress_testrolx; +NOTICE: schema "newschema5" already exists, skipping +CREATE SCHEMA IF NOT EXISTS newschema6 AUTHORIZATION "Public"; +NOTICE: schema "newschema6" already exists, skipping +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION USER; -- error +ERROR: syntax error at or near "USER" +LINE 1: CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION USER; + ^ +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION PUBLIC; -- error +ERROR: role "public" does not exist +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION "public"; -- error +ERROR: role "public" does not exist +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION NONE; -- error +ERROR: role name "none" is reserved +LINE 1: CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION NONE; + ^ +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION nonexistent; -- error +ERROR: role "nonexistent" does not exist +SELECT n.nspname, r.rolname FROM pg_namespace n + JOIN pg_roles r ON (r.oid = n.nspowner) + WHERE n.nspname LIKE 'newschema_' ORDER BY 1; + nspname | rolname +------------+------------------ + newschema1 | regress_testrol2 + newschema2 | current_user + newschema3 | regress_testrol2 + newschema4 | regress_testrol1 + newschema5 | regress_testrolx + newschema6 | Public +(6 rows) + +-- ALTER TABLE OWNER TO +\c - +SET SESSION AUTHORIZATION regress_testrol0; +CREATE TABLE testtab1 (a int); +CREATE TABLE testtab2 (a int); +CREATE TABLE testtab3 (a int); +CREATE TABLE testtab4 (a int); +CREATE TABLE testtab5 (a int); +CREATE TABLE testtab6 (a int); +CREATE TABLE testtab7 (a int); +\c - +SET SESSION AUTHORIZATION regress_testrol1; +SET ROLE regress_testrol2; +ALTER TABLE testtab1 OWNER TO CURRENT_USER; +ALTER TABLE testtab2 OWNER TO "current_user"; +ALTER TABLE testtab3 OWNER TO CURRENT_ROLE; +ALTER TABLE testtab4 OWNER TO SESSION_USER; +ALTER TABLE testtab5 OWNER TO regress_testrolx; +ALTER TABLE testtab6 OWNER TO "Public"; +ALTER TABLE testtab7 OWNER TO USER; --error +ERROR: syntax error at or near "USER" +LINE 1: ALTER TABLE testtab7 OWNER TO USER; + ^ +ALTER TABLE testtab7 OWNER TO PUBLIC; -- error +ERROR: role "public" does not exist +ALTER TABLE testtab7 OWNER TO "public"; -- error +ERROR: role "public" does not exist +ALTER TABLE testtab7 OWNER TO nonexistent; -- error +ERROR: role "nonexistent" does not exist +SELECT c.relname, r.rolname + FROM pg_class c JOIN pg_roles r ON (r.oid = c.relowner) + WHERE relname LIKE 'testtab_' + ORDER BY 1; + relname | rolname +----------+------------------ + testtab1 | regress_testrol2 + testtab2 | current_user + testtab3 | regress_testrol2 + testtab4 | regress_testrol1 + testtab5 | regress_testrolx + testtab6 | Public + testtab7 | regress_testrol0 +(7 rows) + +-- ALTER TABLE, VIEW, MATERIALIZED VIEW, FOREIGN TABLE, SEQUENCE are +-- changed their owner in the same way. +-- ALTER AGGREGATE +\c - +SET SESSION AUTHORIZATION regress_testrol0; +CREATE AGGREGATE testagg1(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg2(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg3(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg4(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg5(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg6(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg7(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg8(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg9(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagga(int2) (SFUNC = int2_sum, STYPE = int8); +\c - +SET SESSION AUTHORIZATION regress_testrol1; +SET ROLE regress_testrol2; +ALTER AGGREGATE testagg1(int2) OWNER TO CURRENT_USER; +ALTER AGGREGATE testagg2(int2) OWNER TO "current_user"; +ALTER AGGREGATE testagg3(int2) OWNER TO CURRENT_ROLE; +ALTER AGGREGATE testagg4(int2) OWNER TO SESSION_USER; +ALTER AGGREGATE testagg5(int2) OWNER TO regress_testrolx; +ALTER AGGREGATE testagg6(int2) OWNER TO "Public"; +ALTER AGGREGATE testagg6(int2) OWNER TO USER; -- error +ERROR: syntax error at or near "USER" +LINE 1: ALTER AGGREGATE testagg6(int2) OWNER TO USER; + ^ +ALTER AGGREGATE testagg6(int2) OWNER TO PUBLIC; -- error +ERROR: role "public" does not exist +ALTER AGGREGATE testagg6(int2) OWNER TO "public"; -- error +ERROR: role "public" does not exist +ALTER AGGREGATE testagg6(int2) OWNER TO nonexistent; -- error +ERROR: role "nonexistent" does not exist +SELECT p.proname, r.rolname + FROM pg_proc p JOIN pg_roles r ON (r.oid = p.proowner) + WHERE proname LIKE 'testagg_' + ORDER BY 1; + proname | rolname +----------+------------------ + testagg1 | regress_testrol2 + testagg2 | current_user + testagg3 | regress_testrol2 + testagg4 | regress_testrol1 + testagg5 | regress_testrolx + testagg6 | Public + testagg7 | regress_testrol0 + testagg8 | regress_testrol0 + testagg9 | regress_testrol0 + testagga | regress_testrol0 +(10 rows) + +-- CREATE USER MAPPING +CREATE FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv1 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv2 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv3 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv4 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv5 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv6 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv7 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv8 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv9 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv10 FOREIGN DATA WRAPPER test_wrapper; +CREATE USER MAPPING FOR CURRENT_USER SERVER sv1 OPTIONS (user 'CURRENT_USER'); +CREATE USER MAPPING FOR "current_user" SERVER sv2 OPTIONS (user '"current_user"'); +CREATE USER MAPPING FOR CURRENT_ROLE SERVER sv3 OPTIONS (user 'CURRENT_ROLE'); +CREATE USER MAPPING FOR USER SERVER sv4 OPTIONS (user 'USER'); +CREATE USER MAPPING FOR "user" SERVER sv5 OPTIONS (user '"USER"'); +CREATE USER MAPPING FOR SESSION_USER SERVER sv6 OPTIONS (user 'SESSION_USER'); +CREATE USER MAPPING FOR PUBLIC SERVER sv7 OPTIONS (user 'PUBLIC'); +CREATE USER MAPPING FOR "Public" SERVER sv8 OPTIONS (user '"Public"'); +CREATE USER MAPPING FOR regress_testrolx SERVER sv9 OPTIONS (user 'regress_testrolx'); +CREATE USER MAPPING FOR nonexistent SERVER sv10 OPTIONS (user 'nonexistent'); -- error; +ERROR: role "nonexistent" does not exist +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+--------------------------- + regress_testrol2 | sv1 | {user=CURRENT_USER} + current_user | sv2 | {"user=\"current_user\""} + regress_testrol2 | sv3 | {user=CURRENT_ROLE} + regress_testrol2 | sv4 | {user=USER} + user | sv5 | {"user=\"USER\""} + regress_testrol1 | sv6 | {user=SESSION_USER} + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(9 rows) + +-- ALTER USER MAPPING +ALTER USER MAPPING FOR CURRENT_USER SERVER sv1 + OPTIONS (SET user 'CURRENT_USER_alt'); +ALTER USER MAPPING FOR "current_user" SERVER sv2 + OPTIONS (SET user '"current_user"_alt'); +ALTER USER MAPPING FOR CURRENT_ROLE SERVER sv3 + OPTIONS (SET user 'CURRENT_ROLE_alt'); +ALTER USER MAPPING FOR USER SERVER sv4 + OPTIONS (SET user 'USER_alt'); +ALTER USER MAPPING FOR "user" SERVER sv5 + OPTIONS (SET user '"user"_alt'); +ALTER USER MAPPING FOR SESSION_USER SERVER sv6 + OPTIONS (SET user 'SESSION_USER_alt'); +ALTER USER MAPPING FOR PUBLIC SERVER sv7 + OPTIONS (SET user 'public_alt'); +ALTER USER MAPPING FOR "Public" SERVER sv8 + OPTIONS (SET user '"Public"_alt'); +ALTER USER MAPPING FOR regress_testrolx SERVER sv9 + OPTIONS (SET user 'regress_testrolx_alt'); +ALTER USER MAPPING FOR nonexistent SERVER sv10 + OPTIONS (SET user 'nonexistent_alt'); -- error +ERROR: role "nonexistent" does not exist +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------------- + regress_testrol2 | sv1 | {user=CURRENT_USER_alt} + current_user | sv2 | {"user=\"current_user\"_alt"} + regress_testrol2 | sv3 | {user=CURRENT_ROLE_alt} + regress_testrol2 | sv4 | {user=USER_alt} + user | sv5 | {"user=\"user\"_alt"} + regress_testrol1 | sv6 | {user=SESSION_USER_alt} + | sv7 | {user=public_alt} + Public | sv8 | {"user=\"Public\"_alt"} + regress_testrolx | sv9 | {user=regress_testrolx_alt} +(9 rows) + +-- DROP USER MAPPING +DROP USER MAPPING FOR CURRENT_USER SERVER sv1; +DROP USER MAPPING FOR "current_user" SERVER sv2; +DROP USER MAPPING FOR CURRENT_ROLE SERVER sv3; +DROP USER MAPPING FOR USER SERVER sv4; +DROP USER MAPPING FOR "user" SERVER sv5; +DROP USER MAPPING FOR SESSION_USER SERVER sv6; +DROP USER MAPPING FOR PUBLIC SERVER sv7; +DROP USER MAPPING FOR "Public" SERVER sv8; +DROP USER MAPPING FOR regress_testrolx SERVER sv9; +DROP USER MAPPING FOR nonexistent SERVER sv10; -- error +ERROR: role "nonexistent" does not exist +SELECT * FROM chkumapping(); + umname | umserver | umoptions +--------+----------+----------- +(0 rows) + +CREATE USER MAPPING FOR CURRENT_USER SERVER sv1 OPTIONS (user 'CURRENT_USER'); +CREATE USER MAPPING FOR "current_user" SERVER sv2 OPTIONS (user '"current_user"'); +CREATE USER MAPPING FOR CURRENT_ROLE SERVER sv3 OPTIONS (user 'CURRENT_ROLE'); +CREATE USER MAPPING FOR USER SERVER sv4 OPTIONS (user 'USER'); +CREATE USER MAPPING FOR "user" SERVER sv5 OPTIONS (user '"USER"'); +CREATE USER MAPPING FOR SESSION_USER SERVER sv6 OPTIONS (user 'SESSION_USER'); +CREATE USER MAPPING FOR PUBLIC SERVER sv7 OPTIONS (user 'PUBLIC'); +CREATE USER MAPPING FOR "Public" SERVER sv8 OPTIONS (user '"Public"'); +CREATE USER MAPPING FOR regress_testrolx SERVER sv9 OPTIONS (user 'regress_testrolx'); +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+--------------------------- + regress_testrol2 | sv1 | {user=CURRENT_USER} + current_user | sv2 | {"user=\"current_user\""} + regress_testrol2 | sv3 | {user=CURRENT_ROLE} + regress_testrol2 | sv4 | {user=USER} + user | sv5 | {"user=\"USER\""} + regress_testrol1 | sv6 | {user=SESSION_USER} + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(9 rows) + +-- DROP USER MAPPING IF EXISTS +DROP USER MAPPING IF EXISTS FOR CURRENT_USER SERVER sv1; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+--------------------------- + current_user | sv2 | {"user=\"current_user\""} + regress_testrol2 | sv3 | {user=CURRENT_ROLE} + regress_testrol2 | sv4 | {user=USER} + user | sv5 | {"user=\"USER\""} + regress_testrol1 | sv6 | {user=SESSION_USER} + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(8 rows) + +DROP USER MAPPING IF EXISTS FOR "current_user" SERVER sv2; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------- + regress_testrol2 | sv3 | {user=CURRENT_ROLE} + regress_testrol2 | sv4 | {user=USER} + user | sv5 | {"user=\"USER\""} + regress_testrol1 | sv6 | {user=SESSION_USER} + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(7 rows) + +DROP USER MAPPING IF EXISTS FOR CURRENT_USER SERVER sv3; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------- + regress_testrol2 | sv4 | {user=USER} + user | sv5 | {"user=\"USER\""} + regress_testrol1 | sv6 | {user=SESSION_USER} + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(6 rows) + +DROP USER MAPPING IF EXISTS FOR USER SERVER sv4; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------- + user | sv5 | {"user=\"USER\""} + regress_testrol1 | sv6 | {user=SESSION_USER} + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(5 rows) + +DROP USER MAPPING IF EXISTS FOR "user" SERVER sv5; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------- + regress_testrol1 | sv6 | {user=SESSION_USER} + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(4 rows) + +DROP USER MAPPING IF EXISTS FOR SESSION_USER SERVER sv6; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------- + | sv7 | {user=PUBLIC} + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(3 rows) + +DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER sv7; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------- + Public | sv8 | {"user=\"Public\""} + regress_testrolx | sv9 | {user=regress_testrolx} +(2 rows) + +DROP USER MAPPING IF EXISTS FOR "Public" SERVER sv8; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +------------------+----------+------------------------- + regress_testrolx | sv9 | {user=regress_testrolx} +(1 row) + +DROP USER MAPPING IF EXISTS FOR regress_testrolx SERVER sv9; +SELECT * FROM chkumapping(); + umname | umserver | umoptions +--------+----------+----------- +(0 rows) + +DROP USER MAPPING IF EXISTS FOR nonexistent SERVER sv10; -- error +NOTICE: role "nonexistent" does not exist, skipping +-- GRANT/REVOKE +GRANT regress_testrol0 TO pg_signal_backend; -- success +SET ROLE pg_signal_backend; --success +RESET ROLE; +CREATE SCHEMA test_roles_schema AUTHORIZATION pg_signal_backend; --success +SET ROLE regress_testrol2; +UPDATE pg_proc SET proacl = null WHERE proname LIKE 'testagg_'; +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + proname | proacl +----------+-------- + testagg1 | + testagg2 | + testagg3 | + testagg4 | + testagg5 | + testagg6 | + testagg7 | + testagg8 | + testagg9 | + testagga | +(10 rows) + +REVOKE ALL PRIVILEGES ON FUNCTION testagg1(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg2(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg3(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg4(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg5(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg6(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg7(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg8(int2) FROM PUBLIC; +GRANT ALL PRIVILEGES ON FUNCTION testagg1(int2) TO PUBLIC; +GRANT ALL PRIVILEGES ON FUNCTION testagg2(int2) TO CURRENT_USER; +GRANT ALL PRIVILEGES ON FUNCTION testagg3(int2) TO "current_user"; +GRANT ALL PRIVILEGES ON FUNCTION testagg4(int2) TO CURRENT_ROLE; +GRANT ALL PRIVILEGES ON FUNCTION testagg5(int2) TO SESSION_USER; +GRANT ALL PRIVILEGES ON FUNCTION testagg6(int2) TO "Public"; +GRANT ALL PRIVILEGES ON FUNCTION testagg7(int2) TO regress_testrolx; +GRANT ALL PRIVILEGES ON FUNCTION testagg8(int2) TO "public"; +GRANT ALL PRIVILEGES ON FUNCTION testagg9(int2) + TO current_user, public, regress_testrolx; +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + proname | proacl +----------+----------------------------------------------------------------------------------------------------------------------------------- + testagg1 | {regress_testrol2=X/regress_testrol2,=X/regress_testrol2} + testagg2 | {current_user=X/current_user,regress_testrol2=X/current_user} + testagg3 | {regress_testrol2=X/regress_testrol2,current_user=X/regress_testrol2} + testagg4 | {regress_testrol1=X/regress_testrol1,regress_testrol2=X/regress_testrol1} + testagg5 | {regress_testrolx=X/regress_testrolx,regress_testrol1=X/regress_testrolx} + testagg6 | {Public=X/Public} + testagg7 | {regress_testrol0=X/regress_testrol0,regress_testrolx=X/regress_testrol0} + testagg8 | {regress_testrol0=X/regress_testrol0,=X/regress_testrol0} + testagg9 | {=X/regress_testrol0,regress_testrol0=X/regress_testrol0,regress_testrol2=X/regress_testrol0,regress_testrolx=X/regress_testrol0} + testagga | +(10 rows) + +GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO USER; --error +ERROR: syntax error at or near "USER" +LINE 1: GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO USER; + ^ +GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO NONE; --error +ERROR: role name "none" is reserved +LINE 1: GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO NONE; + ^ +GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO "none"; --error +ERROR: role name "none" is reserved +LINE 1: GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO "none"; + ^ +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + proname | proacl +----------+----------------------------------------------------------------------------------------------------------------------------------- + testagg1 | {regress_testrol2=X/regress_testrol2,=X/regress_testrol2} + testagg2 | {current_user=X/current_user,regress_testrol2=X/current_user} + testagg3 | {regress_testrol2=X/regress_testrol2,current_user=X/regress_testrol2} + testagg4 | {regress_testrol1=X/regress_testrol1,regress_testrol2=X/regress_testrol1} + testagg5 | {regress_testrolx=X/regress_testrolx,regress_testrol1=X/regress_testrolx} + testagg6 | {Public=X/Public} + testagg7 | {regress_testrol0=X/regress_testrol0,regress_testrolx=X/regress_testrol0} + testagg8 | {regress_testrol0=X/regress_testrol0,=X/regress_testrol0} + testagg9 | {=X/regress_testrol0,regress_testrol0=X/regress_testrol0,regress_testrol2=X/regress_testrol0,regress_testrolx=X/regress_testrol0} + testagga | +(10 rows) + +REVOKE ALL PRIVILEGES ON FUNCTION testagg1(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg2(int2) FROM CURRENT_USER; +REVOKE ALL PRIVILEGES ON FUNCTION testagg3(int2) FROM "current_user"; +REVOKE ALL PRIVILEGES ON FUNCTION testagg4(int2) FROM CURRENT_ROLE; +REVOKE ALL PRIVILEGES ON FUNCTION testagg5(int2) FROM SESSION_USER; +REVOKE ALL PRIVILEGES ON FUNCTION testagg6(int2) FROM "Public"; +REVOKE ALL PRIVILEGES ON FUNCTION testagg7(int2) FROM regress_testrolx; +REVOKE ALL PRIVILEGES ON FUNCTION testagg8(int2) FROM "public"; +REVOKE ALL PRIVILEGES ON FUNCTION testagg9(int2) + FROM current_user, public, regress_testrolx; +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + proname | proacl +----------+--------------------------------------- + testagg1 | {regress_testrol2=X/regress_testrol2} + testagg2 | {current_user=X/current_user} + testagg3 | {regress_testrol2=X/regress_testrol2} + testagg4 | {regress_testrol1=X/regress_testrol1} + testagg5 | {regress_testrolx=X/regress_testrolx} + testagg6 | {} + testagg7 | {regress_testrol0=X/regress_testrol0} + testagg8 | {regress_testrol0=X/regress_testrol0} + testagg9 | {regress_testrol0=X/regress_testrol0} + testagga | +(10 rows) + +REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM USER; --error +ERROR: syntax error at or near "USER" +LINE 1: REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM USER; + ^ +REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM NONE; --error +ERROR: role name "none" is reserved +LINE 1: REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM NONE; + ^ +REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM "none"; --error +ERROR: role name "none" is reserved +LINE 1: ...EVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM "none"; + ^ +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + proname | proacl +----------+--------------------------------------- + testagg1 | {regress_testrol2=X/regress_testrol2} + testagg2 | {current_user=X/current_user} + testagg3 | {regress_testrol2=X/regress_testrol2} + testagg4 | {regress_testrol1=X/regress_testrol1} + testagg5 | {regress_testrolx=X/regress_testrolx} + testagg6 | {} + testagg7 | {regress_testrol0=X/regress_testrol0} + testagg8 | {regress_testrol0=X/regress_testrol0} + testagg9 | {regress_testrol0=X/regress_testrol0} + testagga | +(10 rows) + +-- DEFAULT MONITORING ROLES +CREATE ROLE regress_role_haspriv; +CREATE ROLE regress_role_nopriv; +-- pg_read_all_stats +GRANT pg_read_all_stats TO regress_role_haspriv; +SET SESSION AUTHORIZATION regress_role_haspriv; +-- returns true with role member of pg_read_all_stats +SELECT COUNT(*) = 0 AS haspriv FROM pg_stat_activity + WHERE query = '<insufficient privilege>'; + haspriv +--------- + t +(1 row) + +SET SESSION AUTHORIZATION regress_role_nopriv; +-- returns false with role not member of pg_read_all_stats +SELECT COUNT(*) = 0 AS haspriv FROM pg_stat_activity + WHERE query = '<insufficient privilege>'; + haspriv +--------- + f +(1 row) + +RESET SESSION AUTHORIZATION; +REVOKE pg_read_all_stats FROM regress_role_haspriv; +-- pg_read_all_settings +GRANT pg_read_all_settings TO regress_role_haspriv; +BEGIN; +-- A GUC using GUC_SUPERUSER_ONLY is useful for negative tests. +SET LOCAL session_preload_libraries TO 'path-to-preload-libraries'; +SET SESSION AUTHORIZATION regress_role_haspriv; +-- passes with role member of pg_read_all_settings +SHOW session_preload_libraries; + session_preload_libraries +----------------------------- + "path-to-preload-libraries" +(1 row) + +SET SESSION AUTHORIZATION regress_role_nopriv; +-- fails with role not member of pg_read_all_settings +SHOW session_preload_libraries; +ERROR: must be superuser or have privileges of pg_read_all_settings to examine "session_preload_libraries" +RESET SESSION AUTHORIZATION; +ERROR: current transaction is aborted, commands ignored until end of transaction block +ROLLBACK; +REVOKE pg_read_all_settings FROM regress_role_haspriv; +-- clean up +\c +DROP SCHEMA test_roles_schema; +DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE; +DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; +DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user"; +DROP ROLE regress_role_haspriv, regress_role_nopriv; diff --git a/src/test/modules/unsafe_tests/sql/alter_system_table.sql b/src/test/modules/unsafe_tests/sql/alter_system_table.sql new file mode 100644 index 0000000..b77b68c --- /dev/null +++ b/src/test/modules/unsafe_tests/sql/alter_system_table.sql @@ -0,0 +1,194 @@ +-- +-- Tests for things affected by allow_system_table_mods +-- +-- We run the same set of commands once with allow_system_table_mods +-- off and then again with on. +-- +-- The "on" tests should where possible be wrapped in BEGIN/ROLLBACK +-- blocks so as to not leave a mess around. + +CREATE USER regress_user_ast; + +SET allow_system_table_mods = off; + +-- create new table in pg_catalog +CREATE TABLE pg_catalog.test (a int); + +-- anyarray column +CREATE TABLE t1x (a int, b anyarray); + +-- index on system catalog +ALTER TABLE pg_namespace ADD CONSTRAINT foo UNIQUE USING INDEX pg_namespace_nspname_index; + +-- write to system catalog table as superuser +-- (allowed even without allow_system_table_mods) +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 0, 'foo'); + +-- write to system catalog table as normal user +GRANT INSERT ON pg_description TO regress_user_ast; +SET ROLE regress_user_ast; +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 1, 'foo'); +RESET ROLE; + +-- policy on system catalog +CREATE POLICY foo ON pg_description FOR SELECT USING (description NOT LIKE 'secret%'); + +-- reserved schema name +CREATE SCHEMA pg_foo; + +-- drop system table +DROP TABLE pg_description; + +-- truncate of system table +TRUNCATE pg_description; + +-- rename column of system table +ALTER TABLE pg_description RENAME COLUMN description TO comment; + +-- ATSimplePermissions() +ALTER TABLE pg_description ALTER COLUMN description SET NOT NULL; + +-- SET STATISTICS +ALTER TABLE pg_description ALTER COLUMN description SET STATISTICS -1; + +-- foreign key referencing catalog +CREATE TABLE foo (a oid, b oid, c int, FOREIGN KEY (a, b, c) REFERENCES pg_description); + +-- RangeVarCallbackOwnsRelation() +CREATE INDEX pg_description_test_index ON pg_description (description); + +-- RangeVarCallbackForAlterRelation() +ALTER TABLE pg_description RENAME TO pg_comment; +ALTER TABLE pg_description SET SCHEMA public; + +-- reserved tablespace name +CREATE TABLESPACE pg_foo LOCATION '/no/such/location'; + +-- triggers +CREATE FUNCTION tf1() RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN NULL; +END $$; + +CREATE TRIGGER t1 BEFORE INSERT ON pg_description EXECUTE FUNCTION tf1(); +ALTER TRIGGER t1 ON pg_description RENAME TO t2; +--DROP TRIGGER t2 ON pg_description; + +-- rules +CREATE RULE r1 AS ON INSERT TO pg_description DO INSTEAD NOTHING; +ALTER RULE r1 ON pg_description RENAME TO r2; +-- now make one to test dropping: +SET allow_system_table_mods TO on; +CREATE RULE r2 AS ON INSERT TO pg_description DO INSTEAD NOTHING; +RESET allow_system_table_mods; +DROP RULE r2 ON pg_description; +-- cleanup: +SET allow_system_table_mods TO on; +DROP RULE r2 ON pg_description; +RESET allow_system_table_mods; + + +SET allow_system_table_mods = on; + +-- create new table in pg_catalog +BEGIN; +CREATE TABLE pg_catalog.test (a int); +ROLLBACK; + +-- anyarray column +BEGIN; +CREATE TABLE t1 (a int, b anyarray); +ROLLBACK; + +-- index on system catalog +BEGIN; +ALTER TABLE pg_namespace ADD CONSTRAINT foo UNIQUE USING INDEX pg_namespace_nspname_index; +ROLLBACK; + +-- write to system catalog table as superuser +BEGIN; +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 2, 'foo'); +ROLLBACK; + +-- write to system catalog table as normal user +-- (not allowed) +SET ROLE regress_user_ast; +INSERT INTO pg_description (objoid, classoid, objsubid, description) VALUES (0, 0, 3, 'foo'); +RESET ROLE; + +-- policy on system catalog +BEGIN; +CREATE POLICY foo ON pg_description FOR SELECT USING (description NOT LIKE 'secret%'); +ROLLBACK; + +-- reserved schema name +BEGIN; +CREATE SCHEMA pg_foo; +ROLLBACK; + +-- drop system table +-- (This will fail anyway because it's pinned.) +BEGIN; +DROP TABLE pg_description; +ROLLBACK; + +-- truncate of system table +BEGIN; +TRUNCATE pg_description; +ROLLBACK; + +-- rename column of system table +BEGIN; +ALTER TABLE pg_description RENAME COLUMN description TO comment; +ROLLBACK; + +-- ATSimplePermissions() +BEGIN; +ALTER TABLE pg_description ALTER COLUMN description SET NOT NULL; +ROLLBACK; + +-- SET STATISTICS +BEGIN; +ALTER TABLE pg_description ALTER COLUMN description SET STATISTICS -1; +ROLLBACK; + +-- foreign key referencing catalog +BEGIN; +CREATE TABLE foo (a oid, b oid, c int, FOREIGN KEY (a, b, c) REFERENCES pg_description); +ROLLBACK; + +-- RangeVarCallbackOwnsRelation() +BEGIN; +CREATE INDEX pg_description_test_index ON pg_description (description); +ROLLBACK; + +-- RangeVarCallbackForAlterRelation() +BEGIN; +ALTER TABLE pg_description RENAME TO pg_comment; +ROLLBACK; +BEGIN; +ALTER TABLE pg_description SET SCHEMA public; +ROLLBACK; + +-- reserved tablespace name +SET client_min_messages = error; -- disable ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS warning +CREATE TABLESPACE pg_foo LOCATION '/no/such/location'; +RESET client_min_messages; + +-- triggers +CREATE TRIGGER t1 BEFORE INSERT ON pg_description EXECUTE FUNCTION tf1(); +ALTER TRIGGER t1 ON pg_description RENAME TO t2; +DROP TRIGGER t2 ON pg_description; + +-- rules +CREATE RULE r1 AS ON INSERT TO pg_description DO INSTEAD NOTHING; +ALTER RULE r1 ON pg_description RENAME TO r2; +DROP RULE r2 ON pg_description; + + +-- cleanup +REVOKE ALL ON pg_description FROM regress_user_ast; +DROP USER regress_user_ast; +DROP FUNCTION tf1; diff --git a/src/test/modules/unsafe_tests/sql/guc_privs.sql b/src/test/modules/unsafe_tests/sql/guc_privs.sql new file mode 100644 index 0000000..6c7733f --- /dev/null +++ b/src/test/modules/unsafe_tests/sql/guc_privs.sql @@ -0,0 +1,253 @@ +-- +-- Tests for privileges on GUCs. +-- This is unsafe because changes will affect other databases in the cluster. +-- + +-- Test with a superuser role. +CREATE ROLE regress_admin SUPERUSER; + +-- Perform operations as user 'regress_admin'. +SET SESSION AUTHORIZATION regress_admin; + +-- PGC_BACKEND +SET ignore_system_indexes = OFF; -- fail, cannot be set after connection start +RESET ignore_system_indexes; -- fail, cannot be set after connection start +ALTER SYSTEM SET ignore_system_indexes = OFF; -- ok +ALTER SYSTEM RESET ignore_system_indexes; -- ok +-- PGC_INTERNAL +SET block_size = 50; -- fail, cannot be changed +RESET block_size; -- fail, cannot be changed +ALTER SYSTEM SET block_size = 50; -- fail, cannot be changed +ALTER SYSTEM RESET block_size; -- fail, cannot be changed +-- PGC_POSTMASTER +SET autovacuum_freeze_max_age = 1000050000; -- fail, requires restart +RESET autovacuum_freeze_max_age; -- fail, requires restart +ALTER SYSTEM SET autovacuum_freeze_max_age = 1000050000; -- ok +ALTER SYSTEM RESET autovacuum_freeze_max_age; -- ok +ALTER SYSTEM SET config_file = '/usr/local/data/postgresql.conf'; -- fail, cannot be changed +ALTER SYSTEM RESET config_file; -- fail, cannot be changed +-- PGC_SIGHUP +SET autovacuum = OFF; -- fail, requires reload +RESET autovacuum; -- fail, requires reload +ALTER SYSTEM SET autovacuum = OFF; -- ok +ALTER SYSTEM RESET autovacuum; -- ok +-- PGC_SUSET +SET lc_messages = 'C'; -- ok +RESET lc_messages; -- ok +ALTER SYSTEM SET lc_messages = 'C'; -- ok +ALTER SYSTEM RESET lc_messages; -- ok +-- PGC_SU_BACKEND +SET jit_debugging_support = OFF; -- fail, cannot be set after connection start +RESET jit_debugging_support; -- fail, cannot be set after connection start +ALTER SYSTEM SET jit_debugging_support = OFF; -- ok +ALTER SYSTEM RESET jit_debugging_support; -- ok +-- PGC_USERSET +SET DateStyle = 'ISO, MDY'; -- ok +RESET DateStyle; -- ok +ALTER SYSTEM SET DateStyle = 'ISO, MDY'; -- ok +ALTER SYSTEM RESET DateStyle; -- ok +ALTER SYSTEM SET ssl_renegotiation_limit = 0; -- fail, cannot be changed +ALTER SYSTEM RESET ssl_renegotiation_limit; -- fail, cannot be changed +-- Finished testing superuser + +-- Create non-superuser with privileges to configure host resource usage +CREATE ROLE regress_host_resource_admin NOSUPERUSER; +-- Revoke privileges not yet granted +REVOKE SET, ALTER SYSTEM ON PARAMETER work_mem FROM regress_host_resource_admin; +REVOKE SET, ALTER SYSTEM ON PARAMETER zero_damaged_pages FROM regress_host_resource_admin; +-- Check the new role does not yet have privileges on parameters +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET, ALTER SYSTEM'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); +-- Check inappropriate and nonsense privilege types +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SELECT, UPDATE, CREATE'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'USAGE'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'WHATEVER'); +-- Revoke, grant, and revoke again a SUSET parameter not yet granted +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); +REVOKE SET ON PARAMETER zero_damaged_pages FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); +GRANT SET ON PARAMETER zero_damaged_pages TO regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); +REVOKE SET ON PARAMETER zero_damaged_pages FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'zero_damaged_pages', 'ALTER SYSTEM'); +-- Revoke, grant, and revoke again a USERSET parameter not yet granted +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); +REVOKE SET ON PARAMETER work_mem FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); +GRANT SET ON PARAMETER work_mem TO regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); +REVOKE SET ON PARAMETER work_mem FROM regress_host_resource_admin; +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); + +-- Revoke privileges from a non-existent custom GUC. This should not create +-- entries in the catalog. +REVOKE ALL ON PARAMETER "none.such" FROM regress_host_resource_admin; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such'; +-- Grant and then revoke privileges on the non-existent custom GUC. Check that +-- a do-nothing entry is not left in the catalogs after the revoke. +GRANT ALL ON PARAMETER none.such TO regress_host_resource_admin; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such'; +REVOKE ALL ON PARAMETER "None.Such" FROM regress_host_resource_admin; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such'; +-- Can't grant on a non-existent core GUC. +GRANT ALL ON PARAMETER no_such_guc TO regress_host_resource_admin; -- fail + +-- Initially there are no privileges and no catalog entry for this GUC. +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; +-- GRANT SET creates an entry: +GRANT SET ON PARAMETER enable_material TO PUBLIC; +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; +-- Now grant ALTER SYSTEM: +GRANT ALL ON PARAMETER enable_material TO PUBLIC; +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; +-- REVOKE ALTER SYSTEM brings us back to just the SET privilege: +REVOKE ALTER SYSTEM ON PARAMETER enable_material FROM PUBLIC; +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'ALTER SYSTEM'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET, ALTER SYSTEM'); +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; +-- And this should remove the entry altogether: +REVOKE SET ON PARAMETER enable_material FROM PUBLIC; +SELECT 1 FROM pg_parameter_acl WHERE parname = 'enable_material'; + +-- Grant privileges on parameters to the new non-superuser role +GRANT SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +TO regress_host_resource_admin; +-- Check the new role now has privilges on parameters +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET, ALTER SYSTEM'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'ALTER SYSTEM'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SET WITH GRANT OPTION, ALTER SYSTEM WITH GRANT OPTION'); +-- Check again the inappropriate and nonsense privilege types. The prior +-- similar check was performed before any entry for work_mem existed. +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'SELECT, UPDATE, CREATE'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'USAGE'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'WHATEVER'); +SELECT has_parameter_privilege('regress_host_resource_admin', 'work_mem', 'WHATEVER WITH GRANT OPTION'); + +-- Check other function signatures +SELECT has_parameter_privilege((SELECT oid FROM pg_catalog.pg_authid WHERE rolname = 'regress_host_resource_admin'), + 'max_stack_depth', + 'SET'); +SELECT has_parameter_privilege('hash_mem_multiplier', 'set'); + +-- Check object identity functions +SELECT pg_describe_object(tableoid, oid, 0) +FROM pg_parameter_acl WHERE parname = 'work_mem'; +SELECT pg_identify_object(tableoid, oid, 0) +FROM pg_parameter_acl WHERE parname = 'work_mem'; +SELECT pg_identify_object_as_address(tableoid, oid, 0) +FROM pg_parameter_acl WHERE parname = 'work_mem'; +SELECT classid::regclass, + (SELECT parname FROM pg_parameter_acl WHERE oid = goa.objid) AS parname, + objsubid +FROM pg_get_object_address('parameter ACL', '{work_mem}', '{}') goa; + +-- Make a per-role setting that regress_host_resource_admin can't change +ALTER ROLE regress_host_resource_admin SET lc_messages = 'C'; + +-- Perform some operations as user 'regress_host_resource_admin' +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- ok, privileges have been granted +ALTER SYSTEM SET ignore_system_indexes = OFF; -- fail, insufficient privileges +ALTER SYSTEM RESET autovacuum_multixact_freeze_max_age; -- fail, insufficient privileges +SET jit_provider = 'llvmjit'; -- fail, insufficient privileges +SELECT set_config ('jit_provider', 'llvmjit', true); -- fail, insufficient privileges +ALTER SYSTEM SET shared_buffers = 50; -- ok +ALTER SYSTEM RESET shared_buffers; -- ok +SET autovacuum_work_mem = 50; -- cannot be changed now +ALTER SYSTEM RESET temp_file_limit; -- ok +SET TimeZone = 'Europe/Helsinki'; -- ok +RESET TimeZone; -- ok +SET max_stack_depth = '100kB'; -- ok, privileges have been granted +RESET max_stack_depth; -- ok, privileges have been granted +ALTER SYSTEM SET max_stack_depth = '100kB'; -- ok, privileges have been granted +ALTER SYSTEM RESET max_stack_depth; -- ok, privileges have been granted +SET lc_messages = 'C'; -- fail, insufficient privileges +RESET lc_messages; -- fail, insufficient privileges +ALTER SYSTEM SET lc_messages = 'C'; -- fail, insufficient privileges +ALTER SYSTEM RESET lc_messages; -- fail, insufficient privileges +SELECT set_config ('temp_buffers', '8192', false); -- ok +ALTER SYSTEM RESET autovacuum_work_mem; -- ok, privileges have been granted +ALTER SYSTEM RESET ALL; -- fail, insufficient privileges +ALTER ROLE regress_host_resource_admin SET lc_messages = 'POSIX'; -- fail +ALTER ROLE regress_host_resource_admin SET max_stack_depth = '1MB'; -- ok +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; +ALTER ROLE regress_host_resource_admin RESET max_stack_depth; -- ok +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; +ALTER ROLE regress_host_resource_admin SET max_stack_depth = '1MB'; -- ok +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; +ALTER ROLE regress_host_resource_admin RESET ALL; -- doesn't reset lc_messages +SELECT setconfig FROM pg_db_role_setting + WHERE setrole = 'regress_host_resource_admin'::regrole; + +-- Check dropping/revoking behavior +SET SESSION AUTHORIZATION regress_admin; +DROP ROLE regress_host_resource_admin; -- fail, privileges remain +-- Use "revoke" to remove the privileges and allow the role to be dropped +REVOKE SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +FROM regress_host_resource_admin; +DROP ROLE regress_host_resource_admin; -- ok + +-- Try that again, but use "drop owned by" instead of "revoke" +CREATE ROLE regress_host_resource_admin NOSUPERUSER; +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- fail, privileges not yet granted +SET SESSION AUTHORIZATION regress_admin; +GRANT SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +TO regress_host_resource_admin; +DROP ROLE regress_host_resource_admin; -- fail, privileges remain +DROP OWNED BY regress_host_resource_admin RESTRICT; -- cascade should not be needed +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- fail, "drop owned" has dropped privileges +SET SESSION AUTHORIZATION regress_admin; +DROP ROLE regress_host_resource_admin; -- ok + +-- Check that "reassign owned" doesn't affect privileges +CREATE ROLE regress_host_resource_admin NOSUPERUSER; +CREATE ROLE regress_host_resource_newadmin NOSUPERUSER; +GRANT SET, ALTER SYSTEM ON PARAMETER + autovacuum_work_mem, hash_mem_multiplier, max_stack_depth, + shared_buffers, temp_file_limit, work_mem +TO regress_host_resource_admin; +REASSIGN OWNED BY regress_host_resource_admin TO regress_host_resource_newadmin; +SET SESSION AUTHORIZATION regress_host_resource_admin; +ALTER SYSTEM SET autovacuum_work_mem = 32; -- ok, "reassign owned" did not change privileges +ALTER SYSTEM RESET autovacuum_work_mem; -- ok +SET SESSION AUTHORIZATION regress_admin; +DROP ROLE regress_host_resource_admin; -- fail, privileges remain +DROP ROLE regress_host_resource_newadmin; -- ok, nothing was transferred +-- Use "drop owned by" so we can drop the role +DROP OWNED BY regress_host_resource_admin; -- ok +DROP ROLE regress_host_resource_admin; -- ok + +-- Clean up +RESET SESSION AUTHORIZATION; +DROP ROLE regress_admin; -- ok diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql new file mode 100644 index 0000000..adac365 --- /dev/null +++ b/src/test/modules/unsafe_tests/sql/rolenames.sql @@ -0,0 +1,504 @@ +CREATE FUNCTION chkrolattr() + RETURNS TABLE ("role" name, rolekeyword text, canlogin bool, replication bool) + AS $$ +SELECT r.rolname, v.keyword, r.rolcanlogin, r.rolreplication + FROM pg_roles r + JOIN (VALUES(CURRENT_ROLE, 'current_role'), + (CURRENT_USER, 'current_user'), + (SESSION_USER, 'session_user'), + ('current_role', '-'), + ('current_user', '-'), + ('session_user', '-'), + ('Public', '-'), + ('None', '-')) + AS v(uname, keyword) + ON (r.rolname = v.uname) + ORDER BY 1, 2; +$$ LANGUAGE SQL; + +CREATE FUNCTION chksetconfig() + RETURNS TABLE (db name, "role" name, rolkeyword text, setconfig text[]) + AS $$ +SELECT COALESCE(d.datname, 'ALL'), COALESCE(r.rolname, 'ALL'), + COALESCE(v.keyword, '-'), s.setconfig + FROM pg_db_role_setting s + LEFT JOIN pg_roles r ON (r.oid = s.setrole) + LEFT JOIN pg_database d ON (d.oid = s.setdatabase) + LEFT JOIN (VALUES(CURRENT_ROLE, 'current_role'), + (CURRENT_USER, 'current_user'), + (SESSION_USER, 'session_user')) + AS v(uname, keyword) + ON (r.rolname = v.uname) + WHERE (r.rolname) IN ('Public', 'current_user', 'regress_testrol1', 'regress_testrol2') +ORDER BY 1, 2, 3; +$$ LANGUAGE SQL; + +CREATE FUNCTION chkumapping() + RETURNS TABLE (umname name, umserver name, umoptions text[]) + AS $$ +SELECT r.rolname, s.srvname, m.umoptions + FROM pg_user_mapping m + LEFT JOIN pg_roles r ON (r.oid = m.umuser) + JOIN pg_foreign_server s ON (s.oid = m.umserver) + ORDER BY 2, 1; +$$ LANGUAGE SQL; + +-- +-- We test creation and use of these role names to ensure that the server +-- correctly distinguishes role keywords from quoted names that look like +-- those keywords. In a test environment, creation of these roles may +-- provoke warnings, so hide the warnings by raising client_min_messages. +-- +SET client_min_messages = ERROR; + +CREATE ROLE "Public"; +CREATE ROLE "None"; +CREATE ROLE "current_role"; +CREATE ROLE "current_user"; +CREATE ROLE "session_user"; +CREATE ROLE "user"; + +RESET client_min_messages; + +CREATE ROLE current_user; -- error +CREATE ROLE current_role; -- error +CREATE ROLE session_user; -- error +CREATE ROLE user; -- error +CREATE ROLE all; -- error + +CREATE ROLE public; -- error +CREATE ROLE "public"; -- error +CREATE ROLE none; -- error +CREATE ROLE "none"; -- error + +CREATE ROLE pg_abc; -- error +CREATE ROLE "pg_abc"; -- error +CREATE ROLE pg_abcdef; -- error +CREATE ROLE "pg_abcdef"; -- error + +CREATE ROLE regress_testrol0 SUPERUSER LOGIN; +CREATE ROLE regress_testrolx SUPERUSER LOGIN; +CREATE ROLE regress_testrol2 SUPERUSER; +CREATE ROLE regress_testrol1 SUPERUSER LOGIN IN ROLE regress_testrol2; + +\c - +SET SESSION AUTHORIZATION regress_testrol1; +SET ROLE regress_testrol2; + +-- ALTER ROLE +BEGIN; +SELECT * FROM chkrolattr(); +ALTER ROLE CURRENT_ROLE WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER ROLE "current_role" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER ROLE CURRENT_ROLE WITH NOREPLICATION; +ALTER ROLE CURRENT_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER ROLE "current_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER ROLE SESSION_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER ROLE "session_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER "Public" WITH REPLICATION; +ALTER USER "None" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER regress_testrol1 WITH NOREPLICATION; +ALTER USER regress_testrol2 WITH NOREPLICATION; +SELECT * FROM chkrolattr(); +ROLLBACK; + +ALTER ROLE USER WITH LOGIN; -- error +ALTER ROLE ALL WITH REPLICATION; -- error +ALTER ROLE SESSION_ROLE WITH NOREPLICATION; -- error +ALTER ROLE PUBLIC WITH NOREPLICATION; -- error +ALTER ROLE "public" WITH NOREPLICATION; -- error +ALTER ROLE NONE WITH NOREPLICATION; -- error +ALTER ROLE "none" WITH NOREPLICATION; -- error +ALTER ROLE nonexistent WITH NOREPLICATION; -- error + +-- ALTER USER +BEGIN; +SELECT * FROM chkrolattr(); +ALTER USER CURRENT_ROLE WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER "current_role" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER CURRENT_ROLE WITH NOREPLICATION; +ALTER USER CURRENT_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER "current_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER SESSION_USER WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER "session_user" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER "Public" WITH REPLICATION; +ALTER USER "None" WITH REPLICATION; +SELECT * FROM chkrolattr(); +ALTER USER regress_testrol1 WITH NOREPLICATION; +ALTER USER regress_testrol2 WITH NOREPLICATION; +SELECT * FROM chkrolattr(); +ROLLBACK; + +ALTER USER USER WITH LOGIN; -- error +ALTER USER ALL WITH REPLICATION; -- error +ALTER USER SESSION_ROLE WITH NOREPLICATION; -- error +ALTER USER PUBLIC WITH NOREPLICATION; -- error +ALTER USER "public" WITH NOREPLICATION; -- error +ALTER USER NONE WITH NOREPLICATION; -- error +ALTER USER "none" WITH NOREPLICATION; -- error +ALTER USER nonexistent WITH NOREPLICATION; -- error + +-- ALTER ROLE SET/RESET +SELECT * FROM chksetconfig(); +ALTER ROLE CURRENT_ROLE SET application_name to 'BAZ'; +ALTER ROLE CURRENT_USER SET application_name to 'FOO'; +ALTER ROLE SESSION_USER SET application_name to 'BAR'; +ALTER ROLE "current_user" SET application_name to 'FOOFOO'; +ALTER ROLE "Public" SET application_name to 'BARBAR'; +ALTER ROLE ALL SET application_name to 'SLAP'; +SELECT * FROM chksetconfig(); +ALTER ROLE regress_testrol1 SET application_name to 'SLAM'; +SELECT * FROM chksetconfig(); +ALTER ROLE CURRENT_ROLE RESET application_name; +ALTER ROLE CURRENT_USER RESET application_name; +ALTER ROLE SESSION_USER RESET application_name; +ALTER ROLE "current_user" RESET application_name; +ALTER ROLE "Public" RESET application_name; +ALTER ROLE ALL RESET application_name; +SELECT * FROM chksetconfig(); + + +ALTER ROLE USER SET application_name to 'BOOM'; -- error +ALTER ROLE PUBLIC SET application_name to 'BOMB'; -- error +ALTER ROLE nonexistent SET application_name to 'BOMB'; -- error + +-- ALTER USER SET/RESET +SELECT * FROM chksetconfig(); +ALTER USER CURRENT_ROLE SET application_name to 'BAZ'; +ALTER USER CURRENT_USER SET application_name to 'FOO'; +ALTER USER SESSION_USER SET application_name to 'BAR'; +ALTER USER "current_user" SET application_name to 'FOOFOO'; +ALTER USER "Public" SET application_name to 'BARBAR'; +ALTER USER ALL SET application_name to 'SLAP'; +SELECT * FROM chksetconfig(); +ALTER USER regress_testrol1 SET application_name to 'SLAM'; +SELECT * FROM chksetconfig(); +ALTER USER CURRENT_ROLE RESET application_name; +ALTER USER CURRENT_USER RESET application_name; +ALTER USER SESSION_USER RESET application_name; +ALTER USER "current_user" RESET application_name; +ALTER USER "Public" RESET application_name; +ALTER USER ALL RESET application_name; +SELECT * FROM chksetconfig(); + + +ALTER USER USER SET application_name to 'BOOM'; -- error +ALTER USER PUBLIC SET application_name to 'BOMB'; -- error +ALTER USER NONE SET application_name to 'BOMB'; -- error +ALTER USER nonexistent SET application_name to 'BOMB'; -- error + +-- CREATE SCHEMA +CREATE SCHEMA newschema1 AUTHORIZATION CURRENT_USER; +CREATE SCHEMA newschema2 AUTHORIZATION "current_user"; +CREATE SCHEMA newschema3 AUTHORIZATION CURRENT_ROLE; +CREATE SCHEMA newschema4 AUTHORIZATION SESSION_USER; +CREATE SCHEMA newschema5 AUTHORIZATION regress_testrolx; +CREATE SCHEMA newschema6 AUTHORIZATION "Public"; + +CREATE SCHEMA newschemax AUTHORIZATION USER; -- error +CREATE SCHEMA newschemax AUTHORIZATION PUBLIC; -- error +CREATE SCHEMA newschemax AUTHORIZATION "public"; -- error +CREATE SCHEMA newschemax AUTHORIZATION NONE; -- error +CREATE SCHEMA newschemax AUTHORIZATION nonexistent; -- error + +SELECT n.nspname, r.rolname FROM pg_namespace n + JOIN pg_roles r ON (r.oid = n.nspowner) + WHERE n.nspname LIKE 'newschema_' ORDER BY 1; + +CREATE SCHEMA IF NOT EXISTS newschema1 AUTHORIZATION CURRENT_USER; +CREATE SCHEMA IF NOT EXISTS newschema2 AUTHORIZATION "current_user"; +CREATE SCHEMA IF NOT EXISTS newschema3 AUTHORIZATION CURRENT_ROLE; +CREATE SCHEMA IF NOT EXISTS newschema4 AUTHORIZATION SESSION_USER; +CREATE SCHEMA IF NOT EXISTS newschema5 AUTHORIZATION regress_testrolx; +CREATE SCHEMA IF NOT EXISTS newschema6 AUTHORIZATION "Public"; + +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION USER; -- error +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION PUBLIC; -- error +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION "public"; -- error +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION NONE; -- error +CREATE SCHEMA IF NOT EXISTS newschemax AUTHORIZATION nonexistent; -- error + +SELECT n.nspname, r.rolname FROM pg_namespace n + JOIN pg_roles r ON (r.oid = n.nspowner) + WHERE n.nspname LIKE 'newschema_' ORDER BY 1; + +-- ALTER TABLE OWNER TO +\c - +SET SESSION AUTHORIZATION regress_testrol0; +CREATE TABLE testtab1 (a int); +CREATE TABLE testtab2 (a int); +CREATE TABLE testtab3 (a int); +CREATE TABLE testtab4 (a int); +CREATE TABLE testtab5 (a int); +CREATE TABLE testtab6 (a int); +CREATE TABLE testtab7 (a int); + +\c - +SET SESSION AUTHORIZATION regress_testrol1; +SET ROLE regress_testrol2; + +ALTER TABLE testtab1 OWNER TO CURRENT_USER; +ALTER TABLE testtab2 OWNER TO "current_user"; +ALTER TABLE testtab3 OWNER TO CURRENT_ROLE; +ALTER TABLE testtab4 OWNER TO SESSION_USER; +ALTER TABLE testtab5 OWNER TO regress_testrolx; +ALTER TABLE testtab6 OWNER TO "Public"; + +ALTER TABLE testtab7 OWNER TO USER; --error +ALTER TABLE testtab7 OWNER TO PUBLIC; -- error +ALTER TABLE testtab7 OWNER TO "public"; -- error +ALTER TABLE testtab7 OWNER TO nonexistent; -- error + +SELECT c.relname, r.rolname + FROM pg_class c JOIN pg_roles r ON (r.oid = c.relowner) + WHERE relname LIKE 'testtab_' + ORDER BY 1; + +-- ALTER TABLE, VIEW, MATERIALIZED VIEW, FOREIGN TABLE, SEQUENCE are +-- changed their owner in the same way. + +-- ALTER AGGREGATE +\c - +SET SESSION AUTHORIZATION regress_testrol0; +CREATE AGGREGATE testagg1(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg2(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg3(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg4(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg5(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg6(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg7(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg8(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagg9(int2) (SFUNC = int2_sum, STYPE = int8); +CREATE AGGREGATE testagga(int2) (SFUNC = int2_sum, STYPE = int8); + +\c - +SET SESSION AUTHORIZATION regress_testrol1; +SET ROLE regress_testrol2; + +ALTER AGGREGATE testagg1(int2) OWNER TO CURRENT_USER; +ALTER AGGREGATE testagg2(int2) OWNER TO "current_user"; +ALTER AGGREGATE testagg3(int2) OWNER TO CURRENT_ROLE; +ALTER AGGREGATE testagg4(int2) OWNER TO SESSION_USER; +ALTER AGGREGATE testagg5(int2) OWNER TO regress_testrolx; +ALTER AGGREGATE testagg6(int2) OWNER TO "Public"; + +ALTER AGGREGATE testagg6(int2) OWNER TO USER; -- error +ALTER AGGREGATE testagg6(int2) OWNER TO PUBLIC; -- error +ALTER AGGREGATE testagg6(int2) OWNER TO "public"; -- error +ALTER AGGREGATE testagg6(int2) OWNER TO nonexistent; -- error + +SELECT p.proname, r.rolname + FROM pg_proc p JOIN pg_roles r ON (r.oid = p.proowner) + WHERE proname LIKE 'testagg_' + ORDER BY 1; + +-- CREATE USER MAPPING +CREATE FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv1 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv2 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv3 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv4 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv5 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv6 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv7 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv8 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv9 FOREIGN DATA WRAPPER test_wrapper; +CREATE SERVER sv10 FOREIGN DATA WRAPPER test_wrapper; + +CREATE USER MAPPING FOR CURRENT_USER SERVER sv1 OPTIONS (user 'CURRENT_USER'); +CREATE USER MAPPING FOR "current_user" SERVER sv2 OPTIONS (user '"current_user"'); +CREATE USER MAPPING FOR CURRENT_ROLE SERVER sv3 OPTIONS (user 'CURRENT_ROLE'); +CREATE USER MAPPING FOR USER SERVER sv4 OPTIONS (user 'USER'); +CREATE USER MAPPING FOR "user" SERVER sv5 OPTIONS (user '"USER"'); +CREATE USER MAPPING FOR SESSION_USER SERVER sv6 OPTIONS (user 'SESSION_USER'); +CREATE USER MAPPING FOR PUBLIC SERVER sv7 OPTIONS (user 'PUBLIC'); +CREATE USER MAPPING FOR "Public" SERVER sv8 OPTIONS (user '"Public"'); +CREATE USER MAPPING FOR regress_testrolx SERVER sv9 OPTIONS (user 'regress_testrolx'); + +CREATE USER MAPPING FOR nonexistent SERVER sv10 OPTIONS (user 'nonexistent'); -- error; + +SELECT * FROM chkumapping(); + +-- ALTER USER MAPPING +ALTER USER MAPPING FOR CURRENT_USER SERVER sv1 + OPTIONS (SET user 'CURRENT_USER_alt'); +ALTER USER MAPPING FOR "current_user" SERVER sv2 + OPTIONS (SET user '"current_user"_alt'); +ALTER USER MAPPING FOR CURRENT_ROLE SERVER sv3 + OPTIONS (SET user 'CURRENT_ROLE_alt'); +ALTER USER MAPPING FOR USER SERVER sv4 + OPTIONS (SET user 'USER_alt'); +ALTER USER MAPPING FOR "user" SERVER sv5 + OPTIONS (SET user '"user"_alt'); +ALTER USER MAPPING FOR SESSION_USER SERVER sv6 + OPTIONS (SET user 'SESSION_USER_alt'); +ALTER USER MAPPING FOR PUBLIC SERVER sv7 + OPTIONS (SET user 'public_alt'); +ALTER USER MAPPING FOR "Public" SERVER sv8 + OPTIONS (SET user '"Public"_alt'); +ALTER USER MAPPING FOR regress_testrolx SERVER sv9 + OPTIONS (SET user 'regress_testrolx_alt'); + +ALTER USER MAPPING FOR nonexistent SERVER sv10 + OPTIONS (SET user 'nonexistent_alt'); -- error + +SELECT * FROM chkumapping(); + +-- DROP USER MAPPING +DROP USER MAPPING FOR CURRENT_USER SERVER sv1; +DROP USER MAPPING FOR "current_user" SERVER sv2; +DROP USER MAPPING FOR CURRENT_ROLE SERVER sv3; +DROP USER MAPPING FOR USER SERVER sv4; +DROP USER MAPPING FOR "user" SERVER sv5; +DROP USER MAPPING FOR SESSION_USER SERVER sv6; +DROP USER MAPPING FOR PUBLIC SERVER sv7; +DROP USER MAPPING FOR "Public" SERVER sv8; +DROP USER MAPPING FOR regress_testrolx SERVER sv9; + +DROP USER MAPPING FOR nonexistent SERVER sv10; -- error +SELECT * FROM chkumapping(); + +CREATE USER MAPPING FOR CURRENT_USER SERVER sv1 OPTIONS (user 'CURRENT_USER'); +CREATE USER MAPPING FOR "current_user" SERVER sv2 OPTIONS (user '"current_user"'); +CREATE USER MAPPING FOR CURRENT_ROLE SERVER sv3 OPTIONS (user 'CURRENT_ROLE'); +CREATE USER MAPPING FOR USER SERVER sv4 OPTIONS (user 'USER'); +CREATE USER MAPPING FOR "user" SERVER sv5 OPTIONS (user '"USER"'); +CREATE USER MAPPING FOR SESSION_USER SERVER sv6 OPTIONS (user 'SESSION_USER'); +CREATE USER MAPPING FOR PUBLIC SERVER sv7 OPTIONS (user 'PUBLIC'); +CREATE USER MAPPING FOR "Public" SERVER sv8 OPTIONS (user '"Public"'); +CREATE USER MAPPING FOR regress_testrolx SERVER sv9 OPTIONS (user 'regress_testrolx'); +SELECT * FROM chkumapping(); + +-- DROP USER MAPPING IF EXISTS +DROP USER MAPPING IF EXISTS FOR CURRENT_USER SERVER sv1; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR "current_user" SERVER sv2; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR CURRENT_USER SERVER sv3; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR USER SERVER sv4; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR "user" SERVER sv5; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR SESSION_USER SERVER sv6; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER sv7; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR "Public" SERVER sv8; +SELECT * FROM chkumapping(); +DROP USER MAPPING IF EXISTS FOR regress_testrolx SERVER sv9; +SELECT * FROM chkumapping(); + +DROP USER MAPPING IF EXISTS FOR nonexistent SERVER sv10; -- error + +-- GRANT/REVOKE +GRANT regress_testrol0 TO pg_signal_backend; -- success + +SET ROLE pg_signal_backend; --success +RESET ROLE; +CREATE SCHEMA test_roles_schema AUTHORIZATION pg_signal_backend; --success +SET ROLE regress_testrol2; + +UPDATE pg_proc SET proacl = null WHERE proname LIKE 'testagg_'; +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + +REVOKE ALL PRIVILEGES ON FUNCTION testagg1(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg2(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg3(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg4(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg5(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg6(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg7(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg8(int2) FROM PUBLIC; + +GRANT ALL PRIVILEGES ON FUNCTION testagg1(int2) TO PUBLIC; +GRANT ALL PRIVILEGES ON FUNCTION testagg2(int2) TO CURRENT_USER; +GRANT ALL PRIVILEGES ON FUNCTION testagg3(int2) TO "current_user"; +GRANT ALL PRIVILEGES ON FUNCTION testagg4(int2) TO CURRENT_ROLE; +GRANT ALL PRIVILEGES ON FUNCTION testagg5(int2) TO SESSION_USER; +GRANT ALL PRIVILEGES ON FUNCTION testagg6(int2) TO "Public"; +GRANT ALL PRIVILEGES ON FUNCTION testagg7(int2) TO regress_testrolx; +GRANT ALL PRIVILEGES ON FUNCTION testagg8(int2) TO "public"; +GRANT ALL PRIVILEGES ON FUNCTION testagg9(int2) + TO current_user, public, regress_testrolx; + +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + +GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO USER; --error +GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO NONE; --error +GRANT ALL PRIVILEGES ON FUNCTION testagga(int2) TO "none"; --error + +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + +REVOKE ALL PRIVILEGES ON FUNCTION testagg1(int2) FROM PUBLIC; +REVOKE ALL PRIVILEGES ON FUNCTION testagg2(int2) FROM CURRENT_USER; +REVOKE ALL PRIVILEGES ON FUNCTION testagg3(int2) FROM "current_user"; +REVOKE ALL PRIVILEGES ON FUNCTION testagg4(int2) FROM CURRENT_ROLE; +REVOKE ALL PRIVILEGES ON FUNCTION testagg5(int2) FROM SESSION_USER; +REVOKE ALL PRIVILEGES ON FUNCTION testagg6(int2) FROM "Public"; +REVOKE ALL PRIVILEGES ON FUNCTION testagg7(int2) FROM regress_testrolx; +REVOKE ALL PRIVILEGES ON FUNCTION testagg8(int2) FROM "public"; +REVOKE ALL PRIVILEGES ON FUNCTION testagg9(int2) + FROM current_user, public, regress_testrolx; + +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + +REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM USER; --error +REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM NONE; --error +REVOKE ALL PRIVILEGES ON FUNCTION testagga(int2) FROM "none"; --error + +SELECT proname, proacl FROM pg_proc WHERE proname LIKE 'testagg_'; + +-- DEFAULT MONITORING ROLES +CREATE ROLE regress_role_haspriv; +CREATE ROLE regress_role_nopriv; + +-- pg_read_all_stats +GRANT pg_read_all_stats TO regress_role_haspriv; +SET SESSION AUTHORIZATION regress_role_haspriv; +-- returns true with role member of pg_read_all_stats +SELECT COUNT(*) = 0 AS haspriv FROM pg_stat_activity + WHERE query = '<insufficient privilege>'; +SET SESSION AUTHORIZATION regress_role_nopriv; +-- returns false with role not member of pg_read_all_stats +SELECT COUNT(*) = 0 AS haspriv FROM pg_stat_activity + WHERE query = '<insufficient privilege>'; +RESET SESSION AUTHORIZATION; +REVOKE pg_read_all_stats FROM regress_role_haspriv; + +-- pg_read_all_settings +GRANT pg_read_all_settings TO regress_role_haspriv; +BEGIN; +-- A GUC using GUC_SUPERUSER_ONLY is useful for negative tests. +SET LOCAL session_preload_libraries TO 'path-to-preload-libraries'; +SET SESSION AUTHORIZATION regress_role_haspriv; +-- passes with role member of pg_read_all_settings +SHOW session_preload_libraries; +SET SESSION AUTHORIZATION regress_role_nopriv; +-- fails with role not member of pg_read_all_settings +SHOW session_preload_libraries; +RESET SESSION AUTHORIZATION; +ROLLBACK; +REVOKE pg_read_all_settings FROM regress_role_haspriv; + +-- clean up +\c + +DROP SCHEMA test_roles_schema; +DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE; +DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; +DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user"; +DROP ROLE regress_role_haspriv, regress_role_nopriv; diff --git a/src/test/modules/worker_spi/.gitignore b/src/test/modules/worker_spi/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/src/test/modules/worker_spi/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/worker_spi/Makefile b/src/test/modules/worker_spi/Makefile new file mode 100644 index 0000000..cbf9b2e --- /dev/null +++ b/src/test/modules/worker_spi/Makefile @@ -0,0 +1,26 @@ +# src/test/modules/worker_spi/Makefile + +MODULES = worker_spi + +EXTENSION = worker_spi +DATA = worker_spi--1.0.sql +PGFILEDESC = "worker_spi - background worker example" + +REGRESS = worker_spi + +# enable our module in shared_preload_libraries for dynamic bgworkers +REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/worker_spi/dynamic.conf + +# Disable installcheck to ensure we cover dynamic bgworkers. +NO_INSTALLCHECK = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/worker_spi +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/worker_spi/dynamic.conf b/src/test/modules/worker_spi/dynamic.conf new file mode 100644 index 0000000..bfe015f --- /dev/null +++ b/src/test/modules/worker_spi/dynamic.conf @@ -0,0 +1,2 @@ +shared_preload_libraries = worker_spi +worker_spi.database = contrib_regression diff --git a/src/test/modules/worker_spi/expected/worker_spi.out b/src/test/modules/worker_spi/expected/worker_spi.out new file mode 100644 index 0000000..dc0a79b --- /dev/null +++ b/src/test/modules/worker_spi/expected/worker_spi.out @@ -0,0 +1,50 @@ +CREATE EXTENSION worker_spi; +SELECT worker_spi_launch(4) IS NOT NULL; + ?column? +---------- + t +(1 row) + +-- wait until the worker completes its initialization +DO $$ +DECLARE + visible bool; + loops int := 0; +BEGIN + LOOP + visible := table_name IS NOT NULL + FROM information_schema.tables + WHERE table_schema = 'schema4' AND table_name = 'counted'; + IF visible OR loops > 120 * 10 THEN EXIT; END IF; + PERFORM pg_sleep(0.1); + loops := loops + 1; + END LOOP; +END +$$; +INSERT INTO schema4.counted VALUES ('total', 0), ('delta', 1); +SELECT pg_reload_conf(); + pg_reload_conf +---------------- + t +(1 row) + +-- wait until the worker has processed the tuple we just inserted +DO $$ +DECLARE + count int; + loops int := 0; +BEGIN + LOOP + count := count(*) FROM schema4.counted WHERE type = 'delta'; + IF count = 0 OR loops > 120 * 10 THEN EXIT; END IF; + PERFORM pg_sleep(0.1); + loops := loops + 1; + END LOOP; +END +$$; +SELECT * FROM schema4.counted; + type | value +-------+------- + total | 1 +(1 row) + diff --git a/src/test/modules/worker_spi/sql/worker_spi.sql b/src/test/modules/worker_spi/sql/worker_spi.sql new file mode 100644 index 0000000..4683523 --- /dev/null +++ b/src/test/modules/worker_spi/sql/worker_spi.sql @@ -0,0 +1,35 @@ +CREATE EXTENSION worker_spi; +SELECT worker_spi_launch(4) IS NOT NULL; +-- wait until the worker completes its initialization +DO $$ +DECLARE + visible bool; + loops int := 0; +BEGIN + LOOP + visible := table_name IS NOT NULL + FROM information_schema.tables + WHERE table_schema = 'schema4' AND table_name = 'counted'; + IF visible OR loops > 120 * 10 THEN EXIT; END IF; + PERFORM pg_sleep(0.1); + loops := loops + 1; + END LOOP; +END +$$; +INSERT INTO schema4.counted VALUES ('total', 0), ('delta', 1); +SELECT pg_reload_conf(); +-- wait until the worker has processed the tuple we just inserted +DO $$ +DECLARE + count int; + loops int := 0; +BEGIN + LOOP + count := count(*) FROM schema4.counted WHERE type = 'delta'; + IF count = 0 OR loops > 120 * 10 THEN EXIT; END IF; + PERFORM pg_sleep(0.1); + loops := loops + 1; + END LOOP; +END +$$; +SELECT * FROM schema4.counted; diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql new file mode 100644 index 0000000..e9d5b07 --- /dev/null +++ b/src/test/modules/worker_spi/worker_spi--1.0.sql @@ -0,0 +1,9 @@ +/* src/test/modules/worker_spi/worker_spi--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION worker_spi" to load this file. \quit + +CREATE FUNCTION worker_spi_launch(pg_catalog.int4) +RETURNS pg_catalog.int4 STRICT +AS 'MODULE_PATHNAME' +LANGUAGE C; diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c new file mode 100644 index 0000000..5b541ec --- /dev/null +++ b/src/test/modules/worker_spi/worker_spi.c @@ -0,0 +1,393 @@ +/* ------------------------------------------------------------------------- + * + * worker_spi.c + * Sample background worker code that demonstrates various coding + * patterns: establishing a database connection; starting and committing + * transactions; using GUC variables, and heeding SIGHUP to reread + * the configuration file; reporting to pg_stat_activity; using the + * process latch to sleep and exit in case of postmaster death. + * + * This code connects to a database, creates a schema and table, and summarizes + * the numbers contained therein. To see it working, insert an initial value + * with "total" type and some initial value; then insert some other rows with + * "delta" type. Delta rows will be deleted by this worker and their values + * aggregated into the total. + * + * Copyright (c) 2013-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/worker_spi/worker_spi.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +/* These are always necessary for a bgworker */ +#include "miscadmin.h" +#include "postmaster/bgworker.h" +#include "postmaster/interrupt.h" +#include "storage/ipc.h" +#include "storage/latch.h" +#include "storage/lwlock.h" +#include "storage/proc.h" +#include "storage/shmem.h" + +/* these headers are used by this particular worker's code */ +#include "access/xact.h" +#include "executor/spi.h" +#include "fmgr.h" +#include "lib/stringinfo.h" +#include "pgstat.h" +#include "utils/builtins.h" +#include "utils/snapmgr.h" +#include "tcop/utility.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(worker_spi_launch); + +void _PG_init(void); +void worker_spi_main(Datum) pg_attribute_noreturn(); + +/* GUC variables */ +static int worker_spi_naptime = 10; +static int worker_spi_total_workers = 2; +static char *worker_spi_database = NULL; + + +typedef struct worktable +{ + const char *schema; + const char *name; +} worktable; + +/* + * Initialize workspace for a worker process: create the schema if it doesn't + * already exist. + */ +static void +initialize_worker_spi(worktable *table) +{ + int ret; + int ntup; + bool isnull; + StringInfoData buf; + + SetCurrentStatementStartTimestamp(); + StartTransactionCommand(); + SPI_connect(); + PushActiveSnapshot(GetTransactionSnapshot()); + pgstat_report_activity(STATE_RUNNING, "initializing worker_spi schema"); + + /* XXX could we use CREATE SCHEMA IF NOT EXISTS? */ + initStringInfo(&buf); + appendStringInfo(&buf, "select count(*) from pg_namespace where nspname = '%s'", + table->schema); + + debug_query_string = buf.data; + ret = SPI_execute(buf.data, true, 0); + if (ret != SPI_OK_SELECT) + elog(FATAL, "SPI_execute failed: error code %d", ret); + + if (SPI_processed != 1) + elog(FATAL, "not a singleton result"); + + ntup = DatumGetInt64(SPI_getbinval(SPI_tuptable->vals[0], + SPI_tuptable->tupdesc, + 1, &isnull)); + if (isnull) + elog(FATAL, "null result"); + + if (ntup == 0) + { + debug_query_string = NULL; + resetStringInfo(&buf); + appendStringInfo(&buf, + "CREATE SCHEMA \"%s\" " + "CREATE TABLE \"%s\" (" + " type text CHECK (type IN ('total', 'delta')), " + " value integer)" + "CREATE UNIQUE INDEX \"%s_unique_total\" ON \"%s\" (type) " + "WHERE type = 'total'", + table->schema, table->name, table->name, table->name); + + /* set statement start time */ + SetCurrentStatementStartTimestamp(); + + debug_query_string = buf.data; + ret = SPI_execute(buf.data, false, 0); + + if (ret != SPI_OK_UTILITY) + elog(FATAL, "failed to create my schema"); + + debug_query_string = NULL; /* rest is not statement-specific */ + } + + SPI_finish(); + PopActiveSnapshot(); + CommitTransactionCommand(); + debug_query_string = NULL; + pgstat_report_activity(STATE_IDLE, NULL); +} + +void +worker_spi_main(Datum main_arg) +{ + int index = DatumGetInt32(main_arg); + worktable *table; + StringInfoData buf; + char name[20]; + + table = palloc(sizeof(worktable)); + sprintf(name, "schema%d", index); + table->schema = pstrdup(name); + table->name = pstrdup("counted"); + + /* Establish signal handlers before unblocking signals. */ + pqsignal(SIGHUP, SignalHandlerForConfigReload); + pqsignal(SIGTERM, die); + + /* We're now ready to receive signals */ + BackgroundWorkerUnblockSignals(); + + /* Connect to our database */ + BackgroundWorkerInitializeConnection(worker_spi_database, NULL, 0); + + elog(LOG, "%s initialized with %s.%s", + MyBgworkerEntry->bgw_name, table->schema, table->name); + initialize_worker_spi(table); + + /* + * Quote identifiers passed to us. Note that this must be done after + * initialize_worker_spi, because that routine assumes the names are not + * quoted. + * + * Note some memory might be leaked here. + */ + table->schema = quote_identifier(table->schema); + table->name = quote_identifier(table->name); + + initStringInfo(&buf); + appendStringInfo(&buf, + "WITH deleted AS (DELETE " + "FROM %s.%s " + "WHERE type = 'delta' RETURNING value), " + "total AS (SELECT coalesce(sum(value), 0) as sum " + "FROM deleted) " + "UPDATE %s.%s " + "SET value = %s.value + total.sum " + "FROM total WHERE type = 'total' " + "RETURNING %s.value", + table->schema, table->name, + table->schema, table->name, + table->name, + table->name); + + /* + * Main loop: do this until SIGTERM is received and processed by + * ProcessInterrupts. + */ + for (;;) + { + int ret; + + /* + * Background workers mustn't call usleep() or any direct equivalent: + * instead, they may wait on their process latch, which sleeps as + * necessary, but is awakened if postmaster dies. That way the + * background process goes away immediately in an emergency. + */ + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + worker_spi_naptime * 1000L, + PG_WAIT_EXTENSION); + ResetLatch(MyLatch); + + CHECK_FOR_INTERRUPTS(); + + /* + * In case of a SIGHUP, just reload the configuration. + */ + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + } + + /* + * Start a transaction on which we can run queries. Note that each + * StartTransactionCommand() call should be preceded by a + * SetCurrentStatementStartTimestamp() call, which sets both the time + * for the statement we're about the run, and also the transaction + * start time. Also, each other query sent to SPI should probably be + * preceded by SetCurrentStatementStartTimestamp(), so that statement + * start time is always up to date. + * + * The SPI_connect() call lets us run queries through the SPI manager, + * and the PushActiveSnapshot() call creates an "active" snapshot + * which is necessary for queries to have MVCC data to work on. + * + * The pgstat_report_activity() call makes our activity visible + * through the pgstat views. + */ + SetCurrentStatementStartTimestamp(); + StartTransactionCommand(); + SPI_connect(); + PushActiveSnapshot(GetTransactionSnapshot()); + debug_query_string = buf.data; + pgstat_report_activity(STATE_RUNNING, buf.data); + + /* We can now execute queries via SPI */ + ret = SPI_execute(buf.data, false, 0); + + if (ret != SPI_OK_UPDATE_RETURNING) + elog(FATAL, "cannot select from table %s.%s: error code %d", + table->schema, table->name, ret); + + if (SPI_processed > 0) + { + bool isnull; + int32 val; + + val = DatumGetInt32(SPI_getbinval(SPI_tuptable->vals[0], + SPI_tuptable->tupdesc, + 1, &isnull)); + if (!isnull) + elog(LOG, "%s: count in %s.%s is now %d", + MyBgworkerEntry->bgw_name, + table->schema, table->name, val); + } + + /* + * And finish our transaction. + */ + SPI_finish(); + PopActiveSnapshot(); + CommitTransactionCommand(); + debug_query_string = NULL; + pgstat_report_stat(true); + pgstat_report_activity(STATE_IDLE, NULL); + } + + /* Not reachable */ +} + +/* + * Entrypoint of this module. + * + * We register more than one worker process here, to demonstrate how that can + * be done. + */ +void +_PG_init(void) +{ + BackgroundWorker worker; + + /* get the configuration */ + DefineCustomIntVariable("worker_spi.naptime", + "Duration between each check (in seconds).", + NULL, + &worker_spi_naptime, + 10, + 1, + INT_MAX, + PGC_SIGHUP, + 0, + NULL, + NULL, + NULL); + + if (!process_shared_preload_libraries_in_progress) + return; + + DefineCustomIntVariable("worker_spi.total_workers", + "Number of workers.", + NULL, + &worker_spi_total_workers, + 2, + 1, + 100, + PGC_POSTMASTER, + 0, + NULL, + NULL, + NULL); + + DefineCustomStringVariable("worker_spi.database", + "Database to connect to.", + NULL, + &worker_spi_database, + "postgres", + PGC_POSTMASTER, + 0, + NULL, NULL, NULL); + + MarkGUCPrefixReserved("worker_spi"); + + /* set up common data for all our workers */ + memset(&worker, 0, sizeof(worker)); + worker.bgw_flags = BGWORKER_SHMEM_ACCESS | + BGWORKER_BACKEND_DATABASE_CONNECTION; + worker.bgw_start_time = BgWorkerStart_RecoveryFinished; + worker.bgw_restart_time = BGW_NEVER_RESTART; + sprintf(worker.bgw_library_name, "worker_spi"); + sprintf(worker.bgw_function_name, "worker_spi_main"); + worker.bgw_notify_pid = 0; + + /* + * Now fill in worker-specific data, and do the actual registrations. + */ + for (int i = 1; i <= worker_spi_total_workers; i++) + { + snprintf(worker.bgw_name, BGW_MAXLEN, "worker_spi worker %d", i); + snprintf(worker.bgw_type, BGW_MAXLEN, "worker_spi"); + worker.bgw_main_arg = Int32GetDatum(i); + + RegisterBackgroundWorker(&worker); + } +} + +/* + * Dynamically launch an SPI worker. + */ +Datum +worker_spi_launch(PG_FUNCTION_ARGS) +{ + int32 i = PG_GETARG_INT32(0); + BackgroundWorker worker; + BackgroundWorkerHandle *handle; + BgwHandleStatus status; + pid_t pid; + + memset(&worker, 0, sizeof(worker)); + worker.bgw_flags = BGWORKER_SHMEM_ACCESS | + BGWORKER_BACKEND_DATABASE_CONNECTION; + worker.bgw_start_time = BgWorkerStart_RecoveryFinished; + worker.bgw_restart_time = BGW_NEVER_RESTART; + sprintf(worker.bgw_library_name, "worker_spi"); + sprintf(worker.bgw_function_name, "worker_spi_main"); + snprintf(worker.bgw_name, BGW_MAXLEN, "worker_spi worker %d", i); + snprintf(worker.bgw_type, BGW_MAXLEN, "worker_spi"); + worker.bgw_main_arg = Int32GetDatum(i); + /* set bgw_notify_pid so that we can use WaitForBackgroundWorkerStartup */ + worker.bgw_notify_pid = MyProcPid; + + if (!RegisterDynamicBackgroundWorker(&worker, &handle)) + PG_RETURN_NULL(); + + status = WaitForBackgroundWorkerStartup(handle, &pid); + + if (status == BGWH_STOPPED) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("could not start background process"), + errhint("More details may be available in the server log."))); + if (status == BGWH_POSTMASTER_DIED) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("cannot start background processes without postmaster"), + errhint("Kill all remaining database processes and restart the database."))); + Assert(status == BGWH_STARTED); + + PG_RETURN_INT32(pid); +} diff --git a/src/test/modules/worker_spi/worker_spi.control b/src/test/modules/worker_spi/worker_spi.control new file mode 100644 index 0000000..84d6294 --- /dev/null +++ b/src/test/modules/worker_spi/worker_spi.control @@ -0,0 +1,5 @@ +# worker_spi extension +comment = 'Sample background worker' +default_version = '1.0' +module_pathname = '$libdir/worker_spi' +relocatable = true |