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(); +