From a530f5ef9c369ab6631b8d2c855393180da1895f Mon Sep 17 00:00:00 2001 From: Hou Zhijie Date: Tue, 4 Jul 2023 16:18:33 +0800 Subject: [PATCH] test deparser module When running the regression test(tests in parallel_schedule), we replace the executing ddl statement with the its deparsed version and execute the deparsed statement, so that we can run all the regression with the deparsed statement and can expect the output to be the same as the existing expected/*.out. As developers typically add new regression tests to test new syntax, so we expect this test can automatically identify any new syntax changes. To get the deparsed statement before the execution and replace the current statement with it, the current approach is to create another database with deparser trigger and in the hook we redirect the local statement to that remote database, then the statment will be deparsed and stored somewhere, we can query the remote database to get the deparsed statement and use it to replace the original statment. TO IMPROVE: 1. The current approach needs to handle the ERRORs, WARNINGs, and NOTICEs from the remote database. Currently it will directly rethrow any ERRORs encountered in the remote database. However, for WARNINGs and NOTICEs, we will only rethrow them along with the ERRORs. This is done to prevent duplicate messages in the output file during local statement execution, which would be inconsistent with the existing expected output. Note that this approach may potentially miss some bugs, as there could be additional WARNINGs or NOTICEs caused by the deparser in the remote database. 2. The variable reference and assignment (xxx /gset and select :var_name) will not be sent to the server(only the qualified value will be sent), but it's possible the variable in another session should be set to a different value, so this can cause inconsistent output. 3 .CREATE INDEX CONCURRENTLY will create an invalid index internally even if it reports an ERROR later. But since we will directly rethrow the remote ERROR in the main database, we won't execute the "CREATE INDEX CONCURRENTLY" in the main database. This means that we cannot see the invalid index in the main database. To improve the above points, another variety is: run the regression test twice. The first run is solely intended to collect all the deparsed statements. We can dump these statements from the database and then reload them in the second regression run. This allows us to utilize the deparsed statements to replace the local statements in the second regression run. This approach does not need to handle any remote messages and client variable stuff during execution, although it could take more time to finsh the test. --- src/test/modules/test_deparser/Makefile | 46 ++ .../test_deparser/expected/test_deparser.out | 51 ++ src/test/modules/test_deparser/meson.build | 29 + .../test_deparser/sql/test_deparser.sql | 55 ++ .../test_deparser/test_deparser--1.0.sql | 5 + .../modules/test_deparser/test_deparser.c | 655 ++++++++++++++++++ .../modules/test_deparser/test_deparser.conf | 1 + .../test_deparser/test_deparser.control | 4 + 8 files changed, 846 insertions(+) create mode 100644 src/test/modules/test_deparser/Makefile create mode 100644 src/test/modules/test_deparser/expected/test_deparser.out create mode 100644 src/test/modules/test_deparser/meson.build create mode 100644 src/test/modules/test_deparser/sql/test_deparser.sql create mode 100644 src/test/modules/test_deparser/test_deparser--1.0.sql create mode 100644 src/test/modules/test_deparser/test_deparser.c create mode 100644 src/test/modules/test_deparser/test_deparser.conf create mode 100644 src/test/modules/test_deparser/test_deparser.control diff --git a/src/test/modules/test_deparser/Makefile b/src/test/modules/test_deparser/Makefile new file mode 100644 index 0000000000..944686c6e2 --- /dev/null +++ b/src/test/modules/test_deparser/Makefile @@ -0,0 +1,46 @@ +# src/test/modules/test_deparser/Makefile + +MODULES = test_deparser +PGFILEDESC = "test_deparser - regression testing for DDL deparsing" + +EXTENSION = test_deparser +DATA = test_deparser--1.0.sql + +MODULE_big = test_deparser +OBJS = \ + $(WIN32RES) \ + test_deparser.o + +PG_CPPFLAGS = -I$(libpq_srcdir) +SHLIB_LINK_INTERNAL = $(libpq) + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_deparser +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +REGRESS_OPTS += --load-extension=test_deparser --dlpath=$(top_builddir)/src/test/regress \ + --inputdir=$(top_srcdir)/src/test/regress \ + --host=localhost + +check: all deparse_regress_schedule + $(pg_regress_check) $(REGRESS_OPTS) --schedule=deparse_regress_schedule + +deparse_regress_schedule: + echo "test: test_deparser" > $@ + echo "test: test_setup" >> $@ + echo "test: create_index" >> $@ + echo "test: create_table" >> $@ + echo "test: alter_table" >> $@ + #cat $(top_srcdir)/src/test/regress/parallel_schedule >> $@ + +deparse_clean: clean + rm -f $(OBJS) + rm -rf $(pg_regress_clean_files) + rm -f deparse_regress_schedule diff --git a/src/test/modules/test_deparser/expected/test_deparser.out b/src/test/modules/test_deparser/expected/test_deparser.out new file mode 100644 index 0000000000..22ea6a79fd --- /dev/null +++ b/src/test/modules/test_deparser/expected/test_deparser.out @@ -0,0 +1,51 @@ +\set prevdb :DBNAME +CREATE DATABASE deparser_regress; +\c deparser_regress; +CREATE TABLE public.deparse_test_commands( + test_name text, + backend_id int, + backend_start timestamptz, + lsn pg_lsn, + ord integer, + command TEXT +); +CREATE OR REPLACE FUNCTION public.deparse_test_ddl_command_end() + RETURNS event_trigger + SECURITY DEFINER + LANGUAGE plpgsql +AS $fn$ +BEGIN + BEGIN + INSERT INTO public.deparse_test_commands + (test_name, backend_id, backend_start, command, ord, lsn) + SELECT current_setting('application_name'), + id, pg_stat_get_backend_start(id), + pg_catalog.ddl_deparse_expand_command(pg_catalog.ddl_deparse_to_json(command)), +-- pg_catalog.ddl_deparse_to_json(command), + ordinality, lsn + FROM pg_event_trigger_ddl_commands() WITH ORDINALITY, + pg_current_wal_insert_lsn() lsn, + pg_stat_get_backend_idset() id + WHERE pg_stat_get_backend_pid(id) = pg_backend_pid() AND + NOT command_tag = 'ss'; + + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'state: % errm: %', sqlstate, sqlerrm; + END; +END; +$fn$; +CREATE EXTENSION test_deparser; +CREATE EVENT TRIGGER deparse_test_trg_sql_drop + ON sql_drop + EXECUTE PROCEDURE test_deparser_drop_command(); +CREATE EVENT TRIGGER deparse_test_trg_ddl_command_end + ON ddl_command_end WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE') + EXECUTE PROCEDURE deparse_test_ddl_command_end(); +\c :prevdb +ALTER SYSTEM SET session_preload_libraries = 'test_deparser'; +SELECT pg_reload_conf(); + pg_reload_conf +---------------- + t +(1 row) + diff --git a/src/test/modules/test_deparser/meson.build b/src/test/modules/test_deparser/meson.build new file mode 100644 index 0000000000..fc2f2d15a4 --- /dev/null +++ b/src/test/modules/test_deparser/meson.build @@ -0,0 +1,29 @@ +# Copyright (c) 2022-2023, PostgreSQL Global Development Group + +test_deparser_sources = files( + 'test_deparser.c', +) + +if host_system == 'windows' + test_deparser_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_deparser', + '--FILEDESC', 'test_deparser - allow delay between parsing and execution',]) +endif + +test_deparser = shared_module('test_deparser', + test_deparser_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_deparser + +tests += { + 'name': 'test_deparser', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'isolation': { + 'specs': [ + 'partition-addition', + 'partition-removal-1', + ], + }, +} diff --git a/src/test/modules/test_deparser/sql/test_deparser.sql b/src/test/modules/test_deparser/sql/test_deparser.sql new file mode 100644 index 0000000000..94329f017f --- /dev/null +++ b/src/test/modules/test_deparser/sql/test_deparser.sql @@ -0,0 +1,55 @@ +\set prevdb :DBNAME + +CREATE DATABASE deparser_regress; +\c deparser_regress; + +CREATE TABLE public.deparse_test_commands( + test_name text, + backend_id int, + backend_start timestamptz, + lsn pg_lsn, + ord integer, + command TEXT +); + + +CREATE OR REPLACE FUNCTION public.deparse_test_ddl_command_end() + RETURNS event_trigger + SECURITY DEFINER + LANGUAGE plpgsql +AS $fn$ +BEGIN + BEGIN + INSERT INTO public.deparse_test_commands + (test_name, backend_id, backend_start, command, ord, lsn) + SELECT current_setting('application_name'), + id, pg_stat_get_backend_start(id), + pg_catalog.ddl_deparse_expand_command(pg_catalog.ddl_deparse_to_json(command)), +-- pg_catalog.ddl_deparse_to_json(command), + ordinality, lsn + FROM pg_event_trigger_ddl_commands() WITH ORDINALITY, + pg_current_wal_insert_lsn() lsn, + pg_stat_get_backend_idset() id + WHERE pg_stat_get_backend_pid(id) = pg_backend_pid() AND + NOT command_tag = 'ss'; + + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'state: % errm: %', sqlstate, sqlerrm; + END; +END; +$fn$; + +CREATE EXTENSION test_deparser; + +CREATE EVENT TRIGGER deparse_test_trg_sql_drop + ON sql_drop + EXECUTE PROCEDURE test_deparser_drop_command(); + +CREATE EVENT TRIGGER deparse_test_trg_ddl_command_end + ON ddl_command_end WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE') + EXECUTE PROCEDURE deparse_test_ddl_command_end(); + +\c :prevdb + +ALTER SYSTEM SET session_preload_libraries = 'test_deparser'; +SELECT pg_reload_conf(); diff --git a/src/test/modules/test_deparser/test_deparser--1.0.sql b/src/test/modules/test_deparser/test_deparser--1.0.sql new file mode 100644 index 0000000000..f95eb2d0e9 --- /dev/null +++ b/src/test/modules/test_deparser/test_deparser--1.0.sql @@ -0,0 +1,5 @@ +\echo Use "CREATE EXTENSION test_deparser" to load this file. \quit + +CREATE FUNCTION test_deparser_drop_command() +RETURNS event_trigger STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_deparser/test_deparser.c b/src/test/modules/test_deparser/test_deparser.c new file mode 100644 index 0000000000..0767286bd2 --- /dev/null +++ b/src/test/modules/test_deparser/test_deparser.c @@ -0,0 +1,655 @@ +/*------------------------------------------------------------------------- + * + * test_deparser.c + * Test DDL deparser + * + * Copyright (c) 2020-2023, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_deparser/test_deparser.c + * + * when running the regression test, it will replace the executing ddl + * statement with the deparsed statements in ProcessUtility hook function, so + * that we can run the regression with the deparsed statement and we can expect + * the output to be the same as the existing expected *.out. + * + * To get the deparsed statement, the current approach is to create another + * database and redirect the local statement to that remote database. In doing + * so, the statement will be deparsed and stored in the remote database, + * allowing us to query it and retrieve the deparsed statement. This deparsed + * statement can then be used to replace the original statement. + * + * XXX In this approach, we need to handle the ERRORs, WARNINGs, and NOTICEs + * from the remote database. We will directly rethrow any ERRORs encountered in + * the remote database. However, for WARNINGs and NOTICEs, we will only rethrow + * them along with the ERRORs. This is done to prevent duplicate messages in + * the output file during local statement execution, which would be + * inconsistent with the existing expected output. Note that this approach may + * potentially miss some bugs, as there could be additional WARNINGs or NOTICEs + * caused by the deparser in the remote database. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/dependency.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_class.h" +#include "catalog/pg_database.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" +#include "commands/dbcommands.h" +#include "commands/event_trigger.h" +#include "commands/seclabel.h" +#include "executor/executor.h" +#include "executor/spi.h" +#include "fmgr.h" +#include "libpq-fe.h" +#include "libpq/libpq-be.h" +#include "libpq/libpq-be-fe-helpers.h" +#include "miscadmin.h" +#include "optimizer/planner.h" +#include "tcop/ddldeparse.h" +#include "tcop/utility.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/portal.h" +#include "utils/queryenvironment.h" + +PG_MODULE_MAGIC; + +typedef struct deparser_errdata +{ + int sqlstate; + int level; + int errpos; + char *message_primary; + char *message_detail; + char *message_hint; + char *message_context; +} deparser_errdata; + +static ProcessUtility_hook_type prev_ProcessUtility = NULL; +static ExecutorRun_hook_type prev_ExecutorRun = NULL; +static ExecutorStart_hook_type prev_ExecutorStart = NULL; +static planner_hook_type prev_planner_hook = NULL; +extern EventTriggerQueryState *currentEventTriggerState; + +static void tdeparser_ProcessUtility(PlannedStmt *pstmt, const char *queryString, + bool readOnlyTree, + ProcessUtilityContext context, ParamListInfo params, + QueryEnvironment *queryEnv, + DestReceiver *dest, QueryCompletion *qc); + +static void tdeparser_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count, + bool execute_once); + +static void tdeparser_ExecutorStart(QueryDesc *queryDesc, int eflags); + +static PlannedStmt *tdeparser_planner(Query *parse, + const char *query_string, + int cursorOptions, + ParamListInfo boundParams); + +static void test_deparser_redirect_cmd(const char *query); + +static void test_deparser_report_error(void *arg, const PGresult *res); + +static PGconn *conn = NULL; +static int nesting_level = 0; +static char *current_role = NULL; +static char *session_role = NULL; +static List *deparser_errdata_list = NIL; +static MemoryContext test_deparser = NULL; + +static void +test_deparser_connect(void) +{ + StringInfoData cmd; + + if (conn != NULL) + return; + + conn = libpqsrv_connect("dbname=deparser_regress host=localhost", PG_WAIT_EXTENSION); + + if (PQstatus(conn) == CONNECTION_BAD) + { + char *msg = pchomp(PQerrorMessage(conn)); + + libpqsrv_disconnect(conn); + ereport(ERROR, + (errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION), + errmsg("could not establish connection"), + errdetail_internal("%s", msg))); + } + + initStringInfo(&cmd); + appendStringInfo(&cmd, "SELECT set_config('application_name', '%s', false);", application_name); + + test_deparser_redirect_cmd(cmd.data); + + test_deparser = AllocSetContextCreate(TopMemoryContext, + "test deparser", + ALLOCSET_DEFAULT_SIZES); + + PQsetNoticeReceiver(conn, &test_deparser_report_error, (void *) NULL); +} + +static void +append_errdata_list(int sqlstate, int level, int errpos, char *message_primary, + char *message_detail, char *message_hint, char *message_context) +{ + MemoryContext oldctx; + deparser_errdata *errdata; + + oldctx = MemoryContextSwitchTo(test_deparser); + + errdata = palloc0(sizeof(deparser_errdata)); + errdata->level = level; + errdata->sqlstate = sqlstate; + errdata->errpos = errpos; + + if (message_context) + errdata->message_context = pstrdup(message_context); + + if (message_detail) + errdata->message_detail = pstrdup(message_detail); + + if (message_hint) + errdata->message_hint = pstrdup(message_hint); + + if (message_primary) + errdata->message_primary = pstrdup(message_primary); + + deparser_errdata_list = lappend(deparser_errdata_list, errdata); + + MemoryContextSwitchTo(oldctx); +} + +static void +test_deparser_rethrow_error(int sqlstate, int level, int errpos, char *message_primary, + char *message_detail, char *message_hint, + char *message_context) +{ + ereport(level, + (errcode(sqlstate), + (message_primary != NULL && message_primary[0] != '\0') ? + errmsg_internal("%s", message_primary) : + errmsg("could not obtain message string for remote error"), + message_detail ? errdetail_internal("%s", message_detail) : 0, + message_hint ? errhint("%s", message_hint) : 0, + errpos ? errposition(errpos) : 0, + message_context ? errcontext("%s", message_context) : 0)); +} + +static void +test_deparser_clean_errdata(bool rethrow) +{ + ListCell *lc; + + if (!rethrow) + { + list_free(deparser_errdata_list); + deparser_errdata_list = NIL; + return; + } + + foreach(lc, deparser_errdata_list) + { + deparser_errdata *ed = (deparser_errdata *) lfirst(lc); + test_deparser_rethrow_error(ed->sqlstate, ed->level, ed->errpos, ed->message_primary, + ed->message_detail, ed->message_hint, + ed->message_context); + deparser_errdata_list = foreach_delete_current(deparser_errdata_list, lc); + } + + Assert(deparser_errdata_list == NIL); +} + +/* + * XXX CREATE INDEX CONCURRENTLY will create an invalid index internally even + * if it reports an ERROR later. But since we will directly rethrow the remote + * ERROR in the main database, we won't execute the "CREATE INDEX CONCURRENTLY" + * in the main database. This means that we cannot see the invalid index in the + * main database. To fix this issue, we won't rethrow the ERROR in this + * specific case, allowing the main database to create the invalid index. + * However, it's important to note that this approach may also affect other + * statements that report similar ERRORS and need to be updated when adding new + * similar test cases. + */ +static bool +test_deparser_ignore_error(int sqlstate, char *message_primary) +{ + return (sqlstate == ERRCODE_UNIQUE_VIOLATION && + (strcmp(message_primary, "could not create unique index \"concur_index3\"") == 0 || + strcmp(message_primary, "could not create unique index \"concur_reindex_ind5\"") == 0 || + strcmp(message_primary, "could not create unique index \"concur_reindex_ind5_ccnew\"") == 0)); +} + +static void +test_deparser_report_error(void *arg, const PGresult *res) +{ + char *diag_sqlstate = PQresultErrorField(res, PG_DIAG_SQLSTATE); + char *message_primary = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY); + char *message_detail = PQresultErrorField(res, PG_DIAG_MESSAGE_DETAIL); + char *message_hint = PQresultErrorField(res, PG_DIAG_MESSAGE_HINT); + char *message_context = PQresultErrorField(res, PG_DIAG_CONTEXT); + char *message_level = PQresultErrorField(res, PG_DIAG_SEVERITY); + char *message_position = PQresultErrorField(res, PG_DIAG_STATEMENT_POSITION); + int sqlstate; + int level; + int msgpos; + + if (diag_sqlstate) + sqlstate = MAKE_SQLSTATE(diag_sqlstate[0], + diag_sqlstate[1], + diag_sqlstate[2], + diag_sqlstate[3], + diag_sqlstate[4]); + else + sqlstate = ERRCODE_CONNECTION_FAILURE; + + /* + * If we don't get a message from the PGresult, try the PGconn. This + * is needed because for connection-level failures, PQexec may just + * return NULL, not a PGresult at all. + */ + if (message_primary == NULL) + message_primary = pchomp(PQerrorMessage(conn)); + + if (test_deparser_ignore_error(sqlstate, message_primary)) + return; + + if (message_position) + msgpos = atoi(message_position); + else + msgpos = 0; + + if (message_level == NULL || strcmp(message_level, "ERROR") == 0) + level = ERROR; + else if (strcmp(message_level, "WARNING") == 0) + level = WARNING; + else if (strcmp(message_level, "NOTICE") == 0) + level = NOTICE; + else + return; + + /* WARNING and NOTICE are only re-thrown with ERRORs. */ + if (level == WARNING || level == NOTICE) + { + append_errdata_list(sqlstate, level, msgpos, message_primary, message_detail, + message_hint, message_context); + return; + } + + /* It must ba an ERROR message, so we re-throw all other messages. */ + test_deparser_clean_errdata(true); + test_deparser_rethrow_error(sqlstate, level, msgpos, message_primary, message_detail, + message_hint, message_context); +} + +static void +test_deparser_redirect_cmd(const char *query) +{ + PGresult *res; + + Assert(conn); + + res = PQexec(conn, query); + + switch (PQresultStatus(res)) + { + case PGRES_COMMAND_OK: + case PGRES_TUPLES_OK: + case PGRES_COPY_OUT: + /* ok */ + break; + + case PGRES_NONFATAL_ERROR: + case PGRES_FATAL_ERROR: + test_deparser_report_error(NULL, res); + break; + + case PGRES_COPY_IN: + PQputCopyEnd(conn, NULL); + break; + default: + elog(ERROR, "Unexpected results: %s", PQerrorMessage(conn)); + break; + } + + PQclear(res); +} + +static void +test_deparser_switch_role(void) +{ + PGresult *res; + + res = PQexec(conn, "SELECT CURRENT_USER, SESSION_USER"); + + if (PQresultStatus(res) != PGRES_TUPLES_OK || + PQntuples(res) != 1) + elog(ERROR, "failed: %s", PQerrorMessage(conn)); + + current_role = pstrdup(PQgetvalue(res, 0, 0)); + session_role = pstrdup(PQgetvalue(res, 0, 1)); + elog(LOG, "CURRENT_USER: %s, SESSION_USER: %s", current_role, session_role); + PQclear(res); + + test_deparser_redirect_cmd("RESET ROLE"); + test_deparser_redirect_cmd("RESET SESSION AUTHORIZATION"); +} + +static void +test_deparser_restore_role(void) +{ + StringInfoData cmd; + + initStringInfo(&cmd); + appendStringInfo(&cmd, "SET SESSION AUTHORIZATION %s", session_role); + elog(LOG, "SET SESSION ROLE BACK TO: %s", session_role); + + test_deparser_redirect_cmd(cmd.data); + + resetStringInfo(&cmd); + appendStringInfo(&cmd, "SET ROLE %s", current_role); + elog(LOG, "SET ROLE BACK TO: %s", current_role); + + test_deparser_redirect_cmd(cmd.data); +} + +static void +test_deparser_clean_statement(void) +{ + StringInfoData cleancmd; + + /* Clean the old deparsed commands. */ + initStringInfo(&cleancmd); + appendStringInfo(&cleancmd, + "DELETE FROM public.deparse_test_commands WHERE test_name = '%s'", + application_name); + test_deparser_redirect_cmd(cleancmd.data); +} + + +static List * +change_deparsed_stmt(PlannedStmt *pstmt, const char *queryString) +{ + List *nstmt_list = NIL; + List *parsetree_list; + ListCell *lc; + PGresult *res; + int i; + StringInfoData cmd; + Node *parsetree = pstmt->utilityStmt; + + /* + * XXX the statement "REINDEX SCHEMA CONCURRENTLY :temp_schema_name" in + * create_index.sql cannot be redirected to another database. This is + * because the temp_schema_name has already been replaced with the + * temporary schema name in the current session (pg_temp_3 in this case), + * and the schema name will not be the same in another session. So, we skip + * redirecting these REINDEX SCEHAM statements. Note that although this may + * be acceptable for now if we only want to test the ALTER/CREATE TABLE, it + * will need to be fixed if we plan on supporting deparse INDEX ddl + * statements in the future. + */ + if (IsA(parsetree, ReindexStmt)) + { + ReindexStmt *reindex = (ReindexStmt *) parsetree; + if (reindex->kind == REINDEX_OBJECT_SCHEMA) + return list_make1(pstmt); + } + + test_deparser_connect(); + + /* Redirect the statement to the temp database. */ + test_deparser_redirect_cmd(queryString); + + initStringInfo(&cmd); + + /* + * The test may have swtiched the current role to an untrusted user, we + * need to switch to a safe user temporarily to access the deparsed + * commands in the remote table. + */ + test_deparser_switch_role(); + + appendStringInfo(&cmd, "SELECT command, test_name FROM public.deparse_test_commands WHERE test_name = '%s';", application_name); + + /* Query the deparsed statement. */ + res = PQexec(conn, cmd.data); + if (PQresultStatus(res) != PGRES_TUPLES_OK && + PQresultStatus(res) != PGRES_COMMAND_OK) + elog(ERROR, "failed: %s", PQerrorMessage(conn)); + + if (PQntuples(res) == 0) + { + PQclear(res); + test_deparser_restore_role(); + return list_make1(pstmt); + } + + resetStringInfo(&cmd); + + for (i = 0; i < PQntuples(res); i++) + { + appendStringInfo(&cmd, "%s;", PQgetvalue(res, i, 0)); + elog(LOG, "result: %s", PQgetvalue(res, i, 0)); + } + + PQclear(res); + + /* Clean the old deparsed commands. */ + test_deparser_clean_statement(); + + test_deparser_restore_role(); + + parsetree_list = pg_parse_query(cmd.data); + + /* + * One statment could be deparsed into mulitiple statements, for example: + * "DROP TABLE t1,t2" will be deparsed into "DROP TABLE t1" AND "DROP TABLE + * t2". So we need to maintain them in a list. + */ + foreach(lc, parsetree_list) + { + List *plans; + RawStmt *rs = lfirst_node(RawStmt, lc); + List *querytree_list; + + querytree_list = pg_analyze_and_rewrite_fixedparams(rs, cmd.data, + NULL, 0, NULL); + Assert(list_length(querytree_list) == 1); + + plans = pg_plan_queries(querytree_list, cmd.data, + CURSOR_OPT_PARALLEL_OK, NULL); + Assert(list_length(plans) == 1); + + nstmt_list = lappend(nstmt_list, linitial(plans)); + } + + return nstmt_list; +} + +/* + * ProcessUtility hook + */ +static void +tdeparser_ProcessUtility(PlannedStmt *pstmt, const char *queryString, + bool readOnlyTree, + ProcessUtilityContext context, + ParamListInfo params, QueryEnvironment *queryEnv, + DestReceiver *dest, QueryCompletion *qc) +{ + List *nplan_list; + ListCell *lc; + CommandTag tag = CreateCommandTag(pstmt->utilityStmt); + + if (nesting_level == 0 && + tag != CMDTAG_CREATE_ROLE && + tag != CMDTAG_DROP_ROLE && + tag != CMDTAG_CREATE_TABLESPACE && + tag != CMDTAG_DROP_TABLESPACE) + nplan_list = change_deparsed_stmt(pstmt, queryString); + else + nplan_list = list_make1(pstmt); + + nesting_level++; + PG_TRY(); + { + foreach(lc, nplan_list) + { + PlannedStmt *plan = (PlannedStmt *) lfirst(lc); + if (prev_ProcessUtility) + prev_ProcessUtility(plan, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + else + standard_ProcessUtility(plan, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + } + } + PG_FINALLY(); + { + nesting_level--; + test_deparser_clean_errdata(false); + } + PG_END_TRY(); +} + +static void +tdeparser_ExecutorStart(QueryDesc *queryDesc, int eflags) +{ + /* Redirect the statement to the temp database. */ + if (debug_query_string && nesting_level == 0) + { + test_deparser_connect(); + test_deparser_redirect_cmd(debug_query_string); + test_deparser_clean_statement(); + } + + if (prev_ExecutorStart) + prev_ExecutorStart(queryDesc, eflags); + else + standard_ExecutorStart(queryDesc, eflags); +} + +/* + * ExecutorRun hook: all we need do is track nesting depth + */ +static void +tdeparser_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count, + bool execute_once) +{ + nesting_level++; + PG_TRY(); + { + if (prev_ExecutorRun) + prev_ExecutorRun(queryDesc, direction, count, execute_once); + else + standard_ExecutorRun(queryDesc, direction, count, execute_once); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); +} + +static PlannedStmt * +tdeparser_planner(Query *parse, + const char *query_string, + int cursorOptions, + ParamListInfo boundParams) +{ + PlannedStmt *result; + + nesting_level++; + PG_TRY(); + { + if (prev_planner_hook) + result = prev_planner_hook(parse, query_string, cursorOptions, boundParams); + else + result = standard_planner(parse, query_string, cursorOptions, boundParams); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); + + return result; +} + +PG_FUNCTION_INFO_V1(test_deparser_drop_command); + +Datum +test_deparser_drop_command(PG_FUNCTION_ARGS) +{ + slist_iter iter; + + /* Drop commands are not part commandlist but handled here as part of SQLDropList */ + slist_foreach(iter, &(currentEventTriggerState->SQLDropList)) + { + SQLDropObject *obj; + EventTriggerData *trigdata; + + trigdata = (EventTriggerData *) fcinfo->context; + + obj = slist_container(SQLDropObject, next, iter.cur); + + if (!obj->original) + continue; + + if (strcmp(obj->objecttype, "table") == 0) + { + char *command; + + command = deparse_drop_table(obj->objidentity, trigdata->parsetree); + if (command) + { + StringInfoData cmd; + + command = deparse_ddl_json_to_string(command); + initStringInfo(&cmd); + + appendStringInfo(&cmd, "INSERT INTO public.deparse_test_commands " + "(test_name, command) " + "SELECT current_setting('application_name'), '%s'", + command); + + /* insert deparsed statement into table */ + SPI_connect(); + if (SPI_exec(cmd.data, 8) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", cmd.data); + SPI_finish(); + } + } + } + + return PointerGetDatum(NULL); +} + +/* Module load function */ +void +_PG_init(void) +{ + if (strcmp(get_database_name(MyDatabaseId), "deparser_regress") == 0) + return; + + prev_ProcessUtility = ProcessUtility_hook; + ProcessUtility_hook = tdeparser_ProcessUtility; + + prev_ExecutorRun = ExecutorRun_hook; + ExecutorRun_hook = tdeparser_ExecutorRun; + + prev_ExecutorStart = ExecutorStart_hook; + ExecutorStart_hook = tdeparser_ExecutorStart; + + prev_planner_hook = planner_hook; + planner_hook = tdeparser_planner; +} diff --git a/src/test/modules/test_deparser/test_deparser.conf b/src/test/modules/test_deparser/test_deparser.conf new file mode 100644 index 0000000000..35865b5c5a --- /dev/null +++ b/src/test/modules/test_deparser/test_deparser.conf @@ -0,0 +1 @@ +session_preload_libraries = 'test_deparser' diff --git a/src/test/modules/test_deparser/test_deparser.control b/src/test/modules/test_deparser/test_deparser.control new file mode 100644 index 0000000000..c3e9eaf064 --- /dev/null +++ b/src/test/modules/test_deparser/test_deparser.control @@ -0,0 +1,4 @@ +comment = 'Test code for DDL deparse feature' +default_version = '1.0' +module_pathname = '$libdir/test_deparser' +relocatable = true \ No newline at end of file -- 2.30.0.windows.2