diff --git a/src/test/modules/test_ddl_deparse_regress/.gitignore b/src/test/modules/test_ddl_deparse_regress/.gitignore new file mode 100644 index 0000000000..5dcb3ff972 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_ddl_deparse_regress/Makefile b/src/test/modules/test_ddl_deparse_regress/Makefile new file mode 100644 index 0000000000..6a706483d1 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/Makefile @@ -0,0 +1,44 @@ +#------------------------------------------------------------------------- +# +# Makefile for src/test/modules/test_ddl_deparse_regress +# +# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/test/modules/test_ddl_deparse_regress/Makefile +# +#------------------------------------------------------------------------- + + +MODULES = test_ddl_deparse_regress +PGFILEDESC = "test_ddl_deparse_regress - regression testing for DDL deparsing" + +EXTENSION = test_ddl_deparse_regress +DATA = test_ddl_deparse_regress--1.0.sql + +# test_ddl_deparse must be first +REGRESS = test_ddl_deparse \ + create_extension \ + create_schema \ + create_table + +export REGRESS + +EXTRA_INSTALL = contrib/pg_stat_statements + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_ddl_deparse_regress +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +REGRESS_SHLIB=$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX) +export REGRESS_SHLIB + diff --git a/src/test/modules/test_ddl_deparse_regress/expected/create_extension.out b/src/test/modules/test_ddl_deparse_regress/expected/create_extension.out new file mode 100644 index 0000000000..06ccc62ba9 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/expected/create_extension.out @@ -0,0 +1,6 @@ +--- +--- CREATE_EXTENSION +--- +CREATE EXTENSION pg_stat_statements; +NOTICE: deparsed json: {"fmt": "CREATE EXTENSION %{if_not_exists}s %{name}I %{options: }s", "name": "pg_stat_statements", "options": [{"fmt": "SCHEMA %{schema}I", "type": "schema", "schema": "public"}], "if_not_exists": ""} +NOTICE: re-formed command: CREATE EXTENSION pg_stat_statements SCHEMA public diff --git a/src/test/modules/test_ddl_deparse_regress/expected/create_schema.out b/src/test/modules/test_ddl_deparse_regress/expected/create_schema.out new file mode 100644 index 0000000000..adf1999cf7 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/expected/create_schema.out @@ -0,0 +1,22 @@ +-- +-- CREATE_SCHEMA +-- +CREATE SCHEMA foo; +NOTICE: deparsed json: {"fmt": "CREATE SCHEMA %{if_not_exists}s %{name}I %{authorization}s", "name": "foo", "authorization": {"fmt": "AUTHORIZATION %{authorization_role}I ", "present": false, "authorization_role": null}, "if_not_exists": ""} +NOTICE: re-formed command: CREATE SCHEMA foo +CREATE SCHEMA IF NOT EXISTS bar; +NOTICE: deparsed json: {"fmt": "CREATE SCHEMA %{if_not_exists}s %{name}I %{authorization}s", "name": "bar", "authorization": {"fmt": "AUTHORIZATION %{authorization_role}I ", "present": false, "authorization_role": null}, "if_not_exists": "IF NOT EXISTS"} +NOTICE: re-formed command: CREATE SCHEMA IF NOT EXISTS bar +CREATE SCHEMA baz; +NOTICE: deparsed json: {"fmt": "CREATE SCHEMA %{if_not_exists}s %{name}I %{authorization}s", "name": "baz", "authorization": {"fmt": "AUTHORIZATION %{authorization_role}I ", "present": false, "authorization_role": null}, "if_not_exists": ""} +NOTICE: re-formed command: CREATE SCHEMA baz +-- 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) +NOTICE: deparsed json: {"fmt": "CREATE SCHEMA %{if_not_exists}s %{name}I %{authorization}s", "name": "element_test", "authorization": {"fmt": "AUTHORIZATION %{authorization_role}I ", "present": false, "authorization_role": null}, "if_not_exists": ""} +NOTICE: re-formed command: CREATE SCHEMA element_test +NOTICE: deparsed json: {"fmt": "CREATE %{persistence}s TABLE %{if_not_exists}s %{identity}D (%{table_elements:, }s) %{inherits}s %{tablespace}s %{on_commit}s %{partition_by}s %{access_method}s %{with_clause}s", "identity": {"objname": "foo", "schemaname": "element_test"}, "inherits": {"fmt": "INHERITS (%{parents:, }D)", "parents": null, "present": false}, "on_commit": {"fmt": "ON COMMIT %{on_commit_value}s", "present": false, "on_commit_value": null}, "tablespace": {"fmt": "TABLESPACE %{tablespace}I", "present": false, "tablespace": null}, "persistence": "", "with_clause": {"fmt": "WITH", "present": false}, "partition_by": {"fmt": "PARTITION BY %{definition}s", "present": false, "definition": null}, "access_method": {"fmt": "USING %{access_method}I", "present": false, "access_method": null}, "if_not_exists": "", "table_elements": [{"fmt": "%{name}I %{coltype}T %{compression}s %{collation}s %{not_null}s %{default}s %{identity_column}s %{generated_column}s", "name": "id", "type": "column", "coltype": {"typmod": "", "typarray": false, "typename": "int4", "schemaname": "pg_catalog"}, "default": {"fmt": "DEFAULT", "present": false}, "not_null": "", "collation": {"fmt": "COLLATE", "present": false}, "compression": {"fmt": "COMPRESSION %{compression_method}I", "present": false, "compression_method": null}, "identity_column": {"fmt": "", "present": false}, "generated_column": {"fmt": "GENERATED ALWAYS AS", "present": false}}]} +NOTICE: re-formed command: CREATE TABLE element_test.foo (id pg_catalog.int4 ) diff --git a/src/test/modules/test_ddl_deparse_regress/expected/create_table.out b/src/test/modules/test_ddl_deparse_regress/expected/create_table.out new file mode 100644 index 0000000000..154f6b5d4f --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/expected/create_table.out @@ -0,0 +1,6 @@ +CREATE TABLE simple_table( + id int, + name varchar(5) +); +NOTICE: deparsed json: {"fmt": "CREATE %{persistence}s TABLE %{if_not_exists}s %{identity}D (%{table_elements:, }s) %{inherits}s %{tablespace}s %{on_commit}s %{partition_by}s %{access_method}s %{with_clause}s", "identity": {"objname": "simple_table", "schemaname": "public"}, "inherits": {"fmt": "INHERITS (%{parents:, }D)", "parents": null, "present": false}, "on_commit": {"fmt": "ON COMMIT %{on_commit_value}s", "present": false, "on_commit_value": null}, "tablespace": {"fmt": "TABLESPACE %{tablespace}I", "present": false, "tablespace": null}, "persistence": "", "with_clause": {"fmt": "WITH", "present": false}, "partition_by": {"fmt": "PARTITION BY %{definition}s", "present": false, "definition": null}, "access_method": {"fmt": "USING %{access_method}I", "present": false, "access_method": null}, "if_not_exists": "", "table_elements": [{"fmt": "%{name}I %{coltype}T %{compression}s %{collation}s %{not_null}s %{default}s %{identity_column}s %{generated_column}s", "name": "id", "type": "column", "coltype": {"typmod": "", "typarray": false, "typename": "int4", "schemaname": "pg_catalog"}, "default": {"fmt": "DEFAULT", "present": false}, "not_null": "", "collation": {"fmt": "COLLATE", "present": false}, "compression": {"fmt": "COMPRESSION %{compression_method}I", "present": false, "compression_method": null}, "identity_column": {"fmt": "", "present": false}, "generated_column": {"fmt": "GENERATED ALWAYS AS", "present": false}}, {"fmt": "%{name}I %{coltype}T %{compression}s %{collation}s %{not_null}s %{default}s %{identity_column}s %{generated_column}s", "name": "name", "type": "column", "coltype": {"typmod": "(5)", "typarray": false, "typename": "varchar", "schemaname": "pg_catalog"}, "default": {"fmt": "DEFAULT", "present": false}, "not_null": "", "collation": {"fmt": "COLLATE %{name}D", "name": {"objname": "default", "schemaname": "pg_catalog"}}, "compression": {"fmt": "COMPRESSION %{compression_method}I", "present": false, "compression_method": null}, "identity_column": {"fmt": "", "present": false}, "generated_column": {"fmt": "GENERATED ALWAYS AS", "present": false}}]} +NOTICE: re-formed command: CREATE TABLE public.simple_table (id pg_catalog.int4 , name pg_catalog."varchar"(5) COLLATE pg_catalog."default" ) diff --git a/src/test/modules/test_ddl_deparse_regress/expected/test_ddl_deparse.out b/src/test/modules/test_ddl_deparse_regress/expected/test_ddl_deparse.out new file mode 100644 index 0000000000..b0e2bf251a --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/expected/test_ddl_deparse.out @@ -0,0 +1,18 @@ +CREATE EXTENSION test_ddl_deparse_regress; +CREATE OR REPLACE FUNCTION test_ddl_deparse() + RETURNS event_trigger LANGUAGE plpgsql AS +$$ +DECLARE + r record; + deparsed_json text; +BEGIN + FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() + LOOP + deparsed_json = pg_catalog.ddl_deparse_to_json(r.command); + RAISE NOTICE 'deparsed json: %', deparsed_json; + RAISE NOTICE 're-formed command: %', pg_catalog.ddl_deparse_expand_command(deparsed_json); + 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_regress/meson.build b/src/test/modules/test_ddl_deparse_regress/meson.build new file mode 100644 index 0000000000..7a152a23e2 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/meson.build @@ -0,0 +1,42 @@ +# FIXME: prevent install during main install, but not during test :/ + +test_ddl_deparse_regress_sources = files( + 'test_ddl_deparse_regress.c', +) + +if host_system == 'windows' + test_ddl_deparse_regress_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_ddl_deparse_regress', + '--FILEDESC', 'test_ddl_deparse_regress - regression testing for DDL deparsing',]) +endif + +test_ddl_deparse_regress = shared_module('test_ddl_deparse_regress', + test_ddl_deparse_regress_sources, + kwargs: pg_mod_args, +) +testprep_targets += test_ddl_deparse_regress + +install_data( + 'test_ddl_deparse_regress.control', + 'test_ddl_deparse_regress--1.0.sql', + kwargs: contrib_data_args, +) + +tests += { + 'name': 'test_ddl_deparse_regress', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_ddl_deparse', + 'create_extension', + 'create_schema', + 'create_table', + ], + }, + 'tap': { + 'tests': [ + 't/001_compare_dumped_results.pl', + ], + }, +} diff --git a/src/test/modules/test_ddl_deparse_regress/sql/create_extension.sql b/src/test/modules/test_ddl_deparse_regress/sql/create_extension.sql new file mode 100644 index 0000000000..d23e7fd9ce --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/sql/create_extension.sql @@ -0,0 +1,5 @@ +--- +--- CREATE_EXTENSION +--- + +CREATE EXTENSION pg_stat_statements; diff --git a/src/test/modules/test_ddl_deparse_regress/sql/create_schema.sql b/src/test/modules/test_ddl_deparse_regress/sql/create_schema.sql new file mode 100644 index 0000000000..10b13f0a55 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/sql/create_schema.sql @@ -0,0 +1,16 @@ +-- +-- 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) diff --git a/src/test/modules/test_ddl_deparse_regress/sql/create_table.sql b/src/test/modules/test_ddl_deparse_regress/sql/create_table.sql new file mode 100644 index 0000000000..241e9c8a84 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/sql/create_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE simple_table( + id int, + name varchar(5) +); diff --git a/src/test/modules/test_ddl_deparse_regress/sql/test_ddl_deparse.sql b/src/test/modules/test_ddl_deparse_regress/sql/test_ddl_deparse.sql new file mode 100644 index 0000000000..0889fa02f9 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/sql/test_ddl_deparse.sql @@ -0,0 +1,20 @@ +CREATE EXTENSION test_ddl_deparse_regress; + +CREATE OR REPLACE FUNCTION test_ddl_deparse() + RETURNS event_trigger LANGUAGE plpgsql AS +$$ +DECLARE + r record; + deparsed_json text; +BEGIN + FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() + LOOP + deparsed_json = pg_catalog.ddl_deparse_to_json(r.command); + RAISE NOTICE 'deparsed json: %', deparsed_json; + RAISE NOTICE 're-formed command: %', pg_catalog.ddl_deparse_expand_command(deparsed_json); + 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_regress/t/001_compare_dumped_results.pl b/src/test/modules/test_ddl_deparse_regress/t/001_compare_dumped_results.pl new file mode 100644 index 0000000000..f989f56e59 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/t/001_compare_dumped_results.pl @@ -0,0 +1,179 @@ +use strict; +use warnings; +use Env; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use File::Basename; + +sub execute_test_case { + my $test_name = $_[0]; + my $pub_node = $_[1]; + my $sub_node = $_[2]; + my $dbname = $_[3]; + my $outputdir = $PostgreSQL::Test::Utils::tmp_check; + + # set up deparse testing resources + create_deparse_testing_resources_on_pub_node($pub_node, $dbname); + + my $test_file = "./sql/${test_name}.sql"; + my $content = do{local(@ARGV,$/)=$test_file;<>}; + + $pub_node -> psql($dbname, $content); + + # retrieve SQL commands generated from deparsed DDLs on pub node + my $ddl_sql = ''; + $pub_node -> psql($dbname,q( + select ddl_deparse_expand_command(ddl) || ';' from deparsed_ddls ORDER BY id ASC), + stdout => \$ddl_sql); + + print "\nstart printing re-formed sql\n"; + print $ddl_sql; + print "\nend printing re-formed sql\n"; + # execute SQL commands on sub node + $sub_node -> psql($dbname, $ddl_sql); + + # clean up deparse testing resources + clean_deparse_testing_resources_on_pub_node($pub_node, $dbname); + + # dump from pub node and sub node + mkdir ${outputdir}."/dumps", 0755; + my $pub_dump = ${outputdir}."/dumps/${test_name}_pub.dump"; + my $sub_dump = ${outputdir}."/dumps/${test_name}_sub.dump"; + system("pg_dumpall " + . "-s " + . "-f " + . $pub_dump . " " + . "--no-sync " + . '-p ' + . $pub_node->port) == 0 or die "Dump pub node failed in ${test_name}"; + system("pg_dumpall " + . "-s " + . "-f " + . $sub_dump . " " + . "--no-sync " + . '-p ' + . $sub_node->port) == 0 or die "Dump sub node failed in ${test_name}"; + + # compare dumped results + is(system("diff " + . $pub_dump . " " + . $sub_dump), 0, "Dumped results diff in ${test_name}"); +} + +sub init_node { + my $node_name = $_[0]; + my $node = PostgreSQL::Test::Cluster->new($node_name); + $node->init; + # increase some settings that Cluster->new makes too low by default. + $node->adjust_conf('postgresql.conf', 'max_connections', '25'); + $node->append_conf('postgresql.conf', + 'max_prepared_transactions = 10'); + return $node; +} + +sub init_pub_node { + my $node_name = $_[0]."_pub"; + return init_node($node_name) +} + +sub init_sub_node { + my $node_name = $_[0]."_sub"; + return init_node($node_name) +} + +sub create_deparse_testing_resources_on_pub_node { + my $node = $_[0]; + my $dbname = $_[1]; + $node -> psql($dbname, q( + begin; + create table deparsed_ddls(id SERIAL PRIMARY KEY, tag text, object_identity text, ddl text); + + create or replace function deparse_to_json() + returns event_trigger language plpgsql as + $$ + declare + r record; + begin + for r in select * from pg_event_trigger_ddl_commands() + loop + insert into deparsed_ddls(tag, object_identity, ddl) values (r.command_tag, r.object_identity, pg_catalog.ddl_deparse_to_json(r.command)); + end loop; + END; + $$; + + create or replace function deparse_drops_to_json() + returns event_trigger language plpgsql as + $$ + declare + r record; + begin + for r in select * from pg_event_trigger_dropped_objects() + loop + insert into deparsed_ddls(tag, object_identity, ddl) values (r.object_type, r.object_identity, public.deparse_drop_ddl(r.object_identity, r.object_type)); + end loop; + END; + $$; + + create event trigger ddl_deparse_trig + on ddl_command_end execute procedure deparse_to_json(); + + create event trigger ddl_drops_deparse_trig + on sql_drop execute procedure deparse_drops_to_json(); + + commit; + )); +} + +sub clean_deparse_testing_resources_on_pub_node { + my $node = $_[0]; + my $dbname = $_[1]; + # Drop the event trigger and the function before taking a logical dump. + $node -> safe_psql($dbname,q( + drop event trigger ddl_deparse_trig; + drop event trigger ddl_drops_deparse_trig; + drop function deparse_to_json(); + drop function deparse_drops_to_json(); + drop table deparsed_ddls; + )); +} + +sub trim { + my @out = @_; + for (@out) { + s/^\s+//; + s/\s+$//; + } + return wantarray ? @out : $out[0]; +} + +# Create and start pub sub nodes +my $pub_node = init_pub_node("test"); +my $sub_node = init_sub_node("test"); +my $dbname = "postgres"; +$pub_node -> start; +$sub_node -> start; + +# load test cases from the regression tests +my @regress_tests = split /\s+/, $ENV{REGRESS}; + +foreach(@regress_tests) { + my $test_name = trim($_); + # skip if it's regression test preparation or empty string + if ($test_name eq "" or $test_name eq "test_ddl_deparse") + { + next; + } + eval {execute_test_case($test_name, $pub_node, $sub_node, $dbname);}; + if ($@ ne "") + { + fail($@); + } +} +close; + +# Close nodes +$pub_node->stop; +$sub_node->stop; + +done_testing(); diff --git a/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress--1.0.sql b/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress--1.0.sql new file mode 100644 index 0000000000..14070cd51f --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress--1.0.sql @@ -0,0 +1,9 @@ +/* src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_ddl_deparse_regress" to load this file. \quit + +CREATE FUNCTION deparse_drop_ddl(IN objidentity text, + IN objecttype text) + RETURNS text IMMUTABLE STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; \ No newline at end of file diff --git a/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress.c b/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress.c new file mode 100644 index 0000000000..bd6992bdfd --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress.c @@ -0,0 +1,59 @@ +/*---------------------------------------------------------------------- + * test_ddl_deparse_regress.c + * Support functions for the test_ddl_deparse_regress module + * + * Copyright (c) 2014-2022, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress.c + *---------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_type.h" +#include "funcapi.h" +#include "nodes/execnodes.h" +#include "tcop/deparse_utility.h" +#include "tcop/utility.h" +#include "utils/builtins.h" +#include "tcop/ddl_deparse.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(deparse_drop_ddl); + +/* + * Given object_identity and object_type of dropped object, return a JSON representation of DROP command. + */ +Datum +deparse_drop_ddl(PG_FUNCTION_ARGS) +{ + text *objidentity = PG_GETARG_TEXT_P(0); + const char *objidentity_str = text_to_cstring(objidentity); + text *objecttype = PG_GETARG_TEXT_P(1); + const char *objecttype_str = text_to_cstring(objecttype); + + char *command; + + // constraint is part of alter table command, no need to drop in DROP command + if (strcmp(objecttype_str, "table constraint") == 0) { + PG_RETURN_NULL(); + } else if (strcmp(objecttype_str, "toast table") == 0) { + objecttype_str = "table"; + } else if (strcmp(objecttype_str, "default value") == 0) { + PG_RETURN_NULL(); + } else if (strcmp(objecttype_str, "operator of access method") == 0) { + PG_RETURN_NULL(); + } else if (strcmp(objecttype_str, "function of access method") == 0) { + PG_RETURN_NULL(); + } else if (strcmp(objecttype_str, "table column") == 0) { + PG_RETURN_NULL(); + } + + command = deparse_drop_command(objidentity_str, objecttype_str, DROP_CASCADE); + + if (command) + PG_RETURN_TEXT_P(cstring_to_text(command)); + + PG_RETURN_NULL(); +} \ No newline at end of file diff --git a/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress.control b/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress.control new file mode 100644 index 0000000000..a1f934e658 --- /dev/null +++ b/src/test/modules/test_ddl_deparse_regress/test_ddl_deparse_regress.control @@ -0,0 +1,4 @@ +comment = 'Test code for DDL deparse regress feature' +default_version = '1.0' +module_pathname = '$libdir/test_ddl_deparse_regress' +relocatable = true