/*-------------------------------------------------------------------------
*
* plpgsql_check.c
*
* enhanced checks for plpgsql functions
*
* by Pavel Stehule 2013-2016
*
*-------------------------------------------------------------------------
*
* Notes:
*
* 1) Secondary hash table for function signature is necessary due holding is_checked
* attribute - this protection against unwanted repeated check.
*
* 2) Reusing some plpgsql_xxx functions requires full run-time environment. It is
* emulated by fake expression context and fake fceinfo (these are created when
* active checking is used) - see: setup_fake_fcinfo, setup_cstate.
*
* 3) The environment is referenced by stored execution plans. The actual plan should
* not be linked with fake environment. All expressions created in checking time
* should be relased by release_exprs(cstate.exprs) function.
*
*/
#include "postgres.h"
#include "plpgsql.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "plpgsql_check_builtins.h"
#if PG_VERSION_NUM >= 100000
#define PLPGSQL_STMT_TYPES
#else
#define PLPGSQL_STMT_TYPES (enum PLpgSQL_stmt_types)
#endif
#if PG_VERSION_NUM >= 100000
#include "utils/regproc.h"
#endif
#if PG_VERSION_NUM >= 90300
#include "access/htup_details.h"
#else
/* Older version doesn't support event triggers */
#ifdef _MSC_VER
typedef struct {char nothing[0];} EventTriggerData;
#else
typedef struct {} EventTriggerData;
#endif
typedef enum PLpgSQL_trigtype
{
PLPGSQL_DML_TRIGGER,
PLPGSQL_EVENT_TRIGGER,
PLPGSQL_NOT_TRIGGER
} PLpgSQL_trigtype;
#endif
#include "access/tupconvert.h"
#include "access/tupdesc.h"
#ifndef TupleDescAttr
#define TupleDescAttr(tupdesc, i) ((tupdesc)->attrs[(i)])
#endif
#include "catalog/pg_language.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "executor/spi_priv.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_coerce.h"
#include "tcop/utility.h"
#include "tsearch/ts_locale.h"
#include "utils/builtins.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/syscache.h"
#include "utils/typcache.h"
#include "utils/rel.h"
#include "utils/json.h"
#include "utils/reltrigger.h"
#include "utils/xml.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
/*
* columns of plpgsql_check_function_table result
*
*/
#define Natts_result 11
#define Anum_result_functionid 0
#define Anum_result_lineno 1
#define Anum_result_statement 2
#define Anum_result_sqlstate 3
#define Anum_result_message 4
#define Anum_result_detail 5
#define Anum_result_hint 6
#define Anum_result_level 7
#define Anum_result_position 8
#define Anum_result_query 9
#define Anum_result_context 10
enum
{
PLPGSQL_CHECK_ERROR,
PLPGSQL_CHECK_WARNING_OTHERS,
PLPGSQL_CHECK_WARNING_EXTRA, /* check shadowed variables */
PLPGSQL_CHECK_WARNING_PERFORMANCE
};
enum
{
PLPGSQL_CHECK_FORMAT_ELOG,
PLPGSQL_CHECK_FORMAT_TEXT,
PLPGSQL_CHECK_FORMAT_TABULAR,
PLPGSQL_CHECK_FORMAT_XML,
PLPGSQL_CHECK_FORMAT_JSON
};
enum
{
PLPGSQL_CHECK_CLOSED,
PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS,
PLPGSQL_CHECK_POSSIBLY_CLOSED,
PLPGSQL_CHECK_UNCLOSED,
PLPGSQL_CHECK_UNKNOWN
};
enum
{
PLPGSQL_CHECK_MODE_DISABLED, /* all functionality is disabled */
PLPGSQL_CHECK_MODE_BY_FUNCTION, /* checking is allowed via CHECK function only (default) */
PLPGSQL_CHECK_MODE_FRESH_START, /* check only when function is called first time */
PLPGSQL_CHECK_MODE_EVERY_START /* check on every start */
};
typedef struct PLpgSQL_stmt_stack_item
{
PLpgSQL_stmt *stmt;
char *label;
struct PLpgSQL_stmt_stack_item *outer;
} PLpgSQL_stmt_stack_item;
typedef struct PLpgSQL_checkstate
{
Oid fn_oid; /* oid of checked function */
List *argnames; /* function arg names */
PLpgSQL_execstate *estate; /* check state is estate extension */
Tuplestorestate *tuple_store; /* result target */
TupleDesc tupdesc; /* result description */
bool fatal_errors; /* stop on first error */
bool performance_warnings; /* show performace warnings */
bool other_warnings; /* show other warnings */
bool extra_warnings; /* show extra warnings */
int format; /* output format */
StringInfo sinfo; /* aux. stringInfo used for result string concat */
MemoryContext check_cxt;
List *exprs; /* list of all expression created by checker */
bool is_active_mode; /* true, when checking is started by plpgsql_check_function */
Bitmapset *used_variables; /* track which variables have been used; bit per varno */
Bitmapset *modif_variables; /* track which variables had been changed; bit per varno */
PLpgSQL_stmt_stack_item *top_stmt_stack; /* list of known labels + related command */
bool found_return_query; /* true, when code contains RETURN query */
} PLpgSQL_checkstate;
static void assign_tupdesc_dno(PLpgSQL_checkstate *cstate, int varno, TupleDesc tupdesc, bool isnull);
static void assign_tupdesc_row_or_rec(PLpgSQL_checkstate *cstate,
PLpgSQL_row *row, PLpgSQL_rec *rec,
TupleDesc tupdesc, bool isnull);
static void check_assignment(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
int targetdno);
static void check_assignment_with_possible_slices(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
int targetdno, bool use_element_type);
static void check_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr);
static void check_expr_with_expected_scalar_type(PLpgSQL_checkstate *cstate,
PLpgSQL_expr *expr,
Oid expected_typoid,
bool required);
static void check_expr_as_rvalue(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
int targetdno, bool use_element_type, bool is_expression);
static void check_returned_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool is_expression);
static void check_expr_as_sqlstmt_nodata(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr);
static void check_assign_to_target_type(PLpgSQL_checkstate *cstate,
Oid target_typoid, int32 target_typmod,
Oid value_typoid,
bool isnull);
static void check_function_epilog(PLpgSQL_checkstate *cstate);
static void check_function_prolog(PLpgSQL_checkstate *cstate);
static void check_on_func_beg(PLpgSQL_execstate * estate, PLpgSQL_function * func);
static void check_plpgsql_function(HeapTuple procTuple, Oid relid, PLpgSQL_trigtype trigtype,
TupleDesc tupdesc,
Tuplestorestate *tupstore,
int format,
bool fatal_errors,
bool other_warnings,
bool performance_warnings,
bool extra_warnings);
static void check_row_or_rec(PLpgSQL_checkstate *cstate, PLpgSQL_row *row, PLpgSQL_rec *rec);
static void check_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, int *closing, List **exceptions);
static void check_stmts(PLpgSQL_checkstate *cstate, List *stmts, int *closing, List **exceptions);
static void check_target(PLpgSQL_checkstate *cstate, int varno, Oid *expected_typoid, int *expected_typmod);
static PLpgSQL_datum *copy_plpgsql_datum(PLpgSQL_datum *datum);
static char *datum_get_refname(PLpgSQL_datum *d);
static TupleDesc expr_get_desc(PLpgSQL_checkstate *cstate,
PLpgSQL_expr *query,
bool use_element_type,
bool expand_record,
bool is_expression,
Oid *first_level_typoid);
static void format_error_xml(StringInfo str,
PLpgSQL_execstate *estate,
int sqlerrcode, int lineno,
const char *message, const char *detail, const char *hint,
int level, int position,
const char *query,
const char *context);
static void format_error_json(StringInfo str,
PLpgSQL_execstate *estate,
int sqlerrcode, int lineno,
const char *message, const char *detail, const char *hint,
int level, int position,
const char *query,
const char *context);
static void function_check(PLpgSQL_function *func, FunctionCallInfo fcinfo,
PLpgSQL_execstate *estate, PLpgSQL_checkstate *cstate);
static PLpgSQL_trigtype get_trigtype(HeapTuple procTuple);
static void init_datum_dno(PLpgSQL_checkstate *cstate, int varno);
static bool is_checked(PLpgSQL_function *func);
static int load_configuration(HeapTuple procTuple, bool *reload_config);
static void mark_as_checked(PLpgSQL_function *func);
static void plpgsql_check_HashTableInit(void);
static void prohibit_write_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *query);
static void put_error(PLpgSQL_checkstate *cstate,
int sqlerrcode, int lineno,
const char *message, const char *detail, const char *hint,
int level, int position,
const char *query, const char *context);
static void put_error_edata(PLpgSQL_checkstate *cstate, ErrorData *edata);
static void precheck_conditions(HeapTuple procTuple, PLpgSQL_trigtype trigtype, Oid relid);
static void prepare_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, int cursorOptions);
static void release_exprs(List *exprs);
static void setup_cstate(PLpgSQL_checkstate *cstate,
Oid fn_oid, TupleDesc tupdesc, Tuplestorestate *tupstore,
bool fatal_errors,
bool other_warnings, bool perform_warnings, bool extra_warnings,
int format,
bool is_active_mode);
static void setup_fake_fcinfo(HeapTuple procTuple,
FmgrInfo *flinfo,
FunctionCallInfoData *fcinfo,
ReturnSetInfo *rsinfo,
TriggerData *trigdata,
Oid relid,
EventTriggerData *etrigdata,
Oid funcoid,
PLpgSQL_trigtype trigtype,
Trigger *tg_trigger);
static void setup_plpgsql_estate(PLpgSQL_execstate *estate,
PLpgSQL_function *func, ReturnSetInfo *rsi);
static void trigger_check(PLpgSQL_function *func,
Node *trigdata,
PLpgSQL_execstate *estate, PLpgSQL_checkstate *cstate);
static void tuplestore_put_error_text(Tuplestorestate *tuple_store, TupleDesc tupdesc,
PLpgSQL_execstate *estate, Oid fn_oid,
int sqlerrcode, int lineno,
const char *message, const char *detail, const char *hint,
int level, int position,
const char *query, const char *context);
static void tuplestore_put_error_tabular(Tuplestorestate *tuple_store, TupleDesc tupdesc,
PLpgSQL_execstate *estate, Oid fn_oid,
int sqlerrcode, int lineno,
const char *message, const char *detail, const char *hint,
int level, int position,
const char *query, const char *context);
static void tuplestore_put_text_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
const char *message, int len);
static void report_unused_variables(PLpgSQL_checkstate *cstate);
static void record_variable_usage(PLpgSQL_checkstate *cstate, int dno, bool write);
static bool datum_is_used(PLpgSQL_checkstate *cstate, int dno, bool write);
static bool is_const_null_expr(PLpgSQL_expr *query);
static void prohibit_transaction_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_expr *query);
static int merge_closing(int c, int c_local, List **exceptions, List *exceptions_local, int err_code);
static int possibly_closed(int c);
static char *ExprGetString(PLpgSQL_expr *query, bool *IsConst);
static bool exception_matches_conditions(int err_code, PLpgSQL_condition *cond);
static bool plpgsql_check_other_warnings = false;
static bool plpgsql_check_extra_warnings = false;
static bool plpgsql_check_performance_warnings = false;
static bool plpgsql_check_fatal_errors = true;
static int plpgsql_check_mode = PLPGSQL_CHECK_MODE_BY_FUNCTION;
static PLpgSQL_plugin plugin_funcs = { NULL, check_on_func_beg, NULL, NULL, NULL};
static const struct config_enum_entry plpgsql_check_mode_options[] = {
{"disabled", PLPGSQL_CHECK_MODE_DISABLED, false},
{"by_function", PLPGSQL_CHECK_MODE_BY_FUNCTION, false},
{"fresh_start", PLPGSQL_CHECK_MODE_FRESH_START, false},
{"every_start", PLPGSQL_CHECK_MODE_EVERY_START, false},
{NULL, 0, false}
};
/* ----------
* Hash table for checked functions
* ----------
*/
static HTAB *plpgsql_check_HashTable = NULL;
typedef struct plpgsql_hashent
{
PLpgSQL_func_hashkey key;
TransactionId fn_xmin;
ItemPointerData fn_tid;
bool is_checked;
} plpgsql_check_HashEnt;
#define FUNCS_PER_USER 128 /* initial table size */
PG_FUNCTION_INFO_V1(plpgsql_check_function);
PG_FUNCTION_INFO_V1(plpgsql_check_function_tb);
/*
* Module initialization
*
* join to PLpgSQL executor
*
*/
void
_PG_init(void)
{
PLpgSQL_plugin ** var_ptr = (PLpgSQL_plugin **) find_rendezvous_variable( "PLpgSQL_plugin" );
/* Be sure we do initialization only once (should be redundant now) */
static bool inited = false;
if (inited)
return;
*var_ptr = &plugin_funcs;
DefineCustomEnumVariable("plpgsql_check.mode",
"choose a mode for enhanced checking",
NULL,
&plpgsql_check_mode,
PLPGSQL_CHECK_MODE_BY_FUNCTION,
plpgsql_check_mode_options,
PGC_SUSET, 0,
NULL, NULL, NULL);
DefineCustomBoolVariable("plpgsql_check.show_nonperformance_extra_warnings",
"when is true, then extra warning (except performance warnings) are showed",
NULL,
&plpgsql_check_extra_warnings,
false,
PGC_SUSET, 0,
NULL, NULL, NULL);
DefineCustomBoolVariable("plpgsql_check.show_nonperformance_warnings",
"when is true, then warning (except performance warnings) are showed",
NULL,
&plpgsql_check_other_warnings,
false,
PGC_SUSET, 0,
NULL, NULL, NULL);
DefineCustomBoolVariable("plpgsql_check.show_performance_warnings",
"when is true, then performance warnings are showed",
NULL,
&plpgsql_check_performance_warnings,
false,
PGC_SUSET, 0,
NULL, NULL, NULL);
DefineCustomBoolVariable("plpgsql_check.fatal_errors",
"when is true, then plpgsql check stops execution on detected error",
NULL,
&plpgsql_check_fatal_errors,
true,
PGC_SUSET, 0,
NULL, NULL, NULL);
plpgsql_check_HashTableInit();
inited = true;
}
/*
* plpgsql_check_func_beg
*
* callback function - called by plgsql executor, when function is started
* and local variables are initialized.
*
*/
static void
check_on_func_beg(PLpgSQL_execstate * estate, PLpgSQL_function * func)
{
const char *err_text = estate->err_text;
int closing;
List *exceptions;
if (plpgsql_check_mode == PLPGSQL_CHECK_MODE_FRESH_START ||
plpgsql_check_mode == PLPGSQL_CHECK_MODE_EVERY_START)
{
int i;
PLpgSQL_rec *saved_records;
PLpgSQL_var *saved_vars;
MemoryContext oldcontext,
old_cxt;
ResourceOwner oldowner;
PLpgSQL_checkstate cstate;
/*
* don't allow repeated execution on checked function
* when it is not requsted.
*/
if (plpgsql_check_mode == PLPGSQL_CHECK_MODE_FRESH_START &&
is_checked(func))
{
elog(NOTICE, "function \"%s\" was checked already",
func->fn_signature);
return;
}
mark_as_checked(func);
setup_cstate(&cstate, func->fn_oid, NULL, NULL,
plpgsql_check_fatal_errors,
plpgsql_check_other_warnings,
plpgsql_check_performance_warnings,
plpgsql_check_extra_warnings,
PLPGSQL_CHECK_FORMAT_ELOG,
false);
/* use real estate */
cstate.estate = estate;
old_cxt = MemoryContextSwitchTo(cstate.check_cxt);
/*
* During the check stage a rec and vars variables are modified, so we should
* to save their content
*/
saved_records = palloc(sizeof(PLpgSQL_rec) * estate->ndatums);
saved_vars = palloc(sizeof(PLpgSQL_var) * estate->ndatums);
for (i = 0; i < estate->ndatums; i++)
{
if (estate->datums[i]->dtype == PLPGSQL_DTYPE_REC)
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[i];
saved_records[i].tup = rec->tup;
saved_records[i].tupdesc = rec->tupdesc;
saved_records[i].freetup = rec->freetup;
saved_records[i].freetupdesc = rec->freetupdesc;
/* don't release a original tupdesc and original tup */
rec->freetup = false;
rec->freetupdesc = false;
}
else if (estate->datums[i]->dtype == PLPGSQL_DTYPE_VAR)
{
PLpgSQL_var *var = (PLpgSQL_var *) estate->datums[i];
saved_vars[i].value = var->value;
saved_vars[i].isnull = var->isnull;
saved_vars[i].freeval = var->freeval;
var->freeval = false;
}
}
estate->err_text = NULL;
/*
* Raised exception should be trapped in outer functtion. Protection
* against outer trap is QUERY_CANCELED exception.
*/
oldcontext = CurrentMemoryContext;
oldowner = CurrentResourceOwner;
PG_TRY();
{
/*
* Now check the toplevel block of statements
*/
check_stmt(&cstate, (PLpgSQL_stmt *) func->action, &closing, &exceptions);
estate->err_stmt = NULL;
if (closing != PLPGSQL_CHECK_CLOSED && closing != PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
put_error(&cstate,
ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT, 0,
"control reached end of function without RETURN",
NULL,
NULL,
closing == PLPGSQL_CHECK_UNCLOSED ?
PLPGSQL_CHECK_ERROR : PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
report_unused_variables(&cstate);
}
PG_CATCH();
{
ErrorData *edata;
/* Save error info */
MemoryContextSwitchTo(oldcontext);
edata = CopyErrorData();
FlushErrorState();
CurrentResourceOwner = oldowner;
release_exprs(cstate.exprs);
edata->sqlerrcode = ERRCODE_QUERY_CANCELED;
ReThrowError(edata);
}
PG_END_TRY();
estate->err_text = err_text;
estate->err_stmt = NULL;
/* return back a original rec variables */
for (i = 0; i < estate->ndatums; i++)
{
if (estate->datums[i]->dtype == PLPGSQL_DTYPE_REC)
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[i];
if (rec->freetupdesc)
FreeTupleDesc(rec->tupdesc);
rec->tup = saved_records[i].tup;
rec->tupdesc = saved_records[i].tupdesc;
rec->freetup = saved_records[i].freetup;
rec->freetupdesc = saved_records[i].freetupdesc;
}
else if (estate->datums[i]->dtype == PLPGSQL_DTYPE_VAR)
{
PLpgSQL_var *var = (PLpgSQL_var *) estate->datums[i];
var->value = saved_vars[i].value;
var->isnull = saved_vars[i].isnull;
var->freeval = saved_vars[i].freeval;
}
}
MemoryContextSwitchTo(old_cxt);
MemoryContextDelete(cstate.check_cxt);
}
}
/*
* plpgsql_check_function
*
* Extended check with formatted text output
*
*/
Datum
plpgsql_check_function(PG_FUNCTION_ARGS)
{
Oid funcoid = PG_GETARG_OID(0);
Oid relid = PG_GETARG_OID(1);
char *format_str = text_to_cstring(PG_GETARG_TEXT_PP(2));
bool fatal_errors = PG_GETARG_BOOL(3);
bool other_warnings = PG_GETARG_BOOL(4);
bool performance_warnings = PG_GETARG_BOOL(5);
bool extra_warnings;
TupleDesc tupdesc;
HeapTuple procTuple;
Tuplestorestate *tupstore;
MemoryContext per_query_ctx;
MemoryContext oldcontext;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
PLpgSQL_trigtype trigtype;
char *format_lower_str;
int format = PLPGSQL_CHECK_FORMAT_TEXT;
ErrorContextCallback *prev_errorcontext;
if (PG_NARGS() != 7)
elog(ERROR, "unexpected number of parameters, you should to update extension");
extra_warnings = PG_GETARG_BOOL(6);
format_lower_str = lowerstr(format_str);
if (strcmp(format_lower_str, "text") == 0)
format = PLPGSQL_CHECK_FORMAT_TEXT;
else if (strcmp(format_lower_str, "xml") == 0)
format = PLPGSQL_CHECK_FORMAT_XML;
else if (strcmp(format_lower_str, "json") == 0)
format = PLPGSQL_CHECK_FORMAT_JSON;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("unrecognize format: \"%s\"",
format_lower_str),
errhint("Only \"text\", \"xml\" and \"json\" formats are supported.")));
/* 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);
trigtype = get_trigtype(procTuple);
precheck_conditions(procTuple, trigtype, relid);
/* 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);
prev_errorcontext = error_context_stack;
error_context_stack = NULL;
check_plpgsql_function(procTuple, relid, trigtype,
tupdesc, tupstore,
format,
fatal_errors,
other_warnings, performance_warnings, extra_warnings);
error_context_stack = prev_errorcontext;
ReleaseSysCache(procTuple);
/* clean up and return the tuplestore */
tuplestore_donestoring(tupstore);
rsinfo->returnMode = SFRM_Materialize;
rsinfo->setResult = tupstore;
rsinfo->setDesc = tupdesc;
return (Datum) 0;
}
/*
* plpgsql_check_function_tb
*
* It ensure a detailed validation and returns result as multicolumn table
*
*/
Datum
plpgsql_check_function_tb(PG_FUNCTION_ARGS)
{
Oid funcoid = PG_GETARG_OID(0);
Oid relid = PG_GETARG_OID(1);
bool fatal_errors = PG_GETARG_BOOL(2);
bool other_warnings = PG_GETARG_BOOL(3);
bool performance_warnings = PG_GETARG_BOOL(4);
bool extra_warnings;
TupleDesc tupdesc;
HeapTuple procTuple;
Tuplestorestate *tupstore;
MemoryContext per_query_ctx;
MemoryContext oldcontext;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
PLpgSQL_trigtype trigtype;
ErrorContextCallback *prev_errorcontext;
if (PG_NARGS() != 6)
elog(ERROR, "unexpected number of parameters, you should to update extension");
extra_warnings = PG_GETARG_BOOL(5);
/* 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);
trigtype = get_trigtype(procTuple);
precheck_conditions(procTuple, trigtype, relid);
/* 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);
prev_errorcontext = error_context_stack;
/* Envelope outer plpgsql function is not interesting */
error_context_stack = NULL;
check_plpgsql_function(procTuple, relid, trigtype,
tupdesc, tupstore,
PLPGSQL_CHECK_FORMAT_TABULAR,
fatal_errors,
other_warnings, performance_warnings, extra_warnings);
error_context_stack = prev_errorcontext;
ReleaseSysCache(procTuple);
/* clean up and return the tuplestore */
tuplestore_donestoring(tupstore);
rsinfo->returnMode = SFRM_Materialize;
rsinfo->setResult = tupstore;
rsinfo->setDesc = tupdesc;
return (Datum) 0;
}
/*
* Add label to stack of labels
*/
static PLpgSQL_stmt_stack_item *
push_stmt_to_stmt_stack(PLpgSQL_checkstate *cstate)
{
PLpgSQL_stmt *stmt = cstate->estate->err_stmt;
PLpgSQL_stmt_stack_item *stmt_stack_item;
PLpgSQL_stmt_stack_item *current = cstate->top_stmt_stack;
stmt_stack_item = (PLpgSQL_stmt_stack_item *) palloc(sizeof(PLpgSQL_stmt_stack_item));
stmt_stack_item->stmt = stmt;
switch (PLPGSQL_STMT_TYPES stmt->cmd_type)
{
case PLPGSQL_STMT_BLOCK:
stmt_stack_item->label = ((PLpgSQL_stmt_block *) stmt)->label;
break;
case PLPGSQL_STMT_EXIT:
stmt_stack_item->label = ((PLpgSQL_stmt_exit *) stmt)->label;
break;
case PLPGSQL_STMT_LOOP:
stmt_stack_item->label = ((PLpgSQL_stmt_loop *) stmt)->label;
break;
case PLPGSQL_STMT_WHILE:
stmt_stack_item->label = ((PLpgSQL_stmt_while *) stmt)->label;
break;
case PLPGSQL_STMT_FORI:
stmt_stack_item->label = ((PLpgSQL_stmt_fori *) stmt)->label;
break;
case PLPGSQL_STMT_FORS:
stmt_stack_item->label = ((PLpgSQL_stmt_fors *) stmt)->label;
break;
case PLPGSQL_STMT_FORC:
stmt_stack_item->label = ((PLpgSQL_stmt_forc *) stmt)->label;
break;
case PLPGSQL_STMT_DYNFORS:
stmt_stack_item->label = ((PLpgSQL_stmt_dynfors *) stmt)->label;
break;
case PLPGSQL_STMT_FOREACH_A:
stmt_stack_item->label = ((PLpgSQL_stmt_foreach_a *) stmt)->label;
break;
default:
stmt_stack_item->label = NULL;
}
stmt_stack_item->outer = current;
cstate->top_stmt_stack = stmt_stack_item;
return current;
}
static void
pop_stmt_from_stmt_stack(PLpgSQL_checkstate *cstate)
{
PLpgSQL_stmt_stack_item *current = cstate->top_stmt_stack;
Assert(cstate->top_stmt_stack != NULL);
cstate->top_stmt_stack = current->outer;
pfree(current);
}
/*
* Returns true, when stmt is any loop statement
*/
static bool
is_any_loop_stmt(PLpgSQL_stmt *stmt)
{
switch (PLPGSQL_STMT_TYPES stmt->cmd_type)
{
case PLPGSQL_STMT_LOOP:
case PLPGSQL_STMT_WHILE:
case PLPGSQL_STMT_FORI:
case PLPGSQL_STMT_FORS:
case PLPGSQL_STMT_FORC:
case PLPGSQL_STMT_DYNFORS:
case PLPGSQL_STMT_FOREACH_A:
return true;
default:
return false;
}
}
/*
* Searching a any statement related to CONTINUE/EXIT statement.
* label cannot be NULL.
*/
static PLpgSQL_stmt *
find_stmt_with_label(char *label, PLpgSQL_stmt_stack_item *current)
{
while (current != NULL)
{
if (current->label != NULL
&& strcmp(current->label, label) == 0)
return current->stmt;
current = current->outer;
}
return NULL;
}
static PLpgSQL_stmt *
find_nearest_loop(PLpgSQL_stmt_stack_item *current)
{
while (current != NULL)
{
if (is_any_loop_stmt(current->stmt))
return current->stmt;
current = current->outer;
}
return NULL;
}
/*
* returns false, when a variable doesn't shadows any other variable
*/
static bool
found_shadowed_variable(char *varname, PLpgSQL_stmt_stack_item *current, PLpgSQL_checkstate *cstate)
{
while (current != NULL)
{
if (current->stmt->cmd_type == PLPGSQL_STMT_BLOCK)
{
PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) current->stmt;
int i;
PLpgSQL_datum *d;
for (i = 0; i < stmt_block->n_initvars; i++)
{
char *refname;
d = cstate->estate->func->datums[stmt_block->initvarnos[i]];
refname = datum_get_refname(d);
if (refname != NULL && strcmp(refname, varname) == 0)
return true;
}
}
current = current->outer;
}
return false;
}
/*
* Returns PLpgSQL_trigtype based on prorettype
*/
static PLpgSQL_trigtype
get_trigtype(HeapTuple procTuple)
{
Form_pg_proc proc;
char functyptype;
proc = (Form_pg_proc) GETSTRUCT(procTuple);
functyptype = get_typtype(proc->prorettype);
/*
* Disallow pseudotype result except for TRIGGER, RECORD, VOID, or
* polymorphic
*/
if (functyptype == TYPTYPE_PSEUDO)
{
/* we assume OPAQUE with no arguments means a trigger */
if (proc->prorettype == TRIGGEROID ||
(proc->prorettype == OPAQUEOID && proc->pronargs == 0))
return PLPGSQL_DML_TRIGGER;
#if PG_VERSION_NUM >= 90300
else if (proc->prorettype == EVTTRIGGEROID)
return PLPGSQL_EVENT_TRIGGER;
#endif
else if (proc->prorettype != RECORDOID &&
proc->prorettype != VOIDOID &&
!IsPolymorphicType(proc->prorettype))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("PL/pgSQL functions cannot return type %s",
format_type_be(proc->prorettype))));
}
return PLPGSQL_NOT_TRIGGER;
}
/*
* Process necessary checking before code checking
* a) disallow other than plpgsql check function,
* b) when function is trigger function, then reloid must be defined
*/
static void
precheck_conditions(HeapTuple procTuple, PLpgSQL_trigtype trigtype, Oid relid)
{
Form_pg_proc proc;
Form_pg_language languageStruct;
HeapTuple languageTuple;
char *funcname;
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);
/* dml trigger needs valid relid, others not */
if (trigtype == PLPGSQL_DML_TRIGGER)
{
if (!OidIsValid(relid))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("missing trigger relation"),
errhint("Trigger relation oid must be valid")));
}
else
{
if (OidIsValid(relid))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("function is not trigger"),
errhint("Trigger relation oid must not be valid for non dml trigger function.")));
}
pfree(funcname);
}
/*
* own implementation
*
*/
static void
check_plpgsql_function(HeapTuple procTuple, Oid relid, PLpgSQL_trigtype trigtype,
TupleDesc tupdesc,
Tuplestorestate *tupstore,
int format,
bool fatal_errors,
bool other_warnings,
bool performance_warnings,
bool extra_warnings)
{
PLpgSQL_checkstate cstate;
PLpgSQL_function *volatile function = NULL;
int save_nestlevel = 0;
bool reload_config;
Oid funcoid;
FunctionCallInfoData fake_fcinfo;
FmgrInfo flinfo;
TriggerData trigdata;
EventTriggerData etrigdata;
Trigger tg_trigger;
int rc;
ResourceOwner oldowner;
PLpgSQL_execstate *cur_estate = NULL;
MemoryContext old_cxt;
PLpgSQL_execstate estate;
ReturnSetInfo rsinfo;
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));
setup_fake_fcinfo(procTuple, &flinfo, &fake_fcinfo, &rsinfo, &trigdata, relid, &etrigdata,
funcoid, trigtype, &tg_trigger);
setup_cstate(&cstate, funcoid, tupdesc, tupstore,
fatal_errors,
other_warnings, performance_warnings, extra_warnings,
format,
true);
old_cxt = MemoryContextSwitchTo(cstate.check_cxt);
check_function_prolog(&cstate);
/*
* Copy argument names for later check, only when other warnings are required.
* Argument names are used for check parameter versus local variable collision.
*/
if (other_warnings)
{
int numargs;
Oid *argtypes;
char **argnames;
char *argmodes;
int i;
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]);
}
}
}
oldowner = CurrentResourceOwner;
PG_TRY();
{
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(cstate.check_cxt);
save_nestlevel = load_configuration(procTuple, &reload_config);
/* have to wait for this decision to loaded configuration */
if (plpgsql_check_mode != PLPGSQL_CHECK_MODE_DISABLED)
{
/* Get a compiled function */
function = plpgsql_compile(&fake_fcinfo, false);
/* Must save and restore prior value of cur_estate */
cur_estate = function->cur_estate;
/* recheck trigtype */
#if PG_VERSION_NUM >= 90300
Assert(function->fn_is_trigger == trigtype);
#else
#ifdef USE_ASSERT_CHECKING
if (function->fn_is_trigger)
Assert(trigtype == PLPGSQL_DML_TRIGGER);
else
Assert(trigtype == PLPGSQL_NOT_TRIGGER);
#endif
#endif
setup_plpgsql_estate(&estate, function, (ReturnSetInfo *) fake_fcinfo.resultinfo);
cstate.estate = &estate;
/*
* Mark the function as busy, ensure higher than zero usage. There is no
* reason for protection function against delete, but I afraid of asserts.
*/
function->use_count++;
/* Create a fake runtime environment and process check */
switch (trigtype)
{
case PLPGSQL_DML_TRIGGER:
trigger_check(function, (Node *) &trigdata, &estate, &cstate);
break;
case PLPGSQL_EVENT_TRIGGER:
trigger_check(function, (Node *) &etrigdata, &estate, &cstate);
break;
case PLPGSQL_NOT_TRIGGER:
function_check(function, &fake_fcinfo, &estate, &cstate);
break;
}
function->cur_estate = cur_estate;
function->use_count--;
}
else
elog(NOTICE, "plpgsql_check is disabled");
/*
* reload back a GUC. XXX: isn't this done automatically by subxact
* rollback?
*/
if (reload_config)
AtEOXact_GUC(true, save_nestlevel);
RollbackAndReleaseCurrentSubTransaction();
MemoryContextSwitchTo(cstate.check_cxt);
CurrentResourceOwner = oldowner;
if (OidIsValid(relid))
relation_close(trigdata.tg_relation, AccessShareLock);
release_exprs(cstate.exprs);
SPI_restore_connection();
}
PG_CATCH();
{
ErrorData *edata;
MemoryContextSwitchTo(cstate.check_cxt);
edata = CopyErrorData();
FlushErrorState();
RollbackAndReleaseCurrentSubTransaction();
MemoryContextSwitchTo(cstate.check_cxt);
CurrentResourceOwner = oldowner;
if (OidIsValid(relid))
relation_close(trigdata.tg_relation, AccessShareLock);
if (function)
{
function->cur_estate = cur_estate;
function->use_count--;
release_exprs(cstate.exprs);
}
put_error_edata(&cstate, edata);
/* reconnect spi */
SPI_restore_connection();
}
PG_END_TRY();
check_function_epilog(&cstate);
MemoryContextSwitchTo(old_cxt);
MemoryContextDelete(cstate.check_cxt);
/*
* 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;
int closing = PLPGSQL_CHECK_UNCLOSED;
List *exceptions;
/*
* 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_dno(cstate, func->fn_argvarnos[i]);
}
/*
* Now check the toplevel block of statements
*/
check_stmt(cstate, (PLpgSQL_stmt *) func->action, &closing, &exceptions);
/* clean state values - next errors are not related to any command */
cstate->estate->err_stmt = NULL;
if (closing != PLPGSQL_CHECK_CLOSED && closing != PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
put_error(cstate,
ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT, 0,
"control reached end of function without RETURN",
NULL,
NULL,
closing == PLPGSQL_CHECK_UNCLOSED ? PLPGSQL_CHECK_ERROR : PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
report_unused_variables(cstate);
}
/*
* Check trigger - prepare fake environments for testing trigger
*
*/
static void
trigger_check(PLpgSQL_function *func, Node *tdata,
PLpgSQL_execstate *estate, PLpgSQL_checkstate *cstate)
{
PLpgSQL_rec *rec_new,
*rec_old;
int i;
int closing = PLPGSQL_CHECK_UNCLOSED;
List *exceptions;
/*
* 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]);
if (IsA(tdata, TriggerData))
{
TriggerData *trigdata = (TriggerData *) tdata;
/*
* 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, false);
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, false);
/*
* Assign the special tg_ variables
*/
init_datum_dno(cstate, func->tg_op_varno);
init_datum_dno(cstate, func->tg_name_varno);
init_datum_dno(cstate, func->tg_when_varno);
init_datum_dno(cstate, func->tg_level_varno);
init_datum_dno(cstate, func->tg_relid_varno);
init_datum_dno(cstate, func->tg_relname_varno);
init_datum_dno(cstate, func->tg_table_name_varno);
init_datum_dno(cstate, func->tg_table_schema_varno);
init_datum_dno(cstate, func->tg_nargs_varno);
init_datum_dno(cstate, func->tg_argv_varno);
}
#if PG_VERSION_NUM >= 90300
else if (IsA(tdata, EventTriggerData))
{
init_datum_dno(cstate, func->tg_event_varno);
init_datum_dno(cstate, func->tg_tag_varno);
}
#endif
else
elog(ERROR, "unexpected environment");
/*
* Now check the toplevel block of statements
*/
check_stmt(cstate, (PLpgSQL_stmt *) func->action, &closing, &exceptions);
/* clean state values - next errors are not related to any command */
cstate->estate->err_stmt = NULL;
if (closing != PLPGSQL_CHECK_CLOSED && closing != PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
put_error(cstate,
ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT, 0,
"control reached end of function without RETURN",
NULL,
NULL,
closing == PLPGSQL_CHECK_UNCLOSED ? PLPGSQL_CHECK_ERROR : PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
report_unused_variables(cstate);
}
/*
* 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;
}
/*
* Release all plans created in check time
*
*/
static void
release_exprs(List *exprs)
{
ListCell *l;
foreach(l, exprs)
{
PLpgSQL_expr *expr = (PLpgSQL_expr *) lfirst(l);
SPI_freeplan(expr->plan);
expr->plan = NULL;
}
}
/****************************************************************************************
* Prepare environment
*
****************************************************************************************
*
*/
static bool
is_polymorphic_tupdesc(TupleDesc tupdesc)
{
int i;
for (i = 0; i < tupdesc->natts; i++)
if (IsPolymorphicType(TupleDescAttr(tupdesc, i)->atttypid))
return true;
return false;
}
/*
* Set up a fake fcinfo with just enough info to satisfy plpgsql_compile().
*
* There should be a different real argtypes for polymorphic params.
*
*/
static void
setup_fake_fcinfo(HeapTuple procTuple,
FmgrInfo *flinfo,
FunctionCallInfoData *fcinfo,
ReturnSetInfo *rsinfo,
TriggerData *trigdata,
Oid relid,
EventTriggerData *etrigdata,
Oid funcoid,
PLpgSQL_trigtype trigtype,
Trigger *tg_trigger)
{
Form_pg_proc procform;
Oid rettype;
TupleDesc resultTupleDesc;
procform = (Form_pg_proc) GETSTRUCT(procTuple);
rettype = procform->prorettype;
/* clean structures */
MemSet(fcinfo, 0, sizeof(FunctionCallInfoData));
MemSet(flinfo, 0, sizeof(FmgrInfo));
MemSet(rsinfo, 0, sizeof(ReturnSetInfo));
fcinfo->flinfo = flinfo;
flinfo->fn_oid = funcoid;
flinfo->fn_mcxt = CurrentMemoryContext;
if (trigtype == PLPGSQL_DML_TRIGGER)
{
Assert(trigdata != NULL);
MemSet(trigdata, 0, sizeof(TriggerData));
MemSet(tg_trigger, 0, sizeof(Trigger));
trigdata->type = T_TriggerData;
trigdata->tg_trigger = tg_trigger;
fcinfo->context = (Node *) trigdata;
if (OidIsValid(relid))
trigdata->tg_relation = relation_open(relid, AccessShareLock);
}
#if PG_VERSION_NUM >= 90300
else if (trigtype == PLPGSQL_EVENT_TRIGGER)
{
MemSet(etrigdata, 0, sizeof(etrigdata));
etrigdata->type = T_EventTriggerData;
fcinfo->context = (Node *) etrigdata;
}
#endif
/*
* prepare ReturnSetInfo
*
* necessary for RETURN NEXT and RETURN QUERY
*
*/
resultTupleDesc = build_function_result_tupdesc_t(procTuple);
if (resultTupleDesc)
{
/* we cannot to solve polymorphic params now */
if (is_polymorphic_tupdesc(resultTupleDesc))
{
FreeTupleDesc(resultTupleDesc);
resultTupleDesc = NULL;
}
}
else if (rettype == TRIGGEROID || rettype == OPAQUEOID)
{
/* trigger - return value should be ROW or RECORD based on relid */
if (trigdata->tg_relation)
resultTupleDesc = CreateTupleDescCopy(trigdata->tg_relation->rd_att);
}
else if (!IsPolymorphicType(rettype))
{
if (get_typtype(rettype) == TYPTYPE_COMPOSITE)
resultTupleDesc = lookup_rowtype_tupdesc_copy(rettype, -1);
else
{
resultTupleDesc = CreateTemplateTupleDesc(1, false);
TupleDescInitEntry(resultTupleDesc,
(AttrNumber) 1, "__result__",
rettype, -1, 0);
resultTupleDesc = BlessTupleDesc(resultTupleDesc);
}
}
if (resultTupleDesc)
{
fcinfo->resultinfo = (Node *) rsinfo;
rsinfo->type = T_ReturnSetInfo;
rsinfo->expectedDesc = resultTupleDesc;
rsinfo->allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize);
rsinfo->returnMode = SFRM_ValuePerCall;
/*
* ExprContext is created inside CurrentMemoryContext,
* without any additional source allocation. It is released
* on end of transaction.
*/
rsinfo->econtext = CreateStandaloneExprContext();
}
}
/*
* prepare PLpgSQL_checkstate structure
*
*/
static void
setup_cstate(PLpgSQL_checkstate *cstate,
Oid fn_oid,
TupleDesc tupdesc,
Tuplestorestate *tupstore,
bool fatal_errors,
bool other_warnings,
bool performance_warnings,
bool extra_warnings,
int format,
bool is_active_mode)
{
cstate->fn_oid = fn_oid;
cstate->estate = NULL;
cstate->tupdesc = tupdesc;
cstate->tuple_store = tupstore;
cstate->fatal_errors = fatal_errors;
cstate->other_warnings = other_warnings;
cstate->performance_warnings = performance_warnings;
cstate->extra_warnings = extra_warnings;
cstate->argnames = NIL;
cstate->exprs = NIL;
cstate->used_variables = NULL;
cstate->modif_variables = NULL;
cstate->top_stmt_stack = NULL;
cstate->format = format;
cstate->is_active_mode = is_active_mode;
cstate->sinfo = NULL;
cstate->check_cxt = AllocSetContextCreate(CurrentMemoryContext,
"plpgsql_check temporary cxt",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
cstate->found_return_query = false;
}
/* ----------
* Initialize a plpgsql fake execution state
* ----------
*/
static void
setup_plpgsql_estate(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi)
{
/* this link will be restored at exit from plpgsql_call_handler */
func->cur_estate = estate;
estate->func = func;
estate->retval = (Datum) 0;
estate->retisnull = true;
estate->rettype = InvalidOid;
estate->fn_rettype = func->fn_rettype;
estate->retistuple = func->fn_retistuple;
estate->retisset = func->fn_retset;
estate->readonly_func = func->fn_readonly;
estate->rettupdesc = NULL;
estate->exitlabel = NULL;
estate->cur_error = NULL;
estate->tuple_store = NULL;
if (rsi)
{
estate->tuple_store_cxt = rsi->econtext->ecxt_per_query_memory;
estate->tuple_store_owner = CurrentResourceOwner;
if (estate->retisset)
estate->rettupdesc = rsi->expectedDesc;
}
else
{
estate->tuple_store_cxt = NULL;
estate->tuple_store_owner = NULL;
}
estate->rsi = rsi;
estate->found_varno = func->found_varno;
estate->ndatums = func->ndatums;
estate->datums = palloc(sizeof(PLpgSQL_datum *) * estate->ndatums);
/* caller is expected to fill the datums array */
estate->eval_tuptable = NULL;
estate->eval_processed = 0;
estate->eval_lastoid = InvalidOid;
estate->eval_econtext = NULL;
#if PG_VERSION_NUM < 90500
estate->cur_expr = NULL;
#endif
estate->err_stmt = NULL;
estate->err_text = NULL;
estate->plugin_info = NULL;
}
/*
* 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_dno(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_dno(cstate, row->varnos[fnum]);
}
}
break;
default:
elog(ERROR, "unexpected dtype: %d", cstate->estate->datums[dno]->dtype);
}
}
/*
* initializing local execution variables
*
*/
PLpgSQL_datum *
copy_plpgsql_datum(PLpgSQL_datum *datum)
{
PLpgSQL_datum *result;
switch (datum->dtype)
{
case PLPGSQL_DTYPE_VAR:
{
PLpgSQL_var *new = palloc(sizeof(PLpgSQL_var));
memcpy(new, datum, sizeof(PLpgSQL_var));
/* Ensure the value is null (possibly not needed?) */
new->value = 0;
new->isnull = true;
new->freeval = false;
result = (PLpgSQL_datum *) new;
}
break;
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *new = palloc(sizeof(PLpgSQL_rec));
memcpy(new, datum, sizeof(PLpgSQL_rec));
/* Ensure the value is null (possibly not needed?) */
new->tup = NULL;
new->tupdesc = NULL;
new->freetup = false;
new->freetupdesc = false;
result = (PLpgSQL_datum *) new;
}
break;
case PLPGSQL_DTYPE_ROW:
case PLPGSQL_DTYPE_RECFIELD:
case PLPGSQL_DTYPE_ARRAYELEM:
/*
* These datum records are read-only at runtime, so no need to
* copy them (well, ARRAYELEM contains some cached type data, but
* we'd just as soon centralize the caching anyway)
*/
result = datum;
break;
default:
elog(ERROR, "unrecognized dtype: %d", datum->dtype);
result = NULL; /* keep compiler quiet */
break;
}
return result;
}
/****************************************************************************************
* Extended check walker
*
****************************************************************************************
*
*/
/*
* walk over all plpgsql statements - search and check expressions
*
*/
static void
check_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, int *closing, List **exceptions)
{
TupleDesc tupdesc = NULL;
PLpgSQL_function *func;
ListCell *l;
ResourceOwner oldowner;
MemoryContext oldCxt = CurrentMemoryContext;
PLpgSQL_stmt_stack_item *outer_stmt;
if (stmt == NULL)
return;
cstate->estate->err_stmt = stmt;
func = cstate->estate->func;
/*
* Attention - returns NULL, when there are not any outer level
*/
outer_stmt = push_stmt_to_stmt_stack(cstate);
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(oldCxt);
PG_TRY();
{
switch (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);
put_error(cstate,
0, 0,
str.data,
"Local variable overlap function parameter.",
NULL,
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
pfree(str.data);
}
}
if (found_shadowed_variable(refname, outer_stmt, cstate))
{
StringInfoData str;
initStringInfo(&str);
appendStringInfo(&str, "variable \"%s\" shadows a previously defined variable",
refname);
put_error(cstate,
0, 0,
str.data,
NULL,
"SET plpgsql.extra_warnings TO 'shadowed_variables'",
PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
pfree(str.data);
}
}
}
check_stmts(cstate, stmt_block->body, closing, exceptions);
if (stmt_block->exceptions)
{
int closing_local;
List *exceptions_local = NIL;
int closing_handlers = PLPGSQL_CHECK_UNKNOWN;
List *exceptions_transformed = NIL;
if (*closing == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
{
ListCell *lc;
int i = 0;
int *err_codes = NULL;
int nerr_codes = 0;
/* copy errcodes to a array */
nerr_codes = list_length(*exceptions);
err_codes = palloc(sizeof(int) * nerr_codes);
foreach(lc, *exceptions)
{
err_codes[i++] = lfirst_int(lc);
}
foreach(l, stmt_block->exceptions->exc_list)
{
PLpgSQL_exception *exception = (PLpgSQL_exception *) lfirst(l);
/* RETURN in exception handler ~ is possible closing */
check_stmts(cstate, exception->action,
&closing_local, &exceptions_local);
if (*exceptions != NIL)
{
int i;
for (i = 0; i < nerr_codes; i++)
{
int err_code = err_codes[i];
if (err_code != -1 &&
exception_matches_conditions(err_code, exception->conditions))
{
closing_handlers = merge_closing(closing_handlers, closing_local,
&exceptions_transformed, exceptions_local,
err_code);
*exceptions = list_delete_int(*exceptions, err_code);
err_codes[i] = -1;
}
}
}
}
Assert(err_codes != NULL);
pfree(err_codes);
if (closing_handlers != PLPGSQL_CHECK_UNKNOWN)
{
*closing = closing_handlers;
if (closing_handlers == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
*exceptions = list_concat_unique_int(*exceptions, exceptions_transformed);
else
*exceptions = NIL;
}
}
else
{
foreach(l, stmt_block->exceptions->exc_list)
{
PLpgSQL_exception *exception = (PLpgSQL_exception *) lfirst(l);
/* RETURN in exception handler ~ it is possible closing only */
check_stmts(cstate, exception->action,
&closing_local, &exceptions_local);
closing_handlers = merge_closing(closing_handlers, closing_local,
&exceptions_transformed, exceptions_local,
-1);
}
if (closing_handlers != *closing)
*closing = PLPGSQL_CHECK_POSSIBLY_CLOSED;
}
/*
* Mark the hidden variables SQLSTATE and SQLERRM used
* even if they actually weren't. Not using them
* should practically never be a sign of a problem, so
* there's no point in annoying the user.
*/
record_variable_usage(cstate, stmt_block->exceptions->sqlstate_varno, false);
record_variable_usage(cstate, stmt_block->exceptions->sqlerrm_varno, false);
}
}
break;
#if PG_VERSION_NUM >= 90500
case PLPGSQL_STMT_ASSERT:
{
PLpgSQL_stmt_assert *stmt_assert = (PLpgSQL_stmt_assert *) stmt;
/*
* Should or should not to depends on plpgsql_check_asserts?
* I am thinking, so any code (active or inactive) should be valid,
* so I ignore plpgsql_check_asserts option.
*/
check_expr_with_expected_scalar_type(cstate,
stmt_assert->cond, BOOLOID, true);
if (stmt_assert->message != NULL)
check_expr(cstate, stmt_assert->message);
}
break;
#endif
case PLPGSQL_STMT_ASSIGN:
{
PLpgSQL_stmt_assign *stmt_assign = (PLpgSQL_stmt_assign *) stmt;
check_assignment(cstate, stmt_assign->expr, NULL, NULL,
stmt_assign->varno);
}
break;
case PLPGSQL_STMT_IF:
{
PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt;
ListCell *l;
int closing_local;
int closing_all_paths = PLPGSQL_CHECK_UNKNOWN;
List *exceptions_local;
check_expr_with_expected_scalar_type(cstate,
stmt_if->cond, BOOLOID, true);
check_stmts(cstate, stmt_if->then_body, &closing_local,
&exceptions_local);
closing_all_paths = merge_closing(closing_all_paths,
closing_local,
exceptions,
exceptions_local,
-1);
foreach(l, stmt_if->elsif_list)
{
PLpgSQL_if_elsif *elif = (PLpgSQL_if_elsif *) lfirst(l);
check_expr_with_expected_scalar_type(cstate,
elif->cond, BOOLOID, true);
check_stmts(cstate, elif->stmts, &closing_local,
&exceptions_local);
closing_all_paths = merge_closing(closing_all_paths,
closing_local,
exceptions,
exceptions_local,
-1);
}
check_stmts(cstate, stmt_if->else_body, &closing_local,
&exceptions_local);
closing_all_paths = merge_closing(closing_all_paths,
closing_local,
exceptions,
exceptions_local,
-1);
if (stmt_if->else_body != NULL)
*closing = closing_all_paths;
else if (closing_all_paths == PLPGSQL_CHECK_UNCLOSED)
*closing = PLPGSQL_CHECK_UNCLOSED;
else
*closing = PLPGSQL_CHECK_POSSIBLY_CLOSED;
}
break;
case PLPGSQL_STMT_CASE:
{
PLpgSQL_stmt_case *stmt_case = (PLpgSQL_stmt_case *) stmt;
Oid result_oid;
int closing_local;
List *exceptions_local;
int closing_all_paths = PLPGSQL_CHECK_UNKNOWN;
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 */
NULL);
result_oid = TupleDescAttr(tupdesc, 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, &closing_local, &exceptions_local);
closing_all_paths = merge_closing(closing_all_paths,
closing_local,
exceptions,
exceptions_local,
-1);
}
if (stmt_case->else_stmts)
{
check_stmts(cstate, stmt_case->else_stmts, &closing_local, &exceptions_local);
*closing = merge_closing(closing_all_paths,
closing_local,
exceptions,
exceptions_local,
-1);
}
else
/* is not ensured all path evaluation */
*closing = possibly_closed(closing_all_paths);
}
break;
case PLPGSQL_STMT_LOOP:
check_stmts(cstate, ((PLpgSQL_stmt_loop *) stmt)->body, closing, exceptions);
break;
case PLPGSQL_STMT_WHILE:
{
PLpgSQL_stmt_while *stmt_while = (PLpgSQL_stmt_while *) stmt;
int closing_local;
List *exceptions_local;
check_expr_with_expected_scalar_type(cstate,
stmt_while->cond,
BOOLOID,
true);
check_expr(cstate, stmt_while->cond);
/*
* When is not guaranteed execution (possible zero loops),
* then ignore closing info from body.
*/
check_stmts(cstate, stmt_while->body, &closing_local, &exceptions_local);
*closing = possibly_closed(closing_local);
}
break;
case PLPGSQL_STMT_FORI:
{
PLpgSQL_stmt_fori *stmt_fori = (PLpgSQL_stmt_fori *) stmt;
int dno = stmt_fori->var->dno;
int closing_local;
List *exceptions_local;
/* prepare plan if desn't exist yet */
check_assignment(cstate, stmt_fori->lower, NULL, NULL, dno);
check_assignment(cstate, stmt_fori->upper, NULL, NULL, dno);
if (stmt_fori->step)
check_assignment(cstate, stmt_fori->step, NULL, NULL, dno);
check_stmts(cstate, stmt_fori->body, &closing_local, &exceptions_local);
*closing = possibly_closed(closing_local);
}
break;
case PLPGSQL_STMT_FORS:
{
PLpgSQL_stmt_fors *stmt_fors = (PLpgSQL_stmt_fors *) stmt;
int closing_local;
List *exceptions_local;
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, &closing_local, &exceptions_local);
*closing = possibly_closed(closing_local);
}
break;
case PLPGSQL_STMT_FORC:
{
PLpgSQL_stmt_forc *stmt_forc = (PLpgSQL_stmt_forc *) stmt;
PLpgSQL_var *var = (PLpgSQL_var *) func->datums[stmt_forc->curvar];
int closing_local;
List *exceptions_local;
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, &closing_local, &exceptions_local);
*closing = possibly_closed(closing_local);
cstate->used_variables = bms_add_member(cstate->used_variables,
stmt_forc->curvar);
}
break;
case PLPGSQL_STMT_DYNFORS:
{
PLpgSQL_stmt_dynfors *stmt_dynfors = (PLpgSQL_stmt_dynfors *) stmt;
int closing_local;
List *exceptions_local;
check_row_or_rec(cstate, stmt_dynfors->row, stmt_dynfors->rec);
check_expr(cstate, stmt_dynfors->query);
foreach(l, stmt_dynfors->params)
{
check_expr(cstate, (PLpgSQL_expr *) lfirst(l));
}
if (stmt_dynfors->rec != NULL)
{
put_error(cstate,
0, 0,
"cannot determinate a result of dynamic SQL",
"Cannot to contine in check.",
"Don't use dynamic SQL and record type together, when you would check function.",
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
}
check_stmts(cstate, stmt_dynfors->body, &closing_local, &exceptions_local);
*closing = possibly_closed(closing_local);
}
break;
case PLPGSQL_STMT_FOREACH_A:
{
PLpgSQL_stmt_foreach_a *stmt_foreach_a = (PLpgSQL_stmt_foreach_a *) stmt;
bool use_element_type;
int closing_local;
List *exceptions_local;
check_target(cstate, stmt_foreach_a->varno, NULL, NULL);
/*
* When slice > 0, then result and target are a array.
* We shoudl to disable a array element refencing.
*/
use_element_type = stmt_foreach_a->slice == 0;
check_assignment_with_possible_slices(cstate,
stmt_foreach_a->expr,
NULL, NULL,
stmt_foreach_a->varno,
use_element_type);
check_stmts(cstate, stmt_foreach_a->body, &closing_local, &exceptions_local);
*closing = possibly_closed(closing_local);
}
break;
case PLPGSQL_STMT_EXIT:
{
PLpgSQL_stmt_exit *stmt_exit = (PLpgSQL_stmt_exit *) stmt;
check_expr(cstate, stmt_exit->cond);
if (stmt_exit->label != NULL)
{
PLpgSQL_stmt *labeled_stmt = find_stmt_with_label(stmt_exit->label,
outer_stmt);
if (labeled_stmt == NULL)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("label \"%s\" does not exist", stmt_exit->label)));
/* CONTINUE only allows loop labels */
if (!is_any_loop_stmt(labeled_stmt) && !stmt_exit->is_exit)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("block label \"%s\" cannot be used in CONTINUE",
stmt_exit->label)));
}
else
{
if (find_nearest_loop(outer_stmt) == NULL)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("%s cannot be used outside a loop",
plpgsql_stmt_typename((PLpgSQL_stmt *) stmt_exit))));
}
}
break;
case PLPGSQL_STMT_PERFORM:
check_expr(cstate, ((PLpgSQL_stmt_perform *) stmt)->expr);
break;
case PLPGSQL_STMT_RETURN:
{
PLpgSQL_stmt_return *stmt_rt = (PLpgSQL_stmt_return *) stmt;
if (stmt_rt->retvarno >= 0)
{
PLpgSQL_datum *retvar = cstate->estate->datums[stmt_rt->retvarno];
PLpgSQL_execstate *estate = cstate->estate;
cstate->used_variables = bms_add_member(cstate->used_variables, stmt_rt->retvarno);
switch (retvar->dtype)
{
case PLPGSQL_DTYPE_VAR:
{
PLpgSQL_var *var = (PLpgSQL_var *) retvar;
check_assign_to_target_type(cstate,
cstate->estate->func->fn_rettype, -1,
var->datatype->typoid, false);
}
break;
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
if (rec->tupdesc && estate->rsi && IsA(estate->rsi, ReturnSetInfo))
{
TupleDesc rettupdesc = estate->rsi->expectedDesc;
TupleConversionMap *tupmap ;
tupmap = convert_tuples_by_position(rec->tupdesc, rettupdesc,
gettext_noop("returned record type does not match expected record type"));
if (tupmap)
free_conversion_map(tupmap);
}
}
break;
case PLPGSQL_DTYPE_ROW:
{
PLpgSQL_row *row = (PLpgSQL_row *) retvar;
if (row->rowtupdesc && estate->rsi && IsA(estate->rsi, ReturnSetInfo))
{
TupleDesc rettupdesc = estate->rsi->expectedDesc;
TupleConversionMap *tupmap ;
tupmap = convert_tuples_by_position(row->rowtupdesc, rettupdesc,
gettext_noop("returned record type does not match expected record type"));
if (tupmap)
free_conversion_map(tupmap);
}
}
break;
default:
; /* nope */
}
}
*closing = PLPGSQL_CHECK_CLOSED;
if (stmt_rt->expr)
check_returned_expr(cstate, stmt_rt->expr, true);
}
break;
case PLPGSQL_STMT_RETURN_NEXT:
{
PLpgSQL_stmt_return_next *stmt_rn = (PLpgSQL_stmt_return_next *) stmt;
if (stmt_rn->retvarno >= 0)
{
PLpgSQL_datum *retvar = cstate->estate->datums[stmt_rn->retvarno];
PLpgSQL_execstate *estate = cstate->estate;
TupleDesc tupdesc;
int natts;
cstate->used_variables = bms_add_member(cstate->used_variables, stmt_rn->retvarno);
if (!estate->retisset)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cannot use RETURN NEXT in a non-SETOF function")));
tupdesc = estate->rettupdesc;
natts = tupdesc ? tupdesc->natts : 0;
switch (retvar->dtype)
{
case PLPGSQL_DTYPE_VAR:
{
PLpgSQL_var *var = (PLpgSQL_var *) retvar;
if (natts > 1)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("wrong result type supplied in RETURN NEXT")));
check_assign_to_target_type(cstate,
cstate->estate->func->fn_rettype, -1,
var->datatype->typoid, false);
}
break;
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
TupleConversionMap *tupmap;
if (!HeapTupleIsValid(rec->tup))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("record \"%s\" is not assigned yet",
rec->refname),
errdetail("The tuple structure of a not-yet-assigned"
" record is indeterminate.")));
if (tupdesc)
{
tupmap = convert_tuples_by_position(rec->tupdesc,
tupdesc,
gettext_noop("wrong record type supplied in RETURN NEXT"));
if (tupmap)
free_conversion_map(tupmap);
}
}
break;
case PLPGSQL_DTYPE_ROW:
{
PLpgSQL_row *row = (PLpgSQL_row *) retvar;
bool row_is_valid_result;
row_is_valid_result = true;
if (tupdesc)
{
if (row->nfields == natts)
{
int i;
for (i = 0; i < natts; i++)
{
PLpgSQL_var *var;
if (TupleDescAttr(tupdesc, i)->attisdropped)
continue;
if (row->varnos[i] < 0)
elog(ERROR, "dropped rowtype entry for non-dropped column");
var = (PLpgSQL_var *) (cstate->estate->datums[row->varnos[i]]);
if (var->datatype->typoid != TupleDescAttr(tupdesc, i)->atttypid)
{
row_is_valid_result = false;
break;
}
}
}
else
row_is_valid_result = false;
if (!row_is_valid_result)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("wrong record type supplied in RETURN NEXT")));
}
}
break;
default:
; /* nope */
}
}
if (stmt_rn->expr)
check_returned_expr(cstate, stmt_rn->expr, true);
}
break;
case PLPGSQL_STMT_RETURN_QUERY:
{
PLpgSQL_stmt_return_query *stmt_rq = (PLpgSQL_stmt_return_query *) stmt;
check_expr(cstate, stmt_rq->dynquery);
if (stmt_rq->query)
{
check_returned_expr(cstate, stmt_rq->query, false);
cstate->found_return_query = true;
}
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;
int err_code = 0;
if (stmt_raise->condname != NULL)
err_code = plpgsql_recognize_err_condition(stmt_raise->condname, true);
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);
if (opt->opt_type == PLPGSQL_RAISEOPTION_ERRCODE)
{
bool IsConst;
char *value = ExprGetString(opt->expr, &IsConst);
if (IsConst && value != NULL)
err_code = plpgsql_recognize_err_condition(value, true);
else
err_code = -1; /* cannot be calculated now */
}
}
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")));
if (stmt_raise->elog_level >= ERROR)
{
*closing = PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS;
if (err_code == 0)
err_code = ERRCODE_RAISE_EXCEPTION;
else if (err_code == -1)
err_code = 0; /* cannot be calculated */
*exceptions = list_make1_int(err_code);
}
/* without any parameters it is reRAISE */
if (stmt_raise->condname == NULL && stmt_raise->message == NULL &&
stmt_raise->options == NIL)
{
*closing = PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS;
/* should be enhanced in future */
*exceptions = list_make1_int(-2); /* reRAISE */
}
}
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_as_sqlstmt_nodata(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)
{
put_error(cstate,
0, 0,
"cannot determinate a result of dynamic SQL",
"Cannot to contine in check.",
"Don't use dynamic SQL and record type together, when you would check function.",
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
}
}
}
break;
case PLPGSQL_STMT_OPEN:
{
PLpgSQL_stmt_open *stmt_open = (PLpgSQL_stmt_open *) stmt;
PLpgSQL_var *var = (PLpgSQL_var *) (cstate->estate->datums[stmt_open->curvar]);
if (var->cursor_explicit_expr)
check_expr(cstate, var->cursor_explicit_expr);
check_expr(cstate, stmt_open->query);
if (var != NULL && stmt_open->query != NULL)
var->cursor_explicit_expr = 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));
}
cstate->used_variables = bms_add_member(cstate->used_variables,
stmt_open->curvar);
}
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, NULL, NULL);
}
}
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);
cstate->used_variables = bms_add_member(cstate->used_variables, stmt_fetch->curvar);
}
break;
case PLPGSQL_STMT_CLOSE:
cstate->used_variables = bms_add_member(cstate->used_variables,
((PLpgSQL_stmt_close *) stmt)->curvar);
break;
default:
elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
}
pop_stmt_from_stmt_stack(cstate);
RollbackAndReleaseCurrentSubTransaction();
MemoryContextSwitchTo(oldCxt);
CurrentResourceOwner = oldowner;
SPI_restore_connection();
}
PG_CATCH();
{
ErrorData *edata;
MemoryContextSwitchTo(oldCxt);
edata = CopyErrorData();
FlushErrorState();
RollbackAndReleaseCurrentSubTransaction();
MemoryContextSwitchTo(oldCxt);
CurrentResourceOwner = oldowner;
pop_stmt_from_stmt_stack(cstate);
/*
* 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
put_error_edata(cstate, edata);
MemoryContextSwitchTo(oldCxt);
/* reconnect spi */
SPI_restore_connection();
}
PG_END_TRY();
}
/*
* Ensure check for all statements in list
*
*/
static void
check_stmts(PLpgSQL_checkstate *cstate, List *stmts, int *closing, List **exceptions)
{
ListCell *lc;
int closing_local;
List *exceptions_local;
bool dead_code_alert = false;
*closing = PLPGSQL_CHECK_UNCLOSED;
*exceptions = NIL;
foreach(lc, stmts)
{
PLpgSQL_stmt *stmt = (PLpgSQL_stmt *) lfirst(lc);
closing_local = PLPGSQL_CHECK_UNCLOSED;
exceptions_local = NIL;
check_stmt(cstate, stmt, &closing_local, &exceptions_local);
if (dead_code_alert)
{
put_error(cstate,
0, stmt->lineno,
"unreachable code",
NULL,
NULL,
PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
/* don't raise this warning every line */
dead_code_alert = false;
}
if (closing_local == PLPGSQL_CHECK_CLOSED)
{
dead_code_alert = true;
*closing = PLPGSQL_CHECK_CLOSED;
*exceptions = NIL;
}
else if (closing_local == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
{
dead_code_alert = true;
if (*closing == PLPGSQL_CHECK_UNCLOSED ||
*closing == PLPGSQL_CHECK_POSSIBLY_CLOSED ||
*closing == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
{
*closing = PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS;
*exceptions = exceptions_local;
}
}
else if (closing_local == PLPGSQL_CHECK_POSSIBLY_CLOSED)
{
if (*closing == PLPGSQL_CHECK_UNCLOSED)
{
*closing = PLPGSQL_CHECK_POSSIBLY_CLOSED;
*exceptions = NIL;
}
}
}
}
static int
possibly_closed(int c)
{
switch (c)
{
case PLPGSQL_CHECK_CLOSED:
case PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS:
case PLPGSQL_CHECK_POSSIBLY_CLOSED:
return PLPGSQL_CHECK_POSSIBLY_CLOSED;
default:
return PLPGSQL_CHECK_UNCLOSED;
}
}
static int
merge_closing(int c, int c_local, List **exceptions, List *exceptions_local, int err_code)
{
*exceptions = NIL;
if (c == PLPGSQL_CHECK_UNKNOWN)
{
if (c_local == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
*exceptions = exceptions_local;
return c_local;
}
if (c_local == PLPGSQL_CHECK_UNKNOWN)
return c;
if (c == c_local)
{
if (c == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
{
if (err_code != -1)
{
ListCell *lc;
/* replace reRAISE symbol (-2) by real err_code */
foreach(lc, exceptions_local)
{
int t_err_code = lfirst_int(lc);
*exceptions = list_append_unique_int(*exceptions,
t_err_code != -2 ? t_err_code : err_code);
}
}
else
*exceptions = list_concat_unique_int(*exceptions, exceptions_local);
}
return c_local;
}
if (c == PLPGSQL_CHECK_CLOSED || c_local == PLPGSQL_CHECK_CLOSED)
{
if (c == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS ||
c_local == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS)
return PLPGSQL_CHECK_CLOSED;
}
return PLPGSQL_CHECK_POSSIBLY_CLOSED;
}
static bool
exception_matches_conditions(int sqlerrstate, PLpgSQL_condition *cond)
{
for (; cond != NULL; cond = cond->next)
{
int _sqlerrstate = cond->sqlerrstate;
/*
* OTHERS matches everything *except* query-canceled and
* assert-failure. If you're foolish enough, you can match those
* explicitly.
*/
if (_sqlerrstate == 0)
{
if (sqlerrstate != ERRCODE_QUERY_CANCELED
#if PG_VERSION_NUM >= 90500
&& sqlerrstate != ERRCODE_ASSERT_FAILURE
#endif
)
return true;
}
/* Exact match? */
else if (sqlerrstate == _sqlerrstate)
return true;
/* Category match? */
else if (ERRCODE_IS_CATEGORY(_sqlerrstate) &&
ERRCODE_TO_CATEGORY(sqlerrstate) == _sqlerrstate)
return true;
}
return false;
}
/*
* Verify a expression
*
*/
static void
check_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr)
{
if (expr)
check_expr_as_rvalue(cstate, expr, NULL, NULL, -1, false, false);
}
static void
record_variable_usage(PLpgSQL_checkstate *cstate, int dno, bool write)
{
if (dno >= 0)
{
cstate->used_variables = bms_add_member(cstate->used_variables, dno);
if (write)
cstate->modif_variables = bms_add_member(cstate->modif_variables, dno);
}
}
/*
* Returns true if dno is explicitly declared. It should not be used
* for arguments.
*/
static bool
datum_is_explicit(PLpgSQL_checkstate *cstate, int dno)
{
PLpgSQL_execstate *estate = cstate->estate;
switch (estate->datums[dno]->dtype)
{
case PLPGSQL_DTYPE_VAR:
case PLPGSQL_DTYPE_ROW:
case PLPGSQL_DTYPE_REC:
return ((PLpgSQL_variable *) estate->datums[dno])->lineno > 0;
default:
return false;
}
}
/*
* returns true, when datum or some child is used
*/
static bool
datum_is_used(PLpgSQL_checkstate *cstate, int dno, bool write)
{
PLpgSQL_execstate *estate = cstate->estate;
switch (estate->datums[dno]->dtype)
{
case PLPGSQL_DTYPE_VAR:
{
return bms_is_member(dno,
write ? cstate->modif_variables : cstate->used_variables);
}
break;
case PLPGSQL_DTYPE_ROW:
{
PLpgSQL_row *row = (PLpgSQL_row *) estate->datums[dno];
int i;
if (bms_is_member(dno,
write ? cstate->modif_variables : cstate->used_variables))
return true;
for (i = 0; i < row->nfields; i++)
{
if (row->varnos[i] < 0)
continue;
if (datum_is_used(cstate, row->varnos[i], write))
return true;
}
return false;
}
break;
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[dno];
int i;
if (bms_is_member(dno,
write ? cstate->modif_variables : cstate->used_variables))
return true;
/* search any used recfield with related recparentno */
for (i = 0; i < estate->ndatums; i++)
{
if (estate->datums[i]->dtype == PLPGSQL_DTYPE_RECFIELD)
{
PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) estate->datums[i];
if (recfield->recparentno == rec->dno
&& datum_is_used(cstate, i, write))
return true;
}
}
}
break;
case PLPGSQL_DTYPE_RECFIELD:
return bms_is_member(dno,
write ? cstate->modif_variables : cstate->used_variables);
default:
return false;
}
return false;
}
#define UNUSED_VARIABLE_TEXT "unused variable \"%s\""
#define UNUSED_VARIABLE_TEXT_CHECK_LENGTH 15
#define UNUSED_PARAMETER_TEXT "unused parameter \"%s\""
#define UNMODIFIED_VARIABLE_TEXT "unmodified OUT variable \"%s\""
#define OUT_COMPOSITE_IS_NOT_SINGE_TEXT "composite OUT variable \"%s\" is not single argument"
/*
* Reports all unused variables explicitly DECLAREd by the user. Ignores
* special variables created by PL/PgSQL.
*/
static void
report_unused_variables(PLpgSQL_checkstate *cstate)
{
int i;
PLpgSQL_execstate *estate = cstate->estate;
/* now, there are no active plpgsql statement */
estate->err_stmt = NULL;
for (i = 0; i < estate->ndatums; i++)
if (datum_is_explicit(cstate, i) && !datum_is_used(cstate, i, false))
{
PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[i];
StringInfoData message;
initStringInfo(&message);
appendStringInfo(&message, UNUSED_VARIABLE_TEXT, var->refname);
put_error(cstate,
0, var->lineno,
message.data,
NULL,
NULL,
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
pfree(message.data);
message.data = NULL;
}
if (cstate->extra_warnings)
{
PLpgSQL_function *func = estate->func;
/* check IN parameters */
for (i = 0; i < func->fn_nargs; i++)
{
int varno = func->fn_argvarnos[i];
if (!datum_is_used(cstate, varno, false))
{
PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[varno];
StringInfoData message;
initStringInfo(&message);
appendStringInfo(&message, UNUSED_PARAMETER_TEXT, var->refname);
put_error(cstate,
0, 0,
message.data,
NULL,
NULL,
PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
pfree(message.data); message.data = NULL;
}
}
/* are there some OUT parameters (expect modification)? */
if (func->out_param_varno != -1 && !cstate->found_return_query)
{
int varno = func->out_param_varno;
PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[varno];
if (var->dtype == PLPGSQL_DTYPE_ROW && var->refname == NULL)
{
/* this function has more OUT parameters */
PLpgSQL_row *row = (PLpgSQL_row*) var;
int fnum;
for (fnum = 0; fnum < row->nfields; fnum++)
{
int varno2 = row->varnos[fnum];
PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[varno2];
StringInfoData message;
if (var->dtype == PLPGSQL_DTYPE_ROW ||
var->dtype == PLPGSQL_DTYPE_REC)
{
initStringInfo(&message);
appendStringInfo(&message,
OUT_COMPOSITE_IS_NOT_SINGE_TEXT, var->refname);
put_error(cstate,
0, 0,
message.data,
NULL,
NULL,
PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
pfree(message.data);
message.data = NULL;
}
if (!datum_is_used(cstate, varno2, true))
{
initStringInfo(&message);
appendStringInfo(&message, UNMODIFIED_VARIABLE_TEXT, var->refname);
put_error(cstate,
0, 0,
message.data,
NULL,
NULL,
PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
pfree(message.data);
message.data = NULL;
}
}
}
else
{
if (!datum_is_used(cstate, varno, true))
{
PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[varno];
StringInfoData message;
initStringInfo(&message);
appendStringInfo(&message, UNMODIFIED_VARIABLE_TEXT, var->refname);
put_error(cstate,
0, 0,
message.data,
NULL,
NULL,
PLPGSQL_CHECK_WARNING_EXTRA,
0, NULL, NULL);
pfree(message.data);
message.data = NULL;
}
}
}
}
}
/*
* 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_expr_as_rvalue(cstate, expr, targetrec, targetrow, targetdno, false,
is_expression);
}
/*
* Verify an assignment of 'expr' to 'target' with possible slices
*
* it is used in FOREACH ARRAY where SLICE change a target type
*
*/
static void
check_assignment_with_possible_slices(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow,
int targetdno, bool use_element_type)
{
bool is_expression = (targetrec == NULL && targetrow == NULL);
check_expr_as_rvalue(cstate, expr, targetrec, targetrow, targetdno, use_element_type,
is_expression);
}
/*
* Verify to possible cast to bool, integer, ..
*
*/
static void
check_expr_with_expected_scalar_type(PLpgSQL_checkstate *cstate,
PLpgSQL_expr *expr,
Oid expected_typoid,
bool required)
{
ResourceOwner oldowner;
MemoryContext oldCxt = CurrentMemoryContext;
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(oldCxt);
if (!expr)
{
if (required)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("required expression is empty")));
}
else
{
PG_TRY();
{
TupleDesc tupdesc;
bool is_immutable_null;
prepare_expr(cstate, expr, 0);
/* record all variables used by the query */
cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos);
tupdesc = expr_get_desc(cstate, expr, false, true, true, NULL);
is_immutable_null = is_const_null_expr(expr);
if (tupdesc)
{
/* when we know value or type */
if (!is_immutable_null)
check_assign_to_target_type(cstate,
expected_typoid, -1,
TupleDescAttr(tupdesc, 0)->atttypid,
is_immutable_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
put_error_edata(cstate, edata);
MemoryContextSwitchTo(oldCxt);
/* reconnect spi */
SPI_restore_connection();
}
PG_END_TRY();
}
}
/*
* Checks used for RETURN QUERY
*
*/
static void
check_returned_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool is_expression)
{
PLpgSQL_execstate *estate = cstate->estate;
PLpgSQL_function *func = estate->func;
bool is_return_query = !is_expression;
ResourceOwner oldowner;
MemoryContext oldCxt = CurrentMemoryContext;
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(oldCxt);
PG_TRY();
{
TupleDesc tupdesc;
bool is_immutable_null;
Oid first_level_typ = InvalidOid;
prepare_expr(cstate, expr, 0);
/* record all variables used by the query */
cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos);
tupdesc = expr_get_desc(cstate, expr, false, true, is_expression, &first_level_typ);
is_immutable_null = is_const_null_expr(expr);
if (tupdesc)
{
/* enforce check for trigger function - result must be composit */
if (func->fn_retistuple && is_expression
&& !(type_is_rowtype(TupleDescAttr(tupdesc, 0)->atttypid) ||
type_is_rowtype(first_level_typ) || tupdesc->natts > 1))
{
/* but we should to allow NULL */
if (!is_immutable_null)
put_error(cstate,
ERRCODE_DATATYPE_MISMATCH, 0,
"cannot return non-composite value from function returning composite type",
NULL,
NULL,
PLPGSQL_CHECK_ERROR,
0, NULL, NULL);
}
/* tupmap is used when function returns tuple or RETURN QUERY was used */
else if (func->fn_retistuple || is_return_query)
{
/* should to know expected result */
if (estate->rsi && IsA(estate->rsi, ReturnSetInfo))
{
TupleDesc rettupdesc = estate->rsi->expectedDesc;
TupleConversionMap *tupmap ;
tupmap = convert_tuples_by_position(tupdesc, rettupdesc,
!is_expression ? gettext_noop("structure of query does not match function result type")
: gettext_noop("returned record type does not match expected record type"));
if (tupmap)
free_conversion_map(tupmap);
}
}
else
{
/* returns scalar */
if (!IsPolymorphicType(func->fn_rettype))
{
check_assign_to_target_type(cstate,
func->fn_rettype, -1,
TupleDescAttr(tupdesc, 0)->atttypid,
is_immutable_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
put_error_edata(cstate, edata);
MemoryContextSwitchTo(oldCxt);
/* reconnect spi */
SPI_restore_connection();
}
PG_END_TRY();
}
/*
* Check expression as rvalue - on right in assign statement. It is used for
* only expression check too - when target is unknown.
*
*/
static void
check_expr_as_rvalue(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;
bool is_immutable_null;
bool expand = true;
Oid first_level_typoid;
Oid expected_typoid = InvalidOid;
int expected_typmod = InvalidOid;
if (targetdno != -1)
{
check_target(cstate, targetdno, &expected_typoid, &expected_typmod);
/*
* When target variable is not compossite, then we should not
* to expand result tupdesc.
*/
if (!type_is_rowtype(expected_typoid))
expand = false;
}
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(oldCxt);
PG_TRY();
{
prepare_expr(cstate, expr, 0);
/* record all variables used by the query */
cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos);
tupdesc = expr_get_desc(cstate, expr, use_element_type, expand, is_expression, &first_level_typoid);
is_immutable_null = is_const_null_expr(expr);
if (expected_typoid != InvalidOid && type_is_rowtype(expected_typoid) && first_level_typoid != InvalidOid)
{
/* simple error, scalar source to composite target */
if (!type_is_rowtype(first_level_typoid) && !is_immutable_null)
{
put_error(cstate,
ERRCODE_DATATYPE_MISMATCH, 0,
"cannot assign scalar variable to composite target",
NULL,
NULL,
PLPGSQL_CHECK_ERROR,
0, NULL, NULL);
goto no_other_check;
}
/* simple ok, target and source composite types are same */
if (type_is_rowtype(first_level_typoid)
&& first_level_typoid != RECORDOID && first_level_typoid == expected_typoid)
goto no_other_check;
}
if (tupdesc)
{
if (targetrow != NULL || targetrec != NULL)
assign_tupdesc_row_or_rec(cstate, targetrow, targetrec, tupdesc, is_immutable_null);
if (targetdno != -1)
assign_tupdesc_dno(cstate, targetdno, tupdesc, is_immutable_null);
if (targetrow)
{
if (targetrow->nfields > tupdesc->natts)
put_error(cstate,
0, 0,
"too few attributies for target variables",
"There are more target variables than output columns in query.",
"Check target variables in SELECT INTO statement.",
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
else if (targetrow->nfields < tupdesc->natts)
put_error(cstate,
0, 0,
"too many attributies for target variables",
"There are less target variables than output columns in query.",
"Check target variables in SELECT INTO statement",
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
}
}
no_other_check:
if (tupdesc)
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
put_error_edata(cstate, edata);
MemoryContextSwitchTo(oldCxt);
/* reconnect spi */
SPI_restore_connection();
}
PG_END_TRY();
}
/*
* Check a SQL statement, should not to return data
*
*/
static void
check_expr_as_sqlstmt_nodata(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr)
{
ResourceOwner oldowner;
MemoryContext oldCxt = CurrentMemoryContext;
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(oldCxt);
PG_TRY();
{
prepare_expr(cstate, expr, 0);
/* record all variables used by the query */
cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos);
if (expr_get_desc(cstate, expr, false, false, false, NULL))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("query has no destination for result data")));
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
put_error_edata(cstate, edata);
MemoryContextSwitchTo(oldCxt);
/* reconnect spi */
SPI_restore_connection();
}
PG_END_TRY();
}
/*
* 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;
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], NULL, NULL);
}
record_variable_usage(cstate, row->dno, true);
}
else if (rec != NULL)
{
/*
* There are no checks done on records currently; just record that the
* variable is not unused.
*/
record_variable_usage(cstate, rec->dno, true);
}
}
/*
* Verify lvalue It doesn't repeat a checks that are done. Checks a subscript
* expressions, verify a validity of record's fields.
*/
static void
check_target(PLpgSQL_checkstate *cstate, int varno, Oid *expected_typoid, int *expected_typmod)
{
PLpgSQL_datum *target = cstate->estate->datums[varno];
record_variable_usage(cstate, varno, true);
switch (target->dtype)
{
case PLPGSQL_DTYPE_VAR:
{
PLpgSQL_var *var = (PLpgSQL_var *) target;
PLpgSQL_type *tp = var->datatype;
if (expected_typoid != NULL)
*expected_typoid = tp->typoid;
if (expected_typmod != NULL)
*expected_typmod = tp->atttypmod;
}
break;
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) target;
if (rec->tupdesc != NULL)
{
if (expected_typoid != NULL)
*expected_typoid = rec->tupdesc->tdtypeid;
if (expected_typmod != NULL)
*expected_typmod = rec->tupdesc->tdtypmod;
}
else
{
if (expected_typoid != NULL)
*expected_typoid = RECORDOID;
if (expected_typmod != NULL)
*expected_typmod = -1;
}
}
break;
case PLPGSQL_DTYPE_ROW:
{
PLpgSQL_row *row = (PLpgSQL_row *) target;
if (row->rowtupdesc != NULL)
{
if (expected_typoid != NULL)
*expected_typoid = row->rowtupdesc->tdtypeid;
if (expected_typmod != NULL)
*expected_typmod = row->rowtupdesc->tdtypmod;
}
else
{
if (expected_typoid != NULL)
*expected_typoid = RECORDOID;
if (expected_typmod != NULL)
*expected_typmod = -1;
}
check_row_or_rec(cstate, row, 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)));
if (expected_typoid)
*expected_typoid = SPI_gettypeid(rec->tupdesc, fno);
if (expected_typmod)
*expected_typmod = TupleDescAttr(rec->tupdesc, fno - 1)->atttypmod;
}
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
*/
#if PG_VERSION_NUM >= 90600
arraytypeid = plpgsql_exec_get_datum_type(cstate->estate, target);
#else
arraytypeid = exec_get_datum_type(cstate->estate, target);
#endif
arraytypeid = getBaseType(arraytypeid);
arrayelemtypeid = get_element_type(arraytypeid);
if (!OidIsValid(arrayelemtypeid))
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("subscripted object is not an array")));
if (expected_typoid)
*expected_typoid = arrayelemtypeid;
if (expected_typmod)
*expected_typmod = ((PLpgSQL_var *) target)->datatype->atttypmod;
record_variable_usage(cstate, target->dno, true);
}
break;
default:
; /* nope */
}
}
/*
* 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)
{
/*
* 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));
}
}
/*
* We would to check all plans, but when plan exists, then don't
* overwrite existing plan.
*/
if (expr->plan == NULL)
{
expr->plan = SPI_saveplan(plan);
cstate->exprs = lappend(cstate->exprs, expr);
}
SPI_freeplan(plan);
}
/* Don't allow write plan when function is read only */
if (cstate->estate->readonly_func)
prohibit_write_plan(cstate, expr);
prohibit_transaction_stmt(cstate, expr);
}
/*
* Check so target can accept typoid value
*
*/
static void
check_assign_to_target_type(PLpgSQL_checkstate *cstate,
Oid target_typoid, int32 target_typmod,
Oid value_typoid,
bool isnull)
{
#if PG_VERSION_NUM < 90500
/* any used typmod enforces IO cast - performance warning for older than 9.5*/
if (target_typmod != -1)
put_error(cstate,
ERRCODE_DATATYPE_MISMATCH, 0,
"target type has type modificator",
NULL,
"Usage of type modificator enforces slower IO casting.",
PLPGSQL_CHECK_WARNING_PERFORMANCE,
0, NULL, NULL);
#endif
if (type_is_rowtype(value_typoid))
put_error(cstate,
ERRCODE_DATATYPE_MISMATCH, 0,
"cannot cast composite value to a scalar type",
NULL,
NULL,
PLPGSQL_CHECK_ERROR,
0, NULL, NULL);
else if (target_typoid != value_typoid)
{
StringInfoData str;
initStringInfo(&str);
appendStringInfo(&str, "cast \"%s\" value to \"%s\" type",
format_type_be(value_typoid),
format_type_be(target_typoid));
/* accent warning when cast is without supported explicit casting */
if (!can_coerce_type(1, &value_typoid, &target_typoid, COERCION_EXPLICIT))
put_error(cstate,
ERRCODE_DATATYPE_MISMATCH, 0,
"target type is different type than source type",
str.data,
"There are no possible explicit coercion between those types, possibly bug!",
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
else if (!can_coerce_type(1, &value_typoid, &target_typoid, COERCION_ASSIGNMENT))
put_error(cstate,
ERRCODE_DATATYPE_MISMATCH, 0,
"target type is different type than source type",
str.data,
"The input expression type does not have an assignment cast to the target type.",
PLPGSQL_CHECK_WARNING_OTHERS,
0, NULL, NULL);
else
{
/* highly probably only performance issue */
if (!isnull)
put_error(cstate,
ERRCODE_DATATYPE_MISMATCH, 0,
"target type is different type than source type",
str.data,
"Hidden casting can be a performance issue.",
PLPGSQL_CHECK_WARNING_PERFORMANCE,
0, NULL, NULL);
}
pfree(str.data);
}
}
/*
* Assign a tuple descriptor to variable specified by dno
*/
static void
assign_tupdesc_dno(PLpgSQL_checkstate *cstate, int varno, TupleDesc tupdesc, bool isnull)
{
PLpgSQL_datum *target = cstate->estate->datums[varno];
switch (target->dtype)
{
case PLPGSQL_DTYPE_VAR:
{
PLpgSQL_var *var = (PLpgSQL_var *) target;
check_assign_to_target_type(cstate,
var->datatype->typoid, var->datatype->atttypmod,
TupleDescAttr(tupdesc, 0)->atttypid,
isnull);
}
break;
case PLPGSQL_DTYPE_ROW:
assign_tupdesc_row_or_rec(cstate, (PLpgSQL_row *) target, NULL, tupdesc, isnull);
break;
case PLPGSQL_DTYPE_REC:
assign_tupdesc_row_or_rec(cstate, NULL, (PLpgSQL_rec *) target, tupdesc, isnull);
break;
case PLPGSQL_DTYPE_ARRAYELEM:
{
Oid expected_typoid;
int expected_typmod;
check_target(cstate, varno, &expected_typoid, &expected_typmod);
/* When target is composite type, then source is expanded already */
if (type_is_rowtype(expected_typoid))
{
PLpgSQL_rec rec;
rec.tup = NULL;
rec.freetup = false;
rec.freetupdesc = false;
PG_TRY();
{
rec.tupdesc = lookup_rowtype_tupdesc_noerror(expected_typoid, expected_typmod, true);
assign_tupdesc_row_or_rec(cstate, NULL, &rec, tupdesc, isnull);
if (rec.tupdesc)
ReleaseTupleDesc(rec.tupdesc);
}
PG_CATCH();
{
if (rec.tupdesc)
ReleaseTupleDesc(rec.tupdesc);
PG_RE_THROW();
}
PG_END_TRY();
}
else
check_assign_to_target_type(cstate,
expected_typoid, expected_typmod,
TupleDescAttr(tupdesc, 0)->atttypid,
isnull);
}
break;
default:
; /* nope */
}
}
/*
* 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 isnull)
{
bool *nulls;
HeapTuple tup;
if (tupdesc == NULL)
{
put_error(cstate,
0, 0,
"tuple descriptor is empty", NULL, NULL,
PLPGSQL_CHECK_WARNING_OTHERS,
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");
}
else if (row != NULL && tupdesc != NULL)
{
int td_natts = tupdesc->natts;
int fnum;
int anum;
anum = 0;
for (fnum = 0; fnum < row->nfields; fnum++)
{
if (row->varnos[fnum] < 0)
continue; /* skip dropped column in row struct */
while (anum < td_natts && TupleDescAttr(tupdesc, anum)->attisdropped)
anum++; /* skip dropped column in tuple */
if (anum < td_natts)
{
Oid valtype = SPI_gettypeid(tupdesc, anum + 1);
PLpgSQL_datum *target = cstate->estate->datums[row->varnos[fnum]];
switch (target->dtype)
{
case PLPGSQL_DTYPE_VAR:
{
PLpgSQL_var *var = (PLpgSQL_var *) target;
check_assign_to_target_type(cstate,
var->datatype->typoid,
var->datatype->atttypmod,
valtype,
isnull);
}
break;
case PLPGSQL_DTYPE_RECFIELD:
{
Oid expected_typoid;
int expected_typmod;
check_target(cstate, target->dno, &expected_typoid, &expected_typmod);
check_assign_to_target_type(cstate,
expected_typoid,
expected_typmod,
valtype,
isnull);
}
break;
default:
; /* nope */
}
anum++;
}
}
}
}
/*
* Common part of some expression based analyzes.
*
*/
static CachedPlan *
ExprGetPlan(PLpgSQL_expr *query)
{
CachedPlanSource *plansource = NULL;
CachedPlan *cplan;
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)
elog(ERROR, "query returns no result");
}
else
elog(ERROR, "there are no plan for query: \"%s\"",
query->query);
/*
* 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.
*/
#if PG_VERSION_NUM >= 100000
cplan = GetCachedPlan(plansource, NULL, true, NULL);
#else
cplan = GetCachedPlan(plansource, NULL, true);
#endif
return cplan;
}
/*
* Returns Const Value from expression if it is possible.
*
*/
static Const *
ExprGetConst(PLpgSQL_expr *query, bool *IsConst)
{
PlannedStmt *_stmt;
Plan *_plan;
TargetEntry *tle;
CachedPlan *cplan;
Const *result = NULL;
cplan = ExprGetPlan(query);
_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_Const)
result = (Const *) tle->expr;
}
}
*IsConst = result != NULL;
ReleaseCachedPlan(cplan, true);
return result;
}
/*
* Returns true for entered NULL constant
*
*/
static bool
is_const_null_expr(PLpgSQL_expr *query)
{
Const *c;
bool IsConst;
c = ExprGetConst(query, &IsConst);
return IsConst ? c->constisnull : false;
}
/*
* Returns string for any not null constant.
* When constant is NULL, then returns NULL.
*
*/
static char *
ExprGetString(PLpgSQL_expr *query, bool *IsConst)
{
Const *c;
char *result = NULL;
c = ExprGetConst(query, IsConst);
if (*IsConst && !c->constisnull)
{
Oid typoutput;
bool typisvarlena;
getTypeOutputInfo(c->consttype, &typoutput, &typisvarlena);
result = OidOutputFunctionCall(typoutput, c->constvalue);
}
return result;
}
/*
* 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,
Oid *first_level_typoid)
{
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);
if (is_expression && tupdesc->natts != 1)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("query \"%s\" returned %d columns",
query->query,
tupdesc->natts)));
/*
* 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(TupleDescAttr(tupdesc, 0)->atttypid);
if (!OidIsValid(elemtype))
{
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("FOREACH expression must yield an array, not type %s",
format_type_be(TupleDescAttr(tupdesc, 0)->atttypid))));
FreeTupleDesc(tupdesc);
}
if (is_expression && first_level_typoid != NULL)
*first_level_typoid = elemtype;
/* when elemtype is not composity, prepare single field tupdesc */
if (!type_is_rowtype(elemtype))
{
TupleDesc rettupdesc;
rettupdesc = CreateTemplateTupleDesc(1, false);
TupleDescInitEntry(rettupdesc, 1, "__array_element__", elemtype, -1, 0);
FreeTupleDesc(tupdesc);
BlessTupleDesc(rettupdesc);
tupdesc = rettupdesc;
}
else
{
elemtupdesc = lookup_rowtype_tupdesc_noerror(elemtype, -1, true);
if (elemtupdesc != NULL)
{
FreeTupleDesc(tupdesc);
tupdesc = CreateTupleDescCopy(elemtupdesc);
ReleaseTupleDesc(elemtupdesc);
}
}
}
else
{
if (is_expression && first_level_typoid != NULL)
*first_level_typoid = TupleDescAttr(tupdesc, 0)->atttypid;
}
/*
* 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(TupleDescAttr(tupdesc, 0)->atttypid,
TupleDescAttr(tupdesc, 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 &&
TupleDescAttr(tupdesc, 0)->atttypid == RECORDOID &&
TupleDescAttr(tupdesc, 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.
*/
#if PG_VERSION_NUM >= 100000
cplan = GetCachedPlan(plansource, NULL, true, NULL);
#else
cplan = GetCachedPlan(plansource, NULL, true);
#endif
_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);
switch (((Node *) tle->expr)->type)
{
case 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;
}
break;
case T_RowExpr:
{
RowExpr *row = (RowExpr *) tle->expr;
ListCell *lc_colname;
ListCell *lc_arg;
TupleDesc rettupdesc;
int i = 1;
rettupdesc = CreateTemplateTupleDesc(list_length(row->args), false);
forboth (lc_colname, row->colnames, lc_arg, row->args)
{
Node *arg = lfirst(lc_arg);
char *name = strVal(lfirst(lc_colname));
TupleDescInitEntry(rettupdesc, i,
name,
exprType(arg),
exprTypmod(arg),
0);
i++;
}
FreeTupleDesc(tupdesc);
BlessTupleDesc(rettupdesc);
tupdesc = rettupdesc;
}
break;
default:
/* cannot to take tupdesc */
tupdesc = NULL;
}
}
}
ReleaseCachedPlan(cplan, true);
}
return tupdesc;
}
/*
* Raise a error when plan is not read only
*/
static void
prohibit_write_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *query)
{
CachedPlanSource *plansource = NULL;
SPIPlanPtr plan = query->plan;
CachedPlan *cplan;
List *stmt_list;
ListCell *lc;
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 PG_VERSION_NUM >= 100000
cplan = GetCachedPlan(plansource, NULL, true, NULL);
#else
cplan = GetCachedPlan(plansource, NULL, true);
#endif
stmt_list = cplan->stmt_list;
foreach(lc, stmt_list)
{
#if PG_VERSION_NUM >= 100000
PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc);
Assert(IsA(pstmt, PlannedStmt));
#else
Node *pstmt = (Node *) lfirst(lc);
#endif
if (!CommandIsReadOnly(pstmt))
{
StringInfoData message;
initStringInfo(&message);
appendStringInfo(&message,
"%s is not allowed in a non volatile function",
CreateCommandTag((Node *) pstmt));
put_error(cstate,
ERRCODE_FEATURE_NOT_SUPPORTED, 0,
message.data,
NULL,
NULL,
PLPGSQL_CHECK_ERROR,
0, query->query, NULL);
pfree(message.data);
message.data = NULL;
}
}
ReleaseCachedPlan(cplan, true);
}
/*
* Raise a error when plan is a transactional statement
*/
static void
prohibit_transaction_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_expr *query)
{
CachedPlanSource *plansource = NULL;
SPIPlanPtr plan = query->plan;
CachedPlan *cplan;
List *stmt_list;
ListCell *lc;
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 PG_VERSION_NUM >= 100000
cplan = GetCachedPlan(plansource, NULL, true, NULL);
#else
cplan = GetCachedPlan(plansource, NULL, true);
#endif
stmt_list = cplan->stmt_list;
foreach(lc, stmt_list)
{
Node *pstmt = (Node *) lfirst(lc);
if (IsA(pstmt, TransactionStmt))
{
put_error(cstate,
ERRCODE_FEATURE_NOT_SUPPORTED, 0,
"cannot begin/end transactions in PL/pgSQL",
NULL,
"Use a BEGIN block with an EXCEPTION clause instead.",
PLPGSQL_CHECK_ERROR,
0, query->query, NULL);
}
}
ReleaseCachedPlan(cplan, true);
}
/*
* 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;
}
}
/****************************************************************************************
* Output routines
*
****************************************************************************************
*
*/
#define SET_RESULT_NULL(anum) \
do { \
values[(anum)] = (Datum) 0; \
nulls[(anum)] = true; \
} while (0)
#define SET_RESULT(anum, value) \
do { \
values[(anum)] = (value); \
nulls[(anum)] = false; \
} while(0)
#define SET_RESULT_TEXT(anum, str) \
do { \
if (str != NULL) \
{ \
SET_RESULT((anum), CStringGetTextDatum((str))); \
} \
else \
{ \
SET_RESULT_NULL(anum); \
} \
} while (0)
#define SET_RESULT_INT32(anum, ival) SET_RESULT((anum), Int32GetDatum((ival)))
#define SET_RESULT_OID(anum, oid) SET_RESULT((anum), ObjectIdGetDatum((oid)))
/*
* error processing switch - ignore warnings when it is necessary,
* store fields to result tuplestore or raise exception to out
*
*/
static void
put_error(PLpgSQL_checkstate *cstate,
int sqlerrcode,
int lineno,
const char *message,
const char *detail,
const char *hint,
int level,
int position,
const char *query,
const char *context)
{
/* ignore warnings when is not requested */
if ((level == PLPGSQL_CHECK_WARNING_PERFORMANCE && !cstate->performance_warnings) ||
(level == PLPGSQL_CHECK_WARNING_OTHERS && !cstate->other_warnings) ||
(level == PLPGSQL_CHECK_WARNING_EXTRA && !cstate->extra_warnings))
return;
if (cstate->tuple_store != NULL)
{
switch (cstate->format)
{
case PLPGSQL_CHECK_FORMAT_TABULAR:
tuplestore_put_error_tabular(cstate->tuple_store, cstate->tupdesc,
cstate->estate, cstate->fn_oid,
sqlerrcode, lineno, message, detail,
hint, level, position, query, context);
break;
case PLPGSQL_CHECK_FORMAT_TEXT:
tuplestore_put_error_text(cstate->tuple_store, cstate->tupdesc,
cstate->estate, cstate->fn_oid,
sqlerrcode, lineno, message, detail,
hint, level, position, query, context);
break;
case PLPGSQL_CHECK_FORMAT_XML:
format_error_xml(cstate->sinfo, cstate->estate,
sqlerrcode, lineno, message, detail,
hint, level, position, query, context);
break;
case PLPGSQL_CHECK_FORMAT_JSON:
format_error_json(cstate->sinfo, cstate->estate,
sqlerrcode, lineno, message, detail,
hint, level, position, query, context);
break;
}
}
else
{
int elevel;
/*
* when a passive mode is active and fatal_errors is false, then
* raise warning everytime.
*/
if (!cstate->is_active_mode && !cstate->fatal_errors)
elevel = WARNING;
else
elevel = level == PLPGSQL_CHECK_ERROR ? ERROR : WARNING;
/* use error fields as parameters of postgres exception */
ereport(elevel,
(sqlerrcode ? errcode(sqlerrcode) : 0,
errmsg_internal("%s", message),
(detail != NULL) ? errdetail_internal("%s", detail) : 0,
(hint != NULL) ? errhint("%s", hint) : 0,
(query != NULL) ? internalerrquery(query) : 0,
(position != 0) ? internalerrposition(position) : 0,
(context != NULL) ? errcontext("%s", context) : 0));
}
}
static const char *
error_level_str(int level)
{
switch (level)
{
case PLPGSQL_CHECK_ERROR:
return "error";
case PLPGSQL_CHECK_WARNING_OTHERS:
return "warning";
case PLPGSQL_CHECK_WARNING_EXTRA:
return "warning extra";
case PLPGSQL_CHECK_WARNING_PERFORMANCE:
return "performance";
default:
return "???";
}
}
/*
* store error fields to result tuplestore
*
*/
static void
tuplestore_put_error_tabular(Tuplestorestate *tuple_store, TupleDesc tupdesc,
PLpgSQL_execstate *estate,
Oid fn_oid,
int sqlerrcode,
int lineno,
const char *message,
const char *detail,
const char *hint,
int level,
int position,
const char *query,
const char *context)
{
Datum values[Natts_result];
bool nulls[Natts_result];
Assert(message != NULL);
SET_RESULT_OID(Anum_result_functionid, fn_oid);
/* lineno should be valid */
if (estate != NULL && estate->err_stmt != NULL && estate->err_stmt->lineno > 0)
{
/* use lineno based on err_stmt */
SET_RESULT_INT32(Anum_result_lineno, estate->err_stmt->lineno);
SET_RESULT_TEXT(Anum_result_statement, plpgsql_stmt_typename(estate->err_stmt));
}
else if (strncmp(message, UNUSED_VARIABLE_TEXT, UNUSED_VARIABLE_TEXT_CHECK_LENGTH) == 0)
{
SET_RESULT_INT32(Anum_result_lineno, lineno);
SET_RESULT_TEXT(Anum_result_statement, "DECLARE");
}
else
{
SET_RESULT_NULL(Anum_result_lineno);
SET_RESULT_NULL(Anum_result_statement);
}
SET_RESULT_TEXT(Anum_result_sqlstate, unpack_sql_state(sqlerrcode));
SET_RESULT_TEXT(Anum_result_message, message);
SET_RESULT_TEXT(Anum_result_detail, detail);
SET_RESULT_TEXT(Anum_result_hint, hint);
SET_RESULT_TEXT(Anum_result_level, error_level_str(level));
if (position != 0)
SET_RESULT_INT32(Anum_result_position, position);
else
SET_RESULT_NULL(Anum_result_position);
SET_RESULT_TEXT(Anum_result_query, query);
SET_RESULT_TEXT(Anum_result_context, context);
tuplestore_putvalues(tuple_store, tupdesc, values, nulls);
}
/*
* collects errors and warnings in plain text format
*/
static void
tuplestore_put_error_text(Tuplestorestate *tuple_store, TupleDesc tupdesc,
PLpgSQL_execstate *estate,
Oid fn_oid,
int sqlerrcode,
int lineno,
const char *message,
const char *detail,
const char *hint,
int level,
int position,
const char *query,
const char *context)
{
StringInfoData sinfo;
const char *level_str = error_level_str(level);
Assert(message != NULL);
initStringInfo(&sinfo);
/* lineno should be valid for actual statements */
if (estate != NULL && estate->err_stmt != NULL && estate->err_stmt->lineno > 0)
appendStringInfo(&sinfo, "%s:%s:%d:%s:%s",
level_str,
unpack_sql_state(sqlerrcode),
estate->err_stmt->lineno,
plpgsql_stmt_typename(estate->err_stmt),
message);
else if (strncmp(message, UNUSED_VARIABLE_TEXT, UNUSED_VARIABLE_TEXT_CHECK_LENGTH) == 0)
{
appendStringInfo(&sinfo, "%s:%s:%d:%s:%s",
level_str,
unpack_sql_state(sqlerrcode),
lineno,
"DECLARE",
message);
}
else
{
appendStringInfo(&sinfo, "%s:%s:%s",
level_str,
unpack_sql_state(sqlerrcode),
message);
}
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&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);
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&sinfo);
if (line_caret_pos > 0 && position == 0)
{
appendStringInfo(&sinfo, "-- %*s",
line_caret_pos, "^");
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&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);
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&sinfo);
if (line_caret_pos > 0 && position == 0)
{
appendStringInfo(&sinfo, "-- %*s",
line_caret_pos, "^");
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&sinfo);
}
}
pfree(_query);
}
if (detail != NULL)
{
appendStringInfo(&sinfo, "Detail: %s", detail);
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&sinfo);
}
if (hint != NULL)
{
appendStringInfo(&sinfo, "Hint: %s", hint);
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&sinfo);
}
if (context != NULL)
{
appendStringInfo(&sinfo, "Context: %s", context);
tuplestore_put_text_line(tuple_store, tupdesc, sinfo.data, sinfo.len);
resetStringInfo(&sinfo);
}
pfree(sinfo.data);
}
/*
* format_error_xml formats and collects a identifided issues
*/
static void
format_error_xml(StringInfo str,
PLpgSQL_execstate *estate,
int sqlerrcode,
int lineno,
const char *message,
const char *detail,
const char *hint,
int level,
int position,
const char *query,
const char *context)
{
const char *level_str = error_level_str(level);
Assert(message != NULL);
/* flush tag */
appendStringInfoString(str, " \n");
appendStringInfo(str, " %s\n", level_str);
appendStringInfo(str, " %s\n",
unpack_sql_state(sqlerrcode));
appendStringInfo(str, " %s\n",
escape_xml(message));
if (estate != NULL && estate->err_stmt != NULL)
appendStringInfo(str, " %s\n",
estate->err_stmt->lineno,
plpgsql_stmt_typename(estate->err_stmt));
else if (strcmp(message, "unused declared variable") == 0)
appendStringInfo(str, " DECLARE\n",
lineno);
if (hint != NULL)
appendStringInfo(str, " %s\n",
escape_xml(hint));
if (detail != NULL)
appendStringInfo(str, " %s\n",
escape_xml(detail));
if (query != NULL)
appendStringInfo(str, " %s\n",
position, escape_xml(query));
if (context != NULL)
appendStringInfo(str, " %s\n",
escape_xml(context));
/* flush closing tag */
appendStringInfoString(str, " \n");
}
/*
* format_error_json formats and collects a identifided issues
*/
static void
format_error_json(StringInfo str,
PLpgSQL_execstate *estate,
int sqlerrcode,
int lineno,
const char *message,
const char *detail,
const char *hint,
int level,
int position,
const char *query,
const char *context)
{
const char *level_str = error_level_str(level);
StringInfoData sinfo; /*Holds escaped json*/
Assert(message != NULL);
initStringInfo(&sinfo);
/* flush tag */
appendStringInfoString(str, " {\n");
appendStringInfo(str, " \"level\":\"%s\",\n", level_str);
escape_json(&sinfo, message);
appendStringInfo(str, " \"message\":%s,\n", sinfo.data);
if (estate != NULL && estate->err_stmt != NULL)
appendStringInfo(str, " \"statement\":{\n\"lineNumber\":\"%d\",\n\"text\":\"%s\"\n},\n",
estate->err_stmt->lineno,
plpgsql_stmt_typename(estate->err_stmt));
else if (strcmp(message, "unused declared variable") == 0)
appendStringInfo(str, " \"statement\":{\n\"lineNumber\":\"%d\",\n\"text\":\"DECLARE\"\n},",
lineno);
if (hint != NULL) {
resetStringInfo(&sinfo);
escape_json(&sinfo, hint);
appendStringInfo(str, " \"hint\":%s,\n", sinfo.data);
}
if (detail != NULL) {
resetStringInfo(&sinfo);
escape_json(&sinfo, detail);
appendStringInfo(str, " \"detail\":%s,\n", sinfo.data);
}
if (query != NULL) {
resetStringInfo(&sinfo);
escape_json(&sinfo, query);
appendStringInfo(str, " \"query\":{\n\"position\":\"%d\",\n\"text\":%s\n},\n", position, sinfo.data);
}
if (context != NULL) {
resetStringInfo(&sinfo);
escape_json(&sinfo, context);
appendStringInfo(str, " \"context\":%s,\n", sinfo.data);
}
/* placing this property last as to avoid a trailing comma*/
appendStringInfo(str, " \"sqlState\":\"%s\"\n", unpack_sql_state(sqlerrcode));
/* flush closing tag. Needs comman jus in case there is more than one issue. Comma removed in epilog */
appendStringInfoString(str, " },");
}
/*
* store edata
*/
static void
put_error_edata(PLpgSQL_checkstate *cstate,
ErrorData *edata)
{
put_error(cstate,
edata->sqlerrcode,
edata->lineno,
edata->message,
edata->detail,
edata->hint,
PLPGSQL_CHECK_ERROR,
edata->internalpos,
edata->internalquery,
edata->context);
}
/*
* Append text line (StringInfo) to one column tuple store
*
*/
static void
tuplestore_put_text_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
const char *message, int len)
{
Datum value;
bool isnull = false;
HeapTuple tuple;
if (len >= 0)
value = PointerGetDatum(cstring_to_text_with_len(message, len));
else
value = PointerGetDatum(cstring_to_text(message));
tuple = heap_form_tuple(tupdesc, &value, &isnull);
tuplestore_puttuple(tuple_store, tuple);
}
/*
* routines for beginning and finishing function checking
*
* it is used primary for XML & JSON format - create almost left and almost right tag per function
*
*/
static void
check_function_prolog(PLpgSQL_checkstate *cstate)
{
/* XML format requires StringInfo buffer */
if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
{
if (cstate->sinfo != NULL)
resetStringInfo(cstate->sinfo);
else
cstate->sinfo = makeStringInfo();
/* create a initial tag */
appendStringInfo(cstate->sinfo, "\n", cstate->fn_oid);
}
else if (cstate->format == PLPGSQL_CHECK_FORMAT_JSON) {
if (cstate->sinfo != NULL)
resetStringInfo(cstate->sinfo);
else
cstate->sinfo = makeStringInfo();
/* create a initial tag */
appendStringInfo(cstate->sinfo, "{ \"function\":\"%d\",\n\"issues\":[\n", cstate->fn_oid);
}
}
static void
check_function_epilog(PLpgSQL_checkstate *cstate)
{
if (cstate->format == PLPGSQL_CHECK_FORMAT_XML)
{
appendStringInfoString(cstate->sinfo, "");
tuplestore_put_text_line(cstate->tuple_store, cstate->tupdesc,
cstate->sinfo->data, cstate->sinfo->len);
}
else if (cstate->format == PLPGSQL_CHECK_FORMAT_JSON)
{
if (cstate->sinfo->len > 1 && cstate->sinfo->data[cstate->sinfo->len -1] == ',') {
cstate->sinfo->data[cstate->sinfo->len - 1] = '\n';
}
appendStringInfoString(cstate->sinfo, "\n]\n}");
tuplestore_put_text_line(cstate->tuple_store, cstate->tupdesc,
cstate->sinfo->data, cstate->sinfo->len);
}
}
/****************************************************************************************
* A maintaining of hash table of checked functions
*
****************************************************************************************
*
*/
/*
* We cannot to attach to DELETE event - so we don't need implement delete here.
*/
/* exported so we can call it from plpgsql_check_init() */
static void
plpgsql_check_HashTableInit(void)
{
HASHCTL ctl;
/* don't allow double-initialization */
Assert(plpgsql_check_HashTable == NULL);
memset(&ctl, 0, sizeof(ctl));
ctl.keysize = sizeof(PLpgSQL_func_hashkey);
ctl.entrysize = sizeof(plpgsql_check_HashEnt);
ctl.hash = tag_hash;
plpgsql_check_HashTable = hash_create("plpgsql_check function cache",
FUNCS_PER_USER,
&ctl,
HASH_ELEM | HASH_FUNCTION);
}
static bool
is_checked(PLpgSQL_function *func)
{
plpgsql_check_HashEnt *hentry;
hentry = (plpgsql_check_HashEnt *) hash_search(plpgsql_check_HashTable,
(void *) func->fn_hashkey,
HASH_FIND,
NULL);
if (hentry != NULL && hentry->fn_xmin == func->fn_xmin &&
ItemPointerEquals(&hentry->fn_tid, &func->fn_tid))
return hentry->is_checked;
return false;
}
static void
mark_as_checked(PLpgSQL_function *func)
{
plpgsql_check_HashEnt *hentry;
bool found;
/* don't try to mark anonymous code blocks */
if (func->fn_oid != InvalidOid)
{
hentry = (plpgsql_check_HashEnt *) hash_search(plpgsql_check_HashTable,
(void *) func->fn_hashkey,
HASH_ENTER,
&found);
hentry->fn_xmin = func->fn_xmin;
hentry->fn_tid = func->fn_tid;
hentry->is_checked = true;
}
}