summaryrefslogtreecommitdiffstats
path: root/src/test/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:17:33 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:17:33 +0000
commit5e45211a64149b3c659b90ff2de6fa982a5a93ed (patch)
tree739caf8c461053357daa9f162bef34516c7bf452 /src/test/modules
parentInitial commit. (diff)
downloadpostgresql-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')
-rw-r--r--src/test/modules/Makefile41
-rw-r--r--src/test/modules/README20
-rw-r--r--src/test/modules/brin/.gitignore4
-rw-r--r--src/test/modules/brin/Makefile17
-rw-r--r--src/test/modules/brin/expected/summarization-and-inprogress-insertion.out51
-rw-r--r--src/test/modules/brin/specs/summarization-and-inprogress-insertion.spec45
-rw-r--r--src/test/modules/brin/t/01_workitems.pl46
-rw-r--r--src/test/modules/brin/t/02_wal_consistency.pl75
-rw-r--r--src/test/modules/commit_ts/.gitignore4
-rw-r--r--src/test/modules/commit_ts/Makefile20
-rw-r--r--src/test/modules/commit_ts/commit_ts.conf1
-rw-r--r--src/test/modules/commit_ts/expected/commit_timestamp.out139
-rw-r--r--src/test/modules/commit_ts/expected/commit_timestamp_1.out119
-rw-r--r--src/test/modules/commit_ts/sql/commit_timestamp.sql64
-rw-r--r--src/test/modules/commit_ts/t/001_base.pl38
-rw-r--r--src/test/modules/commit_ts/t/002_standby.pl68
-rw-r--r--src/test/modules/commit_ts/t/003_standby_2.pl69
-rw-r--r--src/test/modules/commit_ts/t/004_restart.pl154
-rw-r--r--src/test/modules/delay_execution/.gitignore3
-rw-r--r--src/test/modules/delay_execution/Makefile22
-rw-r--r--src/test/modules/delay_execution/delay_execution.c98
-rw-r--r--src/test/modules/delay_execution/expected/partition-addition.out27
-rw-r--r--src/test/modules/delay_execution/expected/partition-removal-1.out233
-rw-r--r--src/test/modules/delay_execution/specs/partition-addition.spec38
-rw-r--r--src/test/modules/delay_execution/specs/partition-removal-1.spec58
-rw-r--r--src/test/modules/dummy_index_am/.gitignore3
-rw-r--r--src/test/modules/dummy_index_am/Makefile20
-rw-r--r--src/test/modules/dummy_index_am/README12
-rw-r--r--src/test/modules/dummy_index_am/dummy_index_am--1.0.sql19
-rw-r--r--src/test/modules/dummy_index_am/dummy_index_am.c333
-rw-r--r--src/test/modules/dummy_index_am/dummy_index_am.control5
-rw-r--r--src/test/modules/dummy_index_am/expected/reloptions.out145
-rw-r--r--src/test/modules/dummy_index_am/sql/reloptions.sql83
-rw-r--r--src/test/modules/dummy_seclabel/.gitignore4
-rw-r--r--src/test/modules/dummy_seclabel/Makefile20
-rw-r--r--src/test/modules/dummy_seclabel/README41
-rw-r--r--src/test/modules/dummy_seclabel/dummy_seclabel--1.0.sql8
-rw-r--r--src/test/modules/dummy_seclabel/dummy_seclabel.c63
-rw-r--r--src/test/modules/dummy_seclabel/dummy_seclabel.control4
-rw-r--r--src/test/modules/dummy_seclabel/expected/dummy_seclabel.out117
-rw-r--r--src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql115
-rw-r--r--src/test/modules/libpq_pipeline/.gitignore5
-rw-r--r--src/test/modules/libpq_pipeline/Makefile25
-rw-r--r--src/test/modules/libpq_pipeline/README1
-rw-r--r--src/test/modules/libpq_pipeline/libpq_pipeline.c1818
-rw-r--r--src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl78
-rw-r--r--src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace6
-rw-r--r--src/test/modules/libpq_pipeline/traces/multi_pipelines.trace23
-rw-r--r--src/test/modules/libpq_pipeline/traces/nosync.trace92
-rw-r--r--src/test/modules/libpq_pipeline/traces/pipeline_abort.trace62
-rw-r--r--src/test/modules/libpq_pipeline/traces/pipeline_idle.trace32
-rw-r--r--src/test/modules/libpq_pipeline/traces/prepared.trace18
-rw-r--r--src/test/modules/libpq_pipeline/traces/simple_pipeline.trace12
-rw-r--r--src/test/modules/libpq_pipeline/traces/singlerow.trace59
-rw-r--r--src/test/modules/libpq_pipeline/traces/transaction.trace61
-rw-r--r--src/test/modules/plsample/.gitignore3
-rw-r--r--src/test/modules/plsample/Makefile20
-rw-r--r--src/test/modules/plsample/README6
-rw-r--r--src/test/modules/plsample/expected/plsample.out117
-rw-r--r--src/test/modules/plsample/plsample--1.0.sql14
-rw-r--r--src/test/modules/plsample/plsample.c354
-rw-r--r--src/test/modules/plsample/plsample.control8
-rw-r--r--src/test/modules/plsample/sql/plsample.sql38
-rw-r--r--src/test/modules/snapshot_too_old/.gitignore3
-rw-r--r--src/test/modules/snapshot_too_old/Makefile28
-rw-r--r--src/test/modules/snapshot_too_old/expected/sto_using_cursor.out19
-rw-r--r--src/test/modules/snapshot_too_old/expected/sto_using_hash_index.out19
-rw-r--r--src/test/modules/snapshot_too_old/expected/sto_using_select.out18
-rw-r--r--src/test/modules/snapshot_too_old/specs/sto_using_cursor.spec38
-rw-r--r--src/test/modules/snapshot_too_old/specs/sto_using_hash_index.spec31
-rw-r--r--src/test/modules/snapshot_too_old/specs/sto_using_select.spec37
-rw-r--r--src/test/modules/snapshot_too_old/sto.conf2
-rw-r--r--src/test/modules/spgist_name_ops/.gitignore4
-rw-r--r--src/test/modules/spgist_name_ops/Makefile23
-rw-r--r--src/test/modules/spgist_name_ops/README8
-rw-r--r--src/test/modules/spgist_name_ops/expected/spgist_name_ops.out120
-rw-r--r--src/test/modules/spgist_name_ops/spgist_name_ops--1.0.sql54
-rw-r--r--src/test/modules/spgist_name_ops/spgist_name_ops.c501
-rw-r--r--src/test/modules/spgist_name_ops/spgist_name_ops.control4
-rw-r--r--src/test/modules/spgist_name_ops/sql/spgist_name_ops.sql51
-rw-r--r--src/test/modules/ssl_passphrase_callback/.gitignore1
-rw-r--r--src/test/modules/ssl_passphrase_callback/Makefile40
-rw-r--r--src/test/modules/ssl_passphrase_callback/server.crt19
-rw-r--r--src/test/modules/ssl_passphrase_callback/server.key30
-rw-r--r--src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c85
-rw-r--r--src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl78
-rw-r--r--src/test/modules/test_bloomfilter/.gitignore4
-rw-r--r--src/test/modules/test_bloomfilter/Makefile23
-rw-r--r--src/test/modules/test_bloomfilter/README68
-rw-r--r--src/test/modules/test_bloomfilter/expected/test_bloomfilter.out22
-rw-r--r--src/test/modules/test_bloomfilter/sql/test_bloomfilter.sql19
-rw-r--r--src/test/modules/test_bloomfilter/test_bloomfilter--1.0.sql11
-rw-r--r--src/test/modules/test_bloomfilter/test_bloomfilter.c138
-rw-r--r--src/test/modules/test_bloomfilter/test_bloomfilter.control4
-rw-r--r--src/test/modules/test_ddl_deparse/.gitignore4
-rw-r--r--src/test/modules/test_ddl_deparse/Makefile43
-rw-r--r--src/test/modules/test_ddl_deparse/README8
-rw-r--r--src/test/modules/test_ddl_deparse/expected/alter_extension.out0
-rw-r--r--src/test/modules/test_ddl_deparse/expected/alter_function.out15
-rw-r--r--src/test/modules/test_ddl_deparse/expected/alter_sequence.out15
-rw-r--r--src/test/modules/test_ddl_deparse/expected/alter_table.out29
-rw-r--r--src/test/modules/test_ddl_deparse/expected/alter_ts_config.out8
-rw-r--r--src/test/modules/test_ddl_deparse/expected/alter_type_enum.out7
-rw-r--r--src/test/modules/test_ddl_deparse/expected/comment_on.out23
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_conversion.out6
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_domain.out11
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_extension.out5
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_function.out0
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_operator.out0
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_rule.out30
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_schema.out19
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_sequence_1.out11
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_table.out164
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_transform.out15
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_trigger.out18
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_type.out24
-rw-r--r--src/test/modules/test_ddl_deparse/expected/create_view.out19
-rw-r--r--src/test/modules/test_ddl_deparse/expected/defprivs.out6
-rw-r--r--src/test/modules/test_ddl_deparse/expected/matviews.out8
-rw-r--r--src/test/modules/test_ddl_deparse/expected/opfamily.out68
-rw-r--r--src/test/modules/test_ddl_deparse/expected/test_ddl_deparse.out40
-rw-r--r--src/test/modules/test_ddl_deparse/sql/alter_function.sql17
-rw-r--r--src/test/modules/test_ddl_deparse/sql/alter_sequence.sql15
-rw-r--r--src/test/modules/test_ddl_deparse/sql/alter_table.sql21
-rw-r--r--src/test/modules/test_ddl_deparse/sql/alter_ts_config.sql8
-rw-r--r--src/test/modules/test_ddl_deparse/sql/alter_type_enum.sql6
-rw-r--r--src/test/modules/test_ddl_deparse/sql/comment_on.sql14
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_conversion.sql6
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_domain.sql10
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_extension.sql5
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_rule.sql31
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_schema.sql17
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_sequence_1.sql11
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_table.sql142
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_transform.sql16
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_trigger.sql18
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_type.sql21
-rw-r--r--src/test/modules/test_ddl_deparse/sql/create_view.sql17
-rw-r--r--src/test/modules/test_ddl_deparse/sql/defprivs.sql6
-rw-r--r--src/test/modules/test_ddl_deparse/sql/matviews.sql8
-rw-r--r--src/test/modules/test_ddl_deparse/sql/opfamily.sql52
-rw-r--r--src/test/modules/test_ddl_deparse/sql/test_ddl_deparse.sql42
-rw-r--r--src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql16
-rw-r--r--src/test/modules/test_ddl_deparse/test_ddl_deparse.c296
-rw-r--r--src/test/modules/test_ddl_deparse/test_ddl_deparse.control4
-rw-r--r--src/test/modules/test_extensions/.gitignore4
-rw-r--r--src/test/modules/test_extensions/Makefile34
-rw-r--r--src/test/modules/test_extensions/expected/test_extdepend.out188
-rw-r--r--src/test/modules/test_extensions/expected/test_extensions.out322
-rw-r--r--src/test/modules/test_extensions/sql/test_extdepend.sql90
-rw-r--r--src/test/modules/test_extensions/sql/test_extensions.sql220
-rw-r--r--src/test/modules/test_extensions/test_ext1--1.0.sql3
-rw-r--r--src/test/modules/test_extensions/test_ext1.control5
-rw-r--r--src/test/modules/test_extensions/test_ext2--1.0.sql3
-rw-r--r--src/test/modules/test_extensions/test_ext2.control4
-rw-r--r--src/test/modules/test_extensions/test_ext3--1.0.sql9
-rw-r--r--src/test/modules/test_extensions/test_ext3.control3
-rw-r--r--src/test/modules/test_extensions/test_ext4--1.0.sql3
-rw-r--r--src/test/modules/test_extensions/test_ext4.control4
-rw-r--r--src/test/modules/test_extensions/test_ext5--1.0.sql3
-rw-r--r--src/test/modules/test_extensions/test_ext5.control3
-rw-r--r--src/test/modules/test_extensions/test_ext6--1.0.sql1
-rw-r--r--src/test/modules/test_extensions/test_ext6.control5
-rw-r--r--src/test/modules/test_extensions/test_ext7--1.0--2.0.sql8
-rw-r--r--src/test/modules/test_extensions/test_ext7--1.0.sql13
-rw-r--r--src/test/modules/test_extensions/test_ext7.control4
-rw-r--r--src/test/modules/test_extensions/test_ext8--1.0.sql21
-rw-r--r--src/test/modules/test_extensions/test_ext8.control4
-rw-r--r--src/test/modules/test_extensions/test_ext_cine--1.0--1.1.sql26
-rw-r--r--src/test/modules/test_extensions/test_ext_cine--1.0.sql25
-rw-r--r--src/test/modules/test_extensions/test_ext_cine.control3
-rw-r--r--src/test/modules/test_extensions/test_ext_cor--1.0.sql20
-rw-r--r--src/test/modules/test_extensions/test_ext_cor.control3
-rw-r--r--src/test/modules/test_extensions/test_ext_cyclic1--1.0.sql3
-rw-r--r--src/test/modules/test_extensions/test_ext_cyclic1.control4
-rw-r--r--src/test/modules/test_extensions/test_ext_cyclic2--1.0.sql3
-rw-r--r--src/test/modules/test_extensions/test_ext_cyclic2.control4
-rw-r--r--src/test/modules/test_extensions/test_ext_evttrig--1.0--2.0.sql7
-rw-r--r--src/test/modules/test_extensions/test_ext_evttrig--1.0.sql16
-rw-r--r--src/test/modules/test_extensions/test_ext_evttrig.control3
-rw-r--r--src/test/modules/test_extensions/test_ext_extschema--1.0.sql5
-rw-r--r--src/test/modules/test_extensions/test_ext_extschema.control3
-rw-r--r--src/test/modules/test_ginpostinglist/.gitignore4
-rw-r--r--src/test/modules/test_ginpostinglist/Makefile23
-rw-r--r--src/test/modules/test_ginpostinglist/README2
-rw-r--r--src/test/modules/test_ginpostinglist/expected/test_ginpostinglist.out19
-rw-r--r--src/test/modules/test_ginpostinglist/sql/test_ginpostinglist.sql7
-rw-r--r--src/test/modules/test_ginpostinglist/test_ginpostinglist--1.0.sql8
-rw-r--r--src/test/modules/test_ginpostinglist/test_ginpostinglist.c96
-rw-r--r--src/test/modules/test_ginpostinglist/test_ginpostinglist.control4
-rw-r--r--src/test/modules/test_integerset/.gitignore4
-rw-r--r--src/test/modules/test_integerset/Makefile23
-rw-r--r--src/test/modules/test_integerset/README7
-rw-r--r--src/test/modules/test_integerset/expected/test_integerset.out31
-rw-r--r--src/test/modules/test_integerset/sql/test_integerset.sql7
-rw-r--r--src/test/modules/test_integerset/test_integerset--1.0.sql8
-rw-r--r--src/test/modules/test_integerset/test_integerset.c623
-rw-r--r--src/test/modules/test_integerset/test_integerset.control4
-rw-r--r--src/test/modules/test_misc/.gitignore4
-rw-r--r--src/test/modules/test_misc/Makefile14
-rw-r--r--src/test/modules/test_misc/README4
-rw-r--r--src/test/modules/test_misc/t/001_constraint_validation.pl315
-rw-r--r--src/test/modules/test_misc/t/002_tablespace.pl95
-rw-r--r--src/test/modules/test_misc/t/003_check_guc.pl110
-rw-r--r--src/test/modules/test_oat_hooks/.gitignore4
-rw-r--r--src/test/modules/test_oat_hooks/Makefile26
-rw-r--r--src/test/modules/test_oat_hooks/README86
-rw-r--r--src/test/modules/test_oat_hooks/expected/test_oat_hooks.out309
-rw-r--r--src/test/modules/test_oat_hooks/sql/test_oat_hooks.sql107
-rw-r--r--src/test/modules/test_oat_hooks/test_oat_hooks.c520
-rw-r--r--src/test/modules/test_parser/.gitignore4
-rw-r--r--src/test/modules/test_parser/Makefile23
-rw-r--r--src/test/modules/test_parser/README61
-rw-r--r--src/test/modules/test_parser/expected/test_parser.out44
-rw-r--r--src/test/modules/test_parser/sql/test_parser.sql18
-rw-r--r--src/test/modules/test_parser/test_parser--1.0.sql32
-rw-r--r--src/test/modules/test_parser/test_parser.c127
-rw-r--r--src/test/modules/test_parser/test_parser.control5
-rw-r--r--src/test/modules/test_pg_dump/.gitignore4
-rw-r--r--src/test/modules/test_pg_dump/Makefile21
-rw-r--r--src/test/modules/test_pg_dump/README4
-rw-r--r--src/test/modules/test_pg_dump/expected/test_pg_dump.out93
-rw-r--r--src/test/modules/test_pg_dump/sql/test_pg_dump.sql108
-rw-r--r--src/test/modules/test_pg_dump/t/001_base.pl844
-rw-r--r--src/test/modules/test_pg_dump/test_pg_dump--1.0.sql62
-rw-r--r--src/test/modules/test_pg_dump/test_pg_dump.control3
-rw-r--r--src/test/modules/test_predtest/.gitignore4
-rw-r--r--src/test/modules/test_predtest/Makefile23
-rw-r--r--src/test/modules/test_predtest/README28
-rw-r--r--src/test/modules/test_predtest/expected/test_predtest.out1096
-rw-r--r--src/test/modules/test_predtest/sql/test_predtest.sql442
-rw-r--r--src/test/modules/test_predtest/test_predtest--1.0.sql16
-rw-r--r--src/test/modules/test_predtest/test_predtest.c218
-rw-r--r--src/test/modules/test_predtest/test_predtest.control4
-rw-r--r--src/test/modules/test_rbtree/.gitignore4
-rw-r--r--src/test/modules/test_rbtree/Makefile23
-rw-r--r--src/test/modules/test_rbtree/README13
-rw-r--r--src/test/modules/test_rbtree/expected/test_rbtree.out12
-rw-r--r--src/test/modules/test_rbtree/sql/test_rbtree.sql8
-rw-r--r--src/test/modules/test_rbtree/test_rbtree--1.0.sql8
-rw-r--r--src/test/modules/test_rbtree/test_rbtree.c414
-rw-r--r--src/test/modules/test_rbtree/test_rbtree.control4
-rw-r--r--src/test/modules/test_regex/.gitignore4
-rw-r--r--src/test/modules/test_regex/Makefile23
-rw-r--r--src/test/modules/test_regex/README78
-rw-r--r--src/test/modules/test_regex/expected/test_regex.out5084
-rw-r--r--src/test/modules/test_regex/expected/test_regex_utf8.out206
-rw-r--r--src/test/modules/test_regex/expected/test_regex_utf8_1.out8
-rw-r--r--src/test/modules/test_regex/sql/test_regex.sql1785
-rw-r--r--src/test/modules/test_regex/sql/test_regex_utf8.sql78
-rw-r--r--src/test/modules/test_regex/test_regex--1.0.sql9
-rw-r--r--src/test/modules/test_regex/test_regex.c773
-rw-r--r--src/test/modules/test_regex/test_regex.control4
-rw-r--r--src/test/modules/test_rls_hooks/.gitignore4
-rw-r--r--src/test/modules/test_rls_hooks/Makefile20
-rw-r--r--src/test/modules/test_rls_hooks/README16
-rw-r--r--src/test/modules/test_rls_hooks/expected/test_rls_hooks.out201
-rw-r--r--src/test/modules/test_rls_hooks/sql/test_rls_hooks.sql176
-rw-r--r--src/test/modules/test_rls_hooks/test_rls_hooks.c167
-rw-r--r--src/test/modules/test_rls_hooks/test_rls_hooks.h25
-rw-r--r--src/test/modules/test_shm_mq/.gitignore4
-rw-r--r--src/test/modules/test_shm_mq/Makefile25
-rw-r--r--src/test/modules/test_shm_mq/README49
-rw-r--r--src/test/modules/test_shm_mq/expected/test_shm_mq.out36
-rw-r--r--src/test/modules/test_shm_mq/setup.c316
-rw-r--r--src/test/modules/test_shm_mq/sql/test_shm_mq.sql12
-rw-r--r--src/test/modules/test_shm_mq/test.c267
-rw-r--r--src/test/modules/test_shm_mq/test_shm_mq--1.0.sql19
-rw-r--r--src/test/modules/test_shm_mq/test_shm_mq.control4
-rw-r--r--src/test/modules/test_shm_mq/test_shm_mq.h45
-rw-r--r--src/test/modules/test_shm_mq/worker.c197
-rw-r--r--src/test/modules/unsafe_tests/.gitignore4
-rw-r--r--src/test/modules/unsafe_tests/Makefile17
-rw-r--r--src/test/modules/unsafe_tests/README8
-rw-r--r--src/test/modules/unsafe_tests/expected/alter_system_table.out179
-rw-r--r--src/test/modules/unsafe_tests/expected/guc_privs.out562
-rw-r--r--src/test/modules/unsafe_tests/expected/rolenames.out1091
-rw-r--r--src/test/modules/unsafe_tests/sql/alter_system_table.sql194
-rw-r--r--src/test/modules/unsafe_tests/sql/guc_privs.sql253
-rw-r--r--src/test/modules/unsafe_tests/sql/rolenames.sql504
-rw-r--r--src/test/modules/worker_spi/.gitignore4
-rw-r--r--src/test/modules/worker_spi/Makefile26
-rw-r--r--src/test/modules/worker_spi/dynamic.conf2
-rw-r--r--src/test/modules/worker_spi/expected/worker_spi.out50
-rw-r--r--src/test/modules/worker_spi/sql/worker_spi.sql35
-rw-r--r--src/test/modules/worker_spi/worker_spi--1.0.sql9
-rw-r--r--src/test/modules/worker_spi/worker_spi.c393
-rw-r--r--src/test/modules/worker_spi/worker_spi.control5
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,
+ &REGRESS_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,
+ &REGRESS_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,
+ &REGRESS_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,
+ &REGRESS_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,
+ &REGRESS_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,
+ &REGRESS_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,
+ &REGRESS_userset_variable1,
+ false,
+ PGC_USERSET,
+ GUC_NOT_IN_SAMPLE,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("test_oat_hooks.user_var2",
+ "Dummy parameter settable by public",
+ NULL,
+ &REGRESS_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,
+ &REGRESS_suset_variable1,
+ false,
+ PGC_SUSET,
+ GUC_NOT_IN_SAMPLE,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("test_oat_hooks.super_var2",
+ "Dummy parameter settable by superuser",
+ NULL,
+ &REGRESS_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(&registrant->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