Skip to content

Commit 167075b

Browse files
committed
Add strict_multi_assignment and too_many_rows plpgsql checks
Until now shadowed_variables was the only plpgsql check supported by plpgsql.extra_warnings and plpgsql.extra_errors. This patch introduces two new checks - strict_multi_assignment and too_many_rows. Unlike shadowed_variables, these new checks are enforced at run-time. strict_multi_assignment checks that commands allowing multi-assignment (for example SELECT INTO) have the same number of sources and targets. too_many_rows checks that queries with an INTO clause return one row exactly. These checks are aimed at cases that are technically valid and allowed, but are often a sign of a bug. Therefore those checks are expected to be enabled primarily in development and testing environments. Author: Pavel Stehule Reviewed-by: Stephen Frost, Tomas Vondra Discussion: https://www.postgresql.org/message-id/flat/CAFj8pRA2kKRDKpUNwLY0GeG1OqOp+tLS2yQA1V41gzuSz-hCng@mail.gmail.com
1 parent 2d30675 commit 167075b

File tree

6 files changed

+400
-33
lines changed

6 files changed

+400
-33
lines changed

doc/src/sgml/plpgsql.sgml

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5034,7 +5034,7 @@ a_output := a_output || $$ if v_$$ || referrer_keys.kind || $$ like '$$
50345034

50355035
</sect2>
50365036
<sect2 id="plpgsql-extra-checks">
5037-
<title>Additional Compile-time Checks</title>
5037+
<title>Additional Compile-time and Run-time Checks</title>
50385038

50395039
<para>
50405040
To aid the user in finding instances of simple but common problems before
@@ -5046,26 +5046,64 @@ a_output := a_output || $$ if v_$$ || referrer_keys.kind || $$ like '$$
50465046
so you are advised to test in a separate development environment.
50475047
</para>
50485048

5049-
<para>
5050-
These additional checks are enabled through the configuration variables
5051-
<varname>plpgsql.extra_warnings</varname> for warnings and
5052-
<varname>plpgsql.extra_errors</varname> for errors. Both can be set either to
5053-
a comma-separated list of checks, <literal>"none"</literal> or <literal>"all"</literal>.
5054-
The default is <literal>"none"</literal>. Currently the list of available checks
5055-
includes only one:
5056-
<variablelist>
5057-
<varlistentry>
5058-
<term><varname>shadowed_variables</varname></term>
5059-
<listitem>
5060-
<para>
5061-
Checks if a declaration shadows a previously defined variable.
5062-
</para>
5063-
</listitem>
5064-
</varlistentry>
5065-
</variablelist>
5049+
<para>
5050+
Setting <varname>plpgsql.extra_warnings</varname>, or
5051+
<varname>plpgsql.extra_errors</varname>, as appropriate, to <literal>"all"</literal>
5052+
is encouraged in development and/or testing environments.
5053+
</para>
5054+
5055+
<para>
5056+
These additional checks are enabled through the configuration variables
5057+
<varname>plpgsql.extra_warnings</varname> for warnings and
5058+
<varname>plpgsql.extra_errors</varname> for errors. Both can be set either to
5059+
a comma-separated list of checks, <literal>"none"</literal> or
5060+
<literal>"all"</literal>. The default is <literal>"none"</literal>. Currently
5061+
the list of available checks includes:
5062+
<variablelist>
5063+
<varlistentry>
5064+
<term><varname>shadowed_variables</varname></term>
5065+
<listitem>
5066+
<para>
5067+
Checks if a declaration shadows a previously defined variable.
5068+
</para>
5069+
</listitem>
5070+
</varlistentry>
50665071

5067-
The following example shows the effect of <varname>plpgsql.extra_warnings</varname>
5068-
set to <varname>shadowed_variables</varname>:
5072+
<varlistentry>
5073+
<term><varname>strict_multi_assignment</varname></term>
5074+
<listitem>
5075+
<para>
5076+
Some <application>PL/PgSQL</application> commands allow assigning
5077+
values to more than one variable at a time, such as
5078+
<command>SELECT INTO</command>. Typically, the number of target
5079+
variables and the number of source variables should match, though
5080+
<application>PL/PgSQL</application> will use <literal>NULL</literal>
5081+
for missing values and extra variables are ignored. Enabling this
5082+
check will cause <application>PL/PgSQL</application> to throw a
5083+
<literal>WARNING</literal> or <literal>ERROR</literal> whenever the
5084+
number of target variables and the number of source variables are
5085+
different.
5086+
</para>
5087+
</listitem>
5088+
</varlistentry>
5089+
5090+
<varlistentry>
5091+
<term><varname>too_many_rows</varname></term>
5092+
<listitem>
5093+
<para>
5094+
Enabling this check will cause <application>PL/PgSQL</application> to
5095+
check if a given query returns more than one row when an
5096+
<literal>INTO</literal> clause is used. As an <literal>INTO</literal>
5097+
statement will only ever use one row, having a query return multiple
5098+
rows is generally either inefficient and/or nondeterministic and
5099+
therefore is likely an error.
5100+
</para>
5101+
</listitem>
5102+
</varlistentry>
5103+
</variablelist>
5104+
5105+
The following example shows the effect of <varname>plpgsql.extra_warnings</varname>
5106+
set to <varname>shadowed_variables</varname>:
50695107
<programlisting>
50705108
SET plpgsql.extra_warnings TO 'shadowed_variables';
50715109

@@ -5081,8 +5119,41 @@ LINE 3: f1 int;
50815119
^
50825120
CREATE FUNCTION
50835121
</programlisting>
5084-
</para>
5085-
</sect2>
5122+
The below example shows the effects of setting
5123+
<varname>plpgsql.extra_warnings</varname> to
5124+
<varname>strict_multi_assignment</varname>:
5125+
<programlisting>
5126+
SET plpgsql.extra_warnings TO 'strict_multi_assignment';
5127+
5128+
CREATE OR REPLACE FUNCTION public.foo()
5129+
RETURNS void
5130+
LANGUAGE plpgsql
5131+
AS $$
5132+
DECLARE
5133+
x int;
5134+
y int;
5135+
BEGIN
5136+
SELECT 1 INTO x, y;
5137+
SELECT 1, 2 INTO x, y;
5138+
SELECT 1, 2, 3 INTO x, y;
5139+
END;
5140+
$$;
5141+
5142+
SELECT foo();
5143+
WARNING: number of source and target fields in assignment do not match
5144+
DETAIL: strict_multi_assignment check of extra_warnings is active.
5145+
HINT: Make sure the query returns the exact list of columns.
5146+
WARNING: number of source and target fields in assignment do not match
5147+
DETAIL: strict_multi_assignment check of extra_warnings is active.
5148+
HINT: Make sure the query returns the exact list of columns.
5149+
5150+
foo
5151+
-----
5152+
5153+
(1 row)
5154+
</programlisting>
5155+
</para>
5156+
</sect2>
50865157
</sect1>
50875158

50885159
<!-- **** Porting from Oracle PL/SQL **** -->

src/pl/plpgsql/src/pl_exec.c

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4020,6 +4020,12 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
40204020
long tcount;
40214021
int rc;
40224022
PLpgSQL_expr *expr = stmt->sqlstmt;
4023+
int too_many_rows_level = 0;
4024+
4025+
if (plpgsql_extra_errors & PLPGSQL_XCHECK_TOOMANYROWS)
4026+
too_many_rows_level = ERROR;
4027+
else if (plpgsql_extra_warnings & PLPGSQL_XCHECK_TOOMANYROWS)
4028+
too_many_rows_level = WARNING;
40234029

40244030
/*
40254031
* On the first call for this statement generate the plan, and detect
@@ -4059,9 +4065,10 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
40594065

40604066
/*
40614067
* If we have INTO, then we only need one row back ... but if we have INTO
4062-
* STRICT, ask for two rows, so that we can verify the statement returns
4063-
* only one. INSERT/UPDATE/DELETE are always treated strictly. Without
4064-
* INTO, just run the statement to completion (tcount = 0).
4068+
* STRICT or extra check too_many_rows, ask for two rows, so that we can
4069+
* verify the statement returns only one. INSERT/UPDATE/DELETE are always
4070+
* treated strictly. Without INTO, just run the statement to completion
4071+
* (tcount = 0).
40654072
*
40664073
* We could just ask for two rows always when using INTO, but there are
40674074
* some cases where demanding the extra row costs significant time, eg by
@@ -4070,7 +4077,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
40704077
*/
40714078
if (stmt->into)
40724079
{
4073-
if (stmt->strict || stmt->mod_stmt)
4080+
if (stmt->strict || stmt->mod_stmt || too_many_rows_level)
40744081
tcount = 2;
40754082
else
40764083
tcount = 1;
@@ -4187,19 +4194,23 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
41874194
}
41884195
else
41894196
{
4190-
if (n > 1 && (stmt->strict || stmt->mod_stmt))
4197+
if (n > 1 && (stmt->strict || stmt->mod_stmt || too_many_rows_level))
41914198
{
41924199
char *errdetail;
4200+
int errlevel;
41934201

41944202
if (estate->func->print_strict_params)
41954203
errdetail = format_expr_params(estate, expr);
41964204
else
41974205
errdetail = NULL;
41984206

4199-
ereport(ERROR,
4207+
errlevel = (stmt->strict || stmt->mod_stmt) ? ERROR : too_many_rows_level;
4208+
4209+
ereport(errlevel,
42004210
(errcode(ERRCODE_TOO_MANY_ROWS),
42014211
errmsg("query returned more than one row"),
4202-
errdetail ? errdetail_internal("parameters: %s", errdetail) : 0));
4212+
errdetail ? errdetail_internal("parameters: %s", errdetail) : 0,
4213+
errhint("Make sure the query returns a single row, or use LIMIT 1")));
42034214
}
42044215
/* Put the first result row into the target */
42054216
exec_move_row(estate, target, tuptab->vals[0], tuptab->tupdesc);
@@ -6835,6 +6846,19 @@ exec_move_row_from_fields(PLpgSQL_execstate *estate,
68356846
int td_natts = tupdesc ? tupdesc->natts : 0;
68366847
int fnum;
68376848
int anum;
6849+
int strict_multiassignment_level = 0;
6850+
6851+
/*
6852+
* The extra check strict strict_multi_assignment can be active,
6853+
* only when input tupdesc is specified.
6854+
*/
6855+
if (tupdesc != NULL)
6856+
{
6857+
if (plpgsql_extra_errors & PLPGSQL_XCHECK_STRICTMULTIASSIGNMENT)
6858+
strict_multiassignment_level = ERROR;
6859+
else if (plpgsql_extra_warnings & PLPGSQL_XCHECK_STRICTMULTIASSIGNMENT)
6860+
strict_multiassignment_level = WARNING;
6861+
}
68386862

68396863
/* Handle RECORD-target case */
68406864
if (target->dtype == PLPGSQL_DTYPE_REC)
@@ -6913,10 +6937,23 @@ exec_move_row_from_fields(PLpgSQL_execstate *estate,
69136937
}
69146938
else
69156939
{
6940+
/* no source for destination column */
69166941
value = (Datum) 0;
69176942
isnull = true;
69186943
valtype = UNKNOWNOID;
69196944
valtypmod = -1;
6945+
6946+
/* When source value is missing */
6947+
if (strict_multiassignment_level)
6948+
ereport(strict_multiassignment_level,
6949+
(errcode(ERRCODE_DATATYPE_MISMATCH),
6950+
errmsg("number of source and target fields in assignment do not match"),
6951+
/* translator: %s represents a name of an extra check */
6952+
errdetail("%s check of %s is active.",
6953+
"strict_multi_assignment",
6954+
strict_multiassignment_level == ERROR ? "extra_errors" :
6955+
"extra_warnings"),
6956+
errhint("Make sure the query returns the exact list of columns.")));
69206957
}
69216958

69226959
/* Cast the new value to the right type, if needed. */
@@ -6930,6 +6967,29 @@ exec_move_row_from_fields(PLpgSQL_execstate *estate,
69306967
newnulls[fnum] = isnull;
69316968
}
69326969

6970+
/*
6971+
* When strict_multiassignment extra check is active, then ensure
6972+
* there are no unassigned source attributes.
6973+
*/
6974+
if (strict_multiassignment_level && anum < td_natts)
6975+
{
6976+
/* skip dropped columns in the source descriptor */
6977+
while (anum < td_natts &&
6978+
TupleDescAttr(tupdesc, anum)->attisdropped)
6979+
anum++;
6980+
6981+
if (anum < td_natts)
6982+
ereport(strict_multiassignment_level,
6983+
(errcode(ERRCODE_DATATYPE_MISMATCH),
6984+
errmsg("number of source and target fields in assignment do not match"),
6985+
/* translator: %s represents a name of an extra check */
6986+
errdetail("%s check of %s is active.",
6987+
"strict_multi_assignment",
6988+
strict_multiassignment_level == ERROR ? "extra_errors" :
6989+
"extra_warnings"),
6990+
errhint("Make sure the query returns the exact list of columns.")));
6991+
}
6992+
69336993
values = newvalues;
69346994
nulls = newnulls;
69356995
}
@@ -6986,16 +7046,50 @@ exec_move_row_from_fields(PLpgSQL_execstate *estate,
69867046
}
69877047
else
69887048
{
7049+
/* no source for destination column */
69897050
value = (Datum) 0;
69907051
isnull = true;
69917052
valtype = UNKNOWNOID;
69927053
valtypmod = -1;
7054+
7055+
if (strict_multiassignment_level)
7056+
ereport(strict_multiassignment_level,
7057+
(errcode(ERRCODE_DATATYPE_MISMATCH),
7058+
errmsg("number of source and target fields in assignment do not match"),
7059+
/* translator: %s represents a name of an extra check */
7060+
errdetail("%s check of %s is active.",
7061+
"strict_multi_assignment",
7062+
strict_multiassignment_level == ERROR ? "extra_errors" :
7063+
"extra_warnings"),
7064+
errhint("Make sure the query returns the exact list of columns.")));
69937065
}
69947066

69957067
exec_assign_value(estate, (PLpgSQL_datum *) var,
69967068
value, isnull, valtype, valtypmod);
69977069
}
69987070

7071+
/*
7072+
* When strict_multiassignment extra check is active, ensure there
7073+
* are no unassigned source attributes.
7074+
*/
7075+
if (strict_multiassignment_level && anum < td_natts)
7076+
{
7077+
while (anum < td_natts &&
7078+
TupleDescAttr(tupdesc, anum)->attisdropped)
7079+
anum++; /* skip dropped column in tuple */
7080+
7081+
if (anum < td_natts)
7082+
ereport(strict_multiassignment_level,
7083+
(errcode(ERRCODE_DATATYPE_MISMATCH),
7084+
errmsg("number of source and target fields in assignment do not match"),
7085+
/* translator: %s represents a name of an extra check */
7086+
errdetail("%s check of %s is active.",
7087+
"strict_multi_assignment",
7088+
strict_multiassignment_level == ERROR ? "extra_errors" :
7089+
"extra_warnings"),
7090+
errhint("Make sure the query returns the exact list of columns.")));
7091+
}
7092+
69997093
return;
70007094
}
70017095

src/pl/plpgsql/src/pl_handler.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ plpgsql_extra_checks_check_hook(char **newvalue, void **extra, GucSource source)
9292

9393
if (pg_strcasecmp(tok, "shadowed_variables") == 0)
9494
extrachecks |= PLPGSQL_XCHECK_SHADOWVAR;
95+
else if (pg_strcasecmp(tok, "too_many_rows") == 0)
96+
extrachecks |= PLPGSQL_XCHECK_TOOMANYROWS;
97+
else if (pg_strcasecmp(tok, "strict_multi_assignment") == 0)
98+
extrachecks |= PLPGSQL_XCHECK_STRICTMULTIASSIGNMENT;
9599
else if (pg_strcasecmp(tok, "all") == 0 || pg_strcasecmp(tok, "none") == 0)
96100
{
97101
GUC_check_errdetail("Key word \"%s\" cannot be combined with other key words.", tok);

src/pl/plpgsql/src/plpgsql.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,10 +1135,12 @@ extern bool plpgsql_print_strict_params;
11351135

11361136
extern bool plpgsql_check_asserts;
11371137

1138-
/* extra compile-time checks */
1139-
#define PLPGSQL_XCHECK_NONE 0
1140-
#define PLPGSQL_XCHECK_SHADOWVAR 1
1141-
#define PLPGSQL_XCHECK_ALL ((int) ~0)
1138+
/* extra compile-time and run-time checks */
1139+
#define PLPGSQL_XCHECK_NONE 0
1140+
#define PLPGSQL_XCHECK_SHADOWVAR (1 << 1)
1141+
#define PLPGSQL_XCHECK_TOOMANYROWS (1 << 2)
1142+
#define PLPGSQL_XCHECK_STRICTMULTIASSIGNMENT (1 << 3)
1143+
#define PLPGSQL_XCHECK_ALL ((int) ~0)
11421144

11431145
extern int plpgsql_extra_warnings;
11441146
extern int plpgsql_extra_errors;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy