plpgsql_check_function - rebase for 9.3
Hello
I am sending lightly refreshed patch for checking plpgsql functions..
I checked different implementation, but without success: a) enhancing
of SPI to some fake mode can has negative impact on application, and
patch was not clear, b) generic plpgsql walker doesn't save lines too.
I invite any ideas how to improve this patch
Regards
Pavel
Attachments:
plpgsql_check_function-2012-06-26.diffapplication/octet-stream; name=plpgsql_check_function-2012-06-26.diffDownload
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index ba2c57b..a2157b7 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -4296,6 +4296,24 @@ $$ LANGUAGE plpgsql;
provide convenient features such as escaping single quotes and
making it easier to recreate and debug functions.
</para>
+
+ <sect2 id="plpgsql-check-function">
+ <title>Checking of embedded SQL</title>
+ <para>
+ The SQL statements inside <application>PL/pgSQL</> functions are
+ checked by validator for semantic errors. These errors
+ can be found by <function>plpgsql_check_function</function>:
+<programlisting>
+postgres=# select plpgsql_check_function('fx(int)');
+ plpgsql_check_function
+------------------------------------------------
+ error:42703:3:RETURN:column "b" does not exist
+ Query: SELECT (select a from t1 where b < _a)
+ -- ^
+ (3 rows)
+</programlisting>
+ </para>
+ </sect2>
<sect2 id="plpgsql-quote-tips">
<title>Handling of Quotation Marks</title>
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index e3fef84..cc9b479 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -17,9 +17,10 @@ override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS)
SHLIB_LINK = $(filter -lintl, $(LIBS))
rpath =
-OBJS = pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o
+OBJS = pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o pl_check.o
+
+DATA = plpgsql.control plpgsql--1.0.sql plpgsql--1.1.sql plpgsql--unpackaged--1.0.sql plpgsql--1.0--1.1.sql
-DATA = plpgsql.control plpgsql--1.0.sql plpgsql--unpackaged--1.0.sql
all: all-lib
@@ -52,7 +53,7 @@ uninstall-headers:
# Force these dependencies to be known even without dependency info built:
-pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o: plpgsql.h pl_gram.h plerrcodes.h
+pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o pl_check.o : plpgsql.h pl_gram.h plerrcodes.h
# See notes in src/backend/parser/Makefile about the following two rules
diff --git a/src/pl/plpgsql/src/pl_check.c b/src/pl/plpgsql/src/pl_check.c
new file mode 100644
index 0000000..46bdeb4
--- /dev/null
+++ b/src/pl/plpgsql/src/pl_check.c
@@ -0,0 +1,1772 @@
+/*-------------------------------------------------------------------------
+ *
+ * pl_checker.c - Checker for the PL/pgSQL
+ * procedural language
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/pl/plpgsql/src/pl_check.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "plpgsql.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "executor/spi_priv.h"
+#include "mb/pg_wchar.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+#include "utils/rel.h"
+#include "utils/xml.h"
+
+static void check_row_or_rec(PLpgSQL_checkstate *cstate, PLpgSQL_row *row, PLpgSQL_rec *rec);
+static void check_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr);
+static void assign_tupdesc_row_or_rec(PLpgSQL_checkstate *cstate,
+ PLpgSQL_row *row, PLpgSQL_rec *rec,
+ TupleDesc tupdesc);
+static void assign_tupdesc_dno(PLpgSQL_checkstate *cstate, int varno, TupleDesc tupdesc);
+static TupleDesc expr_get_desc(PLpgSQL_checkstate *cstate,
+ PLpgSQL_expr *query,
+ bool use_element_type,
+ bool expand_record,
+ bool is_expression);
+static void init_datum(PLpgSQL_checkstate *cstate, int varno);
+static void check_stmts(PLpgSQL_checkstate *cstate, List *stmts);
+static void check_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt);
+static void prepare_expr(PLpgSQL_checkstate *cstate,
+ PLpgSQL_expr *expr, int cursorOptions);
+static void check_assignment(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
+ PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
+ int targetdno);
+static void check_element_assignment(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
+ PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
+ int targetdno);
+static void check_assignment_guts(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
+ PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
+ int targetdno, bool use_element_type, bool is_expression);
+static void checker_error_edata(PLpgSQL_checkstate *cstate, ErrorData *edata);
+
+static void checker_error(PLpgSQL_checkstate *cstate,
+ int sqlerrcode,
+ const char *message, const char *detail,
+ const char *hint,const char *level,
+ int position, const char *query,
+ const char *context);
+
+static void cstate_setup(PLpgSQL_checkstate *cstate,
+ TupleDesc tupdesc, Tuplestorestate *tupstore,
+ bool fatal_errors, int format);
+static void cstate_flush(PLpgSQL_checkstate *cstate);
+static void destroy_cstate(PLpgSQL_checkstate *cstate);
+static void function_check(PLpgSQL_function *func, FunctionCallInfo fcinfo,
+ PLpgSQL_execstate *estate, PLpgSQL_checkstate *cstate);
+static void trigger_check(PLpgSQL_function *func,
+ TriggerData *trigdata,
+ PLpgSQL_execstate *estate, PLpgSQL_checkstate *cstate);
+static int load_configuration(HeapTuple procTuple, bool *reload_config);
+
+/*
+ * top checker function
+ *
+ */
+void
+plpgsql_function_check(HeapTuple procTuple, Oid relid,
+ TupleDesc tupdesc,
+ Tuplestorestate *tupstore,
+ bool fatal_errors, int format)
+{
+ PLpgSQL_checkstate cstate;
+ PLpgSQL_function * volatile function = NULL;
+ int save_nestlevel = 0;
+ bool reload_config;
+ Oid funcoid;
+ FunctionCallInfoData fake_fcinfo;
+ FmgrInfo flinfo;
+ TriggerData trigdata;
+ int rc;
+ ResourceOwner oldowner;
+ MemoryContext oldCxt;
+ PLpgSQL_execstate *cur_estate = NULL;
+ int numargs;
+ Oid *argtypes;
+ char **argnames;
+ char *argmodes;
+ int i;
+
+ funcoid = HeapTupleGetOid(procTuple);
+
+ /*
+ * Connect to SPI manager
+ */
+ if ((rc = SPI_connect()) != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed: %s", SPI_result_code_string(rc));
+
+ plpgsql_setup_fake_fcinfo(&flinfo, &fake_fcinfo, &trigdata, funcoid, OidIsValid(relid));
+ cstate_setup(&cstate, tupdesc, tupstore, fatal_errors, format);
+
+ if (OidIsValid(relid))
+ trigdata.tg_relation = relation_open(relid, AccessShareLock);
+
+ numargs = get_func_arg_info(procTuple,
+ &argtypes, &argnames, &argmodes);
+
+ if (argnames != NULL)
+ {
+ for (i = 0; i < numargs; i++)
+ {
+ if (argnames[i][0] != '\0')
+ cstate.argnames = lappend(cstate.argnames, argnames[i]);
+ }
+ }
+
+ oldCxt = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PG_TRY();
+ {
+ PLpgSQL_execstate estate;
+
+ BeginInternalSubTransaction(NULL);
+ MemoryContextSwitchTo(oldCxt);
+
+ save_nestlevel = load_configuration(procTuple, &reload_config);
+
+ /* Get a compiled function */
+ function = plpgsql_compile(&fake_fcinfo, false);
+
+ plpgsql_estate_setup(&estate, function, (ReturnSetInfo *) fake_fcinfo.resultinfo);
+ cstate.estate = &estate;
+
+ /* Must save and restore prior value of cur_estate */
+ cur_estate = function->cur_estate;
+
+ /* Mark the function as busy, so it can't be deleted from under us */
+ function->use_count++;
+
+ /* Create a fake runtime environment and process check */
+ if (!OidIsValid(relid))
+ function_check(function, &fake_fcinfo, &estate, &cstate);
+ else
+ trigger_check(function, &trigdata, &estate, &cstate);
+
+ /* reload back a GUC. XXX: isn't this done automatically by subxact rollback? */
+ if (reload_config)
+ AtEOXact_GUC(true, save_nestlevel);
+
+ plpgsql_destroy_econtext(&estate);
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ SPI_restore_connection();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ MemoryContextSwitchTo(oldCxt);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ checker_error_edata(&cstate, edata);
+ MemoryContextSwitchTo(oldCxt);
+ /* reconnect spi */
+ SPI_restore_connection();
+ }
+ PG_END_TRY();
+
+ if (function)
+ {
+ function->cur_estate = cur_estate;
+ function->use_count--;
+
+ /*
+ * We cannot to preserve instance of this function, because
+ * expressions are not consistent - a tests on simple expression
+ * was be processed newer.
+ */
+ plpgsql_delete_function(function);
+ }
+
+ if (OidIsValid(relid))
+ relation_close(trigdata.tg_relation, AccessShareLock);
+
+ cstate_flush(&cstate);
+
+ /* Cleanup temporary memory */
+ destroy_cstate(&cstate);
+
+ /*
+ * Disconnect from SPI manager
+ */
+ if ((rc = SPI_finish()) != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed: %s", SPI_result_code_string(rc));
+}
+
+/*
+ * Check function - it prepare variables and starts a prepare plan walker
+ */
+static void
+function_check(PLpgSQL_function *func, FunctionCallInfo fcinfo,
+ PLpgSQL_execstate *estate, PLpgSQL_checkstate *cstate)
+{
+ int i;
+
+ /*
+ * Make local execution copies of all the datums
+ */
+ for (i = 0; i < cstate->estate->ndatums; i++)
+ cstate->estate->datums[i] = copy_plpgsql_datum(func->datums[i]);
+
+ /*
+ * Store the actual call argument values (fake) into the appropriate variables
+ */
+ for (i = 0; i < func->fn_nargs; i++)
+ {
+ init_datum(cstate, func->fn_argvarnos[i]);
+ }
+
+ /*
+ * Now check the toplevel block of statements
+ */
+ check_stmt(cstate, (PLpgSQL_stmt *) func->action);
+}
+
+/*
+ * Check trigger - prepare fake environments for testing trigger
+ *
+ */
+static void
+trigger_check(PLpgSQL_function *func, TriggerData *trigdata,
+ PLpgSQL_execstate *estate, PLpgSQL_checkstate *cstate)
+{
+ PLpgSQL_rec *rec_new,
+ *rec_old;
+ int i;
+
+ /*
+ * Make local execution copies of all the datums
+ */
+ for (i = 0; i < cstate->estate->ndatums; i++)
+ cstate->estate->datums[i] = copy_plpgsql_datum(func->datums[i]);
+
+ /*
+ * Put the OLD and NEW tuples into record variables
+ *
+ * We make the tupdescs available in both records even though only one may
+ * have a value. This allows parsing of record references to succeed in
+ * functions that are used for multiple trigger types. For example, we
+ * might have a test like "if (TG_OP = 'INSERT' and NEW.foo = 'xyz')",
+ * which should parse regardless of the current trigger type.
+ */
+ rec_new = (PLpgSQL_rec *) (cstate->estate->datums[func->new_varno]);
+ rec_new->freetup = false;
+ rec_new->freetupdesc = false;
+ assign_tupdesc_row_or_rec(cstate, NULL, rec_new, trigdata->tg_relation->rd_att);
+
+ rec_old = (PLpgSQL_rec *) (cstate->estate->datums[func->old_varno]);
+ rec_old->freetup = false;
+ rec_old->freetupdesc = false;
+ assign_tupdesc_row_or_rec(cstate, NULL, rec_old, trigdata->tg_relation->rd_att);
+
+ /*
+ * Assign the special tg_ variables
+ */
+ init_datum(cstate, func->tg_op_varno);
+ init_datum(cstate, func->tg_name_varno);
+ init_datum(cstate, func->tg_when_varno);
+ init_datum(cstate, func->tg_level_varno);
+ init_datum(cstate, func->tg_relid_varno);
+ init_datum(cstate, func->tg_relname_varno);
+ init_datum(cstate, func->tg_table_name_varno);
+ init_datum(cstate, func->tg_table_schema_varno);
+ init_datum(cstate, func->tg_nargs_varno);
+ init_datum(cstate, func->tg_argv_varno);
+
+ /*
+ * Now check the toplevel block of statements
+ */
+ check_stmt(cstate, (PLpgSQL_stmt *) func->action);
+}
+
+/*
+ * Verify lvalue
+ * It doesn't repeat a checks that are done.
+ * Checks a subscript expressions, verify a validity of record's fields,
+ * Returns true, when target is valid
+ */
+static void
+check_target(PLpgSQL_checkstate *cstate, int varno)
+{
+ PLpgSQL_datum *target = cstate->estate->datums[varno];
+
+ switch (target->dtype)
+ {
+ case PLPGSQL_DTYPE_VAR:
+ case PLPGSQL_DTYPE_REC:
+ /* nothing to check */
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
+ check_row_or_rec(cstate, (PLpgSQL_row *) target, NULL);
+ break;
+
+ case PLPGSQL_DTYPE_RECFIELD:
+ {
+ PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) target;
+ PLpgSQL_rec *rec;
+ int fno;
+
+ rec = (PLpgSQL_rec *) (cstate->estate->datums[recfield->recparentno]);
+
+ /*
+ * Check that there is already a tuple in the record. We need
+ * that because records don't have any predefined field
+ * structure.
+ */
+ if (!HeapTupleIsValid(rec->tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("record \"%s\" is not assigned to tuple structure",
+ rec->refname)));
+
+ /*
+ * Get the number of the records field to change and the
+ * number of attributes in the tuple. Note: disallow system
+ * column names because the code below won't cope.
+ */
+ fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
+ if (fno <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("record \"%s\" has no field \"%s\"",
+ rec->refname, recfield->fieldname)));
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ARRAYELEM:
+ {
+ /*
+ * Target is an element of an array
+ */
+ int nsubscripts;
+ Oid arrayelemtypeid;
+ Oid arraytypeid;
+
+ /*
+ * To handle constructs like x[1][2] := something, we have to
+ * be prepared to deal with a chain of arrayelem datums. Chase
+ * back to find the base array datum, and save the subscript
+ * expressions as we go. (We are scanning right to left here,
+ * but want to evaluate the subscripts left-to-right to
+ * minimize surprises.)
+ */
+ nsubscripts = 0;
+ do
+ {
+ PLpgSQL_arrayelem *arrayelem = (PLpgSQL_arrayelem *) target;
+
+ if (nsubscripts++ >= MAXDIM)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("number of array dimensions (%d) exceeds the maximum allowed (%d)",
+ nsubscripts + 1, MAXDIM)));
+
+ /* Validate expression. */
+ /* XXX is_expression */
+ check_expr(cstate, arrayelem->subscript);
+
+ target = cstate->estate->datums[arrayelem->arrayparentno];
+ } while (target->dtype == PLPGSQL_DTYPE_ARRAYELEM);
+
+ /* If target is domain over array, reduce to base type */
+ arraytypeid = exec_get_datum_type(cstate->estate, target);
+ arraytypeid = getBaseType(arraytypeid);
+
+ arrayelemtypeid = get_element_type(arraytypeid);
+
+ if (!OidIsValid(arrayelemtypeid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("subscripted object is not an array")));
+ }
+ break;
+ }
+}
+
+/*
+ * Check composed lvalue
+ * There is nothing to check on rec variables
+ */
+static void
+check_row_or_rec(PLpgSQL_checkstate *cstate, PLpgSQL_row *row, PLpgSQL_rec *rec)
+{
+ int fnum;
+
+ /* there are nothing to check on rec now */
+ if (row != NULL)
+ {
+ for (fnum = 0; fnum < row->nfields; fnum++)
+ {
+ /* skip dropped columns */
+ if (row->varnos[fnum] < 0)
+ continue;
+
+ check_target(cstate, row->varnos[fnum]);
+ }
+ }
+}
+
+/*
+ * Generate a prepared plan - this is simplified copy from pl_exec.c
+ * Is not necessary to check simple plan,
+ * returns true, when expression is succesfully prepared.
+ */
+static void
+prepare_expr(PLpgSQL_checkstate *cstate,
+ PLpgSQL_expr *expr, int cursorOptions)
+{
+ SPIPlanPtr plan;
+
+ if (expr->plan != NULL)
+ return; /* already checked */
+
+ /*
+ * The grammar can't conveniently set expr->func while building the parse
+ * tree, so make sure it's set before parser hooks need it.
+ */
+ expr->func = cstate->estate->func;
+
+ /*
+ * Generate and save the plan
+ */
+ plan = SPI_prepare_params(expr->query,
+ (ParserSetupHook) plpgsql_parser_setup,
+ (void *) expr,
+ cursorOptions);
+
+ if (plan == NULL)
+ {
+ /* Some SPI errors deserve specific error messages */
+ switch (SPI_result)
+ {
+ case SPI_ERROR_COPY:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot COPY to/from client in PL/pgSQL")));
+ break;
+
+ case SPI_ERROR_TRANSACTION:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot begin/end transactions in PL/pgSQL"),
+ errhint("Use a BEGIN block with an EXCEPTION clause instead.")));
+ break;
+
+ default:
+ elog(ERROR, "SPI_prepare_params failed for \"%s\": %s",
+ expr->query, SPI_result_code_string(SPI_result));
+ }
+ }
+
+ expr->plan = SPI_saveplan(plan);
+ SPI_freeplan(plan);
+}
+
+/*
+ * Verify a expression
+ */
+static void
+check_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr)
+{
+ if (expr)
+ check_assignment_guts(cstate, expr, NULL, NULL, -1, false, false);
+}
+
+/*
+ * Verify an assignment of 'expr' to 'target'
+ */
+static void
+check_assignment(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
+ PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
+ int targetdno)
+{
+ bool is_expression = (targetrec == NULL && targetrow == NULL);
+
+ check_assignment_guts(cstate, expr, targetrec, targetrow, targetdno, false,
+ is_expression);
+}
+
+static void
+check_element_assignment(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
+ PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
+ int targetdno)
+{
+ bool is_expression = (targetrec == NULL && targetrow == NULL);
+
+ check_assignment_guts(cstate, expr, targetrec, targetrow, targetdno, true,
+ is_expression);
+}
+
+static void
+check_assignment_guts(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
+ PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
+ int targetdno, bool use_element_type, bool is_expression)
+{
+ ResourceOwner oldowner;
+ MemoryContext oldCxt = CurrentMemoryContext;
+ TupleDesc tupdesc;
+
+ oldowner = CurrentResourceOwner;
+ BeginInternalSubTransaction(NULL);
+ MemoryContextSwitchTo(oldCxt);
+
+ PG_TRY();
+ {
+ prepare_expr(cstate, expr, 0);
+ tupdesc = expr_get_desc(cstate, expr, use_element_type, true, is_expression);
+ if (tupdesc)
+ {
+ if (targetrow != NULL || targetrec != NULL)
+ assign_tupdesc_row_or_rec(cstate, targetrow, targetrec, tupdesc);
+ if (targetdno != -1)
+ assign_tupdesc_dno(cstate, targetdno, tupdesc);
+
+ if (targetrow)
+ {
+ if (targetrow->nfields > tupdesc->natts)
+ checker_error(cstate,
+ 0,
+ "too few attributies for target variables",
+ "There are more target variables than output columns in query.",
+ "Check target variables in SELECT INTO statement.",
+ "warning",
+ 0, NULL, NULL);
+ else if (targetrow->nfields < tupdesc->natts)
+ checker_error(cstate,
+ 0,
+ "too many attributies for target variables",
+ "There are less target variables than output columns in query.",
+ "Check target variables in SELECT INTO statement",
+ "warning",
+ 0, NULL, NULL);
+ }
+
+ ReleaseTupleDesc(tupdesc);
+ }
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ SPI_restore_connection();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ MemoryContextSwitchTo(oldCxt);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ /*
+ * If fatal_errors is true, we just propagate the error up to the
+ * highest level. Otherwise the error is appended to our current
+ * list of errors, and we continue checking.
+ */
+ if (cstate->fatal_errors)
+ ReThrowError(edata);
+ else
+ checker_error_edata(cstate, edata);
+ MemoryContextSwitchTo(oldCxt);
+
+ /* reconnect spi */
+ SPI_restore_connection();
+ }
+ PG_END_TRY();
+}
+
+/*
+ * We have to assign TupleDesc to all used record variables step by step.
+ * We would to use a exec routines for query preprocessing, so we must
+ * to create a typed NULL value, and this value is assigned to record
+ * variable.
+ */
+static void
+assign_tupdesc_row_or_rec(PLpgSQL_checkstate *cstate,
+ PLpgSQL_row *row, PLpgSQL_rec *rec,
+ TupleDesc tupdesc)
+{
+ bool *nulls;
+ HeapTuple tup;
+
+ if (tupdesc == NULL)
+ {
+ checker_error(cstate,
+ 0,
+ "tuple descriptor is empty", NULL, NULL,
+ "warning",
+ 0, NULL, NULL);
+ return;
+ }
+
+ /*
+ * row variable has assigned TupleDesc already, so don't be processed
+ * here
+ */
+ if (rec != NULL)
+ {
+ PLpgSQL_rec *target = (PLpgSQL_rec *)(cstate->estate->datums[rec->dno]);
+
+ if (target->freetup)
+ heap_freetuple(target->tup);
+
+ if (rec->freetupdesc)
+ FreeTupleDesc(target->tupdesc);
+
+ /* initialize rec by NULLs */
+ nulls = (bool *) palloc(tupdesc->natts * sizeof(bool));
+ memset(nulls, true, tupdesc->natts * sizeof(bool));
+
+ target->tupdesc = CreateTupleDescCopy(tupdesc);
+ target->freetupdesc = true;
+
+ tup = heap_form_tuple(tupdesc, NULL, nulls);
+ if (HeapTupleIsValid(tup))
+ {
+ target->tup = tup;
+ target->freetup = true;
+ }
+ else
+ elog(ERROR, "cannot to build valid composite value");
+ }
+}
+
+/*
+ * Assign a tuple descriptor to variable specified by dno
+ */
+static void
+assign_tupdesc_dno(PLpgSQL_checkstate *cstate, int varno, TupleDesc tupdesc)
+{
+ PLpgSQL_datum *target = cstate->estate->datums[varno];
+
+ if (target->dtype == PLPGSQL_DTYPE_REC)
+ assign_tupdesc_row_or_rec(cstate, NULL, (PLpgSQL_rec *) target, tupdesc);
+}
+
+/*
+ * Returns a tuple descriptor based on existing plan,
+ * When error is detected returns null.
+ */
+static TupleDesc
+expr_get_desc(PLpgSQL_checkstate *cstate,
+ PLpgSQL_expr *query,
+ bool use_element_type,
+ bool expand_record,
+ bool is_expression)
+{
+ TupleDesc tupdesc = NULL;
+ CachedPlanSource *plansource = NULL;
+
+ if (query->plan != NULL)
+ {
+ SPIPlanPtr plan = query->plan;
+
+ if (plan == NULL || plan->magic != _SPI_PLAN_MAGIC)
+ elog(ERROR, "cached plan is not valid plan");
+
+ if (list_length(plan->plancache_list) != 1)
+ elog(ERROR, "plan is not single execution plan");
+
+ plansource = (CachedPlanSource *) linitial(plan->plancache_list);
+
+ if (!plansource->resultDesc)
+ {
+ if (is_expression)
+ elog(ERROR, "query returns no result");
+ else
+ return NULL;
+ }
+ tupdesc = CreateTupleDescCopy(plansource->resultDesc);
+ }
+ else
+ elog(ERROR, "there are no plan for query: \"%s\"",
+ query->query);
+
+ /*
+ * try to get a element type, when result is a array (used with FOREACH ARRAY stmt)
+ */
+ if (use_element_type)
+ {
+ Oid elemtype;
+ TupleDesc elemtupdesc;
+
+ /* result should be a array */
+ if (is_expression && tupdesc->natts != 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("query \"%s\" returned %d columns",
+ query->query,
+ tupdesc->natts)));
+
+ /* check the type of the expression - must be an array */
+ elemtype = get_element_type(tupdesc->attrs[0]->atttypid);
+ if (!OidIsValid(elemtype))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH expression must yield an array, not type %s",
+ format_type_be(tupdesc->attrs[0]->atttypid))));
+ FreeTupleDesc(tupdesc);
+ }
+
+ /* we can't know typmod now */
+ elemtupdesc = lookup_rowtype_tupdesc_noerror(elemtype, -1, true);
+ if (elemtupdesc != NULL)
+ {
+ FreeTupleDesc(tupdesc);
+ tupdesc = CreateTupleDescCopy(elemtupdesc);
+ ReleaseTupleDesc(elemtupdesc);
+ }
+ else
+ /* XXX: should be a warning? */
+ ereport(ERROR,
+ (errmsg("cannot to identify real type for record type variable")));
+ }
+
+ if (is_expression && tupdesc->natts != 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("query \"%s\" returned %d columns",
+ query->query,
+ tupdesc->natts)));
+
+ /*
+ * One spacial case is when record is assigned to composite type, then
+ * we should to unpack composite type.
+ */
+ if (tupdesc->tdtypeid == RECORDOID &&
+ tupdesc->tdtypmod == -1 &&
+ tupdesc->natts == 1 && expand_record)
+ {
+ TupleDesc unpack_tupdesc;
+
+ unpack_tupdesc = lookup_rowtype_tupdesc_noerror(tupdesc->attrs[0]->atttypid,
+ tupdesc->attrs[0]->atttypmod,
+ true);
+ if (unpack_tupdesc != NULL)
+ {
+ FreeTupleDesc(tupdesc);
+ tupdesc = CreateTupleDescCopy(unpack_tupdesc);
+ ReleaseTupleDesc(unpack_tupdesc);
+ }
+ }
+
+ /*
+ * There is special case, when returned tupdesc contains only
+ * unpined record: rec := func_with_out_parameters(). IN this case
+ * we must to dig more deep - we have to find oid of function and
+ * get their parameters,
+ *
+ * This is support for assign statement
+ * recvar := func_with_out_parameters(..)
+ *
+ * XXX: Why don't we always do that?
+ */
+ if (tupdesc->tdtypeid == RECORDOID &&
+ tupdesc->tdtypmod == -1 &&
+ tupdesc->natts == 1 &&
+ tupdesc->attrs[0]->atttypid == RECORDOID &&
+ tupdesc->attrs[0]->atttypmod == -1 &&
+ expand_record)
+ {
+ PlannedStmt *_stmt;
+ Plan *_plan;
+ TargetEntry *tle;
+ CachedPlan *cplan;
+
+ /*
+ * When tupdesc is related to unpined record, we will try
+ * to check plan if it is just function call and if it is
+ * then we can try to derive a tupledes from function's
+ * description.
+ */
+ cplan = GetCachedPlan(plansource, NULL, true);
+ _stmt = (PlannedStmt *) linitial(cplan->stmt_list);
+
+ if (IsA(_stmt, PlannedStmt) && _stmt->commandType == CMD_SELECT)
+ {
+ _plan = _stmt->planTree;
+ if (IsA(_plan, Result) && list_length(_plan->targetlist) == 1)
+ {
+ tle = (TargetEntry *) linitial(_plan->targetlist);
+ if (((Node *) tle->expr)->type == T_FuncExpr)
+ {
+ FuncExpr *fn = (FuncExpr *) tle->expr;
+ FmgrInfo flinfo;
+ FunctionCallInfoData fcinfo;
+ TupleDesc rd;
+ Oid rt;
+
+ fmgr_info(fn->funcid, &flinfo);
+ flinfo.fn_expr = (Node *) fn;
+ fcinfo.flinfo = &flinfo;
+
+ get_call_result_type(&fcinfo, &rt, &rd);
+ if (rd == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("function does not return composite type, is not possible to identify composite type")));
+
+ FreeTupleDesc(tupdesc);
+ BlessTupleDesc(rd);
+
+ tupdesc = rd;
+ }
+ }
+ }
+
+ ReleaseCachedPlan(cplan, true);
+ }
+
+ return tupdesc;
+}
+
+/*
+ * Ensure check for all statements in list
+ */
+void
+check_stmts(PLpgSQL_checkstate *cstate, List *stmts)
+{
+ ListCell *lc;
+
+ foreach(lc, stmts)
+ {
+ check_stmt(cstate, (PLpgSQL_stmt *) lfirst(lc));
+ }
+}
+
+/*
+ * returns refname of PLpgSQL_datum
+ */
+static char *
+datum_get_refname(PLpgSQL_datum *d)
+{
+ switch (d->dtype)
+ {
+ case PLPGSQL_DTYPE_VAR:
+ return ((PLpgSQL_var *) d)->refname;
+
+ case PLPGSQL_DTYPE_ROW:
+ return ((PLpgSQL_row *) d)->refname;
+
+ case PLPGSQL_DTYPE_REC:
+ return ((PLpgSQL_rec *) d)->refname;
+
+ default:
+ return NULL;
+ }
+}
+
+/*
+ * walk over all statements
+ */
+void
+check_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt)
+{
+ TupleDesc tupdesc = NULL;
+ PLpgSQL_function *func;
+ ListCell *l;
+ ResourceOwner oldowner;
+ MemoryContext oldCxt = CurrentMemoryContext;
+
+ if (stmt == NULL)
+ return;
+
+ cstate->estate->err_stmt = stmt;
+ func = cstate->estate->func;
+
+ oldowner = CurrentResourceOwner;
+ BeginInternalSubTransaction(NULL);
+ MemoryContextSwitchTo(oldCxt);
+
+ PG_TRY();
+ {
+ switch ((enum PLpgSQL_stmt_types) stmt->cmd_type)
+ {
+ case PLPGSQL_STMT_BLOCK:
+ {
+ PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) stmt;
+ int i;
+ PLpgSQL_datum *d;
+
+ for (i = 0; i < stmt_block->n_initvars; i++)
+ {
+ char *refname;
+
+ d = func->datums[stmt_block->initvarnos[i]];
+
+ if (d->dtype == PLPGSQL_DTYPE_VAR)
+ {
+ PLpgSQL_var *var = (PLpgSQL_var *) d;
+
+ check_expr(cstate, var->default_val);
+ }
+
+ refname = datum_get_refname(d);
+ if (refname != NULL)
+ {
+ ListCell *l;
+
+ foreach(l, cstate->argnames)
+ {
+ char *argname = (char *) lfirst(l);
+
+ if (strcmp(argname, refname) == 0)
+ {
+ StringInfoData str;
+
+ initStringInfo(&str);
+ appendStringInfo(&str, "parameter \"%s\" is overlapped",
+ refname);
+
+ checker_error(cstate,
+ 0,
+ str.data,
+ "Local variable overlap function parameter.",
+ NULL,
+ "warning",
+ 0, NULL, NULL);
+ pfree(str.data);
+ }
+ }
+ }
+ }
+
+ check_stmts(cstate, stmt_block->body);
+
+ if (stmt_block->exceptions)
+ {
+ foreach(l, stmt_block->exceptions->exc_list)
+ {
+ check_stmts(cstate, ((PLpgSQL_exception *) lfirst(l))->action);
+ }
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_ASSIGN:
+ {
+ PLpgSQL_stmt_assign *stmt_assign = (PLpgSQL_stmt_assign *) stmt;
+
+ check_target(cstate, stmt_assign->varno);
+
+ /* prepare plan if desn't exist yet */
+ check_assignment(cstate, stmt_assign->expr, NULL, NULL,
+ stmt_assign->varno);
+ /* XXX: i thínk I lost some args to prepare_expr here */
+ }
+ break;
+
+ case PLPGSQL_STMT_IF:
+ {
+ PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt;
+ ListCell *l;
+
+ check_expr(cstate, stmt_if->cond);
+ check_stmts(cstate, stmt_if->then_body);
+ foreach(l, stmt_if->elsif_list)
+ {
+ PLpgSQL_if_elsif *elif = (PLpgSQL_if_elsif *) lfirst(l);
+
+ check_expr(cstate, elif->cond);
+ check_stmts(cstate, elif->stmts);
+ }
+
+ check_stmts(cstate, stmt_if->else_body);
+ }
+ break;
+
+ case PLPGSQL_STMT_CASE:
+ {
+ PLpgSQL_stmt_case *stmt_case = (PLpgSQL_stmt_case *) stmt;
+ Oid result_oid;
+
+ if (stmt_case->t_expr != NULL)
+ {
+ PLpgSQL_var *t_var = (PLpgSQL_var *) cstate->estate->datums[stmt_case->t_varno];
+
+ /* we need to set hidden variable type */
+ prepare_expr(cstate, stmt_case->t_expr, 0);
+ tupdesc = expr_get_desc(cstate,
+ stmt_case->t_expr,
+ false, /* no element type */
+ true, /* expand record */
+ true); /* is expression */
+ result_oid = tupdesc->attrs[0]->atttypid;
+
+ /*
+ * When expected datatype is different from real,
+ * change it. Note that what we're modifying here is
+ * an execution copy of the datum, so this doesn't
+ * affect the originally stored function parse tree.
+ */
+ if (t_var->datatype->typoid != result_oid)
+ t_var->datatype = plpgsql_build_datatype(result_oid,
+ -1,
+ cstate->estate->func->fn_input_collation);
+ ReleaseTupleDesc(tupdesc);
+ }
+
+ foreach(l, stmt_case->case_when_list)
+ {
+ PLpgSQL_case_when *cwt = (PLpgSQL_case_when *) lfirst(l);
+
+ check_expr(cstate, cwt->expr);
+ check_stmts(cstate, cwt->stmts);
+ }
+
+ check_stmts(cstate, stmt_case->else_stmts);
+ }
+ break;
+
+ case PLPGSQL_STMT_LOOP:
+ check_stmts(cstate, ((PLpgSQL_stmt_loop *) stmt)->body);
+ break;
+
+ case PLPGSQL_STMT_WHILE:
+ {
+ PLpgSQL_stmt_while *stmt_while = (PLpgSQL_stmt_while *) stmt;
+
+ check_expr(cstate, stmt_while->cond);
+ check_stmts(cstate, stmt_while->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FORI:
+ {
+ PLpgSQL_stmt_fori *stmt_fori = (PLpgSQL_stmt_fori *) stmt;
+
+ check_expr(cstate, stmt_fori->lower);
+ check_expr(cstate, stmt_fori->upper);
+ check_expr(cstate, stmt_fori->step);
+ check_stmts(cstate, stmt_fori->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FORS:
+ {
+ PLpgSQL_stmt_fors *stmt_fors = (PLpgSQL_stmt_fors *) stmt;
+
+ check_row_or_rec(cstate, stmt_fors->row, stmt_fors->rec);
+
+ /* we need to set hidden variable type */
+ check_assignment(cstate, stmt_fors->query,
+ stmt_fors->rec, stmt_fors->row, -1);
+
+ check_stmts(cstate, stmt_fors->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FORC:
+ {
+ PLpgSQL_stmt_forc *stmt_forc = (PLpgSQL_stmt_forc *) stmt;
+ PLpgSQL_var *var = (PLpgSQL_var *) func->datums[stmt_forc->curvar];
+
+ check_row_or_rec(cstate, stmt_forc->row, stmt_forc->rec);
+
+ check_expr(cstate, stmt_forc->argquery);
+
+ if (var->cursor_explicit_expr != NULL)
+ check_assignment(cstate, var->cursor_explicit_expr,
+ stmt_forc->rec, stmt_forc->row, -1);
+
+ check_stmts(cstate, stmt_forc->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_DYNFORS:
+ {
+ PLpgSQL_stmt_dynfors * stmt_dynfors = (PLpgSQL_stmt_dynfors *) stmt;
+
+ if (stmt_dynfors->rec != NULL)
+ {
+ checker_error(cstate,
+ 0,
+ "cannot determinate a result of dynamic SQL",
+ "Cannot to contine in check.",
+ "Don't use dynamic SQL and record type together, when you would check function.",
+ "warning",
+ 0, NULL, NULL);
+
+ /*
+ * don't continue in checking. Behave should be indeterministic.
+ */
+ break;
+ }
+
+ check_expr(cstate, stmt_dynfors->query);
+
+ foreach(l, stmt_dynfors->params)
+ {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+
+ check_stmts(cstate, stmt_dynfors->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FOREACH_A:
+ {
+ PLpgSQL_stmt_foreach_a *stmt_foreach_a = (PLpgSQL_stmt_foreach_a *) stmt;
+
+ check_target(cstate, stmt_foreach_a->varno);
+
+ check_element_assignment(cstate,stmt_foreach_a->expr, NULL, NULL, stmt_foreach_a->varno);
+
+ check_stmts(cstate, stmt_foreach_a->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_EXIT:
+ check_expr(cstate, ((PLpgSQL_stmt_exit *) stmt)->cond);
+ break;
+
+ case PLPGSQL_STMT_PERFORM:
+ check_expr(cstate, ((PLpgSQL_stmt_perform *) stmt)->expr);
+ break;
+
+ case PLPGSQL_STMT_RETURN:
+ check_expr(cstate, ((PLpgSQL_stmt_return *) stmt)->expr);
+ break;
+
+ case PLPGSQL_STMT_RETURN_NEXT:
+ check_expr(cstate, ((PLpgSQL_stmt_return_next *) stmt)->expr);
+ break;
+
+ case PLPGSQL_STMT_RETURN_QUERY:
+ {
+ PLpgSQL_stmt_return_query *stmt_rq = (PLpgSQL_stmt_return_query *) stmt;
+
+ check_expr(cstate, stmt_rq->dynquery);
+
+ check_expr(cstate, stmt_rq->query);
+
+ foreach(l, stmt_rq->params)
+ {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_RAISE:
+ {
+ PLpgSQL_stmt_raise *stmt_raise = (PLpgSQL_stmt_raise *) stmt;
+ ListCell *current_param;
+ char *cp;
+
+ foreach(l, stmt_raise->params)
+ {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+
+ foreach(l, stmt_raise->options)
+ {
+ PLpgSQL_raise_option *opt = (PLpgSQL_raise_option *) lfirst(l);
+
+ check_expr(cstate, opt->expr);
+ }
+
+ current_param = list_head(stmt_raise->params);
+
+ /* ensure any single % has a own parameter */
+ if (stmt_raise->message != NULL)
+ {
+ for (cp = stmt_raise->message; *cp; cp++)
+ {
+ if (cp[0] == '%')
+ {
+ if (cp[1] == '%')
+ {
+ cp++;
+ continue;
+ }
+
+ if (current_param == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("too few parameters specified for RAISE")));
+
+ current_param = lnext(current_param);
+ }
+ }
+ }
+
+ if (current_param != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("too many parameters specified for RAISE")));
+ }
+ break;
+
+ case PLPGSQL_STMT_EXECSQL:
+ {
+ PLpgSQL_stmt_execsql *stmt_execsql = (PLpgSQL_stmt_execsql *) stmt;
+
+ if (stmt_execsql->into)
+ {
+ check_row_or_rec(cstate, stmt_execsql->row, stmt_execsql->rec);
+ check_assignment(cstate, stmt_execsql->sqlstmt,
+ stmt_execsql->rec, stmt_execsql->row, -1);
+ }
+ else
+ {
+ /* only statement */
+ check_expr(cstate, stmt_execsql->sqlstmt);
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_DYNEXECUTE:
+ {
+ PLpgSQL_stmt_dynexecute *stmt_dynexecute = (PLpgSQL_stmt_dynexecute *) stmt;
+
+ check_expr(cstate, stmt_dynexecute->query);
+
+ foreach(l, stmt_dynexecute->params)
+ {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+
+ if (stmt_dynexecute->into)
+ {
+ check_row_or_rec(cstate, stmt_dynexecute->row, stmt_dynexecute->rec);
+
+ if (stmt_dynexecute->rec != NULL)
+ {
+ checker_error(cstate,
+ 0,
+ "cannot determinate a result of dynamic SQL",
+ "Cannot to contine in check.",
+ "Don't use dynamic SQL and record type together, when you would check function.",
+ "warning",
+ 0, NULL, NULL);
+
+ /*
+ * don't continue in checking. Behave should be indeterministic.
+ */
+ break;
+ }
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_OPEN:
+ {
+ PLpgSQL_stmt_open *stmt_open = (PLpgSQL_stmt_open *) stmt;
+ PLpgSQL_var *var = (PLpgSQL_var *) func->datums[stmt_open->curvar];
+
+ if (var->cursor_explicit_expr)
+ check_expr(cstate, var->cursor_explicit_expr);
+
+ check_expr(cstate, stmt_open->query);
+ check_expr(cstate, stmt_open->argquery);
+ check_expr(cstate, stmt_open->dynquery);
+ foreach(l, stmt_open->params)
+ {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_GETDIAG:
+ {
+ PLpgSQL_stmt_getdiag *stmt_getdiag = (PLpgSQL_stmt_getdiag *) stmt;
+ ListCell *lc;
+
+ foreach(lc, stmt_getdiag->diag_items)
+ {
+ PLpgSQL_diag_item *diag_item = (PLpgSQL_diag_item *) lfirst(lc);
+
+ check_target(cstate, diag_item->target);
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_FETCH:
+ {
+ PLpgSQL_stmt_fetch *stmt_fetch = (PLpgSQL_stmt_fetch *) stmt;
+ PLpgSQL_var *var = (PLpgSQL_var *)(cstate->estate->datums[stmt_fetch->curvar]);
+
+ check_row_or_rec(cstate, stmt_fetch->row, stmt_fetch->rec);
+
+ if (var != NULL && var->cursor_explicit_expr != NULL)
+ check_assignment(cstate, var->cursor_explicit_expr,
+ stmt_fetch->rec, stmt_fetch->row, -1);
+ }
+ break;
+
+ case PLPGSQL_STMT_CLOSE:
+ break;
+
+ default:
+ elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
+ }
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ SPI_restore_connection();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ MemoryContextSwitchTo(oldCxt);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ /*
+ * If fatal_errors is true, we just propagate the error up to the
+ * highest level. Otherwise the error is appended to our current
+ * list of errors, and we continue checking.
+ */
+ if (cstate->fatal_errors)
+ ReThrowError(edata);
+ else
+ checker_error_edata(cstate, edata);
+ MemoryContextSwitchTo(oldCxt);
+
+ /* reconnect spi */
+ SPI_restore_connection();
+ }
+ PG_END_TRY();
+}
+
+/*
+ * Initialize plpgsql datum to NULL. This routine is used only for function and trigger parameters
+ * so it should not support all dtypes.
+ */
+static void
+init_datum(PLpgSQL_checkstate *cstate, int dno)
+{
+ switch (cstate->estate->datums[dno]->dtype)
+ {
+ case PLPGSQL_DTYPE_VAR:
+ {
+ PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[dno];
+
+ var->value = (Datum) 0;
+ var->isnull = true;
+ var->freeval = false;
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) cstate->estate->datums[dno];
+ int fnum;
+
+ for (fnum = 0; fnum < row->nfields; fnum++)
+ {
+ if (row->varnos[fnum] < 0)
+ continue; /* skip dropped column in row struct */
+
+ init_datum(cstate, row->varnos[fnum]);
+ }
+ }
+ break;
+
+ default:
+ elog(ERROR, "unexpected dtype: %d", cstate->estate->datums[dno]->dtype);
+ }
+}
+
+/*
+ * forward edata out from checker
+ */
+static void
+checker_error_edata(PLpgSQL_checkstate *cstate,
+ ErrorData *edata)
+{
+ checker_error(cstate,
+ edata->sqlerrcode,
+ edata->message,
+ edata->detail,
+ edata->hint,
+ "error",
+ edata->internalpos,
+ edata->internalquery,
+ edata->context);
+}
+
+/*
+ * Append text line (StringInfo) to tuple store.
+ */
+static void
+checker_store_string(PLpgSQL_checkstate *cstate, StringInfo str)
+{
+ Datum value;
+ bool isnull = false;
+ HeapTuple tuple;
+
+ value = PointerGetDatum(cstring_to_text_with_len(str->data, str->len));
+ tuple = heap_form_tuple(cstate->tupdesc, &value, &isnull);
+
+ tuplestore_puttuple(cstate->tuple_store, tuple);
+
+ resetStringInfo(str);
+}
+
+/*
+ * prepare PLpgSQL_checkstate structure
+ */
+static void
+cstate_setup(PLpgSQL_checkstate *cstate,
+ TupleDesc tupdesc,
+ Tuplestorestate *tupstore,
+ bool fatal_errors,
+ int format)
+{
+ cstate->estate = NULL;
+ cstate->tupdesc = tupdesc;
+ cstate->tuple_store = tupstore;
+ cstate->fatal_errors = fatal_errors;
+ cstate->format = format;
+ cstate->argnames = NIL;
+
+ if (format != PLPGSQL_CHECK_FORMAT_PLAIN)
+ cstate->sinfo = makeStringInfo();
+ else
+ cstate->sinfo = NULL;
+
+ /* put initial tag */
+ if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
+ appendStringInfoString(cstate->sinfo, "<Function>\n");
+}
+
+/*
+ * finishig a result stored in cstate
+ */
+static void
+cstate_flush(PLpgSQL_checkstate *cstate)
+{
+ if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
+ appendStringInfoString(cstate->sinfo, "</Function>");
+
+ if (cstate->format != PLPGSQL_CHECK_FORMAT_PLAIN)
+ checker_store_string(cstate, cstate->sinfo);
+}
+
+/*
+ * release check state
+ */
+static void
+destroy_cstate(PLpgSQL_checkstate *cstate)
+{
+ if (cstate->sinfo != NULL)
+ {
+ if (cstate->sinfo->data != NULL)
+ pfree(cstate->sinfo->data);
+ pfree(cstate->sinfo);
+
+ cstate->sinfo = NULL;
+ }
+}
+
+/*
+ * collects errors and warnings in plain text format
+ */
+static void
+checker_error_plain(PLpgSQL_checkstate *cstate,
+ int sqlerrcode,
+ const char *message,
+ const char *detail,
+ const char *hint,
+ const char *level,
+ int position,
+ const char *query,
+ const char *context)
+{
+ StringInfoData sinfo;
+
+ initStringInfo(&sinfo);
+
+ Assert(message != NULL);
+ Assert(level != NULL);
+
+ if (cstate->estate && cstate->estate->err_stmt != NULL)
+ appendStringInfo(&sinfo, "%s:%s:%d:%s:%s",
+ level,
+ unpack_sql_state(sqlerrcode),
+ cstate->estate->err_stmt->lineno,
+ plpgsql_stmt_typename(cstate->estate->err_stmt),
+ message);
+ else
+ appendStringInfo(&sinfo, "%s:%s:%s",
+ level,
+ unpack_sql_state(sqlerrcode),
+ message);
+
+ checker_store_string(cstate, &sinfo);
+
+ if (query != NULL)
+ {
+ char *query_line; /* pointer to beginning of
+ * current line */
+ int line_caret_pos;
+ bool is_first_line = true;
+ char *_query = pstrdup(query);
+ char *ptr;
+
+ ptr = _query;
+ query_line = ptr;
+ line_caret_pos = position;
+
+ while (*ptr != '\0')
+ {
+ /* search end of lines and replace '\n' by '\0' */
+ if (*ptr == '\n')
+ {
+ *ptr = '\0';
+ if (is_first_line)
+ {
+ appendStringInfo(&sinfo, "Query: %s", query_line);
+ is_first_line = false;
+ }
+ else
+ appendStringInfo(&sinfo, " %s", query_line);
+
+ checker_store_string(cstate, &sinfo);
+
+ if (line_caret_pos > 0 && position == 0)
+ {
+ appendStringInfo(&sinfo, "-- %*s",
+ line_caret_pos, "^");
+ checker_store_string(cstate, &sinfo);
+ line_caret_pos = 0;
+ }
+
+ /* store caret position offset for next line */
+ if (position > 1)
+ line_caret_pos = position - 1;
+
+ /* go to next line */
+ query_line = ptr + 1;
+ }
+
+ ptr += pg_mblen(ptr);
+
+ if (position > 0)
+ position--;
+ }
+
+ /* flush last line */
+ if (query_line != NULL)
+ {
+ if (is_first_line)
+ appendStringInfo(&sinfo, "Query: %s", query_line);
+ else
+ appendStringInfo(&sinfo, " %s", query_line);
+
+ checker_store_string(cstate, &sinfo);
+
+ if (line_caret_pos > 0 && position == 0)
+ {
+ appendStringInfo(&sinfo, "-- %*s",
+ line_caret_pos, "^");
+ checker_store_string(cstate, &sinfo);
+ }
+ }
+
+ pfree(_query);
+ }
+
+ if (detail != NULL)
+ {
+ appendStringInfo(&sinfo, "Detail: %s", detail);
+ checker_store_string(cstate, &sinfo);
+ }
+
+ if (hint != NULL)
+ {
+ appendStringInfo(&sinfo, "Hint: %s", hint);
+ checker_store_string(cstate, &sinfo);
+ }
+
+ if (context != NULL)
+ {
+ appendStringInfo(&sinfo, "Context: %s", context);
+ checker_store_string(cstate, &sinfo);
+ }
+
+ pfree(sinfo.data);
+}
+
+/*
+ * checker_error_xml formats and collects a identifided issues
+ */
+static void
+checker_error_xml(PLpgSQL_checkstate *cstate,
+ int sqlerrcode,
+ const char *message,
+ const char *detail,
+ const char *hint,
+ const char *level,
+ int position,
+ const char *query,
+ const char *context)
+{
+ Assert(message != NULL);
+ Assert(level != NULL);
+
+ /* there have to be prepared StringInfo for result */
+ Assert(cstate->sinfo != NULL);
+
+ /* flush tag */
+ appendStringInfoString(cstate->sinfo, " <Issue>\n");
+
+ appendStringInfo(cstate->sinfo, " <Level>%s</level>\n", level);
+ appendStringInfo(cstate->sinfo, " <Sqlstate>%s</Sqlstate>\n",
+ unpack_sql_state(sqlerrcode));
+ appendStringInfo(cstate->sinfo, " <Message>%s</Message>\n",
+ escape_xml(message));
+ if (cstate->estate->err_stmt != NULL)
+ appendStringInfo(cstate->sinfo, " <Stmt lineno=\"%d\">%s</Stmt>\n",
+ cstate->estate->err_stmt->lineno,
+ plpgsql_stmt_typename(cstate->estate->err_stmt));
+ if (hint != NULL)
+ appendStringInfo(cstate->sinfo, " <Hint>%s</Hint>\n",
+ escape_xml(hint));
+ if (detail != NULL)
+ appendStringInfo(cstate->sinfo, " <Detail>%s</Detail>\n",
+ escape_xml(detail));
+ if(query != NULL)
+ appendStringInfo(cstate->sinfo, " <Query position=\"%d\">%s</Query>\n",
+ position, escape_xml(query));
+ if (context != NULL)
+ appendStringInfo(cstate->sinfo, " <Context>%s</Context>\n",
+ escape_xml(context));
+
+ /* flush closing tag */
+ appendStringInfoString(cstate->sinfo, " </Issue>\n");
+}
+
+/*
+ * checker_error formats and collects a identifided issues
+ */
+static void
+checker_error(PLpgSQL_checkstate *cstate,
+ int sqlerrcode,
+ const char *message,
+ const char *detail,
+ const char *hint,
+ const char *level,
+ int position,
+ const char *query,
+ const char *context)
+{
+ if (cstate->format == PLPGSQL_CHECK_FORMAT_PLAIN)
+ checker_error_plain(cstate, sqlerrcode,
+ message, detail, hint, level,
+ position, query,
+ context);
+ else if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
+ checker_error_xml(cstate, sqlerrcode,
+ message, detail, hint, level,
+ position, query,
+ context);
+}
+
+/*
+ * Loads function's configuration
+ *
+ * Before checking function we have to load configuration related to function.
+ * This is function manager job, but we don't use it for checking.
+ */
+static int
+load_configuration(HeapTuple procTuple, bool *reload_config)
+{
+ Datum datum;
+ bool isnull;
+ int new_nest_level;
+
+ *reload_config = false;
+ new_nest_level = 0;
+
+ datum = SysCacheGetAttr(PROCOID, procTuple, Anum_pg_proc_proconfig, &isnull);
+ if (!isnull)
+ {
+ ArrayType *set_items;
+
+ /* Set per-function configuration parameters */
+ set_items = DatumGetArrayTypeP(datum);
+
+ if (set_items != NULL) /* Need a new GUC nesting level */
+ {
+ new_nest_level = NewGUCNestLevel();
+ *reload_config = true;
+ ProcessGUCArray(set_items,
+ (superuser() ? PGC_SUSET : PGC_USERSET),
+ PGC_S_SESSION,
+ GUC_ACTION_SAVE);
+ }
+ }
+
+ return new_nest_level;
+}
+
+/*
+ * Set up a fake fcinfo with just enough info to satisfy
+ * plpgsql_compile().
+ *
+ * there should be a different real argtypes for polymorphic params
+ */
+void
+plpgsql_setup_fake_fcinfo(FmgrInfo *flinfo,
+ FunctionCallInfoData *fcinfo,
+ TriggerData *trigdata,
+ Oid funcoid, bool istrigger)
+{
+ /* clean structures */
+ MemSet(fcinfo, 0, sizeof(FunctionCallInfoData));
+ MemSet(flinfo, 0, sizeof(FmgrInfo));
+
+ fcinfo->flinfo = flinfo;
+ flinfo->fn_oid = funcoid;
+ flinfo->fn_mcxt = CurrentMemoryContext;
+
+ if (istrigger)
+ {
+ Assert(trigdata != NULL);
+
+ MemSet(trigdata, 0, sizeof(TriggerData));
+ trigdata->type = T_TriggerData;
+ fcinfo->context = (Node *) trigdata;
+ }
+}
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index 5d2f818..245b7e9 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -115,7 +115,6 @@ static PLpgSQL_function *plpgsql_HashTableLookup(PLpgSQL_func_hashkey *func_key)
static void plpgsql_HashTableInsert(PLpgSQL_function *function,
PLpgSQL_func_hashkey *func_key);
static void plpgsql_HashTableDelete(PLpgSQL_function *function);
-static void delete_function(PLpgSQL_function *func);
/* ----------
* plpgsql_compile Make an execution tree for a PL/pgSQL function.
@@ -175,7 +174,7 @@ recheck:
* Nope, so remove it from hashtable and try to drop associated
* storage (if not done already).
*/
- delete_function(function);
+ plpgsql_delete_function(function);
/*
* If the function isn't in active use then we can overwrite the
@@ -2426,7 +2425,7 @@ plpgsql_resolve_polymorphic_argtypes(int numargs,
}
/*
- * delete_function - clean up as much as possible of a stale function cache
+ * plpgsql_delete_function - clean up as much as possible of a stale function cache
*
* We can't release the PLpgSQL_function struct itself, because of the
* possibility that there are fn_extra pointers to it. We can release
@@ -2439,8 +2438,8 @@ plpgsql_resolve_polymorphic_argtypes(int numargs,
* pointers to the same function cache. Hence be careful not to do things
* twice.
*/
-static void
-delete_function(PLpgSQL_function *func)
+void
+plpgsql_delete_function(PLpgSQL_function *func)
{
/* remove function from hash table (might be done already) */
plpgsql_HashTableDelete(func);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 8ca791c..bd84ab5 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -80,7 +80,6 @@ static SimpleEcontextStackEntry *simple_econtext_stack = NULL;
* Local function forward declarations
************************************************************/
static void plpgsql_exec_error_callback(void *arg);
-static PLpgSQL_datum *copy_plpgsql_datum(PLpgSQL_datum *datum);
static int exec_stmt_block(PLpgSQL_execstate *estate,
PLpgSQL_stmt_block *block);
@@ -133,9 +132,6 @@ static int exec_stmt_dynexecute(PLpgSQL_execstate *estate,
static int exec_stmt_dynfors(PLpgSQL_execstate *estate,
PLpgSQL_stmt_dynfors *stmt);
-static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
- PLpgSQL_function *func,
- ReturnSetInfo *rsi);
static void exec_eval_cleanup(PLpgSQL_execstate *estate);
static void exec_prepare_plan(PLpgSQL_execstate *estate,
@@ -204,7 +200,6 @@ static Datum exec_simple_cast_value(PLpgSQL_execstate *estate,
static void exec_init_tuple_store(PLpgSQL_execstate *estate);
static void exec_set_found(PLpgSQL_execstate *estate, bool state);
static void plpgsql_create_econtext(PLpgSQL_execstate *estate);
-static void plpgsql_destroy_econtext(PLpgSQL_execstate *estate);
static void free_var(PLpgSQL_var *var);
static void assign_text_var(PLpgSQL_var *var, const char *str);
static PreparedParamsData *exec_eval_using_params(PLpgSQL_execstate *estate,
@@ -839,7 +834,7 @@ plpgsql_exec_error_callback(void *arg)
* Support function for initializing local execution variables
* ----------
*/
-static PLpgSQL_datum *
+PLpgSQL_datum *
copy_plpgsql_datum(PLpgSQL_datum *datum)
{
PLpgSQL_datum *result;
@@ -2858,7 +2853,7 @@ exec_stmt_raise(PLpgSQL_execstate *estate, PLpgSQL_stmt_raise *stmt)
* Initialize a mostly empty execution state
* ----------
*/
-static void
+void
plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi)
@@ -5924,7 +5919,7 @@ plpgsql_create_econtext(PLpgSQL_execstate *estate)
* We check that it matches the top stack entry, and destroy the stack
* entry along with the context.
*/
-static void
+void
plpgsql_destroy_econtext(PLpgSQL_execstate *estate)
{
SimpleEcontextStackEntry *next;
diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c
index 022ec3f..4d3364c 100644
--- a/src/pl/plpgsql/src/pl_handler.c
+++ b/src/pl/plpgsql/src/pl_handler.c
@@ -15,6 +15,8 @@
#include "plpgsql.h"
+#include "catalog/pg_enum.h"
+#include "catalog/pg_language.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "funcapi.h"
@@ -39,6 +41,8 @@ int plpgsql_variable_conflict = PLPGSQL_RESOLVE_ERROR;
/* Hook for plugins */
PLpgSQL_plugin **plugin_ptr = NULL;
+static void precheck_function(HeapTuple procTuple, bool has_trigger_relation);
+static int check_function_output_format(Oid format);
/*
* _PG_init() - library load-time initialization
@@ -180,11 +184,7 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
* plpgsql_exec_function(). In particular note that this sets things up
* with no arguments passed.
*/
- MemSet(&fake_fcinfo, 0, sizeof(fake_fcinfo));
- MemSet(&flinfo, 0, sizeof(flinfo));
- fake_fcinfo.flinfo = &flinfo;
- flinfo.fn_oid = InvalidOid;
- flinfo.fn_mcxt = CurrentMemoryContext;
+ plpgsql_setup_fake_fcinfo(&flinfo, &fake_fcinfo, NULL, InvalidOid, false);
retval = plpgsql_exec_function(func, &fake_fcinfo);
@@ -286,17 +286,7 @@ plpgsql_validator(PG_FUNCTION_ARGS)
* Set up a fake fcinfo with just enough info to satisfy
* plpgsql_compile().
*/
- MemSet(&fake_fcinfo, 0, sizeof(fake_fcinfo));
- MemSet(&flinfo, 0, sizeof(flinfo));
- fake_fcinfo.flinfo = &flinfo;
- flinfo.fn_oid = funcoid;
- flinfo.fn_mcxt = CurrentMemoryContext;
- if (istrigger)
- {
- MemSet(&trigdata, 0, sizeof(trigdata));
- trigdata.type = T_TriggerData;
- fake_fcinfo.context = (Node *) &trigdata;
- }
+ plpgsql_setup_fake_fcinfo(&flinfo, &fake_fcinfo, &trigdata, funcoid, istrigger);
/* Test-compile the function */
plpgsql_compile(&fake_fcinfo, true);
@@ -312,3 +302,153 @@ plpgsql_validator(PG_FUNCTION_ARGS)
PG_RETURN_VOID();
}
+
+/*
+ * ----------
+ * plpgsql_check_function
+ *
+ * It ensure a detailed validation
+ * ----------
+ */
+PG_FUNCTION_INFO_V1(plpgsql_check_function);
+
+Datum
+plpgsql_check_function(PG_FUNCTION_ARGS)
+{
+ Oid funcoid = PG_GETARG_OID(0);
+ Oid relid = PG_GETARG_OID(1);
+ bool fatal_errors = PG_GETARG_BOOL(2);
+ Oid format_oid = PG_GETARG_OID(3);
+ TupleDesc tupdesc;
+ HeapTuple procTuple;
+ Tuplestorestate *tupstore;
+ MemoryContext per_query_ctx;
+ MemoryContext oldcontext;
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ int format = PLPGSQL_CHECK_FORMAT_PLAIN;
+
+ /* check to see if caller supports us returning a tuplestore */
+ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("set-valued function called in context that cannot accept a set")));
+
+ if (!(rsinfo->allowedModes & SFRM_Materialize))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("materialize mode required, but it is not allowed in this context")));
+
+ procTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcoid));
+ if (!HeapTupleIsValid(procTuple))
+ elog(ERROR, "cache lookup failed for function %u", funcoid);
+
+ precheck_function(procTuple, OidIsValid(relid));
+ format = check_function_output_format(format_oid);
+
+ /* need to build tuplestore in query context */
+ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+ oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+ tupdesc = CreateTupleDescCopy(rsinfo->expectedDesc);
+ tupstore = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+
+ plpgsql_function_check(procTuple, relid,
+ tupdesc, tupstore,
+ fatal_errors, format);
+
+ ReleaseSysCache(procTuple);
+
+ /* clean up and return the tuplestore */
+ tuplestore_donestoring(tupstore);
+
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = tupstore;
+ rsinfo->setDesc = tupdesc;
+
+ return (Datum) 0;
+}
+
+/*
+ * Process necessary checking before code checking
+ */
+static void
+precheck_function(HeapTuple procTuple, bool has_trigger_relation)
+{
+ Form_pg_proc proc;
+ Form_pg_language languageStruct;
+ HeapTuple languageTuple;
+ char functyptype;
+ char *funcname;
+ bool is_trigger = false;
+
+ proc = (Form_pg_proc) GETSTRUCT(procTuple);
+
+ funcname = format_procedure(HeapTupleGetOid(procTuple));
+
+ /* used language must be plpgsql */
+ languageTuple = SearchSysCache1(LANGOID, ObjectIdGetDatum(proc->prolang));
+ Assert(HeapTupleIsValid(languageTuple));
+
+ languageStruct = (Form_pg_language) GETSTRUCT(languageTuple);
+ if (strcmp(NameStr(languageStruct->lanname), "plpgsql") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("%s is not a plpgsql function", funcname)));
+
+ ReleaseSysCache(languageTuple);
+
+ functyptype = get_typtype(proc->prorettype);
+
+ if (functyptype == TYPTYPE_PSEUDO)
+ {
+ /* we assume OPAQUE with no arguments means a trigger */
+ if (proc->prorettype == TRIGGEROID ||
+ (proc->prorettype == OPAQUEOID && proc->pronargs == 0))
+ {
+ is_trigger = true;
+ if (!has_trigger_relation)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("missing trigger relation"),
+ errhint("Trigger relation oid must be valid")));
+ }
+ }
+
+ if (has_trigger_relation && !is_trigger)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("function is not trigger"),
+ errhint("Trigger relation oid must not be valid for non trigger function.")));
+
+ pfree(funcname);
+}
+
+/*
+ * Returns output format for plpgsql_check_function
+ */
+static int
+check_function_output_format(Oid format)
+{
+ HeapTuple tuple;
+ char *label;
+ int result = -1;
+
+ tuple = SearchSysCache1(ENUMOID, ObjectIdGetDatum(format));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+ errmsg("invalid internal value for enum: %u",
+ format)));
+
+ label = NameStr(((Form_pg_enum) GETSTRUCT(tuple))->enumlabel);
+
+ if (strcmp(label, "XML") == 0)
+ result = PLPGSQL_CHECK_FORMAT_XML;
+ else if (strcmp(label, "PLAIN_TEXT") == 0)
+ result = PLPGSQL_CHECK_FORMAT_PLAIN;
+
+ ReleaseSysCache(tuple);
+
+ return result;
+}
diff --git a/src/pl/plpgsql/src/plpgsql--1.0--1.1.sql b/src/pl/plpgsql/src/plpgsql--1.0--1.1.sql
new file mode 100644
index 0000000..62cd8c2
--- /dev/null
+++ b/src/pl/plpgsql/src/plpgsql--1.0--1.1.sql
@@ -0,0 +1,17 @@
+/* src/pl/plpgsql/src/plpgsql--1.1.sql */
+
+/*
+ * Currently, all the interesting stuff is done by CREATE LANGUAGE.
+ * Later we will probably "dumb down" that command and put more of the
+ * knowledge into this script.
+ */
+
+CREATE TYPE plpgsql_check_function_format AS ENUM ('XML', 'PLAIN_TEXT');
+
+CREATE FUNCTION plpgsql_check_function(funcoid regprocedure,
+relid regclass = 0,
+fatal_errors boolean = true,
+format plpgsql_check_function_format = 'PLAIN_TEXT')
+RETURNS SETOF text AS 'MODULE_PATHNAME'
+LANGUAGE C
+RETURNS NULL ON NULL INPUT;
diff --git a/src/pl/plpgsql/src/plpgsql--1.1.sql b/src/pl/plpgsql/src/plpgsql--1.1.sql
new file mode 100644
index 0000000..291fdc5
--- /dev/null
+++ b/src/pl/plpgsql/src/plpgsql--1.1.sql
@@ -0,0 +1,21 @@
+/* src/pl/plpgsql/src/plpgsql--1.1.sql */
+
+/*
+ * Currently, all the interesting stuff is done by CREATE LANGUAGE.
+ * Later we will probably "dumb down" that command and put more of the
+ * knowledge into this script.
+ */
+
+CREATE PROCEDURAL LANGUAGE plpgsql;
+
+COMMENT ON PROCEDURAL LANGUAGE plpgsql IS 'PL/pgSQL procedural language';
+
+CREATE TYPE plpgsql_check_function_format AS ENUM ('XML', 'PLAIN_TEXT');
+
+CREATE FUNCTION plpgsql_check_function(funcoid regprocedure,
+relid regclass = 0,
+fatal_errors boolean = true,
+format plpgsql_check_function_format = 'PLAIN_TEXT')
+RETURNS SETOF text AS 'MODULE_PATHNAME'
+LANGUAGE C
+RETURNS NULL ON NULL INPUT;
diff --git a/src/pl/plpgsql/src/plpgsql.control b/src/pl/plpgsql/src/plpgsql.control
index b320227..4c75c93 100644
--- a/src/pl/plpgsql/src/plpgsql.control
+++ b/src/pl/plpgsql/src/plpgsql.control
@@ -1,6 +1,6 @@
# plpgsql extension
comment = 'PL/pgSQL procedural language'
-default_version = '1.0'
+default_version = '1.1'
module_pathname = '$libdir/plpgsql'
relocatable = false
schema = pg_catalog
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index b63f336..c2a4cc9 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -767,6 +767,21 @@ typedef struct PLpgSQL_execstate
void *plugin_info; /* reserved for use by optional plugin */
} PLpgSQL_execstate;
+enum {
+ PLPGSQL_CHECK_FORMAT_PLAIN,
+ PLPGSQL_CHECK_FORMAT_XML
+};
+
+typedef struct PLpgSQL_checkstate
+{
+ PLpgSQL_execstate *estate; /* check state is estate extension */
+ Tuplestorestate *tuple_store;
+ TupleDesc tupdesc;
+ StringInfo sinfo;
+ bool fatal_errors; /* stop on first error */
+ int format;
+ List *argnames;
+} PLpgSQL_checkstate;
/*
* A PLpgSQL_plugin structure represents an instrumentation plugin.
@@ -902,6 +917,7 @@ extern PLpgSQL_condition *plpgsql_parse_err_condition(char *condname);
extern void plpgsql_adddatum(PLpgSQL_datum *new);
extern int plpgsql_add_initdatums(int **varnos);
extern void plpgsql_HashTableInit(void);
+extern void plpgsql_delete_function(PLpgSQL_function *func);
/* ----------
* Functions in pl_handler.c
@@ -911,6 +927,7 @@ extern void _PG_init(void);
extern Datum plpgsql_call_handler(PG_FUNCTION_ARGS);
extern Datum plpgsql_inline_handler(PG_FUNCTION_ARGS);
extern Datum plpgsql_validator(PG_FUNCTION_ARGS);
+extern Datum plpgsql_check_function(PG_FUNCTION_ARGS);
/* ----------
* Functions in pl_exec.c
@@ -928,6 +945,22 @@ extern Oid exec_get_datum_type(PLpgSQL_execstate *estate,
extern void exec_get_datum_type_info(PLpgSQL_execstate *estate,
PLpgSQL_datum *datum,
Oid *typeid, int32 *typmod, Oid *collation);
+extern PLpgSQL_datum *copy_plpgsql_datum(PLpgSQL_datum *datum);
+extern void plpgsql_estate_setup(PLpgSQL_execstate *estate,
+ PLpgSQL_function *func,
+ ReturnSetInfo *rsi);
+extern void plpgsql_destroy_econtext(PLpgSQL_execstate *estate);
+
+/* ----------
+ * Functions for namespace handling in pl_check.c
+ * ----------
+ */
+extern void plpgsql_function_check(HeapTuple procTuple, Oid relid,
+ TupleDesc tupdesc,
+ Tuplestorestate *tupstore,
+ bool fatal_errors, int format);
+extern void plpgsql_setup_fake_fcinfo(FmgrInfo *flinfo, FunctionCallInfoData *fcinfo,
+ TriggerData *trigdata, Oid funcoid, bool istrigger);
/* ----------
* Functions for namespace handling in pl_funcs.c
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 1e45919..77d575d 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -302,6 +302,12 @@ end;
' language plpgsql;
create trigger tg_hslot_biu before insert or update
on HSlot for each row execute procedure tg_hslot_biu();
+-- check trigger should not fail
+select plpgsql_check_function('tg_hslot_biu()', 'HSlot');
+ plpgsql_check_function
+------------------------
+(0 rows)
+
-- ************************************************************
-- * BEFORE DELETE on HSlot
-- * - prevent from manual manipulation
@@ -635,6 +641,12 @@ begin
raise exception ''illegal backlink beginning with %'', mytype;
end;
' language plpgsql;
+-- check function should not fail
+select plpgsql_check_function('tg_backlink_set(bpchar, bpchar)', 0);
+ plpgsql_check_function
+------------------------
+(0 rows)
+
-- ************************************************************
-- * Support function to clear out the backlink field if
-- * it still points to specific slot
@@ -2950,6 +2962,38 @@ NOTICE: 4 bb cc
(1 row)
+-- check function should not fail
+select plpgsql_check_function('for_vect()');
+ plpgsql_check_function
+------------------------
+(0 rows)
+
+-- recheck after check function
+select for_vect();
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
+NOTICE: 1 BB CC
+NOTICE: 2 BB CC
+NOTICE: 3 BB CC
+NOTICE: 4 BB CC
+NOTICE: 1
+NOTICE: 2
+NOTICE: 3
+NOTICE: 4
+NOTICE: 1 BB CC
+NOTICE: 2 BB CC
+NOTICE: 3 BB CC
+NOTICE: 4 BB CC
+NOTICE: 1 bb cc
+NOTICE: 2 bb cc
+NOTICE: 3 bb cc
+NOTICE: 4 bb cc
+ for_vect
+----------
+
+(1 row)
+
-- regression test: verify that multiple uses of same plpgsql datum within
-- a SQL command all get mapped to the same $n parameter. The return value
-- of the SELECT is not important, we only care that it doesn't fail with
@@ -3431,6 +3475,12 @@ begin
return;
end;
$$ language plpgsql;
+-- check function should not fail
+select plpgsql_check_function('forc01()');
+ plpgsql_check_function
+------------------------
+(0 rows)
+
select forc01();
NOTICE: 5 from c
NOTICE: 6 from c
@@ -3864,6 +3914,12 @@ begin
end case;
end;
$$ language plpgsql immutable;
+-- check function should not fail
+select plpgsql_check_function('case_test(bigint)');
+ plpgsql_check_function
+------------------------
+(0 rows)
+
select case_test(1);
case_test
-----------
@@ -4719,3 +4775,528 @@ 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);
+--
+-- check function statement tests
+--
+--should fail - is not plpgsql
+select plpgsql_check_function('session_user()');
+ERROR: "session_user"() is not a plpgsql function
+create table t1(a int, b int);
+create function f1()
+returns void as $$
+begin
+ if false then
+ update t1 set c = 30;
+ end if;
+ if false then
+ raise notice '% %', r.c;
+ end if;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()', fatal_errors := true);
+ plpgsql_check_function
+------------------------------------------------------------------------
+ error:42703:4:SQL statement:column "c" of relation "t1" does not exist
+ Query: update t1 set c = 30
+ -- ^
+(3 rows)
+
+select plpgsql_check_function('f1()', fatal_errors := false);
+ plpgsql_check_function
+------------------------------------------------------------------------
+ error:42703:4:SQL statement:column "c" of relation "t1" does not exist
+ Query: update t1 set c = 30
+ -- ^
+ error:42P01:7:RAISE:missing FROM-clause entry for table "r"
+ Query: SELECT r.c
+ -- ^
+ error:42601:7:RAISE:too few parameters specified for RAISE
+(7 rows)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+------------------------------------------------------------------------
+ error:42703:4:SQL statement:column "c" of relation "t1" does not exist
+ Query: update t1 set c = 30
+ -- ^
+(3 rows)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+create function g1(out a int, out b int)
+as $$
+ select 10,20;
+$$ language sql;
+create function f1()
+returns void as $$
+declare r record;
+begin
+ r := g1();
+ if false then
+ raise notice '%', r.c;
+ end if;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+-------------------------------------------------
+ error:42703:6:RAISE:record "r" has no field "c"
+ Context: SQL statement "SELECT r.c"
+(2 rows)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+drop function g1();
+create function g1(out a int, out b int)
+returns setof record as $$
+select * from t1;
+$$ language sql;
+create function f1()
+returns void as $$
+declare r record;
+begin
+ for r in select * from g1()
+ loop
+ raise notice '%', r.c;
+ end loop;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+-------------------------------------------------
+ error:42703:6:RAISE:record "r" has no field "c"
+ Context: SQL statement "SELECT r.c"
+(2 rows)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+create or replace function f1()
+returns void as $$
+declare r record;
+begin
+ for r in select * from g1()
+ loop
+ r.c := 20;
+ end loop;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+------------------------------------------------------
+ error:42703:6:assignment:record "r" has no field "c"
+(1 row)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+drop function g1();
+create function f1()
+returns int as $$
+declare r int;
+begin
+ if false then
+ r := a + b;
+ end if;
+ return r;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+----------------------------------------------------
+ error:42703:5:assignment:column "a" does not exist
+ Query: SELECT a + b
+ -- ^
+(3 rows)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+create or replace function f1()
+returns void as $$
+begin
+ if false then
+ raise notice '%', 1, 2;
+ end if;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+-------------------------------------------------------------
+ error:42601:4:RAISE:too many parameters specified for RAISE
+(1 row)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+create or replace function f1()
+returns void as $$
+begin
+ if false then
+ raise notice '% %';
+ end if;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+------------------------------------------------------------
+ error:42601:4:RAISE:too few parameters specified for RAISE
+(1 row)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+create or replace function f1()
+returns void as $$
+declare r int[];
+begin
+ if false then
+ r[c+10] := 20;
+ end if;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+----------------------------------------------------
+ error:42703:5:assignment:column "c" does not exist
+ Query: SELECT c+10
+ -- ^
+(3 rows)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+create or replace function f1()
+returns void as $$
+declare r int;
+begin
+ if false then
+ r[10] := 20;
+ end if;
+end;
+$$ language plpgsql set search_path = public;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+-------------------------------------------------------------
+ error:42804:5:assignment:subscripted object is not an array
+(1 row)
+
+select f1();
+ f1
+----
+
+(1 row)
+
+drop function f1();
+create type _exception_type as (
+ state text,
+ message text,
+ detail text);
+create or replace function f1()
+returns void as $$
+declare
+ _exception record;
+begin
+ _exception := NULL::_exception_type;
+exception when others then
+ get stacked diagnostics
+ _exception.state = RETURNED_SQLSTATE,
+ _exception.message = MESSAGE_TEXT,
+ _exception.detail = PG_EXCEPTION_DETAIL,
+ _exception.hint = PG_EXCEPTION_HINT;
+end;
+$$ language plpgsql;
+select f1();
+ f1
+----
+
+(1 row)
+
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+-----------------------------------------------------------------------
+ error:42703:7:GET DIAGNOSTICS:record "_exception" has no field "hint"
+(1 row)
+
+drop function f1();
+create or replace function f1_trg()
+returns trigger as $$
+begin
+ if new.a > 10 then
+ raise notice '%', new.b;
+ raise notice '%', new.c;
+ end if;
+ return new;
+end;
+$$ language plpgsql;
+create trigger t1_f1 before insert on t1
+ for each row
+ execute procedure f1_trg();
+insert into t1 values(6,30);
+select plpgsql_check_function('f1_trg()','t1');
+ plpgsql_check_function
+---------------------------------------------------
+ error:42703:5:RAISE:record "new" has no field "c"
+ Context: SQL statement "SELECT new.c"
+(2 rows)
+
+insert into t1 values(6,30);
+create or replace function f1_trg()
+returns trigger as $$
+begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ new.c := 30;
+ return new;
+end;
+$$ language plpgsql;
+-- should to fail
+select plpgsql_check_function('f1_trg()','t1');
+ plpgsql_check_function
+--------------------------------------------------------
+ error:42703:5:assignment:record "new" has no field "c"
+(1 row)
+
+-- should to fail but not crash
+insert into t1 values(6,30);
+ERROR: record "new" has no field "c"
+CONTEXT: PL/pgSQL function f1_trg() line 5 at assignment
+create or replace function f1_trg()
+returns trigger as $$
+begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ return new;
+end;
+$$ language plpgsql;
+-- ok
+select plpgsql_check_function('f1_trg()', 't1');
+ plpgsql_check_function
+------------------------
+(0 rows)
+
+-- ok
+insert into t1 values(6,30);
+select * from t1;
+ a | b
+----+----
+ 6 | 30
+ 6 | 30
+ 16 | 40
+(3 rows)
+
+drop trigger t1_f1 on t1;
+drop function f1_trg();
+-- test of showing caret on correct place for multiline queries
+create or replace function f1()
+returns void as $$
+begin
+ select
+ var
+ from
+ foo;
+end;
+$$ language plpgsql;
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+---------------------------------------------------------
+ error:42703:3:SQL statement:column "var" does not exist
+ Query: select
+ var
+ -- ^
+ from
+ foo
+(6 rows)
+
+drop function f1();
+create or replace function f1()
+returns int as $$
+begin
+ return (select a
+ from t1
+ where hh = 20);
+end;
+$$ language plpgsql;
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+-------------------------------------------------
+ error:42703:3:RETURN:column "hh" does not exist
+ Query: SELECT (select a
+ from t1
+ where hh = 20)
+ -- ^
+(5 rows)
+
+create or replace function f1()
+returns int as $$
+begin
+ return (select a
+ from txxxxxxx
+ where hh = 20);
+end;
+$$ language plpgsql;
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+---------------------------------------------------------
+ error:42P01:3:RETURN:relation "txxxxxxx" does not exist
+ Query: SELECT (select a
+ from txxxxxxx
+ -- ^
+ where hh = 20)
+(5 rows)
+
+drop function f1();
+drop table t1;
+drop type _exception_type;
+-- raise warnings when target row has different number of attributies in
+-- SELECT INTO statement
+create or replace function f1()
+returns void as $$
+declare a1 int; a2 int;
+begin
+ select 10,20 into a1,a2;
+end;
+$$ language plpgsql;
+-- should be ok
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+------------------------
+(0 rows)
+
+create or replace function f1()
+returns void as $$
+declare a1 int; a2 int;
+begin
+ select 10,20 into a1;
+end;
+$$ language plpgsql;
+-- raise warning
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+-------------------------------------------------------------------------
+ warning:00000:4:SQL statement:too many attributies for target variables
+ Detail: There are less target variables than output columns in query.
+ Hint: Check target variables in SELECT INTO statement
+(3 rows)
+
+create or replace function f1()
+returns void as $$
+declare a1 int; a2 int;
+begin
+ select 10 into a1,a2;
+end;
+$$ language plpgsql;
+-- raise warning
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+------------------------------------------------------------------------
+ warning:00000:4:SQL statement:too few attributies for target variables
+ Detail: There are more target variables than output columns in query.
+ Hint: Check target variables in SELECT INTO statement.
+(3 rows)
+
+-- bogus code
+set check_function_bodies to off;
+create or replace function f1()
+returns void as $$
+adasdfsadf
+$$ language plpgsql;
+select plpgsql_check_function('f1()');
+ plpgsql_check_function
+------------------------------------------------------------
+ error:42601:syntax error at or near "adasdfsadf"
+ Query:
+ adasdfsadf
+ -- ^
+
+ Context: compilation of PL/pgSQL function "f1" near line 1
+(6 rows)
+
+drop function f1();
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 2b60b67..87a15d3 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -366,6 +366,8 @@ end;
create trigger tg_hslot_biu before insert or update
on HSlot for each row execute procedure tg_hslot_biu();
+-- check trigger should not fail
+select plpgsql_check_function('tg_hslot_biu()', 'HSlot');
-- ************************************************************
-- * BEFORE DELETE on HSlot
@@ -747,6 +749,9 @@ begin
end;
' language plpgsql;
+-- check function should not fail
+select plpgsql_check_function('tg_backlink_set(bpchar, bpchar)', 0);
+
-- ************************************************************
-- * Support function to clear out the backlink field if
@@ -2458,6 +2463,13 @@ $proc$ language plpgsql;
select for_vect();
+-- check function should not fail
+select plpgsql_check_function('for_vect()');
+
+-- recheck after check function
+select for_vect();
+
+
-- regression test: verify that multiple uses of same plpgsql datum within
-- a SQL command all get mapped to the same $n parameter. The return value
-- of the SELECT is not important, we only care that it doesn't fail with
@@ -2837,6 +2849,9 @@ begin
end;
$$ language plpgsql;
+-- check function should not fail
+select plpgsql_check_function('forc01()');
+
select forc01();
-- try updating the cursor's current row
@@ -3171,6 +3186,10 @@ begin
end;
$$ language plpgsql immutable;
+-- check function should not fail
+select plpgsql_check_function('case_test(bigint)');
+
+
select case_test(1);
select case_test(2);
select case_test(3);
@@ -3723,3 +3742,365 @@ select testoa(1,2,1); -- fail at update
drop function arrayassign1();
drop function testoa(x1 int, x2 int, x3 int);
+
+--
+-- check function statement tests
+--
+
+--should fail - is not plpgsql
+select plpgsql_check_function('session_user()');
+
+create table t1(a int, b int);
+
+create function f1()
+returns void as $$
+begin
+ if false then
+ update t1 set c = 30;
+ end if;
+ if false then
+ raise notice '% %', r.c;
+ end if;
+end;
+$$ language plpgsql;
+
+select f1();
+select plpgsql_check_function('f1()', fatal_errors := true);
+select plpgsql_check_function('f1()', fatal_errors := false);
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+
+create function g1(out a int, out b int)
+as $$
+ select 10,20;
+$$ language sql;
+
+create function f1()
+returns void as $$
+declare r record;
+begin
+ r := g1();
+ if false then
+ raise notice '%', r.c;
+ end if;
+end;
+$$ language plpgsql;
+
+select f1();
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+drop function g1();
+
+create function g1(out a int, out b int)
+returns setof record as $$
+select * from t1;
+$$ language sql;
+
+create function f1()
+returns void as $$
+declare r record;
+begin
+ for r in select * from g1()
+ loop
+ raise notice '%', r.c;
+ end loop;
+end;
+$$ language plpgsql;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+create or replace function f1()
+returns void as $$
+declare r record;
+begin
+ for r in select * from g1()
+ loop
+ r.c := 20;
+ end loop;
+end;
+$$ language plpgsql;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+drop function g1();
+
+create function f1()
+returns int as $$
+declare r int;
+begin
+ if false then
+ r := a + b;
+ end if;
+ return r;
+end;
+$$ language plpgsql;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+
+create or replace function f1()
+returns void as $$
+begin
+ if false then
+ raise notice '%', 1, 2;
+ end if;
+end;
+$$ language plpgsql;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+
+create or replace function f1()
+returns void as $$
+begin
+ if false then
+ raise notice '% %';
+ end if;
+end;
+$$ language plpgsql;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+
+create or replace function f1()
+returns void as $$
+declare r int[];
+begin
+ if false then
+ r[c+10] := 20;
+ end if;
+end;
+$$ language plpgsql;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+
+create or replace function f1()
+returns void as $$
+declare r int;
+begin
+ if false then
+ r[10] := 20;
+ end if;
+end;
+$$ language plpgsql set search_path = public;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+select f1();
+
+drop function f1();
+
+create type _exception_type as (
+ state text,
+ message text,
+ detail text);
+
+create or replace function f1()
+returns void as $$
+declare
+ _exception record;
+begin
+ _exception := NULL::_exception_type;
+exception when others then
+ get stacked diagnostics
+ _exception.state = RETURNED_SQLSTATE,
+ _exception.message = MESSAGE_TEXT,
+ _exception.detail = PG_EXCEPTION_DETAIL,
+ _exception.hint = PG_EXCEPTION_HINT;
+end;
+$$ language plpgsql;
+
+select f1();
+
+select plpgsql_check_function('f1()');
+
+drop function f1();
+
+create or replace function f1_trg()
+returns trigger as $$
+begin
+ if new.a > 10 then
+ raise notice '%', new.b;
+ raise notice '%', new.c;
+ end if;
+ return new;
+end;
+$$ language plpgsql;
+
+create trigger t1_f1 before insert on t1
+ for each row
+ execute procedure f1_trg();
+
+insert into t1 values(6,30);
+
+select plpgsql_check_function('f1_trg()','t1');
+
+insert into t1 values(6,30);
+
+create or replace function f1_trg()
+returns trigger as $$
+begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ new.c := 30;
+ return new;
+end;
+$$ language plpgsql;
+
+-- should to fail
+
+select plpgsql_check_function('f1_trg()','t1');
+
+-- should to fail but not crash
+insert into t1 values(6,30);
+
+create or replace function f1_trg()
+returns trigger as $$
+begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ return new;
+end;
+$$ language plpgsql;
+
+-- ok
+select plpgsql_check_function('f1_trg()', 't1');
+
+-- ok
+insert into t1 values(6,30);
+
+select * from t1;
+
+drop trigger t1_f1 on t1;
+
+drop function f1_trg();
+
+-- test of showing caret on correct place for multiline queries
+create or replace function f1()
+returns void as $$
+begin
+ select
+ var
+ from
+ foo;
+end;
+$$ language plpgsql;
+
+select plpgsql_check_function('f1()');
+
+drop function f1();
+
+create or replace function f1()
+returns int as $$
+begin
+ return (select a
+ from t1
+ where hh = 20);
+end;
+$$ language plpgsql;
+
+select plpgsql_check_function('f1()');
+
+create or replace function f1()
+returns int as $$
+begin
+ return (select a
+ from txxxxxxx
+ where hh = 20);
+end;
+$$ language plpgsql;
+
+select plpgsql_check_function('f1()');
+
+drop function f1();
+
+drop table t1;
+drop type _exception_type;
+
+-- raise warnings when target row has different number of attributies in
+-- SELECT INTO statement
+
+create or replace function f1()
+returns void as $$
+declare a1 int; a2 int;
+begin
+ select 10,20 into a1,a2;
+end;
+$$ language plpgsql;
+
+-- should be ok
+select plpgsql_check_function('f1()');
+
+create or replace function f1()
+returns void as $$
+declare a1 int; a2 int;
+begin
+ select 10,20 into a1;
+end;
+$$ language plpgsql;
+
+-- raise warning
+select plpgsql_check_function('f1()');
+
+create or replace function f1()
+returns void as $$
+declare a1 int; a2 int;
+begin
+ select 10 into a1,a2;
+end;
+$$ language plpgsql;
+
+-- raise warning
+select plpgsql_check_function('f1()');
+
+-- bogus code
+set check_function_bodies to off;
+
+create or replace function f1()
+returns void as $$
+adasdfsadf
+$$ language plpgsql;
+
+select plpgsql_check_function('f1()');
+
+drop function f1();
+
Hi!
On Tue, Jun 26, 2012 at 1:19 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
I am sending lightly refreshed patch for checking plpgsql functions..
I checked different implementation, but without success: a) enhancing
of SPI to some fake mode can has negative impact on application, and
patch was not clear, b) generic plpgsql walker doesn't save lines too.I invite any ideas how to improve this patch
I reviewed this and did a clean up for bitrot and a little whitespace.
In particular, it needed to learn a little about event triggers.
This patch is a follow on from an earlier review thread I found:
http://archives.postgresql.org/message-id/D960CB61B694CF459DCFB4B0128514C2072DF447@exadv11.host.magwien.gv.at
I dug through that thread a bit, and I believe issues raised by
Laurenz, Petr and Alvaro were resolved by Pavel over time.
All tests pass, and after a read-through, the code seems fine.
This also represents my inaugural use of pg_bsd_indent. I ran it on
pl_check.c - which made things mostly better. Happy to try and fix it
up more if someone can explain to me what (if anything) I did
incorrectly when using it.
-selena
Attachments:
plpgsql_check_function-20121006.patchapplication/octet-stream; name=plpgsql_check_function-20121006.patchDownload
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 07fba57..9972bf7
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
*************** $$ LANGUAGE plpgsql;
*** 4364,4369 ****
--- 4364,4387 ----
making it easier to recreate and debug functions.
</para>
+ <sect2 id="plpgsql-check-function">
+ <title>Checking of embedded SQL</title>
+ <para>
+ The SQL statements inside <application>PL/pgSQL</> functions are
+ checked by validator for semantic errors. These errors
+ can be found by <function>plpgsql_check_function</function>:
+ <programlisting>
+ postgres=# select plpgsql_check_function('fx(int)');
+ plpgsql_check_function
+ ------------------------------------------------
+ error:42703:3:RETURN:column "b" does not exist
+ Query: SELECT (select a from t1 where b < _a)
+ -- ^
+ (3 rows)
+ </programlisting>
+ </para>
+ </sect2>
+
<sect2 id="plpgsql-quote-tips">
<title>Handling of Quotation Marks</title>
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
new file mode 100644
index 0db0dc5..904cd63
*** a/src/pl/plpgsql/src/Makefile
--- b/src/pl/plpgsql/src/Makefile
*************** override CPPFLAGS := -I. -I$(srcdir) $(C
*** 17,25 ****
SHLIB_LINK = $(filter -lintl, $(LIBS))
rpath =
! OBJS = pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o
- DATA = plpgsql.control plpgsql--1.0.sql plpgsql--unpackaged--1.0.sql
all: all-lib
--- 17,26 ----
SHLIB_LINK = $(filter -lintl, $(LIBS))
rpath =
! OBJS = pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o pl_check.o
!
! DATA = plpgsql.control plpgsql--1.0.sql plpgsql--1.1.sql plpgsql--unpackaged--1.0.sql plpgsql--1.0--1.1.sql
all: all-lib
*************** uninstall-headers:
*** 52,58 ****
# Force these dependencies to be known even without dependency info built:
! pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o: plpgsql.h pl_gram.h plerrcodes.h
# See notes in src/backend/parser/Makefile about the following two rules
--- 53,59 ----
# Force these dependencies to be known even without dependency info built:
! pl_gram.o pl_handler.o pl_comp.o pl_exec.o pl_funcs.o pl_scanner.o pl_check.o : plpgsql.h pl_gram.h plerrcodes.h
# See notes in src/backend/parser/Makefile about the following two rules
diff --git a/src/pl/plpgsql/src/pl_check.c b/src/pl/plpgsql/src/pl_check.c
new file mode 100644
index ...80d0303
*** a/src/pl/plpgsql/src/pl_check.c
--- b/src/pl/plpgsql/src/pl_check.c
***************
*** 0 ****
--- 1,1703 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pl_checker.c - Checker for the PL/pgSQL
+ * procedural language
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/pl/plpgsql/src/pl_check.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #include "plpgsql.h"
+
+ #include "funcapi.h"
+ #include "miscadmin.h"
+
+ #include "access/htup_details.h"
+ #include "catalog/pg_proc.h"
+ #include "catalog/pg_type.h"
+ #include "executor/spi_priv.h"
+ #include "mb/pg_wchar.h"
+ #include "utils/builtins.h"
+ #include "utils/guc.h"
+ #include "utils/lsyscache.h"
+ #include "utils/syscache.h"
+ #include "utils/typcache.h"
+ #include "utils/rel.h"
+ #include "utils/xml.h"
+
+ static void check_row_or_rec(PLpgSQL_checkstate * cstate, PLpgSQL_row * row, PLpgSQL_rec * rec);
+ static void check_expr(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr);
+ static void assign_tupdesc_row_or_rec(PLpgSQL_checkstate * cstate,
+ PLpgSQL_row * row, PLpgSQL_rec * rec,
+ TupleDesc tupdesc);
+ static void assign_tupdesc_dno(PLpgSQL_checkstate * cstate, int varno, TupleDesc tupdesc);
+ static TupleDesc expr_get_desc(PLpgSQL_checkstate * cstate,
+ PLpgSQL_expr * query,
+ bool use_element_type,
+ bool expand_record,
+ bool is_expression);
+ static void init_datum(PLpgSQL_checkstate * cstate, int varno);
+ static void check_stmts(PLpgSQL_checkstate * cstate, List * stmts);
+ static void check_stmt(PLpgSQL_checkstate * cstate, PLpgSQL_stmt * stmt);
+ static void prepare_expr(PLpgSQL_checkstate * cstate,
+ PLpgSQL_expr * expr, int cursorOptions);
+ static void check_assignment(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr,
+ PLpgSQL_rec * targetrec, PLpgSQL_row * targetrow,
+ int targetdno);
+ static void check_element_assignment(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr,
+ PLpgSQL_rec * targetrec, PLpgSQL_row * targetrow,
+ int targetdno);
+ static void check_assignment_guts(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr,
+ PLpgSQL_rec * targetrec, PLpgSQL_row * targetrow,
+ int targetdno, bool use_element_type, bool is_expression);
+ static void checker_error_edata(PLpgSQL_checkstate * cstate, ErrorData * edata);
+
+ static void checker_error(PLpgSQL_checkstate * cstate,
+ int sqlerrcode,
+ const char *message, const char *detail,
+ const char *hint, const char *level,
+ int position, const char *query,
+ const char *context);
+
+ static void cstate_setup(PLpgSQL_checkstate * cstate,
+ TupleDesc tupdesc, Tuplestorestate * tupstore,
+ bool fatal_errors, int format);
+ static void cstate_flush(PLpgSQL_checkstate * cstate);
+ static void destroy_cstate(PLpgSQL_checkstate * cstate);
+ static void function_check(PLpgSQL_function * func, FunctionCallInfo fcinfo,
+ PLpgSQL_execstate * estate, PLpgSQL_checkstate * cstate);
+ static void trigger_check(PLpgSQL_function * func,
+ TriggerData * trigdata,
+ PLpgSQL_execstate * estate, PLpgSQL_checkstate * cstate);
+ static int load_configuration(HeapTuple procTuple, bool * reload_config);
+
+ /*
+ * Top checker function
+ *
+ */
+ void
+ plpgsql_function_check(HeapTuple procTuple, Oid relid,
+ TupleDesc tupdesc,
+ Tuplestorestate * tupstore,
+ bool fatal_errors, int format)
+ {
+ PLpgSQL_checkstate cstate;
+ PLpgSQL_function *volatile function = NULL;
+ int save_nestlevel = 0;
+ bool reload_config;
+ Oid funcoid;
+ FunctionCallInfoData fake_fcinfo;
+ FmgrInfo flinfo;
+ TriggerData trigdata;
+ int rc;
+ ResourceOwner oldowner;
+ MemoryContext oldCxt;
+ PLpgSQL_execstate *cur_estate = NULL;
+ int numargs;
+ Oid *argtypes;
+ char **argnames;
+ char *argmodes;
+ int i;
+
+ funcoid = HeapTupleGetOid(procTuple);
+
+ /*
+ * Connect to SPI manager
+ */
+ if ((rc = SPI_connect()) != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed: %s", SPI_result_code_string(rc));
+
+ plpgsql_setup_fake_fcinfo(&flinfo, &fake_fcinfo, &trigdata, NULL, funcoid, OidIsValid(relid), false);
+ cstate_setup(&cstate, tupdesc, tupstore, fatal_errors, format);
+
+ if (OidIsValid(relid))
+ trigdata.tg_relation = relation_open(relid, AccessShareLock);
+
+ numargs = get_func_arg_info(procTuple,
+ &argtypes, &argnames, &argmodes);
+
+ if (argnames != NULL) {
+ for (i = 0; i < numargs; i++) {
+ if (argnames[i][0] != '\0')
+ cstate.argnames = lappend(cstate.argnames, argnames[i]);
+ }
+ }
+ oldCxt = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PG_TRY();
+ {
+ PLpgSQL_execstate estate;
+
+ BeginInternalSubTransaction(NULL);
+ MemoryContextSwitchTo(oldCxt);
+
+ save_nestlevel = load_configuration(procTuple, &reload_config);
+
+ /* Get a compiled function */
+ function = plpgsql_compile(&fake_fcinfo, false);
+
+ plpgsql_estate_setup(&estate, function, (ReturnSetInfo *) fake_fcinfo.resultinfo);
+ cstate.estate = &estate;
+
+ /* Must save and restore prior value of cur_estate */
+ cur_estate = function->cur_estate;
+
+ /*
+ * Mark the function as busy, so it can't be deleted from
+ * under us
+ */
+ function->use_count++;
+
+ /* Create a fake runtime environment and process check */
+ if (!OidIsValid(relid))
+ function_check(function, &fake_fcinfo, &estate, &cstate);
+ else
+ trigger_check(function, &trigdata, &estate, &cstate);
+
+ /*
+ * reload back a GUC. XXX: isn't this done automatically by
+ * subxact rollback?
+ */
+ if (reload_config)
+ AtEOXact_GUC(true, save_nestlevel);
+
+ plpgsql_destroy_econtext(&estate);
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ SPI_restore_connection();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ MemoryContextSwitchTo(oldCxt);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ checker_error_edata(&cstate, edata);
+ MemoryContextSwitchTo(oldCxt);
+ /* reconnect spi */
+ SPI_restore_connection();
+ }
+ PG_END_TRY();
+
+ if (function) {
+ function->cur_estate = cur_estate;
+ function->use_count--;
+
+ /*
+ * We cannot to preserve instance of this function, because
+ * expressions are not consistent - a tests on simple
+ * expression was be processed newer.
+ */
+ plpgsql_delete_function(function);
+ }
+ if (OidIsValid(relid))
+ relation_close(trigdata.tg_relation, AccessShareLock);
+
+ cstate_flush(&cstate);
+
+ /* Cleanup temporary memory */
+ destroy_cstate(&cstate);
+
+ /*
+ * Disconnect from SPI manager
+ */
+ if ((rc = SPI_finish()) != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed: %s", SPI_result_code_string(rc));
+ }
+
+ /*
+ * Check function - it prepare variables and starts a prepare plan walker
+ */
+ static void
+ function_check(PLpgSQL_function * func, FunctionCallInfo fcinfo,
+ PLpgSQL_execstate * estate, PLpgSQL_checkstate * cstate)
+ {
+ int i;
+
+ /*
+ * Make local execution copies of all the datums
+ */
+ for (i = 0; i < cstate->estate->ndatums; i++)
+ cstate->estate->datums[i] = copy_plpgsql_datum(func->datums[i]);
+
+ /*
+ * Store the actual call argument values (fake) into the appropriate
+ * variables
+ */
+ for (i = 0; i < func->fn_nargs; i++) {
+ init_datum(cstate, func->fn_argvarnos[i]);
+ }
+
+ /*
+ * Now check the toplevel block of statements
+ */
+ check_stmt(cstate, (PLpgSQL_stmt *) func->action);
+ }
+
+ /*
+ * Check trigger - prepare fake environments for testing trigger
+ *
+ */
+ static void
+ trigger_check(PLpgSQL_function * func, TriggerData * trigdata,
+ PLpgSQL_execstate * estate, PLpgSQL_checkstate * cstate)
+ {
+ PLpgSQL_rec *rec_new, *rec_old;
+ int i;
+
+ /*
+ * Make local execution copies of all the datums
+ */
+ for (i = 0; i < cstate->estate->ndatums; i++)
+ cstate->estate->datums[i] = copy_plpgsql_datum(func->datums[i]);
+
+ /*
+ * Put the OLD and NEW tuples into record variables
+ *
+ * We make the tupdescs available in both records even though only
+ * one may have a value. This allows parsing of record references to
+ * succeed in functions that are used for multiple trigger types.
+ * For example, we might have a test like "if (TG_OP = 'INSERT' and
+ * NEW.foo = 'xyz')", which should parse regardless of the current
+ * trigger type.
+ */
+ rec_new = (PLpgSQL_rec *) (cstate->estate->datums[func->new_varno]);
+ rec_new->freetup = false;
+ rec_new->freetupdesc = false;
+ assign_tupdesc_row_or_rec(cstate, NULL, rec_new, trigdata->tg_relation->rd_att);
+
+ rec_old = (PLpgSQL_rec *) (cstate->estate->datums[func->old_varno]);
+ rec_old->freetup = false;
+ rec_old->freetupdesc = false;
+ assign_tupdesc_row_or_rec(cstate, NULL, rec_old, trigdata->tg_relation->rd_att);
+
+ /*
+ * Assign the special tg_ variables
+ */
+ init_datum(cstate, func->tg_op_varno);
+ init_datum(cstate, func->tg_name_varno);
+ init_datum(cstate, func->tg_when_varno);
+ init_datum(cstate, func->tg_level_varno);
+ init_datum(cstate, func->tg_relid_varno);
+ init_datum(cstate, func->tg_relname_varno);
+ init_datum(cstate, func->tg_table_name_varno);
+ init_datum(cstate, func->tg_table_schema_varno);
+ init_datum(cstate, func->tg_nargs_varno);
+ init_datum(cstate, func->tg_argv_varno);
+
+ /*
+ * Now check the toplevel block of statements
+ */
+ check_stmt(cstate, (PLpgSQL_stmt *) func->action);
+ }
+
+ /*
+ * Verify lvalue It doesn't repeat a checks that are done. Checks a subscript
+ * expressions, verify a validity of record's fields, Returns true, when
+ * target is valid
+ */
+ static void
+ check_target(PLpgSQL_checkstate * cstate, int varno)
+ {
+ PLpgSQL_datum *target = cstate->estate->datums[varno];
+
+ switch (target->dtype) {
+ case PLPGSQL_DTYPE_VAR:
+ case PLPGSQL_DTYPE_REC:
+ /* nothing to check */
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
+ check_row_or_rec(cstate, (PLpgSQL_row *) target, NULL);
+ break;
+
+ case PLPGSQL_DTYPE_RECFIELD:
+ {
+ PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) target;
+ PLpgSQL_rec *rec;
+ int fno;
+
+ rec = (PLpgSQL_rec *) (cstate->estate->datums[recfield->recparentno]);
+
+ /*
+ * Check that there is already a tuple in the record.
+ * We need that because records don't have any
+ * predefined field structure.
+ */
+ if (!HeapTupleIsValid(rec->tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("record \"%s\" is not assigned to tuple structure",
+ rec->refname)));
+
+ /*
+ * Get the number of the records field to change and
+ * the number of attributes in the tuple. Note:
+ * disallow system column names because the code
+ * below won't cope.
+ */
+ fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
+ if (fno <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("record \"%s\" has no field \"%s\"",
+ rec->refname, recfield->fieldname)));
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ARRAYELEM:
+ {
+ /*
+ * Target is an element of an array
+ */
+ int nsubscripts;
+ Oid arrayelemtypeid;
+ Oid arraytypeid;
+
+ /*
+ * To handle constructs like x[1][2] := something, we
+ * have to be prepared to deal with a chain of
+ * arrayelem datums. Chase back to find the base
+ * array datum, and save the subscript expressions as
+ * we go. (We are scanning right to left here, but
+ * want to evaluate the subscripts left-to-right to
+ * minimize surprises.)
+ */
+ nsubscripts = 0;
+ do {
+ PLpgSQL_arrayelem *arrayelem = (PLpgSQL_arrayelem *) target;
+
+ if (nsubscripts++ >= MAXDIM)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("number of array dimensions (%d) exceeds the maximum allowed (%d)",
+ nsubscripts + 1, MAXDIM)));
+
+ /* Validate expression. */
+ /* XXX is_expression */
+ check_expr(cstate, arrayelem->subscript);
+
+ target = cstate->estate->datums[arrayelem->arrayparentno];
+ } while (target->dtype == PLPGSQL_DTYPE_ARRAYELEM);
+
+ /*
+ * If target is domain over array, reduce to base
+ * type
+ */
+ arraytypeid = exec_get_datum_type(cstate->estate, target);
+ arraytypeid = getBaseType(arraytypeid);
+
+ arrayelemtypeid = get_element_type(arraytypeid);
+
+ if (!OidIsValid(arrayelemtypeid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("subscripted object is not an array")));
+ }
+ break;
+ }
+ }
+
+ /*
+ * Check composed lvalue There is nothing to check on rec variables
+ */
+ static void
+ check_row_or_rec(PLpgSQL_checkstate * cstate, PLpgSQL_row * row, PLpgSQL_rec * rec)
+ {
+ int fnum;
+
+ /* there are nothing to check on rec now */
+ if (row != NULL) {
+ for (fnum = 0; fnum < row->nfields; fnum++) {
+ /* skip dropped columns */
+ if (row->varnos[fnum] < 0)
+ continue;
+
+ check_target(cstate, row->varnos[fnum]);
+ }
+ }
+ }
+
+ /*
+ * Generate a prepared plan - this is simplified copy from pl_exec.c Is not
+ * necessary to check simple plan, returns true, when expression is
+ * succesfully prepared.
+ */
+ static void
+ prepare_expr(PLpgSQL_checkstate * cstate,
+ PLpgSQL_expr * expr, int cursorOptions)
+ {
+ SPIPlanPtr plan;
+
+ if (expr->plan != NULL)
+ return; /* already checked */
+
+ /*
+ * The grammar can't conveniently set expr->func while building the
+ * parse tree, so make sure it's set before parser hooks need it.
+ */
+ expr->func = cstate->estate->func;
+
+ /*
+ * Generate and save the plan
+ */
+ plan = SPI_prepare_params(expr->query,
+ (ParserSetupHook) plpgsql_parser_setup,
+ (void *) expr,
+ cursorOptions);
+
+ if (plan == NULL) {
+ /* Some SPI errors deserve specific error messages */
+ switch (SPI_result) {
+ case SPI_ERROR_COPY:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot COPY to/from client in PL/pgSQL")));
+ break;
+
+ case SPI_ERROR_TRANSACTION:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot begin/end transactions in PL/pgSQL"),
+ errhint("Use a BEGIN block with an EXCEPTION clause instead.")));
+ break;
+
+ default:
+ elog(ERROR, "SPI_prepare_params failed for \"%s\": %s",
+ expr->query, SPI_result_code_string(SPI_result));
+ }
+ }
+ expr->plan = SPI_saveplan(plan);
+ SPI_freeplan(plan);
+ }
+
+ /*
+ * Verify a expression
+ */
+ static void
+ check_expr(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr)
+ {
+ if (expr)
+ check_assignment_guts(cstate, expr, NULL, NULL, -1, false, false);
+ }
+
+ /*
+ * Verify an assignment of 'expr' to 'target'
+ */
+ static void
+ check_assignment(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr,
+ PLpgSQL_rec * targetrec, PLpgSQL_row * targetrow,
+ int targetdno)
+ {
+ bool is_expression = (targetrec == NULL && targetrow == NULL);
+
+ check_assignment_guts(cstate, expr, targetrec, targetrow, targetdno, false,
+ is_expression);
+ }
+
+ static void
+ check_element_assignment(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr,
+ PLpgSQL_rec * targetrec, PLpgSQL_row * targetrow,
+ int targetdno)
+ {
+ bool is_expression = (targetrec == NULL && targetrow == NULL);
+
+ check_assignment_guts(cstate, expr, targetrec, targetrow, targetdno, true,
+ is_expression);
+ }
+
+ static void
+ check_assignment_guts(PLpgSQL_checkstate * cstate, PLpgSQL_expr * expr,
+ PLpgSQL_rec * targetrec, PLpgSQL_row * targetrow,
+ int targetdno, bool use_element_type, bool is_expression)
+ {
+ ResourceOwner oldowner;
+ MemoryContext oldCxt = CurrentMemoryContext;
+ TupleDesc tupdesc;
+
+ oldowner = CurrentResourceOwner;
+ BeginInternalSubTransaction(NULL);
+ MemoryContextSwitchTo(oldCxt);
+
+ PG_TRY();
+ {
+ prepare_expr(cstate, expr, 0);
+ tupdesc = expr_get_desc(cstate, expr, use_element_type, true, is_expression);
+ if (tupdesc) {
+ if (targetrow != NULL || targetrec != NULL)
+ assign_tupdesc_row_or_rec(cstate, targetrow, targetrec, tupdesc);
+ if (targetdno != -1)
+ assign_tupdesc_dno(cstate, targetdno, tupdesc);
+
+ if (targetrow) {
+ if (targetrow->nfields > tupdesc->natts)
+ checker_error(cstate,
+ 0,
+ "too few attributies for target variables",
+ "There are more target variables than output columns in query.",
+ "Check target variables in SELECT INTO statement.",
+ "warning",
+ 0, NULL, NULL);
+ else if (targetrow->nfields < tupdesc->natts)
+ checker_error(cstate,
+ 0,
+ "too many attributies for target variables",
+ "There are less target variables than output columns in query.",
+ "Check target variables in SELECT INTO statement",
+ "warning",
+ 0, NULL, NULL);
+ }
+ ReleaseTupleDesc(tupdesc);
+ }
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ SPI_restore_connection();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ MemoryContextSwitchTo(oldCxt);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ /*
+ * If fatal_errors is true, we just propagate the error up to
+ * the highest level. Otherwise the error is appended to our
+ * current list of errors, and we continue checking.
+ */
+ if (cstate->fatal_errors)
+ ReThrowError(edata);
+ else
+ checker_error_edata(cstate, edata);
+ MemoryContextSwitchTo(oldCxt);
+
+ /* reconnect spi */
+ SPI_restore_connection();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * We have to assign TupleDesc to all used record variables step by step. We
+ * would to use a exec routines for query preprocessing, so we must to create
+ * a typed NULL value, and this value is assigned to record variable.
+ */
+ static void
+ assign_tupdesc_row_or_rec(PLpgSQL_checkstate * cstate,
+ PLpgSQL_row * row, PLpgSQL_rec * rec,
+ TupleDesc tupdesc)
+ {
+ bool *nulls;
+ HeapTuple tup;
+
+ if (tupdesc == NULL) {
+ checker_error(cstate,
+ 0,
+ "tuple descriptor is empty", NULL, NULL,
+ "warning",
+ 0, NULL, NULL);
+ return;
+ }
+ /*
+ * row variable has assigned TupleDesc already, so don't be processed
+ * here
+ */
+ if (rec != NULL) {
+ PLpgSQL_rec *target = (PLpgSQL_rec *) (cstate->estate->datums[rec->dno]);
+
+ if (target->freetup)
+ heap_freetuple(target->tup);
+
+ if (rec->freetupdesc)
+ FreeTupleDesc(target->tupdesc);
+
+ /* initialize rec by NULLs */
+ nulls = (bool *) palloc(tupdesc->natts * sizeof(bool));
+ memset(nulls, true, tupdesc->natts * sizeof(bool));
+
+ target->tupdesc = CreateTupleDescCopy(tupdesc);
+ target->freetupdesc = true;
+
+ tup = heap_form_tuple(tupdesc, NULL, nulls);
+ if (HeapTupleIsValid(tup)) {
+ target->tup = tup;
+ target->freetup = true;
+ } else
+ elog(ERROR, "cannot to build valid composite value");
+ }
+ }
+
+ /*
+ * Assign a tuple descriptor to variable specified by dno
+ */
+ static void
+ assign_tupdesc_dno(PLpgSQL_checkstate * cstate, int varno, TupleDesc tupdesc)
+ {
+ PLpgSQL_datum *target = cstate->estate->datums[varno];
+
+ if (target->dtype == PLPGSQL_DTYPE_REC)
+ assign_tupdesc_row_or_rec(cstate, NULL, (PLpgSQL_rec *) target, tupdesc);
+ }
+
+ /*
+ * Returns a tuple descriptor based on existing plan, When error is detected
+ * returns null.
+ */
+ static TupleDesc
+ expr_get_desc(PLpgSQL_checkstate * cstate,
+ PLpgSQL_expr * query,
+ bool use_element_type,
+ bool expand_record,
+ bool is_expression)
+ {
+ TupleDesc tupdesc = NULL;
+ CachedPlanSource *plansource = NULL;
+
+ if (query->plan != NULL) {
+ SPIPlanPtr plan = query->plan;
+
+ if (plan == NULL || plan->magic != _SPI_PLAN_MAGIC)
+ elog(ERROR, "cached plan is not valid plan");
+
+ if (list_length(plan->plancache_list) != 1)
+ elog(ERROR, "plan is not single execution plan");
+
+ plansource = (CachedPlanSource *) linitial(plan->plancache_list);
+
+ if (!plansource->resultDesc) {
+ if (is_expression)
+ elog(ERROR, "query returns no result");
+ else
+ return NULL;
+ }
+ tupdesc = CreateTupleDescCopy(plansource->resultDesc);
+ } else
+ elog(ERROR, "there are no plan for query: \"%s\"",
+ query->query);
+
+ /*
+ * try to get a element type, when result is a array (used with
+ * FOREACH ARRAY stmt)
+ */
+ if (use_element_type) {
+ Oid elemtype;
+ TupleDesc elemtupdesc;
+
+ /* result should be a array */
+ if (is_expression && tupdesc->natts != 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("query \"%s\" returned %d columns",
+ query->query,
+ tupdesc->natts)));
+
+ /* check the type of the expression - must be an array */
+ elemtype = get_element_type(tupdesc->attrs[0]->atttypid);
+ if (!OidIsValid(elemtype)) {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FOREACH expression must yield an array, not type %s",
+ format_type_be(tupdesc->attrs[0]->atttypid))));
+ FreeTupleDesc(tupdesc);
+ }
+ /* we can't know typmod now */
+ elemtupdesc = lookup_rowtype_tupdesc_noerror(elemtype, -1, true);
+ if (elemtupdesc != NULL) {
+ FreeTupleDesc(tupdesc);
+ tupdesc = CreateTupleDescCopy(elemtupdesc);
+ ReleaseTupleDesc(elemtupdesc);
+ } else
+ /* XXX: should be a warning? */
+ ereport(ERROR,
+ (errmsg("cannot to identify real type for record type variable")));
+ }
+ if (is_expression && tupdesc->natts != 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("query \"%s\" returned %d columns",
+ query->query,
+ tupdesc->natts)));
+
+ /*
+ * One spacial case is when record is assigned to composite type,
+ * then we should to unpack composite type.
+ */
+ if (tupdesc->tdtypeid == RECORDOID &&
+ tupdesc->tdtypmod == -1 &&
+ tupdesc->natts == 1 && expand_record) {
+ TupleDesc unpack_tupdesc;
+
+ unpack_tupdesc = lookup_rowtype_tupdesc_noerror(tupdesc->attrs[0]->atttypid,
+ tupdesc->attrs[0]->atttypmod,
+ true);
+ if (unpack_tupdesc != NULL) {
+ FreeTupleDesc(tupdesc);
+ tupdesc = CreateTupleDescCopy(unpack_tupdesc);
+ ReleaseTupleDesc(unpack_tupdesc);
+ }
+ }
+ /*
+ * There is special case, when returned tupdesc contains only unpined
+ * record: rec := func_with_out_parameters(). IN this case we must to
+ * dig more deep - we have to find oid of function and get their
+ * parameters,
+ *
+ * This is support for assign statement recvar :=
+ * func_with_out_parameters(..)
+ *
+ * XXX: Why don't we always do that?
+ */
+ if (tupdesc->tdtypeid == RECORDOID &&
+ tupdesc->tdtypmod == -1 &&
+ tupdesc->natts == 1 &&
+ tupdesc->attrs[0]->atttypid == RECORDOID &&
+ tupdesc->attrs[0]->atttypmod == -1 &&
+ expand_record) {
+ PlannedStmt *_stmt;
+ Plan *_plan;
+ TargetEntry *tle;
+ CachedPlan *cplan;
+
+ /*
+ * When tupdesc is related to unpined record, we will try to
+ * check plan if it is just function call and if it is then
+ * we can try to derive a tupledes from function's
+ * description.
+ */
+ cplan = GetCachedPlan(plansource, NULL, true);
+ _stmt = (PlannedStmt *) linitial(cplan->stmt_list);
+
+ if (IsA(_stmt, PlannedStmt) && _stmt->commandType == CMD_SELECT) {
+ _plan = _stmt->planTree;
+ if (IsA(_plan, Result) && list_length(_plan->targetlist) == 1) {
+ tle = (TargetEntry *) linitial(_plan->targetlist);
+ if (((Node *) tle->expr)->type == T_FuncExpr) {
+ FuncExpr *fn = (FuncExpr *) tle->expr;
+ FmgrInfo flinfo;
+ FunctionCallInfoData fcinfo;
+ TupleDesc rd;
+ Oid rt;
+
+ fmgr_info(fn->funcid, &flinfo);
+ flinfo.fn_expr = (Node *) fn;
+ fcinfo.flinfo = &flinfo;
+
+ get_call_result_type(&fcinfo, &rt, &rd);
+ if (rd == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("function does not return composite type, is not possible to identify composite type")));
+
+ FreeTupleDesc(tupdesc);
+ BlessTupleDesc(rd);
+
+ tupdesc = rd;
+ }
+ }
+ }
+ ReleaseCachedPlan(cplan, true);
+ }
+ return tupdesc;
+ }
+
+ /*
+ * Ensure check for all statements in list
+ */
+ void
+ check_stmts(PLpgSQL_checkstate * cstate, List * stmts)
+ {
+ ListCell *lc;
+
+ foreach(lc, stmts) {
+ check_stmt(cstate, (PLpgSQL_stmt *) lfirst(lc));
+ }
+ }
+
+ /*
+ * returns refname of PLpgSQL_datum
+ */
+ static char *
+ datum_get_refname(PLpgSQL_datum * d)
+ {
+ switch (d->dtype) {
+ case PLPGSQL_DTYPE_VAR:
+ return ((PLpgSQL_var *) d)->refname;
+
+ case PLPGSQL_DTYPE_ROW:
+ return ((PLpgSQL_row *) d)->refname;
+
+ case PLPGSQL_DTYPE_REC:
+ return ((PLpgSQL_rec *) d)->refname;
+
+ default:
+ return NULL;
+ }
+ }
+
+ /*
+ * walk over all statements
+ */
+ void
+ check_stmt(PLpgSQL_checkstate * cstate, PLpgSQL_stmt * stmt)
+ {
+ TupleDesc tupdesc = NULL;
+ PLpgSQL_function *func;
+ ListCell *l;
+ ResourceOwner oldowner;
+ MemoryContext oldCxt = CurrentMemoryContext;
+
+ if (stmt == NULL)
+ return;
+
+ cstate->estate->err_stmt = stmt;
+ func = cstate->estate->func;
+
+ oldowner = CurrentResourceOwner;
+ BeginInternalSubTransaction(NULL);
+ MemoryContextSwitchTo(oldCxt);
+
+ PG_TRY();
+ {
+ switch ((enum PLpgSQL_stmt_types) stmt->cmd_type) {
+ case PLPGSQL_STMT_BLOCK:
+ {
+ PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) stmt;
+ int i;
+ PLpgSQL_datum *d;
+
+ for (i = 0; i < stmt_block->n_initvars; i++) {
+ char *refname;
+
+ d = func->datums[stmt_block->initvarnos[i]];
+
+ if (d->dtype == PLPGSQL_DTYPE_VAR) {
+ PLpgSQL_var *var = (PLpgSQL_var *) d;
+
+ check_expr(cstate, var->default_val);
+ }
+ refname = datum_get_refname(d);
+ if (refname != NULL) {
+ ListCell *l;
+
+ foreach(l, cstate->argnames) {
+ char *argname = (char *) lfirst(l);
+
+ if (strcmp(argname, refname) == 0) {
+ StringInfoData str;
+
+ initStringInfo(&str);
+ appendStringInfo(&str, "parameter \"%s\" is overlapped",
+ refname);
+
+ checker_error(cstate,
+ 0,
+ str.data,
+ "Local variable overlap function parameter.",
+ NULL,
+ "warning",
+ 0, NULL, NULL);
+ pfree(str.data);
+ }
+ }
+ }
+ }
+
+ check_stmts(cstate, stmt_block->body);
+
+ if (stmt_block->exceptions) {
+ foreach(l, stmt_block->exceptions->exc_list) {
+ check_stmts(cstate, ((PLpgSQL_exception *) lfirst(l))->action);
+ }
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_ASSIGN:
+ {
+ PLpgSQL_stmt_assign *stmt_assign = (PLpgSQL_stmt_assign *) stmt;
+
+ check_target(cstate, stmt_assign->varno);
+
+ /* prepare plan if desn't exist yet */
+ check_assignment(cstate, stmt_assign->expr, NULL, NULL,
+ stmt_assign->varno);
+ /*
+ * XXX: i thínk I lost some args to
+ * prepare_expr here
+ */
+ }
+ break;
+
+ case PLPGSQL_STMT_IF:
+ {
+ PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt;
+ ListCell *l;
+
+ check_expr(cstate, stmt_if->cond);
+ check_stmts(cstate, stmt_if->then_body);
+ foreach(l, stmt_if->elsif_list) {
+ PLpgSQL_if_elsif *elif = (PLpgSQL_if_elsif *) lfirst(l);
+
+ check_expr(cstate, elif->cond);
+ check_stmts(cstate, elif->stmts);
+ }
+
+ check_stmts(cstate, stmt_if->else_body);
+ }
+ break;
+
+ case PLPGSQL_STMT_CASE:
+ {
+ PLpgSQL_stmt_case *stmt_case = (PLpgSQL_stmt_case *) stmt;
+ Oid result_oid;
+
+ if (stmt_case->t_expr != NULL) {
+ PLpgSQL_var *t_var = (PLpgSQL_var *) cstate->estate->datums[stmt_case->t_varno];
+
+ /*
+ * we need to set hidden variable
+ * type
+ */
+ prepare_expr(cstate, stmt_case->t_expr, 0);
+ tupdesc = expr_get_desc(cstate,
+ stmt_case->t_expr,
+ false, /* no element type */
+ true, /* expand record */
+ true); /* is expression */
+ result_oid = tupdesc->attrs[0]->atttypid;
+
+ /*
+ * When expected datatype is
+ * different from real, change it.
+ * Note that what we're modifying
+ * here is an execution copy of the
+ * datum, so this doesn't affect the
+ * originally stored function parse
+ * tree.
+ */
+ if (t_var->datatype->typoid != result_oid)
+ t_var->datatype = plpgsql_build_datatype(result_oid,
+ -1,
+ cstate->estate->func->fn_input_collation);
+ ReleaseTupleDesc(tupdesc);
+ }
+ foreach(l, stmt_case->case_when_list) {
+ PLpgSQL_case_when *cwt = (PLpgSQL_case_when *) lfirst(l);
+
+ check_expr(cstate, cwt->expr);
+ check_stmts(cstate, cwt->stmts);
+ }
+
+ check_stmts(cstate, stmt_case->else_stmts);
+ }
+ break;
+
+ case PLPGSQL_STMT_LOOP:
+ check_stmts(cstate, ((PLpgSQL_stmt_loop *) stmt)->body);
+ break;
+
+ case PLPGSQL_STMT_WHILE:
+ {
+ PLpgSQL_stmt_while *stmt_while = (PLpgSQL_stmt_while *) stmt;
+
+ check_expr(cstate, stmt_while->cond);
+ check_stmts(cstate, stmt_while->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FORI:
+ {
+ PLpgSQL_stmt_fori *stmt_fori = (PLpgSQL_stmt_fori *) stmt;
+
+ check_expr(cstate, stmt_fori->lower);
+ check_expr(cstate, stmt_fori->upper);
+ check_expr(cstate, stmt_fori->step);
+ check_stmts(cstate, stmt_fori->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FORS:
+ {
+ PLpgSQL_stmt_fors *stmt_fors = (PLpgSQL_stmt_fors *) stmt;
+
+ check_row_or_rec(cstate, stmt_fors->row, stmt_fors->rec);
+
+ /* we need to set hidden variable type */
+ check_assignment(cstate, stmt_fors->query,
+ stmt_fors->rec, stmt_fors->row, -1);
+
+ check_stmts(cstate, stmt_fors->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FORC:
+ {
+ PLpgSQL_stmt_forc *stmt_forc = (PLpgSQL_stmt_forc *) stmt;
+ PLpgSQL_var *var = (PLpgSQL_var *) func->datums[stmt_forc->curvar];
+
+ check_row_or_rec(cstate, stmt_forc->row, stmt_forc->rec);
+
+ check_expr(cstate, stmt_forc->argquery);
+
+ if (var->cursor_explicit_expr != NULL)
+ check_assignment(cstate, var->cursor_explicit_expr,
+ stmt_forc->rec, stmt_forc->row, -1);
+
+ check_stmts(cstate, stmt_forc->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_DYNFORS:
+ {
+ PLpgSQL_stmt_dynfors *stmt_dynfors = (PLpgSQL_stmt_dynfors *) stmt;
+
+ if (stmt_dynfors->rec != NULL) {
+ checker_error(cstate,
+ 0,
+ "cannot determinate a result of dynamic SQL",
+ "Cannot to contine in check.",
+ "Don't use dynamic SQL and record type together, when you would check function.",
+ "warning",
+ 0, NULL, NULL);
+
+ /*
+ * don't continue in checking. Behave
+ * should be indeterministic.
+ */
+ break;
+ }
+ check_expr(cstate, stmt_dynfors->query);
+
+ foreach(l, stmt_dynfors->params) {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+
+ check_stmts(cstate, stmt_dynfors->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_FOREACH_A:
+ {
+ PLpgSQL_stmt_foreach_a *stmt_foreach_a = (PLpgSQL_stmt_foreach_a *) stmt;
+
+ check_target(cstate, stmt_foreach_a->varno);
+
+ check_element_assignment(cstate, stmt_foreach_a->expr, NULL, NULL, stmt_foreach_a->varno);
+
+ check_stmts(cstate, stmt_foreach_a->body);
+ }
+ break;
+
+ case PLPGSQL_STMT_EXIT:
+ check_expr(cstate, ((PLpgSQL_stmt_exit *) stmt)->cond);
+ break;
+
+ case PLPGSQL_STMT_PERFORM:
+ check_expr(cstate, ((PLpgSQL_stmt_perform *) stmt)->expr);
+ break;
+
+ case PLPGSQL_STMT_RETURN:
+ check_expr(cstate, ((PLpgSQL_stmt_return *) stmt)->expr);
+ break;
+
+ case PLPGSQL_STMT_RETURN_NEXT:
+ check_expr(cstate, ((PLpgSQL_stmt_return_next *) stmt)->expr);
+ break;
+
+ case PLPGSQL_STMT_RETURN_QUERY:
+ {
+ PLpgSQL_stmt_return_query *stmt_rq = (PLpgSQL_stmt_return_query *) stmt;
+
+ check_expr(cstate, stmt_rq->dynquery);
+
+ check_expr(cstate, stmt_rq->query);
+
+ foreach(l, stmt_rq->params) {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_RAISE:
+ {
+ PLpgSQL_stmt_raise *stmt_raise = (PLpgSQL_stmt_raise *) stmt;
+ ListCell *current_param;
+ char *cp;
+
+ foreach(l, stmt_raise->params) {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+
+ foreach(l, stmt_raise->options) {
+ PLpgSQL_raise_option *opt = (PLpgSQL_raise_option *) lfirst(l);
+
+ check_expr(cstate, opt->expr);
+ }
+
+ current_param = list_head(stmt_raise->params);
+
+ /* ensure any single % has a own parameter */
+ if (stmt_raise->message != NULL) {
+ for (cp = stmt_raise->message; *cp; cp++) {
+ if (cp[0] == '%') {
+ if (cp[1] == '%') {
+ cp++;
+ continue;
+ }
+ if (current_param == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("too few parameters specified for RAISE")));
+
+ current_param = lnext(current_param);
+ }
+ }
+ }
+ if (current_param != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("too many parameters specified for RAISE")));
+ }
+ break;
+
+ case PLPGSQL_STMT_EXECSQL:
+ {
+ PLpgSQL_stmt_execsql *stmt_execsql = (PLpgSQL_stmt_execsql *) stmt;
+
+ if (stmt_execsql->into) {
+ check_row_or_rec(cstate, stmt_execsql->row, stmt_execsql->rec);
+ check_assignment(cstate, stmt_execsql->sqlstmt,
+ stmt_execsql->rec, stmt_execsql->row, -1);
+ } else {
+ /* only statement */
+ check_expr(cstate, stmt_execsql->sqlstmt);
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_DYNEXECUTE:
+ {
+ PLpgSQL_stmt_dynexecute *stmt_dynexecute = (PLpgSQL_stmt_dynexecute *) stmt;
+
+ check_expr(cstate, stmt_dynexecute->query);
+
+ foreach(l, stmt_dynexecute->params) {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+
+ if (stmt_dynexecute->into) {
+ check_row_or_rec(cstate, stmt_dynexecute->row, stmt_dynexecute->rec);
+
+ if (stmt_dynexecute->rec != NULL) {
+ checker_error(cstate,
+ 0,
+ "cannot determinate a result of dynamic SQL",
+ "Cannot to contine in check.",
+ "Don't use dynamic SQL and record type together, when you would check function.",
+ "warning",
+ 0, NULL, NULL);
+
+ /*
+ * don't continue in
+ * checking. Behave should be
+ * indeterministic.
+ */
+ break;
+ }
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_OPEN:
+ {
+ PLpgSQL_stmt_open *stmt_open = (PLpgSQL_stmt_open *) stmt;
+ PLpgSQL_var *var = (PLpgSQL_var *) func->datums[stmt_open->curvar];
+
+ if (var->cursor_explicit_expr)
+ check_expr(cstate, var->cursor_explicit_expr);
+
+ check_expr(cstate, stmt_open->query);
+ check_expr(cstate, stmt_open->argquery);
+ check_expr(cstate, stmt_open->dynquery);
+ foreach(l, stmt_open->params) {
+ check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_GETDIAG:
+ {
+ PLpgSQL_stmt_getdiag *stmt_getdiag = (PLpgSQL_stmt_getdiag *) stmt;
+ ListCell *lc;
+
+ foreach(lc, stmt_getdiag->diag_items) {
+ PLpgSQL_diag_item *diag_item = (PLpgSQL_diag_item *) lfirst(lc);
+
+ check_target(cstate, diag_item->target);
+ }
+ }
+ break;
+
+ case PLPGSQL_STMT_FETCH:
+ {
+ PLpgSQL_stmt_fetch *stmt_fetch = (PLpgSQL_stmt_fetch *) stmt;
+ PLpgSQL_var *var = (PLpgSQL_var *) (cstate->estate->datums[stmt_fetch->curvar]);
+
+ check_row_or_rec(cstate, stmt_fetch->row, stmt_fetch->rec);
+
+ if (var != NULL && var->cursor_explicit_expr != NULL)
+ check_assignment(cstate, var->cursor_explicit_expr,
+ stmt_fetch->rec, stmt_fetch->row, -1);
+ }
+ break;
+
+ case PLPGSQL_STMT_CLOSE:
+ break;
+
+ default:
+ elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
+ }
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ SPI_restore_connection();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ MemoryContextSwitchTo(oldCxt);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldCxt);
+ CurrentResourceOwner = oldowner;
+
+ /*
+ * If fatal_errors is true, we just propagate the error up to
+ * the highest level. Otherwise the error is appended to our
+ * current list of errors, and we continue checking.
+ */
+ if (cstate->fatal_errors)
+ ReThrowError(edata);
+ else
+ checker_error_edata(cstate, edata);
+ MemoryContextSwitchTo(oldCxt);
+
+ /* reconnect spi */
+ SPI_restore_connection();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Initialize plpgsql datum to NULL. This routine is used only for function
+ * and trigger parameters so it should not support all dtypes.
+ */
+ static void
+ init_datum(PLpgSQL_checkstate * cstate, int dno)
+ {
+ switch (cstate->estate->datums[dno]->dtype) {
+ case PLPGSQL_DTYPE_VAR:
+ {
+ PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[dno];
+
+ var->value = (Datum) 0;
+ var->isnull = true;
+ var->freeval = false;
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
+ {
+ PLpgSQL_row *row = (PLpgSQL_row *) cstate->estate->datums[dno];
+ int fnum;
+
+ for (fnum = 0; fnum < row->nfields; fnum++) {
+ if (row->varnos[fnum] < 0)
+ continue; /* skip dropped column
+ * in row struct */
+
+ init_datum(cstate, row->varnos[fnum]);
+ }
+ }
+ break;
+
+ default:
+ elog(ERROR, "unexpected dtype: %d", cstate->estate->datums[dno]->dtype);
+ }
+ }
+
+ /*
+ * forward edata out from checker
+ */
+ static void
+ checker_error_edata(PLpgSQL_checkstate * cstate,
+ ErrorData * edata)
+ {
+ checker_error(cstate,
+ edata->sqlerrcode,
+ edata->message,
+ edata->detail,
+ edata->hint,
+ "error",
+ edata->internalpos,
+ edata->internalquery,
+ edata->context);
+ }
+
+ /*
+ * Append text line (StringInfo) to tuple store.
+ */
+ static void
+ checker_store_string(PLpgSQL_checkstate * cstate, StringInfo str)
+ {
+ Datum value;
+ bool isnull = false;
+ HeapTuple tuple;
+
+ value = PointerGetDatum(cstring_to_text_with_len(str->data, str->len));
+ tuple = heap_form_tuple(cstate->tupdesc, &value, &isnull);
+
+ tuplestore_puttuple(cstate->tuple_store, tuple);
+
+ resetStringInfo(str);
+ }
+
+ /*
+ * prepare PLpgSQL_checkstate structure
+ */
+ static void
+ cstate_setup(PLpgSQL_checkstate * cstate,
+ TupleDesc tupdesc,
+ Tuplestorestate * tupstore,
+ bool fatal_errors,
+ int format)
+ {
+ cstate->estate = NULL;
+ cstate->tupdesc = tupdesc;
+ cstate->tuple_store = tupstore;
+ cstate->fatal_errors = fatal_errors;
+ cstate->format = format;
+ cstate->argnames = NIL;
+
+ if (format != PLPGSQL_CHECK_FORMAT_PLAIN)
+ cstate->sinfo = makeStringInfo();
+ else
+ cstate->sinfo = NULL;
+
+ /* put initial tag */
+ if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
+ appendStringInfoString(cstate->sinfo, "<Function>\n");
+ }
+
+ /*
+ * finishig a result stored in cstate
+ */
+ static void
+ cstate_flush(PLpgSQL_checkstate * cstate)
+ {
+ if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
+ appendStringInfoString(cstate->sinfo, "</Function>");
+
+ if (cstate->format != PLPGSQL_CHECK_FORMAT_PLAIN)
+ checker_store_string(cstate, cstate->sinfo);
+ }
+
+ /*
+ * release check state
+ */
+ static void
+ destroy_cstate(PLpgSQL_checkstate * cstate)
+ {
+ if (cstate->sinfo != NULL) {
+ if (cstate->sinfo->data != NULL)
+ pfree(cstate->sinfo->data);
+ pfree(cstate->sinfo);
+
+ cstate->sinfo = NULL;
+ }
+ }
+
+ /*
+ * collects errors and warnings in plain text format
+ */
+ static void
+ checker_error_plain(PLpgSQL_checkstate * cstate,
+ int sqlerrcode,
+ const char *message,
+ const char *detail,
+ const char *hint,
+ const char *level,
+ int position,
+ const char *query,
+ const char *context)
+ {
+ StringInfoData sinfo;
+
+ initStringInfo(&sinfo);
+
+ Assert(message != NULL);
+ Assert(level != NULL);
+
+ if (cstate->estate && cstate->estate->err_stmt != NULL)
+ appendStringInfo(&sinfo, "%s:%s:%d:%s:%s",
+ level,
+ unpack_sql_state(sqlerrcode),
+ cstate->estate->err_stmt->lineno,
+ plpgsql_stmt_typename(cstate->estate->err_stmt),
+ message);
+ else
+ appendStringInfo(&sinfo, "%s:%s:%s",
+ level,
+ unpack_sql_state(sqlerrcode),
+ message);
+
+ checker_store_string(cstate, &sinfo);
+
+ if (query != NULL) {
+ char *query_line; /* pointer to beginning of
+ * current line */
+ int line_caret_pos;
+ bool is_first_line = true;
+ char *_query = pstrdup(query);
+ char *ptr;
+
+ ptr = _query;
+ query_line = ptr;
+ line_caret_pos = position;
+
+ while (*ptr != '\0') {
+ /* search end of lines and replace '\n' by '\0' */
+ if (*ptr == '\n') {
+ *ptr = '\0';
+ if (is_first_line) {
+ appendStringInfo(&sinfo, "Query: %s", query_line);
+ is_first_line = false;
+ } else
+ appendStringInfo(&sinfo, " %s", query_line);
+
+ checker_store_string(cstate, &sinfo);
+
+ if (line_caret_pos > 0 && position == 0) {
+ appendStringInfo(&sinfo, "-- %*s",
+ line_caret_pos, "^");
+ checker_store_string(cstate, &sinfo);
+ line_caret_pos = 0;
+ }
+ /* store caret position offset for next line */
+ if (position > 1)
+ line_caret_pos = position - 1;
+
+ /* go to next line */
+ query_line = ptr + 1;
+ }
+ ptr += pg_mblen(ptr);
+
+ if (position > 0)
+ position--;
+ }
+
+ /* flush last line */
+ if (query_line != NULL) {
+ if (is_first_line)
+ appendStringInfo(&sinfo, "Query: %s", query_line);
+ else
+ appendStringInfo(&sinfo, " %s", query_line);
+
+ checker_store_string(cstate, &sinfo);
+
+ if (line_caret_pos > 0 && position == 0) {
+ appendStringInfo(&sinfo, "-- %*s",
+ line_caret_pos, "^");
+ checker_store_string(cstate, &sinfo);
+ }
+ }
+ pfree(_query);
+ }
+ if (detail != NULL) {
+ appendStringInfo(&sinfo, "Detail: %s", detail);
+ checker_store_string(cstate, &sinfo);
+ }
+ if (hint != NULL) {
+ appendStringInfo(&sinfo, "Hint: %s", hint);
+ checker_store_string(cstate, &sinfo);
+ }
+ if (context != NULL) {
+ appendStringInfo(&sinfo, "Context: %s", context);
+ checker_store_string(cstate, &sinfo);
+ }
+ pfree(sinfo.data);
+ }
+
+ /*
+ * checker_error_xml formats and collects a identifided issues
+ */
+ static void
+ checker_error_xml(PLpgSQL_checkstate * cstate,
+ int sqlerrcode,
+ const char *message,
+ const char *detail,
+ const char *hint,
+ const char *level,
+ int position,
+ const char *query,
+ const char *context)
+ {
+ Assert(message != NULL);
+ Assert(level != NULL);
+
+ /* there have to be prepared StringInfo for result */
+ Assert(cstate->sinfo != NULL);
+
+ /* flush tag */
+ appendStringInfoString(cstate->sinfo, " <Issue>\n");
+
+ appendStringInfo(cstate->sinfo, " <Level>%s</level>\n", level);
+ appendStringInfo(cstate->sinfo, " <Sqlstate>%s</Sqlstate>\n",
+ unpack_sql_state(sqlerrcode));
+ appendStringInfo(cstate->sinfo, " <Message>%s</Message>\n",
+ escape_xml(message));
+ if (cstate->estate->err_stmt != NULL)
+ appendStringInfo(cstate->sinfo, " <Stmt lineno=\"%d\">%s</Stmt>\n",
+ cstate->estate->err_stmt->lineno,
+ plpgsql_stmt_typename(cstate->estate->err_stmt));
+ if (hint != NULL)
+ appendStringInfo(cstate->sinfo, " <Hint>%s</Hint>\n",
+ escape_xml(hint));
+ if (detail != NULL)
+ appendStringInfo(cstate->sinfo, " <Detail>%s</Detail>\n",
+ escape_xml(detail));
+ if (query != NULL)
+ appendStringInfo(cstate->sinfo, " <Query position=\"%d\">%s</Query>\n",
+ position, escape_xml(query));
+ if (context != NULL)
+ appendStringInfo(cstate->sinfo, " <Context>%s</Context>\n",
+ escape_xml(context));
+
+ /* flush closing tag */
+ appendStringInfoString(cstate->sinfo, " </Issue>\n");
+ }
+
+ /*
+ * checker_error formats and collects a identifided issues
+ */
+ static void
+ checker_error(PLpgSQL_checkstate * cstate,
+ int sqlerrcode,
+ const char *message,
+ const char *detail,
+ const char *hint,
+ const char *level,
+ int position,
+ const char *query,
+ const char *context)
+ {
+ if (cstate->format == PLPGSQL_CHECK_FORMAT_PLAIN)
+ checker_error_plain(cstate, sqlerrcode,
+ message, detail, hint, level,
+ position, query,
+ context);
+ else if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
+ checker_error_xml(cstate, sqlerrcode,
+ message, detail, hint, level,
+ position, query,
+ context);
+ }
+
+ /*
+ * Loads function's configuration
+ *
+ * Before checking function we have to load configuration related to
+ * function. This is function manager job, but we don't use it for checking.
+ */
+ static int
+ load_configuration(HeapTuple procTuple, bool * reload_config)
+ {
+ Datum datum;
+ bool isnull;
+ int new_nest_level;
+
+ *reload_config = false;
+ new_nest_level = 0;
+
+ datum = SysCacheGetAttr(PROCOID, procTuple, Anum_pg_proc_proconfig, &isnull);
+ if (!isnull) {
+ ArrayType *set_items;
+
+ /* Set per-function configuration parameters */
+ set_items = DatumGetArrayTypeP(datum);
+
+ if (set_items != NULL) { /* Need a new GUC nesting
+ * level */
+ new_nest_level = NewGUCNestLevel();
+ *reload_config = true;
+ ProcessGUCArray(set_items,
+ (superuser() ? PGC_SUSET : PGC_USERSET),
+ PGC_S_SESSION,
+ GUC_ACTION_SAVE);
+ }
+ }
+ return new_nest_level;
+ }
+
+ /*
+ * Set up a fake fcinfo with just enough info to satisfy plpgsql_compile().
+ *
+ * There should be a different real argtypes for polymorphic params.
+ */
+ void
+ plpgsql_setup_fake_fcinfo(FmgrInfo * flinfo,
+ FunctionCallInfoData * fcinfo,
+ TriggerData * trigdata,
+ EventTriggerData * etrigdata,
+ Oid funcoid,
+ bool is_dml_trigger,
+ bool is_event_trigger)
+ {
+ /* clean structures */
+ MemSet(fcinfo, 0, sizeof(FunctionCallInfoData));
+ MemSet(flinfo, 0, sizeof(FmgrInfo));
+
+ fcinfo->flinfo = flinfo;
+ flinfo->fn_oid = funcoid;
+ flinfo->fn_mcxt = CurrentMemoryContext;
+
+ if (is_dml_trigger) {
+ Assert(trigdata != NULL);
+
+ MemSet(trigdata, 0, sizeof(trigdata));
+ trigdata->type = T_TriggerData;
+ fcinfo->context = (Node *) trigdata;
+ } else if (is_event_trigger) {
+ MemSet(etrigdata, 0, sizeof(etrigdata));
+ etrigdata->type = T_EventTriggerData;
+ fcinfo->context = (Node *) etrigdata;
+ }
+ }
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
new file mode 100644
index 35f5721..58382b2
*** a/src/pl/plpgsql/src/pl_comp.c
--- b/src/pl/plpgsql/src/pl_comp.c
*************** static PLpgSQL_function *plpgsql_HashTab
*** 116,122 ****
static void plpgsql_HashTableInsert(PLpgSQL_function *function,
PLpgSQL_func_hashkey *func_key);
static void plpgsql_HashTableDelete(PLpgSQL_function *function);
- static void delete_function(PLpgSQL_function *func);
/* ----------
* plpgsql_compile Make an execution tree for a PL/pgSQL function.
--- 116,121 ----
*************** recheck:
*** 176,182 ****
* Nope, so remove it from hashtable and try to drop associated
* storage (if not done already).
*/
! delete_function(function);
/*
* If the function isn't in active use then we can overwrite the
--- 175,181 ----
* Nope, so remove it from hashtable and try to drop associated
* storage (if not done already).
*/
! plpgsql_delete_function(function);
/*
* If the function isn't in active use then we can overwrite the
*************** plpgsql_resolve_polymorphic_argtypes(int
*** 2465,2471 ****
}
/*
! * delete_function - clean up as much as possible of a stale function cache
*
* We can't release the PLpgSQL_function struct itself, because of the
* possibility that there are fn_extra pointers to it. We can release
--- 2464,2470 ----
}
/*
! * plpgsql_delete_function - clean up as much as possible of a stale function cache
*
* We can't release the PLpgSQL_function struct itself, because of the
* possibility that there are fn_extra pointers to it. We can release
*************** plpgsql_resolve_polymorphic_argtypes(int
*** 2478,2485 ****
* pointers to the same function cache. Hence be careful not to do things
* twice.
*/
! static void
! delete_function(PLpgSQL_function *func)
{
/* remove function from hash table (might be done already) */
plpgsql_HashTableDelete(func);
--- 2477,2484 ----
* pointers to the same function cache. Hence be careful not to do things
* twice.
*/
! void
! plpgsql_delete_function(PLpgSQL_function *func)
{
/* remove function from hash table (might be done already) */
plpgsql_HashTableDelete(func);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 3b5b3bb..6eac4f7
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
*************** static SimpleEcontextStackEntry *simple_
*** 81,87 ****
* Local function forward declarations
************************************************************/
static void plpgsql_exec_error_callback(void *arg);
- static PLpgSQL_datum *copy_plpgsql_datum(PLpgSQL_datum *datum);
static int exec_stmt_block(PLpgSQL_execstate *estate,
PLpgSQL_stmt_block *block);
--- 81,86 ----
*************** static int exec_stmt_dynexecute(PLpgSQL_
*** 134,142 ****
static int exec_stmt_dynfors(PLpgSQL_execstate *estate,
PLpgSQL_stmt_dynfors *stmt);
- static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
- PLpgSQL_function *func,
- ReturnSetInfo *rsi);
static void exec_eval_cleanup(PLpgSQL_execstate *estate);
static void exec_prepare_plan(PLpgSQL_execstate *estate,
--- 133,138 ----
*************** static Datum exec_simple_cast_value(PLpg
*** 205,211 ****
static void exec_init_tuple_store(PLpgSQL_execstate *estate);
static void exec_set_found(PLpgSQL_execstate *estate, bool state);
static void plpgsql_create_econtext(PLpgSQL_execstate *estate);
- static void plpgsql_destroy_econtext(PLpgSQL_execstate *estate);
static void free_var(PLpgSQL_var *var);
static void assign_text_var(PLpgSQL_var *var, const char *str);
static PreparedParamsData *exec_eval_using_params(PLpgSQL_execstate *estate,
--- 201,206 ----
*************** plpgsql_exec_error_callback(void *arg)
*** 933,939 ****
* Support function for initializing local execution variables
* ----------
*/
! static PLpgSQL_datum *
copy_plpgsql_datum(PLpgSQL_datum *datum)
{
PLpgSQL_datum *result;
--- 928,934 ----
* Support function for initializing local execution variables
* ----------
*/
! PLpgSQL_datum *
copy_plpgsql_datum(PLpgSQL_datum *datum)
{
PLpgSQL_datum *result;
*************** exec_stmt_raise(PLpgSQL_execstate *estat
*** 2950,2956 ****
* Initialize a mostly empty execution state
* ----------
*/
! static void
plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi)
--- 2945,2951 ----
* Initialize a mostly empty execution state
* ----------
*/
! void
plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi)
*************** plpgsql_create_econtext(PLpgSQL_execstat
*** 6016,6022 ****
* We check that it matches the top stack entry, and destroy the stack
* entry along with the context.
*/
! static void
plpgsql_destroy_econtext(PLpgSQL_execstate *estate)
{
SimpleEcontextStackEntry *next;
--- 6011,6017 ----
* We check that it matches the top stack entry, and destroy the stack
* entry along with the context.
*/
! void
plpgsql_destroy_econtext(PLpgSQL_execstate *estate)
{
SimpleEcontextStackEntry *next;
diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c
new file mode 100644
index fa74e7d..d4b677c
*** a/src/pl/plpgsql/src/pl_handler.c
--- b/src/pl/plpgsql/src/pl_handler.c
***************
*** 15,20 ****
--- 15,23 ----
#include "plpgsql.h"
+ #include "catalog/pg_enum.h"
+ #include "catalog/pg_language.h"
+
#include "access/htup_details.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
*************** int plpgsql_variable_conflict = PLPGSQ
*** 40,45 ****
--- 43,50 ----
/* Hook for plugins */
PLpgSQL_plugin **plugin_ptr = NULL;
+ static void precheck_function(HeapTuple procTuple, bool has_trigger_relation);
+ static int check_function_output_format(Oid format);
/*
* _PG_init() - library load-time initialization
*************** plpgsql_inline_handler(PG_FUNCTION_ARGS)
*** 187,197 ****
* plpgsql_exec_function(). In particular note that this sets things up
* with no arguments passed.
*/
! MemSet(&fake_fcinfo, 0, sizeof(fake_fcinfo));
! MemSet(&flinfo, 0, sizeof(flinfo));
! fake_fcinfo.flinfo = &flinfo;
! flinfo.fn_oid = InvalidOid;
! flinfo.fn_mcxt = CurrentMemoryContext;
retval = plpgsql_exec_function(func, &fake_fcinfo);
--- 192,198 ----
* plpgsql_exec_function(). In particular note that this sets things up
* with no arguments passed.
*/
! plpgsql_setup_fake_fcinfo(&flinfo, &fake_fcinfo, NULL, NULL, InvalidOid, false, false);
retval = plpgsql_exec_function(func, &fake_fcinfo);
*************** plpgsql_validator(PG_FUNCTION_ARGS)
*** 297,319 ****
* Set up a fake fcinfo with just enough info to satisfy
* plpgsql_compile().
*/
! MemSet(&fake_fcinfo, 0, sizeof(fake_fcinfo));
! MemSet(&flinfo, 0, sizeof(flinfo));
! fake_fcinfo.flinfo = &flinfo;
! flinfo.fn_oid = funcoid;
! flinfo.fn_mcxt = CurrentMemoryContext;
! if (is_dml_trigger)
! {
! MemSet(&trigdata, 0, sizeof(trigdata));
! trigdata.type = T_TriggerData;
! fake_fcinfo.context = (Node *) &trigdata;
! }
! else if (is_event_trigger)
! {
! MemSet(&etrigdata, 0, sizeof(etrigdata));
! etrigdata.type = T_EventTriggerData;
! fake_fcinfo.context = (Node *) &etrigdata;
! }
/* Test-compile the function */
plpgsql_compile(&fake_fcinfo, true);
--- 298,304 ----
* Set up a fake fcinfo with just enough info to satisfy
* plpgsql_compile().
*/
! plpgsql_setup_fake_fcinfo(&flinfo, &fake_fcinfo, &trigdata, &etrigdata, funcoid, is_dml_trigger, is_event_trigger);
/* Test-compile the function */
plpgsql_compile(&fake_fcinfo, true);
*************** plpgsql_validator(PG_FUNCTION_ARGS)
*** 329,331 ****
--- 314,465 ----
PG_RETURN_VOID();
}
+
+ /*
+ * ----------
+ * plpgsql_check_function
+ *
+ * It ensure a detailed validation
+ * ----------
+ */
+ PG_FUNCTION_INFO_V1(plpgsql_check_function);
+
+ Datum
+ plpgsql_check_function(PG_FUNCTION_ARGS)
+ {
+ Oid funcoid = PG_GETARG_OID(0);
+ Oid relid = PG_GETARG_OID(1);
+ bool fatal_errors = PG_GETARG_BOOL(2);
+ Oid format_oid = PG_GETARG_OID(3);
+ TupleDesc tupdesc;
+ HeapTuple procTuple;
+ Tuplestorestate *tupstore;
+ MemoryContext per_query_ctx;
+ MemoryContext oldcontext;
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ int format = PLPGSQL_CHECK_FORMAT_PLAIN;
+
+ /* check to see if caller supports us returning a tuplestore */
+ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("set-valued function called in context that cannot accept a set")));
+
+ if (!(rsinfo->allowedModes & SFRM_Materialize))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("materialize mode required, but it is not allowed in this context")));
+
+ procTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcoid));
+ if (!HeapTupleIsValid(procTuple))
+ elog(ERROR, "cache lookup failed for function %u", funcoid);
+
+ precheck_function(procTuple, OidIsValid(relid));
+ format = check_function_output_format(format_oid);
+
+ /* need to build tuplestore in query context */
+ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+ oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+ tupdesc = CreateTupleDescCopy(rsinfo->expectedDesc);
+ tupstore = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+
+ plpgsql_function_check(procTuple, relid,
+ tupdesc, tupstore, fatal_errors, format);
+
+ ReleaseSysCache(procTuple);
+
+ /* clean up and return the tuplestore */
+ tuplestore_donestoring(tupstore);
+
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = tupstore;
+ rsinfo->setDesc = tupdesc;
+
+ return (Datum) 0;
+ }
+
+ /*
+ * Process necessary checking before code checking
+ */
+ static void
+ precheck_function(HeapTuple procTuple, bool has_trigger_relation)
+ {
+ Form_pg_proc proc;
+ Form_pg_language languageStruct;
+ HeapTuple languageTuple;
+ char functyptype;
+ char *funcname;
+ bool is_trigger = false;
+
+ proc = (Form_pg_proc) GETSTRUCT(procTuple);
+
+ funcname = format_procedure(HeapTupleGetOid(procTuple));
+
+ /* used language must be plpgsql */
+ languageTuple = SearchSysCache1(LANGOID, ObjectIdGetDatum(proc->prolang));
+ Assert(HeapTupleIsValid(languageTuple));
+
+ languageStruct = (Form_pg_language) GETSTRUCT(languageTuple);
+ if (strcmp(NameStr(languageStruct->lanname), "plpgsql") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("%s is not a plpgsql function", funcname)));
+
+ ReleaseSysCache(languageTuple);
+
+ functyptype = get_typtype(proc->prorettype);
+
+ if (functyptype == TYPTYPE_PSEUDO)
+ {
+ /* we assume OPAQUE with no arguments means a trigger */
+ if (proc->prorettype == TRIGGEROID ||
+ (proc->prorettype == OPAQUEOID && proc->pronargs == 0))
+ {
+ is_trigger = true;
+ if (!has_trigger_relation)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("missing trigger relation"),
+ errhint("Trigger relation oid must be valid")));
+ }
+ }
+
+ if (has_trigger_relation && !is_trigger)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("function is not trigger"),
+ errhint("Trigger relation oid must not be valid for non trigger function.")));
+
+ pfree(funcname);
+ }
+
+ /*
+ * Returns output format for plpgsql_check_function
+ */
+ static int
+ check_function_output_format(Oid format)
+ {
+ HeapTuple tuple;
+ char *label;
+ int result = -1;
+
+ tuple = SearchSysCache1(ENUMOID, ObjectIdGetDatum(format));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+ errmsg("invalid internal value for enum: %u",
+ format)));
+
+ label = NameStr(((Form_pg_enum) GETSTRUCT(tuple))->enumlabel);
+
+ if (strcmp(label, "XML") == 0)
+ result = PLPGSQL_CHECK_FORMAT_XML;
+ else if (strcmp(label, "PLAIN_TEXT") == 0)
+ result = PLPGSQL_CHECK_FORMAT_PLAIN;
+
+ ReleaseSysCache(tuple);
+
+ return result;
+ }
diff --git a/src/pl/plpgsql/src/plpgsql--1.0--1.1.sql b/src/pl/plpgsql/src/plpgsql--1.0--1.1.sql
new file mode 100644
index ...62cd8c2
*** a/src/pl/plpgsql/src/plpgsql--1.0--1.1.sql
--- b/src/pl/plpgsql/src/plpgsql--1.0--1.1.sql
***************
*** 0 ****
--- 1,17 ----
+ /* src/pl/plpgsql/src/plpgsql--1.1.sql */
+
+ /*
+ * Currently, all the interesting stuff is done by CREATE LANGUAGE.
+ * Later we will probably "dumb down" that command and put more of the
+ * knowledge into this script.
+ */
+
+ CREATE TYPE plpgsql_check_function_format AS ENUM ('XML', 'PLAIN_TEXT');
+
+ CREATE FUNCTION plpgsql_check_function(funcoid regprocedure,
+ relid regclass = 0,
+ fatal_errors boolean = true,
+ format plpgsql_check_function_format = 'PLAIN_TEXT')
+ RETURNS SETOF text AS 'MODULE_PATHNAME'
+ LANGUAGE C
+ RETURNS NULL ON NULL INPUT;
diff --git a/src/pl/plpgsql/src/plpgsql--1.1.sql b/src/pl/plpgsql/src/plpgsql--1.1.sql
new file mode 100644
index ...291fdc5
*** a/src/pl/plpgsql/src/plpgsql--1.1.sql
--- b/src/pl/plpgsql/src/plpgsql--1.1.sql
***************
*** 0 ****
--- 1,21 ----
+ /* src/pl/plpgsql/src/plpgsql--1.1.sql */
+
+ /*
+ * Currently, all the interesting stuff is done by CREATE LANGUAGE.
+ * Later we will probably "dumb down" that command and put more of the
+ * knowledge into this script.
+ */
+
+ CREATE PROCEDURAL LANGUAGE plpgsql;
+
+ COMMENT ON PROCEDURAL LANGUAGE plpgsql IS 'PL/pgSQL procedural language';
+
+ CREATE TYPE plpgsql_check_function_format AS ENUM ('XML', 'PLAIN_TEXT');
+
+ CREATE FUNCTION plpgsql_check_function(funcoid regprocedure,
+ relid regclass = 0,
+ fatal_errors boolean = true,
+ format plpgsql_check_function_format = 'PLAIN_TEXT')
+ RETURNS SETOF text AS 'MODULE_PATHNAME'
+ LANGUAGE C
+ RETURNS NULL ON NULL INPUT;
diff --git a/src/pl/plpgsql/src/plpgsql.control b/src/pl/plpgsql/src/plpgsql.control
new file mode 100644
index b320227..4c75c93
*** a/src/pl/plpgsql/src/plpgsql.control
--- b/src/pl/plpgsql/src/plpgsql.control
***************
*** 1,6 ****
# plpgsql extension
comment = 'PL/pgSQL procedural language'
! default_version = '1.0'
module_pathname = '$libdir/plpgsql'
relocatable = false
schema = pg_catalog
--- 1,6 ----
# plpgsql extension
comment = 'PL/pgSQL procedural language'
! default_version = '1.1'
module_pathname = '$libdir/plpgsql'
relocatable = false
schema = pg_catalog
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
new file mode 100644
index 7ea6960..729a599
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
*************** typedef struct PLpgSQL_execstate
*** 778,783 ****
--- 778,798 ----
void *plugin_info; /* reserved for use by optional plugin */
} PLpgSQL_execstate;
+ enum {
+ PLPGSQL_CHECK_FORMAT_PLAIN,
+ PLPGSQL_CHECK_FORMAT_XML
+ };
+
+ typedef struct PLpgSQL_checkstate
+ {
+ PLpgSQL_execstate *estate; /* check state is estate extension */
+ Tuplestorestate *tuple_store;
+ TupleDesc tupdesc;
+ StringInfo sinfo;
+ bool fatal_errors; /* stop on first error */
+ int format;
+ List *argnames;
+ } PLpgSQL_checkstate;
/*
* A PLpgSQL_plugin structure represents an instrumentation plugin.
*************** extern PLpgSQL_condition *plpgsql_parse_
*** 913,918 ****
--- 928,934 ----
extern void plpgsql_adddatum(PLpgSQL_datum *new);
extern int plpgsql_add_initdatums(int **varnos);
extern void plpgsql_HashTableInit(void);
+ extern void plpgsql_delete_function(PLpgSQL_function *func);
/* ----------
* Functions in pl_handler.c
*************** extern void _PG_init(void);
*** 922,927 ****
--- 938,944 ----
extern Datum plpgsql_call_handler(PG_FUNCTION_ARGS);
extern Datum plpgsql_inline_handler(PG_FUNCTION_ARGS);
extern Datum plpgsql_validator(PG_FUNCTION_ARGS);
+ extern Datum plpgsql_check_function(PG_FUNCTION_ARGS);
/* ----------
* Functions in pl_exec.c
*************** extern Oid exec_get_datum_type(PLpgSQL_e
*** 941,946 ****
--- 958,980 ----
extern void exec_get_datum_type_info(PLpgSQL_execstate *estate,
PLpgSQL_datum *datum,
Oid *typeid, int32 *typmod, Oid *collation);
+ extern PLpgSQL_datum *copy_plpgsql_datum(PLpgSQL_datum *datum);
+ extern void plpgsql_estate_setup(PLpgSQL_execstate *estate,
+ PLpgSQL_function *func,
+ ReturnSetInfo *rsi);
+ extern void plpgsql_destroy_econtext(PLpgSQL_execstate *estate);
+
+ /* ----------
+ * Functions for namespace handling in pl_check.c
+ * ----------
+ */
+ extern void plpgsql_function_check(HeapTuple procTuple, Oid relid,
+ TupleDesc tupdesc,
+ Tuplestorestate *tupstore,
+ bool fatal_errors, int format);
+ extern void plpgsql_setup_fake_fcinfo(FmgrInfo *flinfo, FunctionCallInfoData *fcinfo,
+ TriggerData *trigdata, EventTriggerData *etrigdata,
+ Oid funcoid, bool is_dml_trigger, bool is_event_trigger);
/* ----------
* Functions for namespace handling in pl_funcs.c
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
new file mode 100644
index 3005336..77cc924
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
*************** end;
*** 302,307 ****
--- 302,313 ----
' language plpgsql;
create trigger tg_hslot_biu before insert or update
on HSlot for each row execute procedure tg_hslot_biu();
+ -- check trigger should not fail
+ select plpgsql_check_function('tg_hslot_biu()', 'HSlot');
+ plpgsql_check_function
+ ------------------------
+ (0 rows)
+
-- ************************************************************
-- * BEFORE DELETE on HSlot
-- * - prevent from manual manipulation
*************** begin
*** 635,640 ****
--- 641,652 ----
raise exception ''illegal backlink beginning with %'', mytype;
end;
' language plpgsql;
+ -- check function should not fail
+ select plpgsql_check_function('tg_backlink_set(bpchar, bpchar)', 0);
+ plpgsql_check_function
+ ------------------------
+ (0 rows)
+
-- ************************************************************
-- * Support function to clear out the backlink field if
-- * it still points to specific slot
*************** NOTICE: 4 bb cc
*** 2947,2952 ****
--- 2959,2996 ----
(1 row)
+ -- check function should not fail
+ select plpgsql_check_function('for_vect()');
+ plpgsql_check_function
+ ------------------------
+ (0 rows)
+
+ -- recheck after check function
+ select for_vect();
+ NOTICE: 1
+ NOTICE: 2
+ NOTICE: 3
+ NOTICE: 1 BB CC
+ NOTICE: 2 BB CC
+ NOTICE: 3 BB CC
+ NOTICE: 4 BB CC
+ NOTICE: 1
+ NOTICE: 2
+ NOTICE: 3
+ NOTICE: 4
+ NOTICE: 1 BB CC
+ NOTICE: 2 BB CC
+ NOTICE: 3 BB CC
+ NOTICE: 4 BB CC
+ NOTICE: 1 bb cc
+ NOTICE: 2 bb cc
+ NOTICE: 3 bb cc
+ NOTICE: 4 bb cc
+ for_vect
+ ----------
+
+ (1 row)
+
-- regression test: verify that multiple uses of same plpgsql datum within
-- a SQL command all get mapped to the same $n parameter. The return value
-- of the SELECT is not important, we only care that it doesn't fail with
*************** begin
*** 3428,3433 ****
--- 3472,3483 ----
return;
end;
$$ language plpgsql;
+ -- check function should not fail
+ select plpgsql_check_function('forc01()');
+ plpgsql_check_function
+ ------------------------
+ (0 rows)
+
select forc01();
NOTICE: 5 from c
NOTICE: 6 from c
*************** begin
*** 3861,3866 ****
--- 3911,3922 ----
end case;
end;
$$ language plpgsql immutable;
+ -- check function should not fail
+ select plpgsql_check_function('case_test(bigint)');
+ plpgsql_check_function
+ ------------------------
+ (0 rows)
+
select case_test(1);
case_test
-----------
*************** ERROR: value for domain orderedarray vi
*** 4716,4718 ****
--- 4772,5299 ----
CONTEXT: PL/pgSQL function testoa(integer,integer,integer) line 5 at assignment
drop function arrayassign1();
drop function testoa(x1 int, x2 int, x3 int);
+ --
+ -- check function statement tests
+ --
+ --should fail - is not plpgsql
+ select plpgsql_check_function('session_user()');
+ ERROR: "session_user"() is not a plpgsql function
+ create table t1(a int, b int);
+ create function f1()
+ returns void as $$
+ begin
+ if false then
+ update t1 set c = 30;
+ end if;
+ if false then
+ raise notice '% %', r.c;
+ end if;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()', fatal_errors := true);
+ plpgsql_check_function
+ ------------------------------------------------------------------------
+ error:42703:4:SQL statement:column "c" of relation "t1" does not exist
+ Query: update t1 set c = 30
+ -- ^
+ (3 rows)
+
+ select plpgsql_check_function('f1()', fatal_errors := false);
+ plpgsql_check_function
+ ------------------------------------------------------------------------
+ error:42703:4:SQL statement:column "c" of relation "t1" does not exist
+ Query: update t1 set c = 30
+ -- ^
+ error:42P01:7:RAISE:missing FROM-clause entry for table "r"
+ Query: SELECT r.c
+ -- ^
+ error:42601:7:RAISE:too few parameters specified for RAISE
+ (7 rows)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ------------------------------------------------------------------------
+ error:42703:4:SQL statement:column "c" of relation "t1" does not exist
+ Query: update t1 set c = 30
+ -- ^
+ (3 rows)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ create function g1(out a int, out b int)
+ as $$
+ select 10,20;
+ $$ language sql;
+ create function f1()
+ returns void as $$
+ declare r record;
+ begin
+ r := g1();
+ if false then
+ raise notice '%', r.c;
+ end if;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ -------------------------------------------------
+ error:42703:6:RAISE:record "r" has no field "c"
+ Context: SQL statement "SELECT r.c"
+ (2 rows)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ drop function g1();
+ create function g1(out a int, out b int)
+ returns setof record as $$
+ select * from t1;
+ $$ language sql;
+ create function f1()
+ returns void as $$
+ declare r record;
+ begin
+ for r in select * from g1()
+ loop
+ raise notice '%', r.c;
+ end loop;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ -------------------------------------------------
+ error:42703:6:RAISE:record "r" has no field "c"
+ Context: SQL statement "SELECT r.c"
+ (2 rows)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ create or replace function f1()
+ returns void as $$
+ declare r record;
+ begin
+ for r in select * from g1()
+ loop
+ r.c := 20;
+ end loop;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ------------------------------------------------------
+ error:42703:6:assignment:record "r" has no field "c"
+ (1 row)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ drop function g1();
+ create function f1()
+ returns int as $$
+ declare r int;
+ begin
+ if false then
+ r := a + b;
+ end if;
+ return r;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ----------------------------------------------------
+ error:42703:5:assignment:column "a" does not exist
+ Query: SELECT a + b
+ -- ^
+ (3 rows)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ create or replace function f1()
+ returns void as $$
+ begin
+ if false then
+ raise notice '%', 1, 2;
+ end if;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ -------------------------------------------------------------
+ error:42601:4:RAISE:too many parameters specified for RAISE
+ (1 row)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ create or replace function f1()
+ returns void as $$
+ begin
+ if false then
+ raise notice '% %';
+ end if;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ------------------------------------------------------------
+ error:42601:4:RAISE:too few parameters specified for RAISE
+ (1 row)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ create or replace function f1()
+ returns void as $$
+ declare r int[];
+ begin
+ if false then
+ r[c+10] := 20;
+ end if;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ----------------------------------------------------
+ error:42703:5:assignment:column "c" does not exist
+ Query: SELECT c+10
+ -- ^
+ (3 rows)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ create or replace function f1()
+ returns void as $$
+ declare r int;
+ begin
+ if false then
+ r[10] := 20;
+ end if;
+ end;
+ $$ language plpgsql set search_path = public;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ -------------------------------------------------------------
+ error:42804:5:assignment:subscripted object is not an array
+ (1 row)
+
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ drop function f1();
+ create type _exception_type as (
+ state text,
+ message text,
+ detail text);
+ create or replace function f1()
+ returns void as $$
+ declare
+ _exception record;
+ begin
+ _exception := NULL::_exception_type;
+ exception when others then
+ get stacked diagnostics
+ _exception.state = RETURNED_SQLSTATE,
+ _exception.message = MESSAGE_TEXT,
+ _exception.detail = PG_EXCEPTION_DETAIL,
+ _exception.hint = PG_EXCEPTION_HINT;
+ end;
+ $$ language plpgsql;
+ select f1();
+ f1
+ ----
+
+ (1 row)
+
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ -----------------------------------------------------------------------
+ error:42703:7:GET DIAGNOSTICS:record "_exception" has no field "hint"
+ (1 row)
+
+ drop function f1();
+ create or replace function f1_trg()
+ returns trigger as $$
+ begin
+ if new.a > 10 then
+ raise notice '%', new.b;
+ raise notice '%', new.c;
+ end if;
+ return new;
+ end;
+ $$ language plpgsql;
+ create trigger t1_f1 before insert on t1
+ for each row
+ execute procedure f1_trg();
+ insert into t1 values(6,30);
+ select plpgsql_check_function('f1_trg()','t1');
+ plpgsql_check_function
+ ---------------------------------------------------
+ error:42703:5:RAISE:record "new" has no field "c"
+ Context: SQL statement "SELECT new.c"
+ (2 rows)
+
+ insert into t1 values(6,30);
+ create or replace function f1_trg()
+ returns trigger as $$
+ begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ new.c := 30;
+ return new;
+ end;
+ $$ language plpgsql;
+ -- should to fail
+ select plpgsql_check_function('f1_trg()','t1');
+ plpgsql_check_function
+ --------------------------------------------------------
+ error:42703:5:assignment:record "new" has no field "c"
+ (1 row)
+
+ -- should to fail but not crash
+ insert into t1 values(6,30);
+ ERROR: record "new" has no field "c"
+ CONTEXT: PL/pgSQL function f1_trg() line 5 at assignment
+ create or replace function f1_trg()
+ returns trigger as $$
+ begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ return new;
+ end;
+ $$ language plpgsql;
+ -- ok
+ select plpgsql_check_function('f1_trg()', 't1');
+ plpgsql_check_function
+ ------------------------
+ (0 rows)
+
+ -- ok
+ insert into t1 values(6,30);
+ select * from t1;
+ a | b
+ ----+----
+ 6 | 30
+ 6 | 30
+ 16 | 40
+ (3 rows)
+
+ drop trigger t1_f1 on t1;
+ drop function f1_trg();
+ -- test of showing caret on correct place for multiline queries
+ create or replace function f1()
+ returns void as $$
+ begin
+ select
+ var
+ from
+ foo;
+ end;
+ $$ language plpgsql;
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ---------------------------------------------------------
+ error:42703:3:SQL statement:column "var" does not exist
+ Query: select
+ var
+ -- ^
+ from
+ foo
+ (6 rows)
+
+ drop function f1();
+ create or replace function f1()
+ returns int as $$
+ begin
+ return (select a
+ from t1
+ where hh = 20);
+ end;
+ $$ language plpgsql;
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ -------------------------------------------------
+ error:42703:3:RETURN:column "hh" does not exist
+ Query: SELECT (select a
+ from t1
+ where hh = 20)
+ -- ^
+ (5 rows)
+
+ create or replace function f1()
+ returns int as $$
+ begin
+ return (select a
+ from txxxxxxx
+ where hh = 20);
+ end;
+ $$ language plpgsql;
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ---------------------------------------------------------
+ error:42P01:3:RETURN:relation "txxxxxxx" does not exist
+ Query: SELECT (select a
+ from txxxxxxx
+ -- ^
+ where hh = 20)
+ (5 rows)
+
+ drop function f1();
+ drop table t1;
+ drop type _exception_type;
+ -- raise warnings when target row has different number of attributies in
+ -- SELECT INTO statement
+ create or replace function f1()
+ returns void as $$
+ declare a1 int; a2 int;
+ begin
+ select 10,20 into a1,a2;
+ end;
+ $$ language plpgsql;
+ -- should be ok
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ------------------------
+ (0 rows)
+
+ create or replace function f1()
+ returns void as $$
+ declare a1 int; a2 int;
+ begin
+ select 10,20 into a1;
+ end;
+ $$ language plpgsql;
+ -- raise warning
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ -------------------------------------------------------------------------
+ warning:00000:4:SQL statement:too many attributies for target variables
+ Detail: There are less target variables than output columns in query.
+ Hint: Check target variables in SELECT INTO statement
+ (3 rows)
+
+ create or replace function f1()
+ returns void as $$
+ declare a1 int; a2 int;
+ begin
+ select 10 into a1,a2;
+ end;
+ $$ language plpgsql;
+ -- raise warning
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ------------------------------------------------------------------------
+ warning:00000:4:SQL statement:too few attributies for target variables
+ Detail: There are more target variables than output columns in query.
+ Hint: Check target variables in SELECT INTO statement.
+ (3 rows)
+
+ -- bogus code
+ set check_function_bodies to off;
+ create or replace function f1()
+ returns void as $$
+ adasdfsadf
+ $$ language plpgsql;
+ select plpgsql_check_function('f1()');
+ plpgsql_check_function
+ ------------------------------------------------------------
+ error:42601:syntax error at or near "adasdfsadf"
+ Query:
+ adasdfsadf
+ -- ^
+
+ Context: compilation of PL/pgSQL function "f1" near line 1
+ (6 rows)
+
+ drop function f1();
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
new file mode 100644
index 2b60b67..206edbf
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
*************** end;
*** 366,371 ****
--- 366,373 ----
create trigger tg_hslot_biu before insert or update
on HSlot for each row execute procedure tg_hslot_biu();
+ -- check trigger should not fail
+ select plpgsql_check_function('tg_hslot_biu()', 'HSlot');
-- ************************************************************
-- * BEFORE DELETE on HSlot
*************** begin
*** 747,752 ****
--- 749,757 ----
end;
' language plpgsql;
+ -- check function should not fail
+ select plpgsql_check_function('tg_backlink_set(bpchar, bpchar)', 0);
+
-- ************************************************************
-- * Support function to clear out the backlink field if
*************** $proc$ language plpgsql;
*** 2458,2463 ****
--- 2463,2475 ----
select for_vect();
+ -- check function should not fail
+ select plpgsql_check_function('for_vect()');
+
+ -- recheck after check function
+ select for_vect();
+
+
-- regression test: verify that multiple uses of same plpgsql datum within
-- a SQL command all get mapped to the same $n parameter. The return value
-- of the SELECT is not important, we only care that it doesn't fail with
*************** begin
*** 2837,2842 ****
--- 2849,2857 ----
end;
$$ language plpgsql;
+ -- check function should not fail
+ select plpgsql_check_function('forc01()');
+
select forc01();
-- try updating the cursor's current row
*************** begin
*** 3171,3176 ****
--- 3186,3195 ----
end;
$$ language plpgsql immutable;
+ -- check function should not fail
+ select plpgsql_check_function('case_test(bigint)');
+
+
select case_test(1);
select case_test(2);
select case_test(3);
*************** select testoa(1,2,1); -- fail at update
*** 3723,3725 ****
--- 3742,4105 ----
drop function arrayassign1();
drop function testoa(x1 int, x2 int, x3 int);
+
+ --
+ -- check function statement tests
+ --
+
+ --should fail - is not plpgsql
+ select plpgsql_check_function('session_user()');
+
+ create table t1(a int, b int);
+
+ create function f1()
+ returns void as $$
+ begin
+ if false then
+ update t1 set c = 30;
+ end if;
+ if false then
+ raise notice '% %', r.c;
+ end if;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+ select plpgsql_check_function('f1()', fatal_errors := true);
+ select plpgsql_check_function('f1()', fatal_errors := false);
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+
+ create function g1(out a int, out b int)
+ as $$
+ select 10,20;
+ $$ language sql;
+
+ create function f1()
+ returns void as $$
+ declare r record;
+ begin
+ r := g1();
+ if false then
+ raise notice '%', r.c;
+ end if;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+ drop function g1();
+
+ create function g1(out a int, out b int)
+ returns setof record as $$
+ select * from t1;
+ $$ language sql;
+
+ create function f1()
+ returns void as $$
+ declare r record;
+ begin
+ for r in select * from g1()
+ loop
+ raise notice '%', r.c;
+ end loop;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ create or replace function f1()
+ returns void as $$
+ declare r record;
+ begin
+ for r in select * from g1()
+ loop
+ r.c := 20;
+ end loop;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+ drop function g1();
+
+ create function f1()
+ returns int as $$
+ declare r int;
+ begin
+ if false then
+ r := a + b;
+ end if;
+ return r;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+
+ create or replace function f1()
+ returns void as $$
+ begin
+ if false then
+ raise notice '%', 1, 2;
+ end if;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+
+ create or replace function f1()
+ returns void as $$
+ begin
+ if false then
+ raise notice '% %';
+ end if;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+
+ create or replace function f1()
+ returns void as $$
+ declare r int[];
+ begin
+ if false then
+ r[c+10] := 20;
+ end if;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+
+ create or replace function f1()
+ returns void as $$
+ declare r int;
+ begin
+ if false then
+ r[10] := 20;
+ end if;
+ end;
+ $$ language plpgsql set search_path = public;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ select f1();
+
+ drop function f1();
+
+ create type _exception_type as (
+ state text,
+ message text,
+ detail text);
+
+ create or replace function f1()
+ returns void as $$
+ declare
+ _exception record;
+ begin
+ _exception := NULL::_exception_type;
+ exception when others then
+ get stacked diagnostics
+ _exception.state = RETURNED_SQLSTATE,
+ _exception.message = MESSAGE_TEXT,
+ _exception.detail = PG_EXCEPTION_DETAIL,
+ _exception.hint = PG_EXCEPTION_HINT;
+ end;
+ $$ language plpgsql;
+
+ select f1();
+
+ select plpgsql_check_function('f1()');
+
+ drop function f1();
+
+ create or replace function f1_trg()
+ returns trigger as $$
+ begin
+ if new.a > 10 then
+ raise notice '%', new.b;
+ raise notice '%', new.c;
+ end if;
+ return new;
+ end;
+ $$ language plpgsql;
+
+ create trigger t1_f1 before insert on t1
+ for each row
+ execute procedure f1_trg();
+
+ insert into t1 values(6,30);
+
+ select plpgsql_check_function('f1_trg()','t1');
+
+ insert into t1 values(6,30);
+
+ create or replace function f1_trg()
+ returns trigger as $$
+ begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ new.c := 30;
+ return new;
+ end;
+ $$ language plpgsql;
+
+ -- should to fail
+
+ select plpgsql_check_function('f1_trg()','t1');
+
+ -- should to fail but not crash
+ insert into t1 values(6,30);
+
+ create or replace function f1_trg()
+ returns trigger as $$
+ begin
+ new.a := new.a + 10;
+ new.b := new.b + 10;
+ return new;
+ end;
+ $$ language plpgsql;
+
+ -- ok
+ select plpgsql_check_function('f1_trg()', 't1');
+
+ -- ok
+ insert into t1 values(6,30);
+
+ select * from t1;
+
+ drop trigger t1_f1 on t1;
+
+ drop function f1_trg();
+
+ -- test of showing caret on correct place for multiline queries
+ create or replace function f1()
+ returns void as $$
+ begin
+ select
+ var
+ from
+ foo;
+ end;
+ $$ language plpgsql;
+
+ select plpgsql_check_function('f1()');
+
+ drop function f1();
+
+ create or replace function f1()
+ returns int as $$
+ begin
+ return (select a
+ from t1
+ where hh = 20);
+ end;
+ $$ language plpgsql;
+
+ select plpgsql_check_function('f1()');
+
+ create or replace function f1()
+ returns int as $$
+ begin
+ return (select a
+ from txxxxxxx
+ where hh = 20);
+ end;
+ $$ language plpgsql;
+
+ select plpgsql_check_function('f1()');
+
+ drop function f1();
+
+ drop table t1;
+ drop type _exception_type;
+
+ -- raise warnings when target row has different number of attributies in
+ -- SELECT INTO statement
+
+ create or replace function f1()
+ returns void as $$
+ declare a1 int; a2 int;
+ begin
+ select 10,20 into a1,a2;
+ end;
+ $$ language plpgsql;
+
+ -- should be ok
+ select plpgsql_check_function('f1()');
+
+ create or replace function f1()
+ returns void as $$
+ declare a1 int; a2 int;
+ begin
+ select 10,20 into a1;
+ end;
+ $$ language plpgsql;
+
+ -- raise warning
+ select plpgsql_check_function('f1()');
+
+ create or replace function f1()
+ returns void as $$
+ declare a1 int; a2 int;
+ begin
+ select 10 into a1,a2;
+ end;
+ $$ language plpgsql;
+
+ -- raise warning
+ select plpgsql_check_function('f1()');
+
+ -- bogus code
+ set check_function_bodies to off;
+
+ create or replace function f1()
+ returns void as $$
+ adasdfsadf
+ $$ language plpgsql;
+
+ select plpgsql_check_function('f1()');
+
+ drop function f1();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index a831a1e..b91d97f
*** a/src/tools/pgindent/typedefs.list
--- b/src/tools/pgindent/typedefs.list
*************** AlterDatabaseStmt
*** 57,62 ****
--- 57,63 ----
AlterDefaultPrivilegesStmt
AlterDomainStmt
AlterEnumStmt
+ AlterEventTrigStmt
AlterExtensionContentsStmt
AlterExtensionStmt
AlterFdwStmt
*************** CreateCastStmt
*** 319,324 ****
--- 320,326 ----
CreateConversionStmt
CreateDomainStmt
CreateEnumStmt
+ CreateEventTrigStmt
CreateExtensionStmt
CreateFdwStmt
CreateForeignServerStmt
*************** EquivalenceClass
*** 428,433 ****
--- 430,441 ----
EquivalenceMember
ErrorContextCallback
ErrorData
+ EventTriggerCacheEntry
+ EventTriggerCacheItem
+ EventTriggerCacheStateType
+ EventTriggerData
+ EventTriggerEvent
+ EventTriggerInfo
ExceptionLabelMap
ExceptionMap
ExecAuxRowMark
*************** FormData_pg_database
*** 513,518 ****
--- 521,527 ----
FormData_pg_default_acl
FormData_pg_depend
FormData_pg_enum
+ FormData_pg_event_trigger
FormData_pg_extension
FormData_pg_foreign_data_wrapper
FormData_pg_foreign_server
*************** Form_pg_database
*** 559,564 ****
--- 568,574 ----
Form_pg_default_acl
Form_pg_depend
Form_pg_enum
+ Form_pg_event_trigger
Form_pg_extension
Form_pg_foreign_data_wrapper
Form_pg_foreign_server
*************** IOFuncSelector
*** 750,755 ****
--- 760,766 ----
IPCompareMethod
ITEM
IV
+ IdentLine
IdentifierLookup
IdentifySystemCmd
IncrementVarSublevelsUp_context
*************** LWLockPadded
*** 860,865 ****
--- 871,877 ----
LabelProvider
LargeObjectDesc
Latch
+ LateralJoinInfo
LexDescr
LexemeEntry
LexemeHashKey
*************** PLpgSQL_stmt_return
*** 1104,1109 ****
--- 1116,1122 ----
PLpgSQL_stmt_return_next
PLpgSQL_stmt_return_query
PLpgSQL_stmt_while
+ PLpgSQL_trigtype
PLpgSQL_type
PLpgSQL_var
PLpgSQL_variable
*************** PQconninfoOption
*** 1142,1148 ****
PQnoticeProcessor
PQnoticeReceiver
PQprintOpt
- PQrowProcessor
PREDICATELOCK
PREDICATELOCKTAG
PREDICATELOCKTARGET
--- 1155,1160 ----
*************** Page
*** 1171,1176 ****
--- 1183,1189 ----
PageHeader
PageHeaderData
PageSplitRecord
+ PageXLogRecPtr
PagetableEntry
Pairs
ParallelSlot
*************** ParamPathInfo
*** 1186,1191 ****
--- 1199,1206 ----
ParamRef
ParentMapEntry
ParseCallbackState
+ ParseExprKind
+ ParseNamespaceItem
ParseParamRefHook
ParseState
ParsedLex
*************** PathKeysComparison
*** 1201,1207 ****
Pattern_Prefix_Status
Pattern_Type
PendingOperationEntry
- PendingOperationTag
PendingRelDelete
PendingUnlinkEntry
PerlInterpreter
--- 1216,1221 ----
*************** ProcLangInfo
*** 1295,1300 ****
--- 1309,1315 ----
ProcSignalReason
ProcSignalSlot
ProcState
+ ProcessUtilityContext
ProcessUtility_hook_type
ProcessingMode
ProjectionInfo
*************** TimeOffset
*** 1680,1685 ****
--- 1695,1701 ----
TimeStamp
TimeTzADT
TimeZoneAbbrevTable
+ TimeoutId
Timestamp
TimestampTz
TmFromChar
*************** WalSnd
*** 1793,1798 ****
--- 1809,1815 ----
WalSndCtlData
WalSndState
WalSndrMessage
+ WholeRowVarExprState
WindowAgg
WindowAggState
WindowClause
*************** X509_NAME
*** 1824,1830 ****
X509_NAME_ENTRY
X509_STORE
X509_STORE_CTX
- XLogContRecord
XLogCtlData
XLogCtlInsert
XLogCtlWrite
--- 1841,1846 ----
*************** XLogPageHeaderData
*** 1835,1840 ****
--- 1851,1857 ----
XLogRecData
XLogRecPtr
XLogRecord
+ XLogSegNo
XLogwrtResult
XLogwrtRqst
XPVIV
*************** cached_re_str
*** 1882,1887 ****
--- 1899,1905 ----
cashKEY
celt
cfp
+ check_agg_arguments_context
check_network_data
check_object_relabel_type
check_password_hook_type
*************** ean13
*** 1920,1932 ****
eary
emit_log_hook_type
eval_const_expressions_context
execution_state
explain_get_index_name_hook_type
f_smgr
fd_set
finalize_primnode_context
find_expr_references_context
- find_minimum_var_level_context
fix_join_expr_context
fix_scan_expr_context
fix_upper_expr_context
--- 1938,1951 ----
eary
emit_log_hook_type
eval_const_expressions_context
+ event_trigger_command_tag_check_result
+ event_trigger_support_data
execution_state
explain_get_index_name_hook_type
f_smgr
fd_set
finalize_primnode_context
find_expr_references_context
fix_join_expr_context
fix_scan_expr_context
fix_upper_expr_context
*************** ino_t
*** 1986,1997 ****
instr_time
int16
int16KEY
- int2
int2vector
int32
int32KEY
int32_t
- int4
int64
int64KEY
int8
--- 2005,2014 ----
*************** line_t
*** 2018,2024 ****
locale_t
locate_agg_of_level_context
locate_var_of_level_context
- locate_var_of_relation_context
locate_windowfunc_context
logstreamer_param
lquery
--- 2035,2040 ----
*************** ltxtquery
*** 2031,2036 ****
--- 2047,2053 ----
mXactCacheEnt
macKEY
macaddr
+ map_variable_attnos_context
mb2wchar_with_len_converter
mbcharacter_incrementer
mbdisplaylen_converter
*************** pthread_t
*** 2139,2144 ****
--- 2156,2162 ----
pull_var_clause_context
pull_varattnos_context
pull_varnos_context
+ pull_vars_context
pullup_replace_vars_context
qsort_arg_comparator
radius_attribute
*************** rb_comparator
*** 2151,2157 ****
rb_freefunc
reduce_outer_joins_state
regex_t
- regexp
regexp_matches_ctx
regmatch_t
regoff_t
--- 2169,2174 ----
*************** temp_tablespaces_extra
*** 2232,2237 ****
--- 2249,2256 ----
text
timeKEY
time_t
+ timeout_handler
+ timeout_params
timerCA
timezone_extra
tlist_vinfo
*************** walrcv_connect_type
*** 2269,2274 ****
--- 2288,2294 ----
walrcv_disconnect_type
walrcv_receive_type
walrcv_send_type
+ wchar2mb_with_len_converter
wchar_t
win32_deadchild_waitinfo
win32_pthread
*************** xmlBufferPtr
*** 2323,2328 ****
--- 2343,2349 ----
xmlChar
xmlDocPtr
xmlErrorPtr
+ xmlExternalEntityLoader
xmlGenericErrorFunc
xmlNodePtr
xmlNodeSetPtr
*************** xmlXPathContextPtr
*** 2336,2342 ****
--- 2357,2365 ----
xmlXPathObjectPtr
xmltype
xpath_workspace
+ xsltSecurityPrefsPtr
xsltStylesheetPtr
+ xsltTransformContextPtr
yy_parser
yy_size_t
yyscan_t
Selena Deckelmann <selena@chesnok.com> writes:
This also represents my inaugural use of pg_bsd_indent. I ran it on
pl_check.c - which made things mostly better. Happy to try and fix it
up more if someone can explain to me what (if anything) I did
incorrectly when using it.
It looks like you didn't give it a typedef list? Fetch the file from
http://buildfarm.postgresql.org/cgi-bin/typedefs.pl
and pass it with --typedefs=filename. If you're dealing with something
that adds new typedef names, you can add them to the list file manually.
regards, tom lane
On 10/07/2012 11:25 AM, Tom Lane wrote:
Selena Deckelmann <selena@chesnok.com> writes:
This also represents my inaugural use of pg_bsd_indent. I ran it on
pl_check.c - which made things mostly better. Happy to try and fix it
up more if someone can explain to me what (if anything) I did
incorrectly when using it.It looks like you didn't give it a typedef list? Fetch the file from
http://buildfarm.postgresql.org/cgi-bin/typedefs.pl
and pass it with --typedefs=filename. If you're dealing with something
that adds new typedef names, you can add them to the list file manually.
You's not supposed to be calling pg_bsd_indent directly at all. You's
supposed to be calling pgindent - it will call pg_bsd_indent as needed
and adjust the results. See src/tools/pgindent/README.
cheers
andrew
Hello
I am sending a updated version - now it is prepared for event triggers
and it is little bit more robust
I run pgindent, but I have no experience with it, so I am not sure about success
Regards
Pavel Stehule
2012/10/7 Selena Deckelmann <selena@chesnok.com>:
Show quoted text
Hi!
On Tue, Jun 26, 2012 at 1:19 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
I am sending lightly refreshed patch for checking plpgsql functions..
I checked different implementation, but without success: a) enhancing
of SPI to some fake mode can has negative impact on application, and
patch was not clear, b) generic plpgsql walker doesn't save lines too.I invite any ideas how to improve this patch
I reviewed this and did a clean up for bitrot and a little whitespace.
In particular, it needed to learn a little about event triggers.This patch is a follow on from an earlier review thread I found:
http://archives.postgresql.org/message-id/D960CB61B694CF459DCFB4B0128514C2072DF447@exadv11.host.magwien.gv.atI dug through that thread a bit, and I believe issues raised by
Laurenz, Petr and Alvaro were resolved by Pavel over time.All tests pass, and after a read-through, the code seems fine.
This also represents my inaugural use of pg_bsd_indent. I ran it on
pl_check.c - which made things mostly better. Happy to try and fix it
up more if someone can explain to me what (if anything) I did
incorrectly when using it.-selena
Attachments:
Hello
a some updated version
* possibility to raise (and filter) performance warnings - detects IO castings
* detects assign composite value to scalar variable
Regards
Pavel Stehule
2012/11/27 Pavel Stehule <pavel.stehule@gmail.com>:
Show quoted text
Hello
I am sending a updated version - now it is prepared for event triggers
and it is little bit more robustI run pgindent, but I have no experience with it, so I am not sure about success
Regards
Pavel Stehule
2012/10/7 Selena Deckelmann <selena@chesnok.com>:
Hi!
On Tue, Jun 26, 2012 at 1:19 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
I am sending lightly refreshed patch for checking plpgsql functions..
I checked different implementation, but without success: a) enhancing
of SPI to some fake mode can has negative impact on application, and
patch was not clear, b) generic plpgsql walker doesn't save lines too.I invite any ideas how to improve this patch
I reviewed this and did a clean up for bitrot and a little whitespace.
In particular, it needed to learn a little about event triggers.This patch is a follow on from an earlier review thread I found:
http://archives.postgresql.org/message-id/D960CB61B694CF459DCFB4B0128514C2072DF447@exadv11.host.magwien.gv.atI dug through that thread a bit, and I believe issues raised by
Laurenz, Petr and Alvaro were resolved by Pavel over time.All tests pass, and after a read-through, the code seems fine.
This also represents my inaugural use of pg_bsd_indent. I ran it on
pl_check.c - which made things mostly better. Happy to try and fix it
up more if someone can explain to me what (if anything) I did
incorrectly when using it.-selena
Attachments:
-----Original Message-----
From: pgsql-hackers-owner@postgresql.org [mailto:pgsql-hackers-
owner@postgresql.org] On Behalf Of Pavel Stehule
Sent: 28 November 2012 11:07
To: Selena Deckelmann
Cc: PostgreSQL Hackers
Subject: Re: [HACKERS] plpgsql_check_function - rebase for 9.3Hello
a some updated version
* possibility to raise (and filter) performance warnings - detects IO castings
* detects assign composite value to scalar variable
Hello,
I was wondering if maybe this could be split to 2 separate patches, one would be all the changes to the existing plpgsql code - rename delete_function to plpgsql_delete_function and extern plpgsql_delete_function, copy_plpgsql_datum, plpgsql_estate_setup, plpgsql_destroy_econtext and the other patch would be the actual checker.
Reasoning for this is that the first patch (exporting some of plpgsql internals) should be very safe to commit and would be useful by itself even if the checker does not get in 9.3 since it would enable users to write lints/checkers/analysers for plpgsql as standalone extensions (for my use case this is actually way more useful than having the checker as part of core).
Regards
Petr
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Petr Jelinek" <pjmodos@pjmodos.net> writes:
I was wondering if maybe this could be split to 2 separate patches, one would be all the changes to the existing plpgsql code - rename delete_function to plpgsql_delete_function and extern plpgsql_delete_function, copy_plpgsql_datum, plpgsql_estate_setup, plpgsql_destroy_econtext and the other patch would be the actual checker.
Reasoning for this is that the first patch (exporting some of plpgsql internals) should be very safe to commit and would be useful by itself even if the checker does not get in 9.3 since it would enable users to write lints/checkers/analysers for plpgsql as standalone extensions (for my use case this is actually way more useful than having the checker as part of core).
What exactly do you have in mind there? The way we load extensions,
they can't (AFAIK) see each other's defined symbols, so you couldn't
really make an independent extension that would call functions in
plpgsql. This is not really open for debate, either, as changing that
would risk creating symbol collisions between modules that never had to
worry about polluting global namespace before.
I would note also that we absolutely, positively do not guarantee not
to change plpgsql's private data structures in minor releases.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
-----Original Message-----
From: pgsql-hackers-owner@postgresql.org [mailto:pgsql-hackers-
owner@postgresql.org] On Behalf Of Tom Lane
Sent: 26 January 2013 18:16
Subject: Re: [HACKERS] plpgsql_check_function - rebase for 9.3"Petr Jelinek" <pjmodos@pjmodos.net> writes:
I was wondering if maybe this could be split to 2 separate patches, one
would be all the changes to the existing plpgsql code - rename
delete_function to plpgsql_delete_function and extern
plpgsql_delete_function, copy_plpgsql_datum, plpgsql_estate_setup,
plpgsql_destroy_econtext and the other patch would be the actual checker.Reasoning for this is that the first patch (exporting some of plpgsql
internals) should be very safe to commit and would be useful by itself
even if
the checker does not get in 9.3 since it would enable users to write
lints/checkers/analysers for plpgsql as standalone extensions (for my use
case this is actually way more useful than having the checker as part of
core).
What exactly do you have in mind there? The way we load extensions, they
can't (AFAIK) see each other's defined symbols, so you couldn't really
make
an independent extension that would call functions in plpgsql. This is
not
really open for debate, either, as changing that would risk creating
symbol
collisions between modules that never had to worry about polluting global
namespace before.
I can call functions that are exported by plpgsql.so just fine from
different extension now, I just have to preload the plpgsql.so (via LOAD or
guc) first, so I don't see what is the problem here.
I would note also that we absolutely, positively do not guarantee not to
change plpgsql's private data structures in minor releases.
That didn't stop people from from writing plpgsql extensions before, don't
see why it would now, the problem is that they have to use hooks now and
those require the function to be executed, making those plpgsql interfaces
external would help with setting up things so that extension can work with
function without function being executed or duplicating a lot of plpgsql
code. As an example of all of this you can see
https://github.com/okbob/plpgsql_lint which is the original module Pavel
wrote before writing this patch.
The other thing is this is something the patch in current form does anyway
so I don't see the harm.
Regards
Petr
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/1/26 Petr Jelinek <pjmodos@pjmodos.net>:
-----Original Message-----
From: pgsql-hackers-owner@postgresql.org [mailto:pgsql-hackers-
owner@postgresql.org] On Behalf Of Tom Lane
Sent: 26 January 2013 18:16
Subject: Re: [HACKERS] plpgsql_check_function - rebase for 9.3"Petr Jelinek" <pjmodos@pjmodos.net> writes:
I was wondering if maybe this could be split to 2 separate patches, one
would be all the changes to the existing plpgsql code - rename
delete_function to plpgsql_delete_function and extern
plpgsql_delete_function, copy_plpgsql_datum, plpgsql_estate_setup,
plpgsql_destroy_econtext and the other patch would be the actual checker.Reasoning for this is that the first patch (exporting some of plpgsql
internals) should be very safe to commit and would be useful by itself
even if
the checker does not get in 9.3 since it would enable users to write
lints/checkers/analysers for plpgsql as standalone extensions (for my use
case this is actually way more useful than having the checker as part ofcore).
A significant improvement in this are can be placing plpgsql.h to
other header files. Now plpgsql extensions have to use private copy of
this file - what is really ugly
Pavel
What exactly do you have in mind there? The way we load extensions, they
can't (AFAIK) see each other's defined symbols, so you couldn't reallymake
an independent extension that would call functions in plpgsql. This is
not
really open for debate, either, as changing that would risk creating
symbol
collisions between modules that never had to worry about polluting global
namespace before.I can call functions that are exported by plpgsql.so just fine from
different extension now, I just have to preload the plpgsql.so (via LOAD or
guc) first, so I don't see what is the problem here.I would note also that we absolutely, positively do not guarantee not to
change plpgsql's private data structures in minor releases.That didn't stop people from from writing plpgsql extensions before, don't
see why it would now, the problem is that they have to use hooks now and
those require the function to be executed, making those plpgsql interfaces
external would help with setting up things so that extension can work with
function without function being executed or duplicating a lot of plpgsql
code. As an example of all of this you can see
https://github.com/okbob/plpgsql_lint which is the original module Pavel
wrote before writing this patch.The other thing is this is something the patch in current form does anyway
so I don't see the harm.Regards
Petr
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Petr Jelinek" <pjmodos@pjmodos.net> writes:
What exactly do you have in mind there? The way we load extensions, they
can't (AFAIK) see each other's defined symbols, so you couldn't really make
an independent extension that would call functions in plpgsql. This is not
really open for debate, either, as changing that would risk creating symbol
collisions between modules that never had to worry about polluting global
namespace before.
I can call functions that are exported by plpgsql.so just fine from
different extension now, I just have to preload the plpgsql.so (via LOAD or
guc) first, so I don't see what is the problem here.
[ pokes around... ] Hm, it appears that that does work on Linux,
because for some reason we're specifying RTLD_GLOBAL to dlopen().
TBH that seems like a truly horrid idea that we should reconsider.
Aside from the danger of unexpected symbol collisions between
independent loadable modules, I seriously doubt that it works like
that on every platform we support --- so I'd be very strongly against
accepting any code that depends on this working.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I wrote:
[ pokes around... ] Hm, it appears that that does work on Linux,
because for some reason we're specifying RTLD_GLOBAL to dlopen().
TBH that seems like a truly horrid idea that we should reconsider.
A bit of research in the archives revealed that we're using it because
back in 2001, the lame hack that then passed for a shared-library
version of python required it:
/messages/by-id/Pine.LNX.4.30.0105121914200.757-100000@peter.localdomain
There was subsequent discussion of removing it, because reportedly now
(a) that's no longer the case, and (b) we need to get rid of it to allow
plpython2 and plpython3 to coexist in one session. See for instance:
/messages/by-id/1277506674.5356.27.camel@vanquo.pezone.net
Nothing's been done about that yet, but I think that assuming that we'll
be using RTLD_GLOBAL forever would be foolish.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
-----Original Message-----
From: Tom Lane [mailto:tgl@sss.pgh.pa.us]
Sent: 26 January 2013 20:12
Subject: Re: [HACKERS] plpgsql_check_function - rebase for 9.3I wrote:
[ pokes around... ] Hm, it appears that that does work on Linux,
because for some reason we're specifying RTLD_GLOBAL to dlopen().
TBH that seems like a truly horrid idea that we should reconsider.A bit of research in the archives revealed that we're using it because
back in
2001, the lame hack that then passed for a shared-library version of
python
required it:
/messages/by-id/Pine.LNX.4.30.0105121914200.757-
100000@peter.localdomainThere was subsequent discussion of removing it, because reportedly now
(a) that's no longer the case, and (b) we need to get rid of it to allow
plpython2 and plpython3 to coexist in one session. See for instance:
http://www.postgresql.org/message-
id/1277506674.5356.27.camel@vanquo.pezone.netNothing's been done about that yet, but I think that assuming that we'll
be
using RTLD_GLOBAL forever would be foolish.
Ok then it was a bad idea after all.
Regards
Petr
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 1/26/13 1:53 PM, Tom Lane wrote:
[ pokes around... ] Hm, it appears that that does work on Linux,
because for some reason we're specifying RTLD_GLOBAL to dlopen().
TBH that seems like a truly horrid idea that we should reconsider.
Aside from the danger of unexpected symbol collisions between
independent loadable modules, I seriously doubt that it works like
that on every platform we support --- so I'd be very strongly against
accepting any code that depends on this working.
Well, that would kill a lot of potentially useful features, including
the transforms feature I've been working on and any kind of hook or
debugger or profiler on an existing module. (How do plpgsql plugins
work?) We also couldn't transparently move functionality out of the
postgres binary into a module.
I see the concern about symbol collisions. But you can normally work
around that by prefixing exported symbols.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/28/2013 10:11 AM, Peter Eisentraut wrote:
On 1/26/13 1:53 PM, Tom Lane wrote:
[ pokes around... ] Hm, it appears that that does work on Linux,
because for some reason we're specifying RTLD_GLOBAL to dlopen().
TBH that seems like a truly horrid idea that we should reconsider.
Aside from the danger of unexpected symbol collisions between
independent loadable modules, I seriously doubt that it works like
that on every platform we support --- so I'd be very strongly against
accepting any code that depends on this working.Well, that would kill a lot of potentially useful features, including
the transforms feature I've been working on and any kind of hook or
debugger or profiler on an existing module. (How do plpgsql plugins
work?) We also couldn't transparently move functionality out of the
postgres binary into a module.I see the concern about symbol collisions. But you can normally work
around that by prefixing exported symbols.
Yeah, I was just writing something a couple of days ago that leveraged
stuff in an extension, so it looks like this is wanted functionality. In
general we want to be able to layer addon modules, ISTM.
cheers
andrew
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello
I though about any possibility how to reduce a size of this patch.
I see a one solution - if we would not support a detection of multiple
errors - it stops on first error, we can push this code to compilation
stage.
a plpgsql_validator can be overloaded by function
plpgsql_validator(oid, reloid, level)
reloid - is requested by triggers - it is related class oid
level - it is level of checking
0 - same as current implementation - check only syntax errors
1 - stop on first object error - no table, no field, no expected data type
2 - stop on first performance issue - implicit casting identification
(should be removed or moved to next releases)
This proposal reduces functionality proposed for
plpgsql_check_function - but basic functionality is there.
It has not a impact on performance and it allow check all paths in
compile time.
It will not change behave of current CREATE OR REPLACE function -
there is not a problem with back compatibility
It is good for you?
I can have this modification to end of this week.
Regards
Pavel
2012/11/28 Pavel Stehule <pavel.stehule@gmail.com>:
Hello
a some updated version
* possibility to raise (and filter) performance warnings - detects IO castings
* detects assign composite value to scalar variableRegards
Pavel Stehule
2012/11/27 Pavel Stehule <pavel.stehule@gmail.com>:
Hello
I am sending a updated version - now it is prepared for event triggers
and it is little bit more robustI run pgindent, but I have no experience with it, so I am not sure about success
Regards
Pavel Stehule
2012/10/7 Selena Deckelmann <selena@chesnok.com>:
Hi!
On Tue, Jun 26, 2012 at 1:19 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
I am sending lightly refreshed patch for checking plpgsql functions..
I checked different implementation, but without success: a) enhancing
of SPI to some fake mode can has negative impact on application, and
patch was not clear, b) generic plpgsql walker doesn't save lines too.I invite any ideas how to improve this patch
I reviewed this and did a clean up for bitrot and a little whitespace.
In particular, it needed to learn a little about event triggers.This patch is a follow on from an earlier review thread I found:
http://archives.postgresql.org/message-id/D960CB61B694CF459DCFB4B0128514C2072DF447@exadv11.host.magwien.gv.atI dug through that thread a bit, and I believe issues raised by
Laurenz, Petr and Alvaro were resolved by Pavel over time.All tests pass, and after a read-through, the code seems fine.
This also represents my inaugural use of pg_bsd_indent. I ran it on
pl_check.c - which made things mostly better. Happy to try and fix it
up more if someone can explain to me what (if anything) I did
incorrectly when using it.-selena
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Folks,
Where are we with this patch? I'm a bit unclear from the discussion on
whether it passes muster or not. Things seem to have petered out.
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Josh Berkus <josh@agliodbs.com> writes:
Where are we with this patch? I'm a bit unclear from the discussion on
whether it passes muster or not. Things seem to have petered out.
I took another look at this patch tonight. I think the thing that is
bothering everybody (including Pavel) is that as submitted, pl_check.c
involves a huge amount of duplication of knowledge and code from
pl_exec.c, and to a lesser extent pl_comp.c. It certainly looks like a
maintenance disaster in the making. It doesn't bother me so much that
pl_check.c knows about each syntactic structure in plpgsql: there are
already four or five places you have to touch when adding new syntax.
Rather, it's that there's so much duplication of knowledge about
semantic details, which is largely expressed by copied-and-pasted code
from pl_exec.c. It seems like a safe bet that we'll sometimes miss the
need to fix pl_check.c when we fix something in pl_exec.c. There are
annoying duplications from pl_comp.c as well, eg knowledge of exactly
which magic variables are defined in trigger functions.
Having said all that, it's not clear that we can really do any better.
The only obvious alternative is pushing support for a checking mode
directly into pl_exec.c, which would obfuscate and slow down that code
to an unacceptable degree if Pavel's results at
/messages/by-id/CAFj8pRAKuJmVjPjzfSryE7+uB8jF8Wtz5rkxK-0ykXme-k81kA@mail.gmail.com
are any indication. (In that message, Pavel proposes shoveling the
problem into the compile code instead, but that seems unlikely to work
IMO: the main problem in pl_check.c as it stands is duplication of code
from pl_exec.c not pl_comp.c. So I think that path could only lead to
duplicating the same code into pl_comp.c.)
So question 1 is do we want to accept that this is the implementation
pathway we're going to settle for, or are we going to hold out for a
better one? I'd be the first in line to hold out if I had a clue how
to move towards a better implementation, but I have none. Are we
prepared to reject this type of feature entirely because of the
code-duplication problem? That doesn't seem attractive either.
But, even granting that this implementation approach is acceptable,
the patch does not seem close to being committable as-is: there's
a lot of fit-and-finish work yet to be done. To make my point I will
just quote from one of the regression test additions:
create or replace function f1()
returns void as $$
declare a1 int; a2 int;
begin
select 10,20 into a1;
end;
$$ language plpgsql;
-- raise warning
select plpgsql_check_function('f1()');
plpgsql_check_function
-------------------------------------------------------------------------
warning:00000:4:SQL statement:too many attributies for target variables
Detail: There are less target variables than output columns in query.
Hint: Check target variables in SELECT INTO statement
(3 rows)
Do we like this output format? I don't. The unlabeled, undocumented
fields crammed into a single line with colon separators are neither
readable nor useful. If we actually need these fields, why aren't we
splitting the output into multiple columns? (I'm also wondering why
the patch bothers with an option to emit this same info in XML. Surely
there is vanishingly small use-case for mechanical examination of this
output.)
This example also shows that the user-visible messages have had neither
editing from a native speaker of English, nor even attention from a
spell checker. I don't fault Pavel for that (his English is far better
than my Czech) but this patch has been passed by at least one reviewer
who is a native speaker. Personally I'd also say that neither the
Detail nor Hint messages in this example are worth the electrons they
take up, as they add nothing useful to the basic error message. I'd be
embarrassed to be presenting output like this to end users of Postgres.
(The code comments are in even worse shape than the user-visible
messages, by the by, where they exist at all.)
A somewhat related point is that it doesn't look like any thought
has been taken about localized message output.
This stuff is certainly all fixable if we agree on the basic
implementation approach; and I can't really fault Pavel for not
worrying much about such details while the implementation approach
hasn't been agreed to. But I'd judge that there's a week or more's
worth of work here to get to a patch I'd be happy about committing,
even without any change in the basic approach. That's not time I'm
willing to put in personally right now.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello all
2013/3/26 Tom Lane <tgl@sss.pgh.pa.us>:
Josh Berkus <josh@agliodbs.com> writes:
Where are we with this patch? I'm a bit unclear from the discussion on
whether it passes muster or not. Things seem to have petered out.I took another look at this patch tonight. I think the thing that is
bothering everybody (including Pavel) is that as submitted, pl_check.c
involves a huge amount of duplication of knowledge and code from
pl_exec.c, and to a lesser extent pl_comp.c. It certainly looks like a
maintenance disaster in the making. It doesn't bother me so much that
pl_check.c knows about each syntactic structure in plpgsql: there are
already four or five places you have to touch when adding new syntax.
Rather, it's that there's so much duplication of knowledge about
semantic details, which is largely expressed by copied-and-pasted code
from pl_exec.c. It seems like a safe bet that we'll sometimes miss the
need to fix pl_check.c when we fix something in pl_exec.c. There are
annoying duplications from pl_comp.c as well, eg knowledge of exactly
which magic variables are defined in trigger functions.Having said all that, it's not clear that we can really do any better.
The only obvious alternative is pushing support for a checking mode
directly into pl_exec.c, which would obfuscate and slow down that code
to an unacceptable degree if Pavel's results at
/messages/by-id/CAFj8pRAKuJmVjPjzfSryE7+uB8jF8Wtz5rkxK-0ykXme-k81kA@mail.gmail.com
are any indication. (In that message, Pavel proposes shoveling the
problem into the compile code instead, but that seems unlikely to work
IMO: the main problem in pl_check.c as it stands is duplication of code
from pl_exec.c not pl_comp.c. So I think that path could only lead to
duplicating the same code into pl_comp.c.)So question 1 is do we want to accept that this is the implementation
pathway we're going to settle for, or are we going to hold out for a
better one? I'd be the first in line to hold out if I had a clue how
to move towards a better implementation, but I have none. Are we
prepared to reject this type of feature entirely because of the
code-duplication problem? That doesn't seem attractive either.
I wrote lot of versions and proposed code is redundant, but most
simple and clean.
I am really against to pushing check to pl_exec, because it
significantly decrease code readability and increase some bottle neck
in CPU extensive tests. More, there are too less place for some future
feature - performance advising, more verbose error messages, etc
In PL/pgPSM I used a little bit different architecture - necessary for
PSM and maybe better for PL/pgSQL too. It is two stage compiler -
parsing to AST, and AST compilation. It simplify gram.y and processing
order depends on AST deep iteration and not on bizon rules. It can
have a impact on speed of very large procedures probably - I don't see
different disadvantages. With this architecture I was able do lot of
controls to compile stage without problems.
Most complexity in current code is related to detecting record types
from expressions without expression evaluation. Maybe this code can be
in core or pl_compile.c. Code for Describe (F) message is not too
reusable. It is necessary for
DECLARE r RECORD;
FOR r IN SELECT ...
LOOP
RAISE NOTICE '>>%<<', r.xx;
END LOOP;
But, even granting that this implementation approach is acceptable,
the patch does not seem close to being committable as-is: there's
a lot of fit-and-finish work yet to be done. To make my point I will
just quote from one of the regression test additions:create or replace function f1()
returns void as $$
declare a1 int; a2 int;
begin
select 10,20 into a1;
end;
$$ language plpgsql;
-- raise warning
select plpgsql_check_function('f1()');
plpgsql_check_function
-------------------------------------------------------------------------
warning:00000:4:SQL statement:too many attributies for target variables
Detail: There are less target variables than output columns in query.
Hint: Check target variables in SELECT INTO statement
(3 rows)Do we like this output format? I don't. The unlabeled, undocumented
fields crammed into a single line with colon separators are neither
readable nor useful. If we actually need these fields, why aren't we
splitting the output into multiple columns? (I'm also wondering why
the patch bothers with an option to emit this same info in XML. Surely
there is vanishingly small use-case for mechanical examination of this
output.)
This format can be reduced, redesigned, changed. It is designed like
gcc output and optimized for using from psql console.
I tested table output - in original CHECK statement implementation,
but it is not too friendly for showing in monitor - it is just too
wide. There are similar arguments like using tabular output for
EXPLAIN, although there are higher complexity and nested structures
(in EXPLAIN). Personally, I can live with tabular output, it is not
user friendly, but it can be used for machine processing simply, and
any advanced user can write some transform functions to any output
format.
This example also shows that the user-visible messages have had neither
editing from a native speaker of English, nor even attention from a
spell checker. I don't fault Pavel for that (his English is far better
than my Czech) but this patch has been passed by at least one reviewer
who is a native speaker. Personally I'd also say that neither the
Detail nor Hint messages in this example are worth the electrons they
take up, as they add nothing useful to the basic error message. I'd be
embarrassed to be presenting output like this to end users of Postgres.(The code comments are in even worse shape than the user-visible
messages, by the by, where they exist at all.)A somewhat related point is that it doesn't look like any thought
has been taken about localized message output.This stuff is certainly all fixable if we agree on the basic
implementation approach; and I can't really fault Pavel for not
worrying much about such details while the implementation approach
hasn't been agreed to. But I'd judge that there's a week or more's
worth of work here to get to a patch I'd be happy about committing,
even without any change in the basic approach. That's not time I'm
willing to put in personally right now.
Yes. A details should be precised after final decision. I am hope so
this code doesn't contains significant bug - I have not any reported
issue related to this functionality - it is based on plpgsql_lint -
and this code is used in production by some very large companies. Of
course, there are not too large community - it should be manually
compiled.
Regards
Pavel
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello
I redesigned output from plpgsql_check_function. Now, it returns table
everytime.
Litlle bit code simplification.
postgres=# \sf fx
CREATE OR REPLACE FUNCTION public.fx(integer)
RETURNS integer
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN (SELECT a FROM t1 WHERE b < $1);
END;
$function$
postgres=# select * from plpgsql_check_function('fx(int)');
-[ RECORD 1 ]--------------------------------------
functionid | fx
lineno | 3
statement | RETURN
sqlstate | 42703
message | column "b" does not exist
detail | [null]
hint | [null]
level | error
position | 32
query | SELECT (SELECT a FROM t1 WHERE b < $1)
context | [null]
Regards
Pavel
2013/3/26 Tom Lane <tgl@sss.pgh.pa.us>:
Show quoted text
Josh Berkus <josh@agliodbs.com> writes:
Where are we with this patch? I'm a bit unclear from the discussion on
whether it passes muster or not. Things seem to have petered out.I took another look at this patch tonight. I think the thing that is
bothering everybody (including Pavel) is that as submitted, pl_check.c
involves a huge amount of duplication of knowledge and code from
pl_exec.c, and to a lesser extent pl_comp.c. It certainly looks like a
maintenance disaster in the making. It doesn't bother me so much that
pl_check.c knows about each syntactic structure in plpgsql: there are
already four or five places you have to touch when adding new syntax.
Rather, it's that there's so much duplication of knowledge about
semantic details, which is largely expressed by copied-and-pasted code
from pl_exec.c. It seems like a safe bet that we'll sometimes miss the
need to fix pl_check.c when we fix something in pl_exec.c. There are
annoying duplications from pl_comp.c as well, eg knowledge of exactly
which magic variables are defined in trigger functions.Having said all that, it's not clear that we can really do any better.
The only obvious alternative is pushing support for a checking mode
directly into pl_exec.c, which would obfuscate and slow down that code
to an unacceptable degree if Pavel's results at
/messages/by-id/CAFj8pRAKuJmVjPjzfSryE7+uB8jF8Wtz5rkxK-0ykXme-k81kA@mail.gmail.com
are any indication. (In that message, Pavel proposes shoveling the
problem into the compile code instead, but that seems unlikely to work
IMO: the main problem in pl_check.c as it stands is duplication of code
from pl_exec.c not pl_comp.c. So I think that path could only lead to
duplicating the same code into pl_comp.c.)So question 1 is do we want to accept that this is the implementation
pathway we're going to settle for, or are we going to hold out for a
better one? I'd be the first in line to hold out if I had a clue how
to move towards a better implementation, but I have none. Are we
prepared to reject this type of feature entirely because of the
code-duplication problem? That doesn't seem attractive either.But, even granting that this implementation approach is acceptable,
the patch does not seem close to being committable as-is: there's
a lot of fit-and-finish work yet to be done. To make my point I will
just quote from one of the regression test additions:create or replace function f1()
returns void as $$
declare a1 int; a2 int;
begin
select 10,20 into a1;
end;
$$ language plpgsql;
-- raise warning
select plpgsql_check_function('f1()');
plpgsql_check_function
-------------------------------------------------------------------------
warning:00000:4:SQL statement:too many attributies for target variables
Detail: There are less target variables than output columns in query.
Hint: Check target variables in SELECT INTO statement
(3 rows)Do we like this output format? I don't. The unlabeled, undocumented
fields crammed into a single line with colon separators are neither
readable nor useful. If we actually need these fields, why aren't we
splitting the output into multiple columns? (I'm also wondering why
the patch bothers with an option to emit this same info in XML. Surely
there is vanishingly small use-case for mechanical examination of this
output.)This example also shows that the user-visible messages have had neither
editing from a native speaker of English, nor even attention from a
spell checker. I don't fault Pavel for that (his English is far better
than my Czech) but this patch has been passed by at least one reviewer
who is a native speaker. Personally I'd also say that neither the
Detail nor Hint messages in this example are worth the electrons they
take up, as they add nothing useful to the basic error message. I'd be
embarrassed to be presenting output like this to end users of Postgres.(The code comments are in even worse shape than the user-visible
messages, by the by, where they exist at all.)A somewhat related point is that it doesn't look like any thought
has been taken about localized message output.This stuff is certainly all fixable if we agree on the basic
implementation approach; and I can't really fault Pavel for not
worrying much about such details while the implementation approach
hasn't been agreed to. But I'd judge that there's a week or more's
worth of work here to get to a patch I'd be happy about committing,
even without any change in the basic approach. That's not time I'm
willing to put in personally right now.regards, tom lane
Attachments:
On Wed, 2013-03-27 at 23:25 +0100, Pavel Stehule wrote:
I redesigned output from plpgsql_check_function. Now, it returns table
everytime.
Litlle bit code simplification.
This patch is in the 2013-09 commitfest but needs a rebase.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
rebased
Regards
Pavel
2013/8/22 Peter Eisentraut <peter_e@gmx.net>
Show quoted text
On Wed, 2013-03-27 at 23:25 +0100, Pavel Stehule wrote:
I redesigned output from plpgsql_check_function. Now, it returns table
everytime.
Litlle bit code simplification.This patch is in the 2013-09 commitfest but needs a rebase.
Attachments:
On 08/22/2013 02:02 AM, Pavel Stehule wrote:
rebased
Regards
Pavel
This patch again needs a rebase, it no longer applies cleanly.
plpgsql_estate_setup now takes a shared estate parameter and the call in
pl_check needs that. I passed NULL in and things seem to work.
I think this is another example where are processes aren't working as
we'd like.
The last time this patch got a review was in March when Tom said
/messages/by-id/27661.1364267665@sss.pgh.pa.us
Shortly after Pavel responded with a new patch that changes the output
format.
/messages/by-id/CAFj8pRAqpAEWqL465xkTmwe-DaZ8+7N-oEQ=uv_JjoP21FJmrQ@mail.gmail.com
I don't much has progress in the following 9 months on this patch.
In Tom's review the issue of code duplication and asks:
"do we want to accept that this is the implementation pathway we're
going to settle for, or are we going to hold out for a better one? I'd
be the first in line to hold out if I had a clue how to move towards a
better implementation, but I have none."
This question is still outstanding.
Here are my comments on this version of the patch:
This version of the patch gives output as a table with the structure:
functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context
------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+---------
I like this much better than the previous format.
I think the patch needs more documentation.
The extent of the documentation is
<title>Checking of embedded SQL</title>
+ <para>
+ The SQL statements inside <application>PL/pgSQL</> functions are
+ checked by validator for semantic errors. These errors
+ can be found by <function>plpgsql_check_function</function>:
I a don't think that this adequately describes the function. It isn't clear to me what we mean by 'embedded' SQL.
and 'semantic errors'.
I think this would read better as
<sect2 id="plpgsql-check-function">
<title>Checking SQL Inside Functions</title>
<para>
The SQL Statements inside of <application>PL/pgsql</> functions can be
checked by a validation function for semantic errors. You can check
<application>PL/pgsl</application> functions by passing the name of the
function to <function>plpgsql_check_function</function>.
</para>
<para>
The <function>plpgsql_check_function</function> will check the SQL
inside of a <application>PL/pgsql</application> function for certain
types of errors and warnings.
</para>
but that needs to be expanded on.
I was expecting something like:
create function x() returns void as $$ declare a int4; begin a='f'; end $$ language plpgsql;
to return an error but it doesn't
but
create temp table x(a int4);
create or replace function x() returns void as $$ declare a int4; begin insert into x values('h'); end $$ language plpgsql;
does give an error when I pass it to the validator. Is this the intended behavior of the patch? If so we need to explain why the first function doesn't generate an error.
I feel we need to document what each of the columns in the output means. What is the difference between statement, query and context?
Some random comments on the messages you return:
Line 816:
if (is_expression && tupdesc->natts != 1)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("qw",
query->query,
tupdesc->natts)));
Should this be "query \"%s\" returned %d columns but only 1 is allowed" or something similar?
Line 837
else
/* XXX: should be a warning? */
ereport(ERROR,
(errmsg("cannot to identify real type for record type variable")));
In what situation does this come up? Should it be a warning or error?
Do we mean "the real type for record variable" ?
Line 1000
ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("function does not return
composite type, is not possible to identify composite type")));
Should this be "function does not return a composite type, it is not
possible to identify the composite type" ?
Line 1109:
appendStringInfo(&str, "parameter \"%s\" is overlapped",
+ refname);
gives warnings like:
select message,detail FROM plpgsql_check_function('b(int)');
message | detail
-----------------------------+--------------------------------------------
parameter "a" is overlapped | Local variable overlap function parameter.
How about instead
"parameter "a" is duplicated" | Local variable is named the same as the
function parameter
Line 1278:
+ checker_error(cstate,
+ 0, 0,
+ "cannot determinate a result of
dynamic SQL",
+ "Cannot to contine in check.",
+ "Don't use dynamic SQL and record type together,
when you would check function.",
+ "warning",
+ 0, NULL, NULL);
How about
"cannot determine the result of dynamic SQL" , "Cannot continue
validating the function", "Do not use plpgsql_check_function on
functions with dynamic SQL"
Also this limitation should be explained in the documentation.
I also thing we need to distinguish between warnings generated because
of problems in the function versus warnings generated because of
limitations in the validator. This implies that there is maybe
something wrong with my function but there is nothing wrong with using
dynamic SQL in functions this is just telling users about a runtime
warning of the validator itself.
Same thing around line 1427
I have not done an in-depth read of the code.
I'm sending this out this patch at least gets some review. I don't
think that I will have a lot more time in the next week to do a more
thorough review or follow-up review
If we aren't happy with the overal approach of this patch then we need
to tell Pavel.
My vote would be to try to get the patch (documentation, error messages,
'XXX' items, etc) into a better state so it can eventually be committed
Steve
2013/8/22 Peter Eisentraut <peter_e@gmx.net <mailto:peter_e@gmx.net>>
On Wed, 2013-03-27 at 23:25 +0100, Pavel Stehule wrote:
I redesigned output from plpgsql_check_function. Now, it returns
table
everytime.
Litlle bit code simplification.This patch is in the 2013-09 commitfest but needs a rebase.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello
thank you for review
2013/12/7 Steve Singer <steve@ssinger.info>
On 08/22/2013 02:02 AM, Pavel Stehule wrote:
rebased
Regards
Pavel
This patch again needs a rebase, it no longer applies cleanly.
plpgsql_estate_setup now takes a shared estate parameter and the call in
pl_check needs that. I passed NULL in and things seem to work.I think this is another example where are processes aren't working as we'd
like.The last time this patch got a review was in March when Tom said
/messages/by-id/27661.1364267665@sss.pgh.pa.usShortly after Pavel responded with a new patch that changes the output
format. http://www.postgresql.org/message-id/
CAFj8pRAqpAEWqL465xkTmwe-DaZ8+7N-oEQ=uv_JjoP21FJmrQ@mail.gmail.comI don't much has progress in the following 9 months on this patch.
In Tom's review the issue of code duplication and asks:
"do we want to accept that this is the implementation pathway we're going
to settle for, or are we going to hold out for a better one? I'd be the
first in line to hold out if I had a clue how to move towards a better
implementation, but I have none."
yes, I am waiting still to a) have some better idea, or b) have
significantly more free time for larger plpgsql refactoring. Nothing of it
happens :(
This question is still outstanding.
Here are my comments on this version of the patch:
This version of the patch gives output as a table with the structure:
functionid | lineno | statement | sqlstate | message | detail | hint |
level | position | query | context
------------+--------+-----------+----------+---------+-----
---+------+-------+----------+-------+---------I like this much better than the previous format.
I think the patch needs more documentation.
The extent of the documentation is
<title>Checking of embedded SQL</title> + <para> + The SQL statements inside <application>PL/pgSQL</> functions are + checked by validator for semantic errors. These errors + can be found by <function>plpgsql_check_function</function>:I a don't think that this adequately describes the function. It isn't
clear to me what we mean by 'embedded' SQL.
and 'semantic errors'.I think this would read better as
<sect2 id="plpgsql-check-function">
<title>Checking SQL Inside Functions</title>
<para>
The SQL Statements inside of <application>PL/pgsql</> functions can be
checked by a validation function for semantic errors. You can check
<application>PL/pgsl</application> functions by passing the name of
the
function to <function>plpgsql_check_function</function>.
</para>
<para>
The <function>plpgsql_check_function</function> will check the SQL
inside of a <application>PL/pgsql</application> function for certain
types of errors and warnings.
</para>but that needs to be expanded on.
I was expecting something like:
create function x() returns void as $$ declare a int4; begin a='f'; end $$
language plpgsql;to return an error but it doesn't
This is expected. PL/pgSQL use a late casting - so a := '10'; is
acceptable. And in this moment plpgsql check doesn't evaluate constant and
doesn't try to cast to target type. But this check can be implemented
sometime. In this moment, we have no simple way how to identify constant
expression in plpgsql. So any constants are expressions from plpgsql
perspective.
but
create temp table x(a int4);
create or replace function x() returns void as $$ declare a int4; begin
insert into x values('h'); end $$ language plpgsql;
it is expected too. SQL doesn't use late casting - casting is controlled by
available casts and constants are evaluated early - due possible
optimization.
does give an error when I pass it to the validator. Is this the intended
behavior of the patch? If so we need to explain why the first function
doesn't generate an error.I feel we need to document what each of the columns in the output means.
What is the difference between statement, query and context?Some random comments on the messages you return:
Line 816:
if (is_expression && tupdesc->natts != 1)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("qw",
query->query,
tupdesc->natts)));Should this be "query \"%s\" returned %d columns but only 1 is allowed"
or something similar?Line 837
else
/* XXX: should be a warning? */
ereport(ERROR,
(errmsg("cannot to identify real
type for record type variable")));In what situation does this come up? Should it be a warning or error?
Do we mean "the real type for record variable" ?
a most difficult part of plpgsql check function is about transformation
dynamic types to know row types. It is necessary for checking usable
functions and operators.
typical pattern is:
declare r record;
begin
for r in select a, b from some_tab
loop
raise notice '%', extract(day from r.a);
end loop;
and we should to detect type of r.a. Sometimes we cannot to do it due using
dynamic SQL - plpgsql check doesn't try to evaluate expressions (as
protection against side efects).
Line 1000 ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("function does not return composite type, is not possible to identify composite type")));Should this be "function does not return a composite type, it is not
possible to identify the composite type" ?Line 1109:
appendStringInfo(&str, "parameter \"%s\" is overlapped",
+ refname);gives warnings like:
select message,detail FROM plpgsql_check_function('b(int)');
message | detail
-----------------------------+--------------------------------------------
parameter "a" is overlapped | Local variable overlap function parameter.How about instead
"parameter "a" is duplicated" | Local variable is named the same as the
function parameter
I have no idea about good sentence, but "duplicate" probably is not best.
Any variable can be locally overlapped. Probably any overlapping should be
signalized - it is a bad design always.
Line 1278:
+ checker_error(cstate, + 0, 0, + "cannot determinate a result of dynamic SQL", + "Cannot to contine in check.", + "Don't use dynamic SQL and record type together, when you would check function.", + "warning", + 0, NULL, NULL);How about
"cannot determine the result of dynamic SQL" , "Cannot continue validating
the function", "Do not use plpgsql_check_function on functions with dynamic
SQL"
Also this limitation should be explained in the documentation.I also thing we need to distinguish between warnings generated because of
problems in the function versus warnings generated because of limitations
in the validator. This implies that there is maybe something wrong with
my function but there is nothing wrong with using dynamic SQL in functions
this is just telling users about a runtime warning of the validator itself.Same thing around line 1427
I have not done an in-depth read of the code.
I'm sending this out this patch at least gets some review. I don't think
that I will have a lot more time in the next week to do a more thorough
review or follow-up reviewIf we aren't happy with the overal approach of this patch then we need to
tell Pavel.My vote would be to try to get the patch (documentation, error messages,
'XXX' items, etc) into a better state so it can eventually be committed
Thank you
Regards
Pavel
Show quoted text
Steve
2013/8/22 Peter Eisentraut <peter_e@gmx.net <mailto:peter_e@gmx.net>>
On Wed, 2013-03-27 at 23:25 +0100, Pavel Stehule wrote:
I redesigned output from plpgsql_check_function. Now, it returns
table
everytime.
Litlle bit code simplification.This patch is in the 2013-09 commitfest but needs a rebase.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
There is still valid a variant to move check function to contrib for fist
moment.
Regards
Pavel
2013/12/7 Pavel Stehule <pavel.stehule@gmail.com>
Show quoted text
Hello
thank you for review
2013/12/7 Steve Singer <steve@ssinger.info>
On 08/22/2013 02:02 AM, Pavel Stehule wrote:
rebased
Regards
Pavel
This patch again needs a rebase, it no longer applies cleanly.
plpgsql_estate_setup now takes a shared estate parameter and the call in
pl_check needs that. I passed NULL in and things seem to work.I think this is another example where are processes aren't working as
we'd like.The last time this patch got a review was in March when Tom said
/messages/by-id/27661.1364267665@sss.pgh.pa.usShortly after Pavel responded with a new patch that changes the output
format. http://www.postgresql.org/message-id/
CAFj8pRAqpAEWqL465xkTmwe-DaZ8+7N-oEQ=uv_JjoP21FJmrQ@mail.gmail.comI don't much has progress in the following 9 months on this patch.
In Tom's review the issue of code duplication and asks:
"do we want to accept that this is the implementation pathway we're going
to settle for, or are we going to hold out for a better one? I'd be the
first in line to hold out if I had a clue how to move towards a better
implementation, but I have none."yes, I am waiting still to a) have some better idea, or b) have
significantly more free time for larger plpgsql refactoring. Nothing of it
happens :(This question is still outstanding.
Here are my comments on this version of the patch:
This version of the patch gives output as a table with the structure:
functionid | lineno | statement | sqlstate | message | detail | hint |
level | position | query | context
------------+--------+-----------+----------+---------+-----
---+------+-------+----------+-------+---------I like this much better than the previous format.
I think the patch needs more documentation.
The extent of the documentation is
<title>Checking of embedded SQL</title> + <para> + The SQL statements inside <application>PL/pgSQL</> functions are + checked by validator for semantic errors. These errors + can be found by <function>plpgsql_check_function</function>:I a don't think that this adequately describes the function. It isn't
clear to me what we mean by 'embedded' SQL.
and 'semantic errors'.I think this would read better as
<sect2 id="plpgsql-check-function">
<title>Checking SQL Inside Functions</title>
<para>
The SQL Statements inside of <application>PL/pgsql</> functions can
be
checked by a validation function for semantic errors. You can check
<application>PL/pgsl</application> functions by passing the name of
the
function to <function>plpgsql_check_function</function>.
</para>
<para>
The <function>plpgsql_check_function</function> will check the SQL
inside of a <application>PL/pgsql</application> function for certain
types of errors and warnings.
</para>but that needs to be expanded on.
I was expecting something like:
create function x() returns void as $$ declare a int4; begin a='f'; end
$$ language plpgsql;to return an error but it doesn't
This is expected. PL/pgSQL use a late casting - so a := '10'; is
acceptable. And in this moment plpgsql check doesn't evaluate constant and
doesn't try to cast to target type. But this check can be implemented
sometime. In this moment, we have no simple way how to identify constant
expression in plpgsql. So any constants are expressions from plpgsql
perspective.but
create temp table x(a int4);
create or replace function x() returns void as $$ declare a int4; begin
insert into x values('h'); end $$ language plpgsql;it is expected too. SQL doesn't use late casting - casting is controlled
by available casts and constants are evaluated early - due possible
optimization.does give an error when I pass it to the validator. Is this the
intended behavior of the patch? If so we need to explain why the first
function doesn't generate an error.I feel we need to document what each of the columns in the output means.
What is the difference between statement, query and context?Some random comments on the messages you return:
Line 816:
if (is_expression && tupdesc->natts != 1)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("qw",
query->query,
tupdesc->natts)));Should this be "query \"%s\" returned %d columns but only 1 is allowed"
or something similar?Line 837
else
/* XXX: should be a warning? */
ereport(ERROR,
(errmsg("cannot to identify real
type for record type variable")));In what situation does this come up? Should it be a warning or error?
Do we mean "the real type for record variable" ?a most difficult part of plpgsql check function is about transformation
dynamic types to know row types. It is necessary for checking usable
functions and operators.typical pattern is:
declare r record;
begin
for r in select a, b from some_tab
loop
raise notice '%', extract(day from r.a);
end loop;and we should to detect type of r.a. Sometimes we cannot to do it due
using dynamic SQL - plpgsql check doesn't try to evaluate expressions (as
protection against side efects).Line 1000 ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("function does not return composite type, is not possible to identify composite type")));Should this be "function does not return a composite type, it is not
possible to identify the composite type" ?Line 1109:
appendStringInfo(&str, "parameter \"%s\" is overlapped",
+ refname);gives warnings like:
select message,detail FROM plpgsql_check_function('b(int)');
message | detail
-----------------------------+------------------------------
--------------
parameter "a" is overlapped | Local variable overlap function parameter.How about instead
"parameter "a" is duplicated" | Local variable is named the same as the
function parameterI have no idea about good sentence, but "duplicate" probably is not best.
Any variable can be locally overlapped. Probably any overlapping should be
signalized - it is a bad design always.Line 1278:
+ checker_error(cstate, + 0, 0, + "cannot determinate a result of dynamic SQL", + "Cannot to contine in check.", + "Don't use dynamic SQL and record type together, when you would check function.", + "warning", + 0, NULL, NULL);How about
"cannot determine the result of dynamic SQL" , "Cannot continue
validating the function", "Do not use plpgsql_check_function on functions
with dynamic SQL"
Also this limitation should be explained in the documentation.I also thing we need to distinguish between warnings generated because of
problems in the function versus warnings generated because of limitations
in the validator. This implies that there is maybe something wrong with
my function but there is nothing wrong with using dynamic SQL in functions
this is just telling users about a runtime warning of the validator itself.Same thing around line 1427
I have not done an in-depth read of the code.
I'm sending this out this patch at least gets some review. I don't think
that I will have a lot more time in the next week to do a more thorough
review or follow-up reviewIf we aren't happy with the overal approach of this patch then we need to
tell Pavel.My vote would be to try to get the patch (documentation, error messages,
'XXX' items, etc) into a better state so it can eventually be committedThank you
Regards
Pavel
Steve
2013/8/22 Peter Eisentraut <peter_e@gmx.net <mailto:peter_e@gmx.net>>
On Wed, 2013-03-27 at 23:25 +0100, Pavel Stehule wrote:
I redesigned output from plpgsql_check_function. Now, it returns
table
everytime.
Litlle bit code simplification.This patch is in the 2013-09 commitfest but needs a rebase.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
In my opinion, the idea of having a separate lint checker for a language
is antiquated. If there are problems, they should be diagnosed at
compile time or run time. You can add options about warning levels or
strictness if there are concerns about which diagnostics are
appropriate.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, 2013-12-08 at 09:45 +0100, Pavel Stehule wrote:
There is still valid a variant to move check function to contrib for
fist moment.
If we are not happy with the code or the interface, then moving it to
contrib is not a solution. We are still committed to main things in
contrib indefinitely.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/12/8 Peter Eisentraut <peter_e@gmx.net>
In my opinion, the idea of having a separate lint checker for a language
is antiquated. If there are problems, they should be diagnosed at
compile time or run time. You can add options about warning levels or
strictness if there are concerns about which diagnostics are
appropriate.
There are two points, that should be solved
a) introduction a dependency to other object in schema - now CREATE
FUNCTION is fully independent on others
b) slow start - if we check all paths on start, then start can be slower -
and some functions should not work due dependency on temporary tables.
I am thinking about possible marking a function by #option (we have same
idea)
some like
#option check_on_first_start
#option check_on_create
#option check_newer
But still I have no idea, how to push check without possible slowdown
execution with code duplication
Pavel
Show quoted text
On Sun, Dec 8, 2013 at 10:31 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2013/12/8 Peter Eisentraut <peter_e@gmx.net>
In my opinion, the idea of having a separate lint checker for a language
is antiquated. If there are problems, they should be diagnosed at
compile time or run time. You can add options about warning levels or
strictness if there are concerns about which diagnostics are
appropriate.There are two points, that should be solved
a) introduction a dependency to other object in schema - now CREATE FUNCTION
is fully independent on othersb) slow start - if we check all paths on start, then start can be slower -
and some functions should not work due dependency on temporary tables.I am thinking about possible marking a function by #option (we have same
idea)some like
#option check_on_first_start
#option check_on_create
#option check_newer
what exactly check_newer means, does it mean whenever a function is
replaced (changed)?
But still I have no idea, how to push check without possible slowdown
execution with code duplication
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/12/9 Amit Kapila <amit.kapila16@gmail.com>
On Sun, Dec 8, 2013 at 10:31 PM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:2013/12/8 Peter Eisentraut <peter_e@gmx.net>
In my opinion, the idea of having a separate lint checker for a language
is antiquated. If there are problems, they should be diagnosed at
compile time or run time. You can add options about warning levels or
strictness if there are concerns about which diagnostics are
appropriate.There are two points, that should be solved
a) introduction a dependency to other object in schema - now CREATE
FUNCTION
is fully independent on others
b) slow start - if we check all paths on start, then start can be slower
-
and some functions should not work due dependency on temporary tables.
I am thinking about possible marking a function by #option (we have same
idea)some like
#option check_on_first_start
#option check_on_create
#option check_newerwhat exactly check_newer means, does it mean whenever a function is
replaced (changed)?
no, it means, so request for check will be ignored ever - some functions
cannot be deeply checked due using dynamic SQL or dynamic created data
types - temporary tables created in functions.
Regards
Pavel
Show quoted text
But still I have no idea, how to push check without possible slowdown
execution with code duplicationWith Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 12/8/13 11:24 PM, Pavel Stehule wrote:
#option check_on_first_start
#option check_on_create
#option check_newerwhat exactly check_newer means, does it mean whenever a function is
replaced (changed)?no, it means, so request for check will be ignored ever - some functions cannot be deeply checked due using dynamic SQL or dynamic created data types - temporary tables created in functions.
So presumably it would be check_never, not check_newer... :) BTW, it's not terribly hard to work around the temp table issue; you just need to create the expected table in the session when you create the function. But even in this case, I think it would still be good to check what we can, like at least basic plpgsql syntax.
Do we really need first_start? ISTM that if you're dependent on run state then you're basically out of luck.
--
Jim C. Nasby, Data Architect jim@nasby.net
512.569.9461 (cell) http://jim.nasby.net
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/12/9 Jim Nasby <jim@nasby.net>
On 12/8/13 11:24 PM, Pavel Stehule wrote:
#option check_on_first_start
#option check_on_create
#option check_newerwhat exactly check_newer means, does it mean whenever a function is
replaced (changed)?no, it means, so request for check will be ignored ever - some functions
cannot be deeply checked due using dynamic SQL or dynamic created data
types - temporary tables created in functions.So presumably it would be check_never, not check_newer... :) BTW, it's not
terribly hard to work around the temp table issue; you just need to create
the expected table in the session when you create the function. But even in
this case, I think it would still be good to check what we can, like at
least basic plpgsql syntax.
I sorry.
You cannot to create temporary table - this check should not have any side
effect - and creating temporary table can run some event trigger.
But there should be some hints for check like annotations or some similar.
Or you can minimize a area where check will be disabled.
Do we really need first_start? ISTM that if you're dependent on run state
then you're basically out of luck.
I afraid so checking on creation time is not enough for plpgsql.
and I have a very good experience with check on start from plpgsql_lint
usage when I wrote a regression tests.
A "first start" doesn't create dependency on state - but just more
preciously define a time, when checking will be done. Probably a option
check_create_and_start can be useful.
Regards
Pavel
Show quoted text
--
Jim C. Nasby, Data Architect jim@nasby.net
512.569.9461 (cell) http://jim.nasby.net
On 12/9/13 1:08 PM, Pavel Stehule wrote:
So presumably it would be check_never, not check_newer... :) BTW, it's not terribly hard to work around the temp table issue; you just need to create the expected table in the session when you create the function. But even in this case, I think it would still be good to check what we can, like at least basic plpgsql syntax.
I sorry.
You cannot to create temporary table - this check should not have any side effect - and creating temporary table can run some event trigger.
But there should be some hints for check like annotations or some similar. Or you can minimize a area where check will be disabled.
Sorry, I meant that the user can work around it by creating the table. I didn't mean to imply that we would magically create a temp table to do the checking.
--
Jim C. Nasby, Data Architect jim@nasby.net
512.569.9461 (cell) http://jim.nasby.net
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/8/13, 12:01 PM, Pavel Stehule wrote:
But still I have no idea, how to push check without possible slowdown
execution with code duplication
Create a GUC parameter plpgsql.slow_checks or whatever (perhaps more
specific), which people can turn on when they run their test suites.
This doesn't really have to be all that much different from what we are
currently doing in C with scan-build and address sanitizer, for example.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Dec 9, 2013 at 10:54 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2013/12/9 Amit Kapila <amit.kapila16@gmail.com>
There are two points, that should be solved
a) introduction a dependency to other object in schema - now CREATE
FUNCTION
is fully independent on othersb) slow start - if we check all paths on start, then start can be slower
-
and some functions should not work due dependency on temporary tables.I am thinking about possible marking a function by #option (we have same
idea)some like
#option check_on_first_start
#option check_on_create
#option check_newerwhat exactly check_newer means, does it mean whenever a function is
replaced (changed)?no, it means, so request for check will be ignored ever - some functions
cannot be deeply checked due using dynamic SQL or dynamic created data types
- temporary tables created in functions.
Thanks for clarification, the part of name 'newer' has created
confusion. I understand
that creating/identifying dependency in some of the cases will be
quite tricky, does other
similar languages for other databases does that for all cases (objects
in dynamic statements).
Is the main reason for identifying/creating dependency is to mark
function as invalid when
any dependent object is Dropped/Altered?
This thread is from quite some time, so please excuse me if I had
asked anything which has
been discussed previously.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/12/9 Peter Eisentraut <peter_e@gmx.net>
On 12/8/13, 12:01 PM, Pavel Stehule wrote:
But still I have no idea, how to push check without possible slowdown
execution with code duplicationCreate a GUC parameter plpgsql.slow_checks or whatever (perhaps more
specific), which people can turn on when they run their test suites.This doesn't really have to be all that much different from what we are
currently doing in C with scan-build and address sanitizer, for example.
A main issue is placing these tests on critical path.
You have to check it in every expression, in every internal switch
There are two main purposes
a) ensure a expression/query is valid (not only syntax valid)
b) search all expressions/queries - visit all possible paths in code.
so you should to place new switch everywhere in plpgsql executor, where is
entry to some path.
Second issue - these check decrease a readability of plpgsql statement
executor handlers. This code is relative very readable now. With new switch
is little bit (more) less clean.
I think so fact we use a two other large statement switch (printing, free
expressions) is natural, and it hard to write it better.
Regards
Pavel
2013/12/10 Amit Kapila <amit.kapila16@gmail.com>
On Mon, Dec 9, 2013 at 10:54 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:2013/12/9 Amit Kapila <amit.kapila16@gmail.com>
There are two points, that should be solved
a) introduction a dependency to other object in schema - now CREATE
FUNCTION
is fully independent on othersb) slow start - if we check all paths on start, then start can be
slower
-
and some functions should not work due dependency on temporary tables.I am thinking about possible marking a function by #option (we have
same
idea)
some like
#option check_on_first_start
#option check_on_create
#option check_newerwhat exactly check_newer means, does it mean whenever a function is
replaced (changed)?no, it means, so request for check will be ignored ever - some functions
cannot be deeply checked due using dynamic SQL or dynamic created datatypes
- temporary tables created in functions.
Thanks for clarification, the part of name 'newer' has created
confusion. I understand
that creating/identifying dependency in some of the cases will be
quite tricky, does other
similar languages for other databases does that for all cases (objects
in dynamic statements).
I am sorry
PL/pgSQL is really specific due invisible SQL base and mixing two
environments and languages in user visible area.
Is the main reason for identifying/creating dependency is to mark
function as invalid when
any dependent object is Dropped/Altered?
Now, PG has no any tool for checking dependency between functions and other
objects. What has positive effects - we have very simply deploying, that
works in almost use cases very well - and works with our temporary tables
implementation. There is simple rule - depended object must living before
they are >>used in runtime<<. But checking should not be runtime event and
we would to decrease a false alarms. So we have to expect so almost all
necessary object are created - it is reason, why we decided don't do check
in create function statement time. We don't would to introduce new
dependency if it will be possible.
Regards
Pavel
Show quoted text
This thread is from quite some time, so please excuse me if I had
asked anything which has
been discussed previously.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On Tue, Dec 10, 2013 at 1:45 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
Now, PG has no any tool for checking dependency between functions and other
objects. What has positive effects - we have very simply deploying, that
works in almost use cases very well - and works with our temporary tables
implementation. There is simple rule - depended object must living before
they are >>used in runtime<<. But checking should not be runtime event and
we would to decrease a false alarms. So we have to expect so almost all
necessary object are created - it is reason, why we decided don't do check
in create function statement time. We don't would to introduce new
dependency if it will be possible.
This is a very good point. Annotating the function itself with
markers that cause it to be more strictly checked will create a
dump/reload problem that we won't enjoy solving. The decision to
check the function more strictly or not would need to be based on some
kind of session state that users could establish but dump restore
would not.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
This is a very good point. Annotating the function itself with
markers that cause it to be more strictly checked will create a
dump/reload problem that we won't enjoy solving. The decision to
check the function more strictly or not would need to be based on some
kind of session state that users could establish but dump restore
would not.
One would hope that turning off check_function_bodies would be sufficient
to disable any added checking, though, so I don't see this being a problem
for pg_dump. But there might be other scenarios where an additional knob
would be useful.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/10/2013 12:50 PM, Tom Lane wrote:
One would hope that turning off check_function_bodies would be sufficient
to disable any added checking, though, so I don't see this being a problem
for pg_dump. But there might be other scenarios where an additional knob
would be useful.
I can't think of one, offhand. And +1 for NOT adding a new knob.
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WM802541b282dba10027f0b67bb49b7c3b8a3fa2d65c69be1ff21ca07ce44bebb508ebc3b687e830aba15e7acec31e7c02@asav-1.01.com
On Tue, Dec 10, 2013 at 12:15 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2013/12/10 Amit Kapila <amit.kapila16@gmail.com>
On Mon, Dec 9, 2013 at 10:54 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:2013/12/9 Amit Kapila <amit.kapila16@gmail.com>
There are two points, that should be solved
a) introduction a dependency to other object in schema - now CREATE
FUNCTION
is fully independent on othersb) slow start - if we check all paths on start, then start can be
slowersome like
#option check_on_first_start
#option check_on_create
#option check_newerwhat exactly check_newer means, does it mean whenever a function is
replaced (changed)?no, it means, so request for check will be ignored ever - some functions
cannot be deeply checked due using dynamic SQL or dynamic created data
types
- temporary tables created in functions.Now, PG has no any tool for checking dependency between functions and other
objects. What has positive effects - we have very simply deploying, that
works in almost use cases very well - and works with our temporary tables
implementation. There is simple rule - depended object must living before
they are >>used in runtime<<. But checking should not be runtime event and
we would to decrease a false alarms. So we have to expect so almost all
necessary object are created - it is reason, why we decided don't do check
in create function statement time.
Isn't that already done for SQL function's (fmgr_sql_validator)?
postgres=# CREATE FUNCTION clean_emp() RETURNS void AS
postgres'# DELETE FROM emp
postgres'# WHERE salary < 0;
postgres'# ' LANGUAGE SQL;
ERROR: relation "emp" does not exist
LINE 2: DELETE FROM emp
^
I mean to say that the above rule stated by you ("There is simple rule
- depended object must living before
they are >>used in runtime<<") doesn't seem to be true for SQL functions.
So isn't it better to do same for plpgsql functions as well?
For doing at runtime (during first execution of function) are you
planing to add it as a extra step
such that if parameter check_on_first_start is set, then do it.
We don't would to introduce new dependency if it will be possible.
In that case what exactly you mean to say in point a) ("introduction
a dependency to other object..") above in you mail.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Amit Kapila <amit.kapila16@gmail.com> writes:
On Tue, Dec 10, 2013 at 12:15 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
Now, PG has no any tool for checking dependency between functions and other
objects.
Isn't that already done for SQL function's (fmgr_sql_validator)?
Pavel's point is that the only way to find out if the validator will fail
is to run it and see if it fails; and even if it does, how much will you
know about why? That's not particularly helpful for pg_dump, which needs
to understand dependencies in a fairly deep fashion. It not only needs to
figure out how to dump the database objects in a dependency-safe order,
but what to do to break dependency loops.
Right now I believe that pg_dump has a workable strategy for every
possible case of circular dependencies, because they are all caused by
secondary attributes of objects that can be split out and applied later,
for example applying a column default via ALTER TABLE ALTER COLUMN SET
DEFAULT rather than listing the default right in the CREATE TABLE command.
However, if function A depends on B and also vice-versa (mutual recursion
is not exactly an unheard-of technique), there is no way to load them both
if the function bodies are both checked at creation time.
I guess we could invent some equivalent of a forward declaration, but
that still leaves us short of understanding what the function body is
depending on.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Dec 11, 2013 at 10:40 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Amit Kapila <amit.kapila16@gmail.com> writes:
On Tue, Dec 10, 2013 at 12:15 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
Now, PG has no any tool for checking dependency between functions and other
objects.Isn't that already done for SQL function's (fmgr_sql_validator)?
Pavel's point is that the only way to find out if the validator will fail
is to run it and see if it fails; and even if it does, how much will you
know about why?
One of the important thing at time of function creation, users are
interested in knowing
is that if there are any objects (table/view/sequence ..) that are
used in function body
and are missing and the reason is I think they don't want such
things to come up during execution.
Similar thing happens for prepared statements in PostgreSQL, like
at time of parse message
only it checks both syntax errors and semantic check (which ensures
statement is meaningful,
for ex. whether objects and columns used in the statements exist)
Like we do checks other than syntax check at time of creation of
prepared statement, same
thing should be considered meaning full at time of function creation.
As you mentioned, there are checks (like dependency, mutual
recursion) which are difficult or not
feasible in current design to perform, but so will be the case for
them to execute during first execution
of function. So is it not better to do what is more feasible during
function creation rather than leaving
most of the things at execution phase?
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I dislike it. a too early check means a issues with temporary tables and
mainy new dependency between functions in complex projects. It is some what
we don't want.
Dne 12. 12. 2013 5:30 "Amit Kapila" <amit.kapila16@gmail.com> napsal(a):
Show quoted text
On Wed, Dec 11, 2013 at 10:40 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Amit Kapila <amit.kapila16@gmail.com> writes:
On Tue, Dec 10, 2013 at 12:15 PM, Pavel Stehule <
pavel.stehule@gmail.com> wrote:
Now, PG has no any tool for checking dependency between functions and
other
objects.
Isn't that already done for SQL function's (fmgr_sql_validator)?
Pavel's point is that the only way to find out if the validator will fail
is to run it and see if it fails; and even if it does, how much will you
know about why?One of the important thing at time of function creation, users are
interested in knowing
is that if there are any objects (table/view/sequence ..) that are
used in function body
and are missing and the reason is I think they don't want such
things to come up during execution.Similar thing happens for prepared statements in PostgreSQL, like
at time of parse message
only it checks both syntax errors and semantic check (which ensures
statement is meaningful,
for ex. whether objects and columns used in the statements exist)Like we do checks other than syntax check at time of creation of
prepared statement, same
thing should be considered meaning full at time of function creation.As you mentioned, there are checks (like dependency, mutual
recursion) which are difficult or not
feasible in current design to perform, but so will be the case for
them to execute during first execution
of function. So is it not better to do what is more feasible during
function creation rather than leaving
most of the things at execution phase?With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com