Allow SQL-language functions to reference parameters by name.

Matthew Draper, reviewed by Hitoshi Harada
This commit is contained in:
Tom Lane 2012-02-04 19:23:49 -05:00
parent 342b83fdca
commit 9bff0780cf
6 changed files with 411 additions and 67 deletions

View File

@ -154,14 +154,50 @@ SELECT clean_emp();
the function (see <xref linkend="sql-syntax-strings">).
</para>
<para>
Arguments to the SQL function are referenced in the function
body using the syntax <literal>$<replaceable>n</></>: <literal>$1</>
refers to the first argument, <literal>$2</> to the second, and so on.
If an argument is of a composite type, then the dot notation,
e.g., <literal>$1.name</literal>, can be used to access attributes
of the argument. The arguments can only be used as data values,
not as identifiers. Thus for example this is reasonable:
<sect2 id="xfunc-sql-function-arguments">
<title>Arguments for <acronym>SQL</acronym> Functions</title>
<indexterm>
<primary>function</primary>
<secondary>named argument</secondary>
</indexterm>
<para>
Arguments of a SQL function can be referenced in the function
body using either names or numbers. Examples of both methods appear
below.
</para>
<para>
To use a name, declare the function argument as having a name, and
then just write that name in the function body. If the argument name
is the same as any column name in the current SQL command within the
function, the column name will take precedence. To override this,
qualify the argument name with the name of the function itself, that is
<literal><replaceable>function_name</>.<replaceable>argument_name</></literal>.
(If this would conflict with a qualified column name, again the column
name wins. You can avoid the ambiguity by choosing a different alias for
the table within the SQL command.)
</para>
<para>
In the older numeric approach, arguments are referenced using the syntax
<literal>$<replaceable>n</></>: <literal>$1</> refers to the first input
argument, <literal>$2</> to the second, and so on. This will work
whether or not the particular argument was declared with a name.
</para>
<para>
If an argument is of a composite type, then the dot notation,
e.g., <literal>argname.fieldname</literal> or
<literal>$1.fieldname</literal>, can be used to access attributes of the
argument. Again, you might need to qualify the argument's name with the
function name to make the form with an argument name unambiguous.
</para>
<para>
SQL function arguments can only be used as data values,
not as identifiers. Thus for example this is reasonable:
<programlisting>
INSERT INTO mytable VALUES ($1);
</programlisting>
@ -169,7 +205,16 @@ but this will not work:
<programlisting>
INSERT INTO $1 VALUES (42);
</programlisting>
</para>
</para>
<note>
<para>
The ability to use names to reference SQL function arguments was added
in <productname>PostgreSQL</productname> 9.2. Functions to be used in
older servers must use the <literal>$<replaceable>n</></> notation.
</para>
</note>
</sect2>
<sect2 id="xfunc-sql-base-functions">
<title><acronym>SQL</acronym> Functions on Base Types</title>
@ -205,9 +250,24 @@ SELECT one();
<para>
It is almost as easy to define <acronym>SQL</acronym> functions
that take base types as arguments. In the example below, notice
how we refer to the arguments within the function as <literal>$1</>
and <literal>$2</>.
that take base types as arguments:
<screen>
CREATE FUNCTION add_em(x integer, y integer) RETURNS integer AS $$
SELECT x + y;
$$ LANGUAGE SQL;
SELECT add_em(1, 2) AS answer;
answer
--------
3
</screen>
</para>
<para>
Alternatively, we could dispense with names for the arguments and
use numbers:
<screen>
CREATE FUNCTION add_em(integer, integer) RETURNS integer AS $$
@ -227,10 +287,10 @@ SELECT add_em(1, 2) AS answer;
bank account:
<programlisting>
CREATE FUNCTION tf1 (integer, numeric) RETURNS integer AS $$
CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS integer AS $$
UPDATE bank
SET balance = balance - $2
WHERE accountno = $1;
SET balance = balance - debit
WHERE accountno = tf1.accountno;
SELECT 1;
$$ LANGUAGE SQL;
</programlisting>
@ -243,17 +303,26 @@ SELECT tf1(17, 100.0);
</programlisting>
</para>
<para>
In this example, we chose the name <literal>accountno</> for the first
argument, but this is the same as the name of a column in the
<literal>bank</> table. Within the <command>UPDATE</> command,
<literal>accountno</> refers to the column <literal>bank.accountno</>,
so <literal>tf1.accountno</> must be used to refer to the argument.
We could of course avoid this by using a different name for the argument.
</para>
<para>
In practice one would probably like a more useful result from the
function than a constant 1, so a more likely definition
is:
<programlisting>
CREATE FUNCTION tf1 (integer, numeric) RETURNS numeric AS $$
CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS integer AS $$
UPDATE bank
SET balance = balance - $2
WHERE accountno = $1;
SELECT balance FROM bank WHERE accountno = $1;
SET balance = balance - debit
WHERE accountno = tf1.accountno;
SELECT balance FROM bank WHERE accountno = tf1.accountno;
$$ LANGUAGE SQL;
</programlisting>
@ -261,10 +330,10 @@ $$ LANGUAGE SQL;
The same thing could be done in one command using <literal>RETURNING</>:
<programlisting>
CREATE FUNCTION tf1 (integer, numeric) RETURNS numeric AS $$
CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS integer AS $$
UPDATE bank
SET balance = balance - $2
WHERE accountno = $1
SET balance = balance - debit
WHERE accountno = tf1.accountno
RETURNING balance;
$$ LANGUAGE SQL;
</programlisting>
@ -275,11 +344,9 @@ $$ LANGUAGE SQL;
<title><acronym>SQL</acronym> Functions on Composite Types</title>
<para>
When writing functions with arguments of composite
types, we must not only specify which
argument we want (as we did above with <literal>$1</> and <literal>$2</literal>) but
also the desired attribute (field) of that argument. For example,
suppose that
When writing functions with arguments of composite types, we must not
only specify which argument we want but also the desired attribute
(field) of that argument. For example, suppose that
<type>emp</type> is a table containing employee data, and therefore
also the name of the composite type of each row of the table. Here
is a function <function>double_salary</function> that computes what someone's
@ -524,39 +591,6 @@ SELECT getname(new_emp());
</para>
</sect2>
<sect2 id="xfunc-named-parameters">
<title><acronym>SQL</> Functions with Parameter Names</title>
<indexterm>
<primary>function</primary>
<secondary>named parameter</secondary>
</indexterm>
<para>
It is possible to attach names to a function's parameters, for example
<programlisting>
CREATE FUNCTION tf1 (acct_no integer, debit numeric) RETURNS numeric AS $$
UPDATE bank
SET balance = balance - $2
WHERE accountno = $1
RETURNING balance;
$$ LANGUAGE SQL;
</programlisting>
Here the first parameter has been given the name <literal>acct_no</>,
and the second parameter the name <literal>debit</>.
So far as the SQL function itself is concerned, these names are just
decoration; you must still refer to the parameters as <literal>$1</>,
<literal>$2</>, etc within the function body. (Some procedural
languages let you use the parameter names instead.) However,
attaching names to the parameters is useful for documentation purposes.
When a function has many parameters, it is also useful to use the names
while calling the function, as described in
<xref linkend="sql-syntax-calling-funcs">.
</para>
</sect2>
<sect2 id="xfunc-output-parameters">
<title><acronym>SQL</> Functions with Output Parameters</title>
@ -571,7 +605,7 @@ $$ LANGUAGE SQL;
<screen>
CREATE FUNCTION add_em (IN x int, IN y int, OUT sum int)
AS 'SELECT $1 + $2'
AS 'SELECT x + y'
LANGUAGE SQL;
SELECT add_em(3,7);
@ -588,7 +622,7 @@ SELECT add_em(3,7);
<screen>
CREATE FUNCTION sum_n_product (x int, y int, OUT sum int, OUT product int)
AS 'SELECT $1 + $2, $1 * $2'
AS 'SELECT x + y, x * y'
LANGUAGE SQL;
SELECT * FROM sum_n_product(11,42);

View File

@ -23,6 +23,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_coerce.h"
#include "parser/parse_func.h"
#include "tcop/utility.h"
#include "utils/builtins.h"
#include "utils/datum.h"
@ -115,14 +116,23 @@ typedef SQLFunctionCache *SQLFunctionCachePtr;
*/
typedef struct SQLFunctionParseInfo
{
Oid *argtypes; /* resolved types of input arguments */
char *fname; /* function's name */
int nargs; /* number of input arguments */
Oid *argtypes; /* resolved types of input arguments */
char **argnames; /* names of input arguments; NULL if none */
/* Note that argnames[i] can be NULL, if some args are unnamed */
Oid collation; /* function's input collation, if known */
} SQLFunctionParseInfo;
/* non-export function prototypes */
static Node *sql_fn_param_ref(ParseState *pstate, ParamRef *pref);
static Node *sql_fn_post_column_ref(ParseState *pstate,
ColumnRef *cref, Node *var);
static Node *sql_fn_make_param(SQLFunctionParseInfoPtr pinfo,
int paramno, int location);
static Node *sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo,
const char *paramname, int location);
static List *init_execution_state(List *queryTree_list,
SQLFunctionCachePtr fcache,
bool lazyEvalOK);
@ -163,6 +173,9 @@ prepare_sql_fn_parse_info(HeapTuple procedureTuple,
pinfo = (SQLFunctionParseInfoPtr) palloc0(sizeof(SQLFunctionParseInfo));
/* Function's name (only) can be used to qualify argument names */
pinfo->fname = pstrdup(NameStr(procedureStruct->proname));
/* Save the function's input collation */
pinfo->collation = inputCollation;
@ -200,6 +213,38 @@ prepare_sql_fn_parse_info(HeapTuple procedureTuple,
pinfo->argtypes = argOidVect;
}
/*
* Collect names of arguments, too, if any
*/
if (nargs > 0)
{
Datum proargnames;
Datum proargmodes;
int n_arg_names;
bool isNull;
proargnames = SysCacheGetAttr(PROCNAMEARGSNSP, procedureTuple,
Anum_pg_proc_proargnames,
&isNull);
if (isNull)
proargnames = PointerGetDatum(NULL); /* just to be sure */
proargmodes = SysCacheGetAttr(PROCNAMEARGSNSP, procedureTuple,
Anum_pg_proc_proargmodes,
&isNull);
if (isNull)
proargmodes = PointerGetDatum(NULL); /* just to be sure */
n_arg_names = get_func_input_arg_names(proargnames, proargmodes,
&pinfo->argnames);
/* Paranoia: ignore the result if too few array entries */
if (n_arg_names < nargs)
pinfo->argnames = NULL;
}
else
pinfo->argnames = NULL;
return pinfo;
}
@ -209,14 +254,121 @@ prepare_sql_fn_parse_info(HeapTuple procedureTuple,
void
sql_fn_parser_setup(struct ParseState *pstate, SQLFunctionParseInfoPtr pinfo)
{
/* Later we might use these hooks to support parameter names */
pstate->p_pre_columnref_hook = NULL;
pstate->p_post_columnref_hook = NULL;
pstate->p_post_columnref_hook = sql_fn_post_column_ref;
pstate->p_paramref_hook = sql_fn_param_ref;
/* no need to use p_coerce_param_hook */
pstate->p_ref_hook_state = (void *) pinfo;
}
/*
* sql_fn_post_column_ref parser callback for ColumnRefs
*/
static Node *
sql_fn_post_column_ref(ParseState *pstate, ColumnRef *cref, Node *var)
{
SQLFunctionParseInfoPtr pinfo = (SQLFunctionParseInfoPtr) pstate->p_ref_hook_state;
int nnames;
Node *field1;
Node *subfield = NULL;
const char *name1;
const char *name2 = NULL;
Node *param;
/*
* Never override a table-column reference. This corresponds to
* considering the parameter names to appear in a scope outside the
* individual SQL commands, which is what we want.
*/
if (var != NULL)
return NULL;
/*----------
* The allowed syntaxes are:
*
* A A = parameter name
* A.B A = function name, B = parameter name
* OR: A = record-typed parameter name, B = field name
* (the first possibility takes precedence)
* A.B.C A = function name, B = record-typed parameter name,
* C = field name
*----------
*/
nnames = list_length(cref->fields);
if (nnames > 3)
return NULL;
field1 = (Node *) linitial(cref->fields);
Assert(IsA(field1, String));
name1 = strVal(field1);
if (nnames > 1)
{
subfield = (Node *) lsecond(cref->fields);
Assert(IsA(subfield, String));
name2 = strVal(subfield);
}
if (nnames == 3)
{
/*
* Three-part name: if the first part doesn't match the function name,
* we can fail immediately. Otherwise, look up the second part, and
* take the third part to be a field reference.
*/
if (strcmp(name1, pinfo->fname) != 0)
return NULL;
param = sql_fn_resolve_param_name(pinfo, name2, cref->location);
subfield = (Node *) lthird(cref->fields);
Assert(IsA(subfield, String));
}
else if (nnames == 2 && strcmp(name1, pinfo->fname) == 0)
{
/*
* Two-part name with first part matching function name: first see if
* second part matches any parameter name.
*/
param = sql_fn_resolve_param_name(pinfo, name2, cref->location);
if (param)
{
/* Yes, so this is a parameter reference, no subfield */
subfield = NULL;
}
else
{
/* No, so try to match as parameter name and subfield */
param = sql_fn_resolve_param_name(pinfo, name1, cref->location);
}
}
else
{
/* Single name, or parameter name followed by subfield */
param = sql_fn_resolve_param_name(pinfo, name1, cref->location);
}
if (!param)
return NULL; /* No match */
if (subfield)
{
/*
* Must be a reference to a field of a composite parameter; otherwise
* ParseFuncOrColumn will return NULL, and we'll fail back at the
* caller.
*/
param = ParseFuncOrColumn(pstate,
list_make1(subfield),
list_make1(param),
NIL, false, false, false,
NULL, true, cref->location);
}
return param;
}
/*
* sql_fn_param_ref parser callback for ParamRefs ($n symbols)
*/
@ -225,19 +377,30 @@ sql_fn_param_ref(ParseState *pstate, ParamRef *pref)
{
SQLFunctionParseInfoPtr pinfo = (SQLFunctionParseInfoPtr) pstate->p_ref_hook_state;
int paramno = pref->number;
Param *param;
/* Check parameter number is valid */
if (paramno <= 0 || paramno > pinfo->nargs)
return NULL; /* unknown parameter number */
return sql_fn_make_param(pinfo, paramno, pref->location);
}
/*
* sql_fn_make_param construct a Param node for the given paramno
*/
static Node *
sql_fn_make_param(SQLFunctionParseInfoPtr pinfo,
int paramno, int location)
{
Param *param;
param = makeNode(Param);
param->paramkind = PARAM_EXTERN;
param->paramid = paramno;
param->paramtype = pinfo->argtypes[paramno - 1];
param->paramtypmod = -1;
param->paramcollid = get_typcollation(param->paramtype);
param->location = pref->location;
param->location = location;
/*
* If we have a function input collation, allow it to override the
@ -250,6 +413,29 @@ sql_fn_param_ref(ParseState *pstate, ParamRef *pref)
return (Node *) param;
}
/*
* Search for a function parameter of the given name; if there is one,
* construct and return a Param node for it. If not, return NULL.
* Helper function for sql_fn_post_column_ref.
*/
static Node *
sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo,
const char *paramname, int location)
{
int i;
if (pinfo->argnames == NULL)
return NULL;
for (i = 0; i < pinfo->nargs; i++)
{
if (pinfo->argnames[i] && strcmp(pinfo->argnames[i], paramname) == 0)
return sql_fn_make_param(pinfo, i + 1, location);
}
return NULL;
}
/*
* Set up the per-query execution_state records for a SQL function.
*

View File

@ -13,6 +13,12 @@ CREATE FUNCTION hobby_construct(text, text)
LANGUAGE SQL;
CREATE FUNCTION hobby_construct_named(name text, hobby text)
RETURNS hobbies_r
AS 'select name, hobby'
LANGUAGE SQL;
CREATE FUNCTION hobbies_by_name(hobbies_r.name%TYPE)
RETURNS hobbies_r.person%TYPE
AS 'select person from hobbies_r where name = $1'
@ -25,6 +31,37 @@ CREATE FUNCTION equipment(hobbies_r)
LANGUAGE SQL;
CREATE FUNCTION equipment_named(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where equipment_r.hobby = equipment_named.hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_1a(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where hobby = equipment_named_ambiguous_1a.hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_1b(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where equipment_r.hobby = hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_1c(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where hobby = hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_2a(hobby text)
RETURNS setof equipment_r
AS 'select * from equipment_r where hobby = equipment_named_ambiguous_2a.hobby'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_2b(hobby text)
RETURNS setof equipment_r
AS 'select * from equipment_r where equipment_r.hobby = hobby'
LANGUAGE SQL;
CREATE FUNCTION user_relns()
RETURNS setof name
AS 'select relname

View File

@ -223,6 +223,20 @@ SELECT user_relns() AS user_relns
SELECT name(equipment(hobby_construct(text 'skywalking', text 'mer')));
SELECT name(equipment(hobby_construct_named(text 'skywalking', text 'mer')));
SELECT name(equipment_named(hobby_construct_named(text 'skywalking', text 'mer')));
SELECT name(equipment_named_ambiguous_1a(hobby_construct_named(text 'skywalking', text 'mer')));
SELECT name(equipment_named_ambiguous_1b(hobby_construct_named(text 'skywalking', text 'mer')));
SELECT name(equipment_named_ambiguous_1c(hobby_construct_named(text 'skywalking', text 'mer')));
SELECT name(equipment_named_ambiguous_2a(text 'skywalking'));
SELECT name(equipment_named_ambiguous_2b(text 'skywalking'));
SELECT hobbies_by_name('basketball');
SELECT name, overpaid(emp.*) FROM emp;

View File

@ -9,6 +9,10 @@ CREATE FUNCTION hobby_construct(text, text)
RETURNS hobbies_r
AS 'select $1 as name, $2 as hobby'
LANGUAGE SQL;
CREATE FUNCTION hobby_construct_named(name text, hobby text)
RETURNS hobbies_r
AS 'select name, hobby'
LANGUAGE SQL;
CREATE FUNCTION hobbies_by_name(hobbies_r.name%TYPE)
RETURNS hobbies_r.person%TYPE
AS 'select person from hobbies_r where name = $1'
@ -19,6 +23,30 @@ CREATE FUNCTION equipment(hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where hobby = $1.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where equipment_r.hobby = equipment_named.hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_1a(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where hobby = equipment_named_ambiguous_1a.hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_1b(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where equipment_r.hobby = hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_1c(hobby hobbies_r)
RETURNS setof equipment_r
AS 'select * from equipment_r where hobby = hobby.name'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_2a(hobby text)
RETURNS setof equipment_r
AS 'select * from equipment_r where hobby = equipment_named_ambiguous_2a.hobby'
LANGUAGE SQL;
CREATE FUNCTION equipment_named_ambiguous_2b(hobby text)
RETURNS setof equipment_r
AS 'select * from equipment_r where equipment_r.hobby = hobby'
LANGUAGE SQL;
CREATE FUNCTION user_relns()
RETURNS setof name
AS 'select relname

View File

@ -693,6 +693,51 @@ SELECT name(equipment(hobby_construct(text 'skywalking', text 'mer')));
guts
(1 row)
SELECT name(equipment(hobby_construct_named(text 'skywalking', text 'mer')));
name
------
guts
(1 row)
SELECT name(equipment_named(hobby_construct_named(text 'skywalking', text 'mer')));
name
------
guts
(1 row)
SELECT name(equipment_named_ambiguous_1a(hobby_construct_named(text 'skywalking', text 'mer')));
name
------
guts
(1 row)
SELECT name(equipment_named_ambiguous_1b(hobby_construct_named(text 'skywalking', text 'mer')));
name
------
guts
(1 row)
SELECT name(equipment_named_ambiguous_1c(hobby_construct_named(text 'skywalking', text 'mer')));
name
------
guts
(1 row)
SELECT name(equipment_named_ambiguous_2a(text 'skywalking'));
name
------
guts
(1 row)
SELECT name(equipment_named_ambiguous_2b(text 'skywalking'));
name
---------------
advil
peet's coffee
hightops
guts
(4 rows)
SELECT hobbies_by_name('basketball');
hobbies_by_name
-----------------