From 831283256796d1c20858862b568d73e505eb4a84 Mon Sep 17 00:00:00 2001 From: Stephen Frost Date: Wed, 24 Jul 2013 18:53:27 -0400 Subject: [PATCH] Add GET DIAGNOSTICS ... PG_CONTEXT in PL/PgSQL This adds the ability to get the call stack as a string from within a PL/PgSQL function, which can be handy for logging to a table, or to include in a useful message to an end-user. Pavel Stehule, reviewed by Rushabh Lathia and rather heavily whacked around by Stephen Frost. --- doc/src/sgml/plpgsql.sgml | 57 ++++++++++++++++++++++ src/backend/utils/error/elog.c | 69 +++++++++++++++++++++++++++ src/include/utils/elog.h | 2 + src/pl/plpgsql/src/pl_exec.c | 10 ++++ src/pl/plpgsql/src/pl_funcs.c | 2 + src/pl/plpgsql/src/pl_gram.y | 6 +++ src/pl/plpgsql/src/pl_scanner.c | 1 + src/pl/plpgsql/src/plpgsql.h | 1 + src/test/regress/expected/plpgsql.out | 48 +++++++++++++++++++ src/test/regress/sql/plpgsql.sql | 33 +++++++++++++ 10 files changed, 229 insertions(+) diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 6fffec18b70..0fe6bcf470d 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -2613,6 +2613,15 @@ SELECT merge_db(1, 'dennis'); expected. + + + + Getting Diagnostics Information + + + diagnostics + in PL/pgSQL + Obtaining information about an error @@ -2736,6 +2745,54 @@ END; + + + Obtaining the call stack context information + + + + +GET CURRENT DIAGNOSTICS variable = PG_CONTEXT , ... ; + + + + Calling GET DIAGNOSTICS with status + item PG_CONTEXT will return a text string with line(s) of + text describing the call stack. The first row refers to the + current function and currently executing GET DIAGNOSTICS + command. The second and any subsequent rows refer to the calling functions + up the call stack. + + +CREATE OR REPLACE FUNCTION public.outer_func() RETURNS integer AS $$ +BEGIN + RETURN inner_func(); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION public.inner_func() RETURNS integer AS $$ +DECLARE + stack text; +BEGIN + GET DIAGNOSTICS stack = PG_CONTEXT; + RAISE NOTICE e'--- Call Stack ---\n%', stack; + RETURN 1; +END; +$$ LANGUAGE plpgsql; + +SELECT outer_func(); + +NOTICE: --- Call Stack --- +PL/pgSQL function inner_func() line 4 at GET DIAGNOSTICS +PL/pgSQL function outer_func() line 3 at RETURN + outer_func + ------------ + 1 +(1 row) + + + + diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index 706c01eca55..5090ba5d1bb 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -1626,6 +1626,75 @@ pg_re_throw(void) } +/* + * GetErrorContextStack - Return the error context stack + * + * Returns a pstrdup'd string in the caller's context which includes the full + * call stack. It is the caller's responsibility to ensure this string is + * pfree'd (or its context cleaned up) when done. + * + * This information is collected by traversing the error contexts and calling + * each context's callback function, each of which is expected to call + * errcontext() to return a string which can be presented to the user. + */ +char * +GetErrorContextStack(void) +{ + char *result = NULL; + ErrorData *edata; + ErrorContextCallback *econtext; + MemoryContext oldcontext = CurrentMemoryContext; + + /* this function should not be called from an exception handler */ + Assert(recursion_depth == 0); + + /* Check that we have enough room on the stack for ourselves */ + if (++errordata_stack_depth >= ERRORDATA_STACK_SIZE) + { + /* + * Stack not big enough.. Something bad has happened, therefore + * PANIC as we may be in an infinite loop. + */ + errordata_stack_depth = -1; /* make room on stack */ + ereport(PANIC, (errmsg_internal("ERRORDATA_STACK_SIZE exceeded"))); + } + + /* Initialize data for this error frame */ + edata = &errordata[errordata_stack_depth]; + MemSet(edata, 0, sizeof(ErrorData)); + + /* Use ErrorContext as a short lived context for the callbacks */ + MemoryContextSwitchTo(ErrorContext); + + /* + * Call any context callback functions to collect the context information + * into edata->context. + * + * Errors occurring in callback functions should go through the regular + * error handling code which should handle any recursive errors. + */ + for (econtext = error_context_stack; + econtext != NULL; + econtext = econtext->previous) + (*econtext->callback) (econtext->arg); + + MemoryContextSwitchTo(oldcontext); + + /* + * Copy out the string into the caller's context, so we can free our + * error context and reset the error stack. Caller is expected to + * pfree() the result or throw away the context. + */ + if (edata->context) + result = pstrdup(edata->context); + + /* Reset error stack */ + FlushErrorState(); + + return result; +} + + /* * Initialization of error output file */ diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h index 85bd2fdf8a2..add08729ced 100644 --- a/src/include/utils/elog.h +++ b/src/include/utils/elog.h @@ -406,6 +406,8 @@ extern void FlushErrorState(void); extern void ReThrowError(ErrorData *edata) __attribute__((noreturn)); extern void pg_re_throw(void) __attribute__((noreturn)); +extern char *GetErrorContextStack(void); + /* Hook for intercepting messages before they are sent to the server log */ typedef void (*emit_log_hook_type) (ErrorData *edata); extern PGDLLIMPORT emit_log_hook_type emit_log_hook; diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 5b142e3bee6..f74326a53c5 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -1599,6 +1599,16 @@ exec_stmt_getdiag(PLpgSQL_execstate *estate, PLpgSQL_stmt_getdiag *stmt) estate->cur_error->schema_name); break; + case PLPGSQL_GETDIAG_CONTEXT: + { + char *contextstackstr = GetErrorContextStack(); + + exec_assign_c_string(estate, var, contextstackstr); + + pfree(contextstackstr); + } + break; + default: elog(ERROR, "unrecognized diagnostic item kind: %d", diag_item->kind); diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index 87e528fe5bf..d9f1dc8c64c 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -277,6 +277,8 @@ plpgsql_getdiag_kindname(int kind) return "ROW_COUNT"; case PLPGSQL_GETDIAG_RESULT_OID: return "RESULT_OID"; + case PLPGSQL_GETDIAG_CONTEXT: + return "PG_CONTEXT"; case PLPGSQL_GETDIAG_ERROR_CONTEXT: return "PG_EXCEPTION_CONTEXT"; case PLPGSQL_GETDIAG_ERROR_DETAIL: diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index 086987a58a4..263abeff44b 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -303,6 +303,7 @@ static List *read_raise_options(void); %token K_OPTION %token K_OR %token K_PERFORM +%token K_PG_CONTEXT %token K_PG_DATATYPE_NAME %token K_PG_EXCEPTION_CONTEXT %token K_PG_EXCEPTION_DETAIL @@ -894,6 +895,7 @@ stmt_getdiag : K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';' /* these fields are disallowed in stacked case */ case PLPGSQL_GETDIAG_ROW_COUNT: case PLPGSQL_GETDIAG_RESULT_OID: + case PLPGSQL_GETDIAG_CONTEXT: if (new->is_stacked) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -976,6 +978,9 @@ getdiag_item : else if (tok_is_keyword(tok, &yylval, K_RESULT_OID, "result_oid")) $$ = PLPGSQL_GETDIAG_RESULT_OID; + else if (tok_is_keyword(tok, &yylval, + K_PG_CONTEXT, "pg_context")) + $$ = PLPGSQL_GETDIAG_CONTEXT; else if (tok_is_keyword(tok, &yylval, K_PG_EXCEPTION_DETAIL, "pg_exception_detail")) $$ = PLPGSQL_GETDIAG_ERROR_DETAIL; @@ -2287,6 +2292,7 @@ unreserved_keyword : | K_NO | K_NOTICE | K_OPTION + | K_PG_CONTEXT | K_PG_DATATYPE_NAME | K_PG_EXCEPTION_CONTEXT | K_PG_EXCEPTION_DETAIL diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c index 84c51260d25..35771c2595e 100644 --- a/src/pl/plpgsql/src/pl_scanner.c +++ b/src/pl/plpgsql/src/pl_scanner.c @@ -135,6 +135,7 @@ static const ScanKeyword unreserved_keywords[] = { PG_KEYWORD("no", K_NO, UNRESERVED_KEYWORD) PG_KEYWORD("notice", K_NOTICE, UNRESERVED_KEYWORD) PG_KEYWORD("option", K_OPTION, UNRESERVED_KEYWORD) + PG_KEYWORD("pg_context", K_PG_CONTEXT, UNRESERVED_KEYWORD) PG_KEYWORD("pg_datatype_name", K_PG_DATATYPE_NAME, UNRESERVED_KEYWORD) PG_KEYWORD("pg_exception_context", K_PG_EXCEPTION_CONTEXT, UNRESERVED_KEYWORD) PG_KEYWORD("pg_exception_detail", K_PG_EXCEPTION_DETAIL, UNRESERVED_KEYWORD) diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index cdf39929e0f..d49e0b00217 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -124,6 +124,7 @@ enum { PLPGSQL_GETDIAG_ROW_COUNT, PLPGSQL_GETDIAG_RESULT_OID, + PLPGSQL_GETDIAG_CONTEXT, PLPGSQL_GETDIAG_ERROR_CONTEXT, PLPGSQL_GETDIAG_ERROR_DETAIL, PLPGSQL_GETDIAG_ERROR_HINT, diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 7feea0ac476..4394a3a1a7a 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -4897,3 +4897,51 @@ ERROR: value for domain orderedarray violates check constraint "sorted" CONTEXT: PL/pgSQL function testoa(integer,integer,integer) line 5 at assignment drop function arrayassign1(); drop function testoa(x1 int, x2 int, x3 int); +-- access to call stack +create function inner_func(int) +returns int as $$ +declare _context text; +begin + get diagnostics _context = pg_context; + raise notice '***%***', _context; + return 2 * $1; +end; +$$ language plpgsql; +create or replace function outer_func(int) +returns int as $$ +begin + return inner_func($1); +end; +$$ language plpgsql; +create or replace function outer_outer_func(int) +returns int as $$ +begin + return outer_func($1); +end; +$$ language plpgsql; +select outer_outer_func(10); +NOTICE: ***PL/pgSQL function inner_func(integer) line 4 at GET DIAGNOSTICS +PL/pgSQL function outer_func(integer) line 3 at RETURN +PL/pgSQL function outer_outer_func(integer) line 3 at RETURN*** +CONTEXT: PL/pgSQL function outer_func(integer) line 3 at RETURN +PL/pgSQL function outer_outer_func(integer) line 3 at RETURN + outer_outer_func +------------------ + 20 +(1 row) + +-- repeated call should to work +select outer_outer_func(20); +NOTICE: ***PL/pgSQL function inner_func(integer) line 4 at GET DIAGNOSTICS +PL/pgSQL function outer_func(integer) line 3 at RETURN +PL/pgSQL function outer_outer_func(integer) line 3 at RETURN*** +CONTEXT: PL/pgSQL function outer_func(integer) line 3 at RETURN +PL/pgSQL function outer_outer_func(integer) line 3 at RETURN + outer_outer_func +------------------ + 40 +(1 row) + +drop function outer_outer_func(int); +drop function outer_func(int); +drop function inner_func(int); diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index 0f8465faa47..b59715267e6 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -3880,3 +3880,36 @@ select testoa(1,2,1); -- fail at update drop function arrayassign1(); drop function testoa(x1 int, x2 int, x3 int); + +-- access to call stack +create function inner_func(int) +returns int as $$ +declare _context text; +begin + get diagnostics _context = pg_context; + raise notice '***%***', _context; + return 2 * $1; +end; +$$ language plpgsql; + +create or replace function outer_func(int) +returns int as $$ +begin + return inner_func($1); +end; +$$ language plpgsql; + +create or replace function outer_outer_func(int) +returns int as $$ +begin + return outer_func($1); +end; +$$ language plpgsql; + +select outer_outer_func(10); +-- repeated call should to work +select outer_outer_func(20); + +drop function outer_outer_func(int); +drop function outer_func(int); +drop function inner_func(int); -- 2.30.2