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.
+
+
+ Checking of embedded SQL
+
+ The SQL statements inside PL/pgSQL> functions are
+ checked by validator for semantic errors. These errors
+ can be found by plpgsql_check_function:
+
+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)
+
+
+
Handling of Quotation Marks
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, "\n");
+}
+
+/*
+ * finishig a result stored in cstate
+ */
+static void
+cstate_flush(PLpgSQL_checkstate *cstate)
+{
+ if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
+ appendStringInfoString(cstate->sinfo, "");
+
+ 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, " \n");
+
+ appendStringInfo(cstate->sinfo, " %s\n", level);
+ appendStringInfo(cstate->sinfo, " %s\n",
+ unpack_sql_state(sqlerrcode));
+ appendStringInfo(cstate->sinfo, " %s\n",
+ escape_xml(message));
+ if (cstate->estate->err_stmt != NULL)
+ appendStringInfo(cstate->sinfo, " %s\n",
+ cstate->estate->err_stmt->lineno,
+ plpgsql_stmt_typename(cstate->estate->err_stmt));
+ if (hint != NULL)
+ appendStringInfo(cstate->sinfo, " %s\n",
+ escape_xml(hint));
+ if (detail != NULL)
+ appendStringInfo(cstate->sinfo, " %s\n",
+ escape_xml(detail));
+ if(query != NULL)
+ appendStringInfo(cstate->sinfo, " %s\n",
+ position, escape_xml(query));
+ if (context != NULL)
+ appendStringInfo(cstate->sinfo, " %s\n",
+ escape_xml(context));
+
+ /* flush closing tag */
+ appendStringInfoString(cstate->sinfo, " \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();
+