Consider syntactic form when disambiguating function vs column reference.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 18 Jun 2018 15:39:33 +0000 (11:39 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 18 Jun 2018 15:39:33 +0000 (11:39 -0400)
Postgres has traditionally considered the syntactic forms f(x) and x.f
to be equivalent, allowing tricks such as writing a function and then
using it as though it were a computed-on-demand column.  However, our
behavior when both interpretations are feasible left something to be
desired: we always chose the column interpretation.  This could lead
to very surprising results, as in a recent bug report from Neil Conway.
It also created a dump-and-reload hazard, since what was a function
call in a dumped view could get interpreted as a column reference
at reload, if a matching column name had been added to the underlying
table since the view was created.

What seems better, in ambiguous situations, is to prefer the choice
matching the syntactic form of the reference.  This seems much less
astonishing in general, and it fixes the dump/reload hazard.

Although this could be called a bug fix, there have been few complaints
and there's some small risk of breaking applications that depend on the
old behavior, so no back-patch.  It does seem reasonable to slip it
into v11, though.

Discussion: https://postgr.es/m/CAOW5sYa3Wp7KozCuzjOdw6PiOYPi6D=VvRybtH2S=2C0SVmRmA@mail.gmail.com

doc/src/sgml/rowtypes.sgml
src/backend/parser/parse_func.c
src/test/regress/expected/rowtypes.out
src/test/regress/sql/rowtypes.sql

index 3f24293175dfcd4928cc2789a621b43aa9d32197..2f924b1f85df234ac4f080820a83f399dfe6be62 100644 (file)
@@ -441,9 +441,12 @@ SELECT c.somefunc FROM inventory_item c;
     Because of this behavior, it's unwise to give a function that takes a
     single composite-type argument the same name as any of the fields of
     that composite type.  If there is ambiguity, the field-name
-    interpretation will be preferred, so that such a function could not be
-    called without tricks.  One way to force the function interpretation is
-    to schema-qualify the function name, that is, write
+    interpretation will be chosen if field-name syntax is used, while the
+    function will be chosen if function-call syntax is used.  However,
+    <productname>PostgreSQL</productname> versions before 11 always chose the
+    field-name interpretation, unless the syntax of the call required it to
+    be a function call.  One way to force the function interpretation in
+    older versions is to schema-qualify the function name, that is, write
     <literal><replaceable>schema</replaceable>.<replaceable>func</replaceable>(<replaceable>compositevalue</replaceable>)</literal>.
    </para>
   </tip>
index 21ddd5b7e01737fc8bb24ef455ef2ea0505cb0a6..abe1dbc521cdaae0917c3cecb8042039343b7e31 100644 (file)
@@ -49,15 +49,17 @@ static Node *ParseComplexProjection(ParseState *pstate, const char *funcname,
  * For historical reasons, Postgres tries to treat the notations tab.col
  * and col(tab) as equivalent: if a single-argument function call has an
  * argument of complex type and the (unqualified) function name matches
- * any attribute of the type, we take it as a column projection.  Conversely
- * a function of a single complex-type argument can be written like a
- * column reference, allowing functions to act like computed columns.
+ * any attribute of the type, we can interpret it as a column projection.
+ * Conversely a function of a single complex-type argument can be written
+ * like a column reference, allowing functions to act like computed columns.
+ *
+ * If both interpretations are possible, we prefer the one matching the
+ * syntactic form, but otherwise the form does not matter.
  *
  * Hence, both cases come through here.  If fn is null, we're dealing with
- * column syntax not function syntax, but in principle that should not
- * affect the lookup behavior, only which error messages we deliver.
- * The FuncCall struct is needed however to carry various decoration that
- * applies to aggregate and window functions.
+ * column syntax not function syntax.  In the function-syntax case,
+ * the FuncCall struct is needed to carry various decoration that applies
+ * to aggregate and window functions.
  *
  * Also, when fn is null, we return NULL on failure rather than
  * reporting a no-such-function error.
@@ -84,6 +86,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
    bool        agg_distinct = (fn ? fn->agg_distinct : false);
    bool        func_variadic = (fn ? fn->func_variadic : false);
    WindowDef  *over = (fn ? fn->over : NULL);
+   bool        could_be_projection;
    Oid         rettype;
    Oid         funcid;
    ListCell   *l;
@@ -202,36 +205,39 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
    }
 
    /*
-    * Check for column projection: if function has one argument, and that
-    * argument is of complex type, and function name is not qualified, then
-    * the "function call" could be a projection.  We also check that there
-    * wasn't any aggregate or variadic decoration, nor an argument name.
+    * Decide whether it's legitimate to consider the construct to be a column
+    * projection.  For that, there has to be a single argument of complex
+    * type, the function name must not be qualified, and there cannot be any
+    * syntactic decoration that'd require it to be a function (such as
+    * aggregate or variadic decoration, or named arguments).
     */
-   if (nargs == 1 && !proc_call &&
-       agg_order == NIL && agg_filter == NULL && !agg_star &&
-       !agg_distinct && over == NULL && !func_variadic && argnames == NIL &&
-       list_length(funcname) == 1)
-   {
-       Oid         argtype = actual_arg_types[0];
+   could_be_projection = (nargs == 1 && !proc_call &&
+                          agg_order == NIL && agg_filter == NULL &&
+                          !agg_star && !agg_distinct && over == NULL &&
+                          !func_variadic && argnames == NIL &&
+                          list_length(funcname) == 1 &&
+                          (actual_arg_types[0] == RECORDOID ||
+                           ISCOMPLEX(actual_arg_types[0])));
 
-       if (argtype == RECORDOID || ISCOMPLEX(argtype))
-       {
-           retval = ParseComplexProjection(pstate,
-                                           strVal(linitial(funcname)),
-                                           first_arg,
-                                           location);
-           if (retval)
-               return retval;
+   /*
+    * If it's column syntax, check for column projection case first.
+    */
+   if (could_be_projection && is_column)
+   {
+       retval = ParseComplexProjection(pstate,
+                                       strVal(linitial(funcname)),
+                                       first_arg,
+                                       location);
+       if (retval)
+           return retval;
 
-           /*
-            * If ParseComplexProjection doesn't recognize it as a projection,
-            * just press on.
-            */
-       }
+       /*
+        * If ParseComplexProjection doesn't recognize it as a projection,
+        * just press on.
+        */
    }
 
    /*
-    * Okay, it's not a column projection, so it must really be a function.
     * func_get_detail looks up the function in the catalogs, does
     * disambiguation for polymorphic functions, handles inheritance, and
     * returns the funcid and type and set or singleton status of the
@@ -334,7 +340,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
    }
 
    /*
-    * So far so good, so do some routine-type-specific processing.
+    * So far so good, so do some fdresult-type-specific processing.
     */
    if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
    {
@@ -524,30 +530,55 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
                           actual_arg_types[0], rettype, -1,
                           COERCION_EXPLICIT, COERCE_EXPLICIT_CALL, location);
    }
+   else if (fdresult == FUNCDETAIL_MULTIPLE)
+   {
+       /*
+        * We found multiple possible functional matches.  If we are dealing
+        * with attribute notation, return failure, letting the caller report
+        * "no such column" (we already determined there wasn't one).  If
+        * dealing with function notation, report "ambiguous function",
+        * regardless of whether there's also a column by this name.
+        */
+       if (is_column)
+           return NULL;
+
+       ereport(ERROR,
+               (errcode(ERRCODE_AMBIGUOUS_FUNCTION),
+                errmsg("function %s is not unique",
+                       func_signature_string(funcname, nargs, argnames,
+                                             actual_arg_types)),
+                errhint("Could not choose a best candidate function. "
+                        "You might need to add explicit type casts."),
+                parser_errposition(pstate, location)));
+   }
    else
    {
        /*
-        * Oops.  Time to die.
-        *
-        * If we are dealing with the attribute notation rel.function, let the
-        * caller handle failure.
+        * Not found as a function.  If we are dealing with attribute
+        * notation, return failure, letting the caller report "no such
+        * column" (we already determined there wasn't one).
         */
        if (is_column)
            return NULL;
 
        /*
-        * Else generate a detailed complaint for a function
+        * Check for column projection interpretation, since we didn't before.
         */
-       if (fdresult == FUNCDETAIL_MULTIPLE)
-           ereport(ERROR,
-                   (errcode(ERRCODE_AMBIGUOUS_FUNCTION),
-                    errmsg("function %s is not unique",
-                           func_signature_string(funcname, nargs, argnames,
-                                                 actual_arg_types)),
-                    errhint("Could not choose a best candidate function. "
-                            "You might need to add explicit type casts."),
-                    parser_errposition(pstate, location)));
-       else if (list_length(agg_order) > 1 && !agg_within_group)
+       if (could_be_projection)
+       {
+           retval = ParseComplexProjection(pstate,
+                                           strVal(linitial(funcname)),
+                                           first_arg,
+                                           location);
+           if (retval)
+               return retval;
+       }
+
+       /*
+        * No function, and no column either.  Since we're dealing with
+        * function notation, report "function does not exist".
+        */
+       if (list_length(agg_order) > 1 && !agg_within_group)
        {
            /* It's agg(x, ORDER BY y,z) ... perhaps misplaced ORDER BY */
            ereport(ERROR,
index 45cb6ff3dab82f11a213c33e2758495bc9a04292..30053d07dff34cec50ac8e413f1f9ee53176b3c9 100644 (file)
@@ -797,6 +797,50 @@ select (row('Jim', 'Beam')).text;  -- error
 ERROR:  could not identify column "text" in record data type
 LINE 1: select (row('Jim', 'Beam')).text;
                 ^
+--
+-- Check the equivalence of functional and column notation
+--
+insert into fullname values ('Joe', 'Blow');
+select f.last from fullname f;
+ last 
+------
+ Blow
+(1 row)
+
+select last(f) from fullname f;
+ last 
+------
+ Blow
+(1 row)
+
+create function longname(fullname) returns text language sql
+as $$select $1.first || ' ' || $1.last$$;
+select f.longname from fullname f;
+ longname 
+----------
+ Joe Blow
+(1 row)
+
+select longname(f) from fullname f;
+ longname 
+----------
+ Joe Blow
+(1 row)
+
+-- Starting in v11, the notational form does matter if there's ambiguity
+alter table fullname add column longname text;
+select f.longname from fullname f;
+ longname 
+----------
+(1 row)
+
+select longname(f) from fullname f;
+ longname 
+----------
+ Joe Blow
+(1 row)
+
 --
 -- Test that composite values are seen to have the correct column names
 -- (bug #11210 and other reports)
index 305639f05dbca2d583dac36deb9b2b392ffa540f..faf2e108d6acd8583d89b527c043447f618cb972 100644 (file)
@@ -345,6 +345,26 @@ select (row('Jim', 'Beam'))::text;
 select text(row('Jim', 'Beam'));  -- error
 select (row('Jim', 'Beam')).text;  -- error
 
+--
+-- Check the equivalence of functional and column notation
+--
+insert into fullname values ('Joe', 'Blow');
+
+select f.last from fullname f;
+select last(f) from fullname f;
+
+create function longname(fullname) returns text language sql
+as $$select $1.first || ' ' || $1.last$$;
+
+select f.longname from fullname f;
+select longname(f) from fullname f;
+
+-- Starting in v11, the notational form does matter if there's ambiguity
+alter table fullname add column longname text;
+
+select f.longname from fullname f;
+select longname(f) from fullname f;
+
 --
 -- Test that composite values are seen to have the correct column names
 -- (bug #11210 and other reports)