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.
+
+ 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
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, "\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,
+ 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