Thread: UNNEST with multiple args, and TABLE with multiple funcs
Summary: This patch implements a method for expanding multiple SRFs in parallel that does not have the surprising LCM behaviour of SRFs-in-select-list. (Functions returning fewer rows are padded with nulls instead.) It then uses this method combined with a parse-time hack to implement the (intended to be) spec-conforming behaviour of UNNEST with multiple parameters, including flattening of composite results. The upshot is that given a table like this: postgres=# select * from t1; a | b | c ---------------+-------------------+---------------------------------------------- {11,12,13} | {wombat} | {5,10} | {foo,bar} | {"(123,xyzzy)","(456,plugh)","(789,plover)"} {21,31,41,51} | {fred,jim,sheila} | {"(111,xyzzy)","(222,plugh)"} (3 rows) (where column "c" is an array of a composite type with 2 cols, "x" and "y") You can do this: postgres=# select u.* from t1, unnest(a,b,c) with ordinality as u; ?column? | ?column? | x | y | ordinality ----------+----------+-----+--------+------------ 11 | wombat | | | 1 12 | | | | 2 13 | | | | 3 5 | foo | 123 | xyzzy | 1 10 | bar | 456 | plugh | 2 | | 789 | plover | 3 21 | fred | 111 | xyzzy | 1 31 | jim | 222 | plugh | 2 41 | sheila | | | 3 51 | | | | 4 (10 rows) Or for an example of general combination of functions: postgres=# select * from table(generate_series(10,20,5), unnest(array['fred','jim'])); ?column? | ?column? ----------+---------- 10 | fred 15 | jim 20 | (3 rows) Implementation Details: The spec syntax for table function calls, <table function derived table> in <table reference>, looks like TABLE(func(args...)) AS ... This patch implements that, plus an extension: it allows multiple functions, TABLE(func1(...), func2(...), func3(...)) [WITH ORDINALITY] and defines this as meaning that the functions are to be evaluated in parallel. This is implemented by changing RangeFunction, function RTEs, and the FunctionScan node to take lists of function calls rather than a single function. The calling convention for SRFs is completely unchanged; each function returns its own rows (or a tuplestore in materialize mode) just as before, and FunctionScan combines the results into a single output tuple (keeping track of which functions are exhausted in order to correctly fill in nulls on a backwards scan). Then, a hack in the parser converts unnest(...) appearing as a func_table (and only there) into a list of unnest() calls, one for each parameter. So select ... from unnest(a,b,c) is converted to select ... from TABLE(unnest(a),unnest(b),unnest(c)) and if unnest appears as part of an existing list inside TABLE(), it's expanded to multiple entries there too. This parser hackery is of course somewhat ugly. But given the objective of implementing the spec's unnest syntax, it seems to be the least ugly of the possible approaches. (The hard part of doing it any other way would be generating the description of the result type; composite array parameters expand into multiple result columns.) Overall, it's my intention here to remove as many as feasible of the old reasons why one might use an SRF in the select list. This should also address the points that Josh brought up in discussion of ORDINALITY regarding use of SRF-in-select to unnest multiple arrays. (As a side issue, this patch also sets up pathkeys for ordinality along the lines of a patch I suggested to Greg a while back in response to his.) Current patch status: This is a first working cut: no docs, no tests, not enough comments, the deparse logic probably needs more work (it deparses correctly but the formatting may be suboptimal). However all the functionality is believed to be in place. -- Andrew (irc:RhodiumToad)
Attachment
On 08/13/2013 06:54 AM, Andrew Gierth wrote: > Summary: > > This patch implements a method for expanding multiple SRFs in parallel > that does not have the surprising LCM behaviour of SRFs-in-select-list. > (Functions returning fewer rows are padded with nulls instead.) BTW, if anyone is unsure of the use-case for this, I have some uses for it: 1. denormalized data stored in same-length arrays (usually for compression reasons) 2. use with PL/Python-Numpy and PL/R functions which return multiple arrays or 2D arrays. In other words, I have *lots* of uses for this functionality, and I think the analytics crowd will like it. Which means that I need to get on testing it, of course ... -- Josh Berkus PostgreSQL Experts Inc. http://pgexperts.com
On 08/14/2013 08:22 AM, Josh Berkus wrote: > On 08/13/2013 06:54 AM, Andrew Gierth wrote: >> Summary: >> >> This patch implements a method for expanding multiple SRFs in parallel >> that does not have the surprising LCM behaviour of SRFs-in-select-list. >> (Functions returning fewer rows are padded with nulls instead.) > > BTW, if anyone is unsure of the use-case for this, I have some uses for it: > > 1. denormalized data stored in same-length arrays (usually for > compression reasons) > > 2. use with PL/Python-Numpy and PL/R functions which return multiple > arrays or 2D arrays. > > In other words, I have *lots* of uses for this functionality, and I > think the analytics crowd will like it. Which means that I need to get > on testing it, of course ... Similarly, I see uses for this come up a lot, and usually have to work around it with ugly invocations of multiple SRFs in the SELECT list in a subquery. I was thinking of implementing multi-argument unnest directly with `any` parameters if I could get it to work, but hadn't started on it yet. This looks like a really clever approach and it handles multiple spec-compliance items. I'll grab the patch and try it out. -- Craig Ringer http://www.2ndQuadrant.com/PostgreSQL Development, 24x7 Support, Training & Services
Hi, 2013-08-13 15:54 keltezéssel, Andrew Gierth írta: > Summary: > > This patch implements a method for expanding multiple SRFs in parallel > that does not have the surprising LCM behaviour of SRFs-in-select-list. > (Functions returning fewer rows are padded with nulls instead.) > > It then uses this method combined with a parse-time hack to implement > the (intended to be) spec-conforming behaviour of UNNEST with multiple > parameters, including flattening of composite results. > > The upshot is that given a table like this: > > postgres=# select * from t1; > a | b | c > ---------------+-------------------+---------------------------------------------- > {11,12,13} | {wombat} | > {5,10} | {foo,bar} | {"(123,xyzzy)","(456,plugh)","(789,plover)"} > {21,31,41,51} | {fred,jim,sheila} | {"(111,xyzzy)","(222,plugh)"} > (3 rows) > > (where column "c" is an array of a composite type with 2 cols, "x" and "y") > > You can do this: > > postgres=# select u.* from t1, unnest(a,b,c) with ordinality as u; > ?column? | ?column? | x | y | ordinality > ----------+----------+-----+--------+------------ > 11 | wombat | | | 1 > 12 | | | | 2 > 13 | | | | 3 > 5 | foo | 123 | xyzzy | 1 > 10 | bar | 456 | plugh | 2 > | | 789 | plover | 3 > 21 | fred | 111 | xyzzy | 1 > 31 | jim | 222 | plugh | 2 > 41 | sheila | | | 3 > 51 | | | | 4 > (10 rows) > > Or for an example of general combination of functions: > > postgres=# select * from table(generate_series(10,20,5), unnest(array['fred','jim'])); > ?column? | ?column? > ----------+---------- > 10 | fred > 15 | jim > 20 | > (3 rows) > > Implementation Details: > > The spec syntax for table function calls, <table function derived table> > in <table reference>, looks like TABLE(func(args...)) AS ... > > This patch implements that, plus an extension: it allows multiple > functions, TABLE(func1(...), func2(...), func3(...)) [WITH ORDINALITY] > and defines this as meaning that the functions are to be evaluated in > parallel. > > This is implemented by changing RangeFunction, function RTEs, and the > FunctionScan node to take lists of function calls rather than a single > function. The calling convention for SRFs is completely unchanged; each > function returns its own rows (or a tuplestore in materialize mode) just > as before, and FunctionScan combines the results into a single output > tuple (keeping track of which functions are exhausted in order to > correctly fill in nulls on a backwards scan). > > Then, a hack in the parser converts unnest(...) appearing as a > func_table (and only there) into a list of unnest() calls, one for each > parameter. So > > select ... from unnest(a,b,c) > > is converted to > > select ... from TABLE(unnest(a),unnest(b),unnest(c)) > > and if unnest appears as part of an existing list inside TABLE(), it's > expanded to multiple entries there too. > > This parser hackery is of course somewhat ugly. But given the objective > of implementing the spec's unnest syntax, it seems to be the least ugly > of the possible approaches. (The hard part of doing it any other way > would be generating the description of the result type; composite array > parameters expand into multiple result columns.) Harder maybe but it may still be cleaner in the long run. > Overall, it's my intention here to remove as many as feasible of the old > reasons why one might use an SRF in the select list. Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having WITH ORDINALITY and this feature, I would vote for removing SRF-in-targetlist and call the release PostgreSQL 10.0. > This should also > address the points that Josh brought up in discussion of ORDINALITY > regarding use of SRF-in-select to unnest multiple arrays. > > (As a side issue, this patch also sets up pathkeys for ordinality along > the lines of a patch I suggested to Greg a while back in response to > his.) > > Current patch status: > > This is a first working cut: no docs, no tests, not enough comments, the > deparse logic probably needs more work (it deparses correctly but the > formatting may be suboptimal). However all the functionality is believed > to be in place. With this last paragraph in mind, I am trying a little review. * Is the patch in a patch format which has context? (eg: context diff format) Yes. * Does it apply cleanly to the current git master? Applies with some offsets on a few files but without fuzz. * Does it include reasonable tests, necessary doc patches, etc? No, as told by the patch author. * Does the patch actually implement what it's supposed to do? Yes. * Do we want that? Yes. * Do we already have it? No. * Does it follow SQL spec, or the community-agreed behavior? The SQL spec says these: In 7.6 <table reference> <table primary> ::= ... | <table function derived table> [ AS ] <correlation name> [ <left paren> <derived column list> <right paren> ] also later in the same section: <table function derived table> ::= TABLE <left paren> <collection value expression> <right paren> In 6.26 <value expression> <collection value expression> ::= <array value expression> | <multiset value expression> In 6.36 <array value expression> <array value expression> ::= <array concatenation> | <array primary> <array concatenation> ::= <array value expression 1> <concatenation operator> <array primary> <array value expression 1> ::= <array value expression> <array primary> ::= <array value function> | <value expression primary> 6.3 <value expression primary> <value expression primary> ::= <parenthesized value expression> | <nonparenthesized value expression primary> <parenthesized value expression> ::= <left paren> <value expression> <right paren> <nonparenthesized value expression primary> ::= <unsigned value specification> | <column reference> | <set function specification> | <window function> | <nested window function> | <scalar subquery> | <case expression> | <cast specification> | <field reference> | <subtype treatment> | <methodinvocation> | <static method invocation> | <new specification> | <attribute or method reference> | <reference resolution> | <collection value constructor> | <array element reference> | <multiset element reference> | <next value expression> | <routine invocation> collection value constructor> ::= | <array value constructor> | <multiset value constructor> So, the FROM TABLE(...) AS (...) syntax is a big can of worms and I haven't even quoted <multiset value expression>. As far as I can tell, these should also be allowed but isn't: zozo=# select * from table('a'::text) as x; ERROR: syntax error at or near "'a'" LINE 1: select * from table('a'::text) as x; ^ zozo=# select x.* from t1, table(t1.a) as x; ERROR: syntax error at or near ")" LINE 1: select x.* from t1, table(t1.a) as x; ^ zozo=# select x.* from table((6)) as x(a int4); ERROR: syntax error at or near "(" LINE 1: select x.* from table((6)) as x(a int4); ^ zozo=# select x.* from table(values (6)) as x(a int4); ERROR: syntax error at or near "(" LINE 1: select x.* from table(values (6)) as x(a int4); ^ zozo=# select x.* from table(values(6)) as x(a int4); ERROR: syntax error at or near "(" LINE 1: select x.* from table(values(6)) as x(a int4); ^ What the patch implements is only the last choice for <nonparenthesized value expression primary>: <routine invocation> When you add documentation, it would be nice to mention it. Also, the grammar extension is a start for adding all the other standard choices for TABLE(). * Does it include pg_dump support (if applicable)? n/a * Are there dangers? I can't see any. 8-) * Have all the bases been covered? My previous comments about the TABLE() syntax says it all. You can interpret it either way. :-) * Does the feature work as advertised? Yes. * Are there corner cases the author has failed to consider? I don't know. * Are there any assertion failures or crashes? No. * Does the patch slow down simple tests? No. * If it claims to improve performance, does it? It certainly improves writing queries, as functions inside unnest() get processed in one scan. * Does it slow down other things? I don't think so. * Does it follow the project coding guidelines? Yes. * Are there portability issues? No. * Will it work on Windows/BSD etc? It should, the code uses standard internal PostgreSQL APIs and extends them. No new system call. * Are the comments sufficient and accurate? According to the author, no. * Does it do what it says, correctly? Yes. * Does it produce compiler warnings? No. * Can you make it crash? No. * Is everything done in a way that fits together coherently with other features/modules? I think so * Are there interdependencies that can cause problems? I don't know. Best regards, Zoltán Böszörményi -- ---------------------------------- Zoltán Böszörményi Cybertec Schönig & Schönig GmbH Gröhrmühlgasse 26 A-2700 Wiener Neustadt, Austria Web: http://www.postgresql-support.de http://www.postgresql.at/
Hello
Harder maybe but it may still be cleaner in the long run.Overall, it's my intention here to remove as many as feasible of the old
reasons why one might use an SRF in the select list.
Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having
WITH ORDINALITY and this feature, I would vote for removing
SRF-in-targetlist and call the release PostgreSQL 10.0.
Although I would to remove SRF from targetlist, I don't think so this hurry strategy is good idea. We should to provide new functionality and old functionality one year as minimum, and we should to announce so this feature is deprecated - and maybe use a GUC for disabling, warning and deprecating. More, I would to see 9.4 release:). x.4 are happy PostgreSQL releases :)
Regards
Pavel
Pavel
On 08/19/2013 09:23 AM, Boszormenyi Zoltan wrote: > > Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having > WITH ORDINALITY and this feature, I would vote for removing > SRF-in-targetlist and call the release PostgreSQL 10.0. That's not realistic. We'd have to deprecate that syntax and repeatedly and loudly warn people about it for at least 3 years after the release of 9.3. You're talking about asking people to refactor hundreds or thousands of lines of code which makes current use of things like regex_match() in the target list. -- Josh Berkus PostgreSQL Experts Inc. http://pgexperts.com
2013-08-19 20:03 keltezéssel, Josh Berkus írta: > On 08/19/2013 09:23 AM, Boszormenyi Zoltan wrote: >> Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having >> WITH ORDINALITY and this feature, I would vote for removing >> SRF-in-targetlist and call the release PostgreSQL 10.0. > That's not realistic. I know. I am such an agent provocateur. :-) > We'd have to deprecate that syntax and > repeatedly and loudly warn people about it for at least 3 years after > the release of 9.3. You're talking about asking people to refactor > hundreds or thousands of lines of code which makes current use of things > like regex_match() in the target list. > -- ---------------------------------- Zoltán Böszörményi Cybertec Schönig & Schönig GmbH Gröhrmühlgasse 26 A-2700 Wiener Neustadt, Austria Web: http://www.postgresql-support.de http://www.postgresql.at/
Boszormenyi Zoltan wrote: >> This parser hackery is of course somewhat ugly. But given the objective >> of implementing the spec's unnest syntax, it seems to be the least ugly >> of the possible approaches. (The hard part of doing it any other way >> would be generating the description of the result type; composite array >> parameters expand into multiple result columns.) > > Harder maybe but it may still be cleaner in the long run. I'm not so sure. As far as I'm concerned, though, the situation is fairly simple: there are no proposals on the table for any mechanism that would allow the deduction of a return type structure for multi-arg unnest, I have tried and failed to come up with a usable alternative proposal, and there is no prospect of one resulting from any other work that I know about. So the parser hack is the way it goes, and anyone who doesn't like it is welcome to suggest a better idea. >> Overall, it's my intention here to remove as many as feasible of the old >> reasons why one might use an SRF in the select list. > > Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having > WITH ORDINALITY and this feature, I would vote for removing > SRF-in-targetlist and call the release PostgreSQL 10.0. I want to make this ABSOLUTELY clear: I am not advocating removing SRF-in-targetlist in the near future and I will not support anyone who does. Please do not use this code as an argument for that (at least until a few releases have elapsed). All I'm interested in at this point is providing an alternative with better semantics. > The SQL spec says these: > > In 7.6 <table reference> [mega-snip] > > As far as I can tell, these should also be allowed but isn't: No, they're not allowed. You missed this rule (in 7.6 Syntax Rules): 2) If a <table primary> TP simply contains a <table function derived table> TFDT, then: a) The <collection value expression> immediately contained in TFDT shall be a <routine invocation>. In other words, func(...) is the only allowed form for the collection value expression inside TABLE( ). (Same rule exists in 201x, but numbered 6 rather than 2.) Largely as a matter of consistency, the patch does presently allow expressions that are not <routine invocation>s but which are part of the func_expr_windowless production, so things like TABLE(USER) work. (This is because historically these are allowed in the FROM clause as tables.) I'm not sure this is a good idea in general; should it be tightened up to only allow func_application? > * If it claims to improve performance, does it? > > It certainly improves writing queries, as functions inside > unnest() get processed in one scan. I'm not making any specific performance claims, but I have tested it against the idea of doing separate function scans with a full join on ordinality columns, and my approach is faster (1.5x to 2x in my tests) even with pathkey support and with elimination of extra materialize nodes (by allowing mark/restore in FunctionScan). ------ Since the original patch was posted I have done further work on it, including some tests. I have also come up with an additional possibility: that of allowing multiple SRFs that return RECORD with column definition lists, and SRFs-returning-RECORD combined with ORDINALITY, by extending the syntax further: select * from TABLE(func1() AS (a text, b integer), func2() AS (c integer, d text)); select * from TABLE(func1() AS (a text, b integer)) WITH ORDINALITY AS f(a,b,o); -- shame to have to duplicate the column names, but avoiding that would -- not have been easy This removes the restriction of the previous ORDINALITY patch that prevented its use with SRFs that needed coldef lists. I'm open to other suggestions on the syntax for this. (My implementation of this works by making the column definition list a property of the function call, rather than of the RTE or the FunctionScan node. This eliminates a few places where TYPEFUNC_RECORD had to be handled as a special case.) -- Andrew. (irc:RhodiumToad)
2013-08-19 22:04 keltezéssel, Andrew Gierth írta: > Boszormenyi Zoltan wrote: >>> This parser hackery is of course somewhat ugly. But given the objective >>> of implementing the spec's unnest syntax, it seems to be the least ugly >>> of the possible approaches. (The hard part of doing it any other way >>> would be generating the description of the result type; composite array >>> parameters expand into multiple result columns.) >> Harder maybe but it may still be cleaner in the long run. > I'm not so sure. > > As far as I'm concerned, though, the situation is fairly simple: there > are no proposals on the table for any mechanism that would allow the > deduction of a return type structure for multi-arg unnest, I have > tried and failed to come up with a usable alternative proposal, and > there is no prospect of one resulting from any other work that I know > about. So the parser hack is the way it goes, and anyone who doesn't > like it is welcome to suggest a better idea. > >>> Overall, it's my intention here to remove as many as feasible of the old >>> reasons why one might use an SRF in the select list. >> Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having >> WITH ORDINALITY and this feature, I would vote for removing >> SRF-in-targetlist and call the release PostgreSQL 10.0. > I want to make this ABSOLUTELY clear: I am not advocating removing > SRF-in-targetlist in the near future and I will not support anyone who > does. Please do not use this code as an argument for that (at least > until a few releases have elapsed). All I'm interested in at this > point is providing an alternative with better semantics. > >> The SQL spec says these: >> >> In 7.6 <table reference> > [mega-snip] >> As far as I can tell, these should also be allowed but isn't: > No, they're not allowed. You missed this rule (in 7.6 Syntax Rules): > > 2) If a <table primary> TP simply contains a <table function derived > table> TFDT, then: > > a) The <collection value expression> immediately contained in TFDT > shall be a <routine invocation>. > > In other words, func(...) is the only allowed form for the collection > value expression inside TABLE( ). (Same rule exists in 201x, but > numbered 6 rather than 2.) You are right, I missed it in the standard. Sorry. > Largely as a matter of consistency, the patch does presently allow > expressions that are not <routine invocation>s but which are part of > the func_expr_windowless production, so things like TABLE(USER) work. > (This is because historically these are allowed in the FROM clause as > tables.) I'm not sure this is a good idea in general; should it be > tightened up to only allow func_application? > >> * If it claims to improve performance, does it? >> >> It certainly improves writing queries, as functions inside >> unnest() get processed in one scan. > I'm not making any specific performance claims, but I have tested it > against the idea of doing separate function scans with a full join on > ordinality columns, and my approach is faster (1.5x to 2x in my tests) > even with pathkey support and with elimination of extra materialize > nodes (by allowing mark/restore in FunctionScan). > > ------ > > Since the original patch was posted I have done further work on it, > including some tests. I have also come up with an additional > possibility: that of allowing multiple SRFs that return RECORD with > column definition lists, and SRFs-returning-RECORD combined with > ORDINALITY, by extending the syntax further: > > select * from TABLE(func1() AS (a text, b integer), > func2() AS (c integer, d text)); > > select * from TABLE(func1() AS (a text, b integer)) > WITH ORDINALITY AS f(a,b,o); > > -- shame to have to duplicate the column names, but avoiding that would > -- not have been easy > > This removes the restriction of the previous ORDINALITY patch that > prevented its use with SRFs that needed coldef lists. Very nice. > I'm open to other suggestions on the syntax for this. It's consistent with straight SRFs in FROM, the function parameters are attached to the functions themselves. I think it's good as is. > (My implementation of this works by making the column definition list > a property of the function call, Which also makes it easier on the eyes and brain when reading someone else's SQL. > rather than of the RTE or the > FunctionScan node. This eliminates a few places where TYPEFUNC_RECORD > had to be handled as a special case.) This is the other plus. No special casing is good. The only minus is that you haven't attached the new patch. :-) Best regards, Zoltán Böszörményi -- ---------------------------------- Zoltán Böszörményi Cybertec Schönig & Schönig GmbH Gröhrmühlgasse 26 A-2700 Wiener Neustadt, Austria Web: http://www.postgresql-support.de http://www.postgresql.at/
On 08/20/2013 02:03 AM, Josh Berkus wrote: > On 08/19/2013 09:23 AM, Boszormenyi Zoltan wrote: >> >> Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having >> WITH ORDINALITY and this feature, I would vote for removing >> SRF-in-targetlist and call the release PostgreSQL 10.0. > > That's not realistic. We'd have to deprecate that syntax and > repeatedly and loudly warn people about it for at least 3 years after > the release of 9.3. You're talking about asking people to refactor > hundreds or thousands of lines of code which makes current use of things > like regex_match() in the target list. Agreed. Even three years is optimistic; after that long it could probably be made into an ERROR by default with a backward-compat GUC, but certainly not removed. I'm still running into people running 8.2 and having issues upgrading due to the 8.3 removal of implicit casts from text, and even the removal of add_missing_from . If we want people to upgrade this century it's worth minimising the amount of unnecessary breakage. SRF-in-SELECT might be ugly, but simply ripping it out certainly counts as unnecessary breakage. -- Craig Ringer http://www.2ndQuadrant.com/PostgreSQL Development, 24x7 Support, Training & Services
On Mon, Aug 19, 2013 at 07:45:23PM +0200, Pavel Stehule wrote: > Hello > > Harder maybe but it may still be cleaner in the long run. > > > > Overall, it's my intention here to remove as many as feasible of the old > >> reasons why one might use an SRF in the select list. > >> > > > > Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having > > WITH ORDINALITY and this feature, I would vote for removing > > SRF-in-targetlist and call the release PostgreSQL 10.0. > > > > Although I would to remove SRF from targetlist, I don't think so this hurry > strategy is good idea. We should to provide new functionality and old > functionality one year as minimum, and we should to announce so this > feature is deprecated We could do this in 9.3, but all it would be is an announcement, i.e. no code change of any nature. > - and maybe use a GUC for disabling, warning and deprecating. With utmost respect, I think the general idea of setting SQL grammar via GUC is a really bad one. When we've done so in the past, it's done more harm than good, and we should not repeat it. > More, I would to see 9.4 release:). Same here! :) > x.4 are happy PostgreSQL releases :) Each one has been at least baseline happy for me since 7.1. Some have made me overjoyed, though. Cheers, David. -- David Fetter <david@fetter.org> http://fetter.org/ Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter Skype: davidfetter XMPP: david.fetter@gmail.com iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics Remember to vote! Consider donating to Postgres: http://www.postgresql.org/about/donate
2013/8/20 David Fetter <david@fetter.org>
On Mon, Aug 19, 2013 at 07:45:23PM +0200, Pavel Stehule wrote:
> Hello
>
> Harder maybe but it may still be cleaner in the long run.
> >
> > Overall, it's my intention here to remove as many as feasible of the old
> >> reasons why one might use an SRF in the select list.
> >>
> >
> > Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having
> > WITH ORDINALITY and this feature, I would vote for removing
> > SRF-in-targetlist and call the release PostgreSQL 10.0.
> >
>
> Although I would to remove SRF from targetlist, I don't think so this hurry
> strategy is good idea. We should to provide new functionality and old
> functionality one year as minimum, and we should to announce so this
> feature is deprecated
We could do this in 9.3, but all it would be is an announcement, i.e.
no code change of any nature.
> - and maybe use a GUC for disabling, warning and deprecating.
With utmost respect, I think the general idea of setting SQL grammar
via GUC is a really bad one. When we've done so in the past, it's
done more harm than good, and we should not repeat it.
so as minumum is controlling warning via GUC, we should to help with identification of problematic queries.
Regards
Pavel
> More, I would to see 9.4 release:).
Same here! :)
> x.4 are happy PostgreSQL releases :)
Each one has been at least baseline happy for me since 7.1. Some have
made me overjoyed, though.
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
2013-08-20 08:13 keltezéssel, Pavel Stehule írta:
2013/8/20 David Fetter <david@fetter.org>On Mon, Aug 19, 2013 at 07:45:23PM +0200, Pavel Stehule wrote:
> Hello
>
> Harder maybe but it may still be cleaner in the long run.
> >
> > Overall, it's my intention here to remove as many as feasible of the old
> >> reasons why one might use an SRF in the select list.
> >>
> >
> > Indeed, it's a big nail in the coffin for SRFs-in-targetlist. Having
> > WITH ORDINALITY and this feature, I would vote for removing
> > SRF-in-targetlist and call the release PostgreSQL 10.0.
> >
>
> Although I would to remove SRF from targetlist, I don't think so this hurry
> strategy is good idea. We should to provide new functionality and old
> functionality one year as minimum, and we should to announce so this
> feature is deprecated
We could do this in 9.3, but all it would be is an announcement, i.e.
no code change of any nature.
> - and maybe use a GUC for disabling, warning and deprecating.
To really ensure backward compatibility, this sentence should read as
"add a GUC for disabling *the* warning and deprecating." :->
As I said, I am such an agent provocateur.
Let this side track die and concentrate on the merits of the patch itself. :-)
With utmost respect, I think the general idea of setting SQL grammar
via GUC is a really bad one. When we've done so in the past, it's
done more harm than good, and we should not repeat it.so as minumum is controlling warning via GUC, we should to help with identification of problematic queries.RegardsPavel
> More, I would to see 9.4 release:).
Same here! :)
> x.4 are happy PostgreSQL releases :)
Each one has been at least baseline happy for me since 7.1. Some have
made me overjoyed, though.
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
-- ---------------------------------- Zoltán Böszörményi Cybertec Schönig & Schönig GmbH Gröhrmühlgasse 26 A-2700 Wiener Neustadt, Austria Web: http://www.postgresql-support.de http://www.postgresql.at/
On Tue, 2013-08-13 at 13:54 +0000, Andrew Gierth wrote: > Summary: > > This patch implements a method for expanding multiple SRFs in parallel > that does not have the surprising LCM behaviour of SRFs-in-select-list. > (Functions returning fewer rows are padded with nulls instead.) Fails to build in contrib: pg_stat_statements.c -MMD -MP -MF .deps/pg_stat_statements.Po pg_stat_statements.c: In function ‘JumbleRangeTable’: pg_stat_statements.c:1459:27: error: ‘RangeTblEntry’ has no member named ‘funcexpr’
Latest version of patch. This should be it as far as code goes; there may be some more regression test work, and a doc patch will be forthcoming. This version supports, in addition to the previous stuff: SELECT ... FROM TABLE(func() AS (colname coltype, ...)); i.e. the column definition list required for functions that return arbitrary RECORD results can go inside the TABLE() construct. This allows more than one such function in a call: SELECT ... FROM TABLE(func1() AS (a integer), func2() AS (b text)); or mixing RECORD functions with ORDINALITY: SELECT ... FROM TABLE(func1() AS (c text)) WITH ORDINALITY; The existing FROM func() AS f(c text) is still supported of course, and the variation FROM TABLE(func()) AS f(c text) is also supported but only when there's exactly one function and no ORDINALITY. Other changes: - function dependence on executor parameters is now tracked per-function, so that on rescan, only affected functions are re-executed, and others are simply rescanned from the existing tuplestore - some cases where deparse or other code broke because an element of funcexprs was not actually a FuncExpr have been fixed - fixed the pg_stat_statements issue A change I _didn't_ include, but did test, was adding mark/restore to FunctionScan to allow mergejoins on ordinality columns to work without needing extra nodes (which I did to do some performance tests referred to in a previous message). I took this code back out because it didn't seem to make much difference: the planner often (not always) adds the Materialize node even when it's not needed, in the belief that it is faster; the overhead of the extra node doesn't seem serious; and the case is of limited applicability (only useful when joining against something other than a function using the ordinal column alone). -- Andrew (irc:RhodiumToad)
Attachment
2013-08-27 01:24 keltezéssel, Andrew Gierth írta: > Latest version of patch. This should be it as far as code goes; there > may be some more regression test work, and a doc patch will be > forthcoming. > > This version supports, in addition to the previous stuff: > > [snip] In my limited testing, it works well, and the patch looks good. When adding regression tests, can you please add intentional syntax error cases to exercise all the new ereport()s? Best regards, Zoltán Böszörményi -- ---------------------------------- Zoltán Böszörményi Cybertec Schönig & Schönig GmbH Gröhrmühlgasse 26 A-2700 Wiener Neustadt, Austria Web: http://www.postgresql-support.de http://www.postgresql.at/
Boszormenyi Zoltan <zb@cybertec.at> writes: > When adding regression tests, can you please add intentional > syntax error cases to exercise all the new ereport()s? Please do not add test cases merely to prove that. Yeah, you should probably have exercised each error case in devel testing, but that does not mean that every future run of the regression tests needs to do it too. regards, tom lane
On Tue, 2013-08-27 at 09:44 -0400, Tom Lane wrote: > Boszormenyi Zoltan <zb@cybertec.at> writes: > > When adding regression tests, can you please add intentional > > syntax error cases to exercise all the new ereport()s? > > Please do not add test cases merely to prove that. Yeah, you should > probably have exercised each error case in devel testing, but that does > not mean that every future run of the regression tests needs to do it too. I disagree. The next person who wants to hack on this feature should be given the confidence that he's not breaking behavior that the last guy put in.
On Mon, 2013-08-26 at 23:24 +0000, Andrew Gierth wrote: > Latest version of patch. This should be it as far as code goes; there > may be some more regression test work, and a doc patch will be > forthcoming. In src/include/optimizer/paths.h, you are using "operator" as a function argument name, which breaks cpluspluscheck. Try using opid or operatorid.
On Wed, Aug 28, 2013 at 12:09:05AM -0400, Peter Eisentraut wrote: > On Tue, 2013-08-27 at 09:44 -0400, Tom Lane wrote: > > Boszormenyi Zoltan <zb@cybertec.at> writes: > > > When adding regression tests, can you please add intentional > > > syntax error cases to exercise all the new ereport()s? > > > > Please do not add test cases merely to prove that. Yeah, you should > > probably have exercised each error case in devel testing, but that does > > not mean that every future run of the regression tests needs to do it too. > > I disagree. The next person who wants to hack on this feature should be > given the confidence that he's not breaking behavior that the last guy > put in. +1. I wouldn't make full error-outcome test coverage a condition of patch acceptance. However, when an author chooses to submit high-quality tests with that level of detail, our source tree is the place to archive them. I share Tom's desire for a Makefile target that completes quickly and checks only those behaviors most likely to break, but not at the cost of letting deep test coverage dissipate in a mailing list attachment or in the feature author's home directory. -- Noah Misch EnterpriseDB http://www.enterprisedb.com
Latest version of patch, incorporating regression tests and docs, and fixing the "operator" issue previously raised. -- Andrew (irc:RhodiumToad)
Attachment
2013-09-13 21:03 keltezéssel, Andrew Gierth írta: > Latest version of patch, incorporating regression tests and docs, and > fixing the "operator" issue previously raised. It looks good. I think it's ready for a committer. Best regards, Zoltán Böszörményi -- ---------------------------------- Zoltán Böszörményi Cybertec Schönig & Schönig GmbH Gröhrmühlgasse 26 A-2700 Wiener Neustadt, Austria Web: http://www.postgresql-support.de http://www.postgresql.at/
I've spent some time reviewing this patch - looks pretty good! I'm not through yet, but I wanted to post an update. Attached is a new version, with some modifications I made. Notably: I added a new struct to hold the per-function executor state - tupdesc, tuplestore, rowcount and slot - instead of the lists that need to be kept in sync. This is more readable. I replaced the CreateTupleDescCopyMany() function with a function called TupleDescCopyEntry(), which initializes a single attribute like TupleDescInitEntry(), but copies the information from another tupledesc. It is used in a loop to construct the composite tupledec in multiple function or ordinality case. This is more flexible, no need to create the dummy single-attribute TupleDesc for ordinality anymore, for example. I refactored the grammar a bit; the way func_table rule returned a one- or two element list to essentially pass up a flag was an ugly hack. Below are a couple of more comments: On 13.09.2013 22:03, Andrew Gierth wrote: > *************** > *** 3529,3534 **** static Expr * > --- 3539,3545 ---- > simplify_function(Oid funcid, Oid result_type, int32 result_typmod, > Oid result_collid, Oid input_collid, List **args_p, > bool funcvariadic, bool process_args, bool allow_non_const, > + FuncExpr *orig_funcexpr, > eval_const_expressions_context *context) > { > List *args = *args_p; The new argument needs to be explained in the comment above. > ! <para> > ! The special table function <literal>UNNEST</literal> may be called with > ! any number of array parameters, and returns a corresponding number of > ! columns, as if <literal>UNNEST</literal> > ! (see <xref linkend="functions-array">) had been called on each parameter > ! separately and combined using the <literal>TABLE</literal> construct. The > ! number of result columns is determined by the sum of the arities of the > ! array element types; that is to say, any array of composite type is > ! expanded into separate result columns for each field of the type. > ! Likewise, the number of rows returned is determined by the largest array > ! parameter, with smaller values padded with NULLs. > ! </para> "Arities", really? :-). Please reword that into more plain English. I'm going to put this aside for a day or two now, but will get back to it later to finish the review and commit (unless someone beats me to it). Meanwhile, if you could do something about that comment and manual paragraph above, and re-review the changes I made, that would be great. - Heikki
Attachment
>>>>> "Heikki" == Heikki Linnakangas <hlinnakangas@vmware.com> writes: Heikki> I've spent some time reviewing this patch - looks prettyHeikki> good! I'm not through yet, but I wanted to post anHeikki>update. Attached is a new version, with some modifications IHeikki> made. Notably: Heikki> I refactored the grammar a bit And broke the ability to do TABLE(unnest(a,b,c), otherfunc(d)). Yes, you can still do TABLE(unnest(a), unnest(b), unnest(c), otherfunc(d)) but I view this as a significant loss of functionality; I will see about fixing that. I am still looking at the other changes. -- Andrew (irc:RhodiumToad)
Here is a new patch with the following changes on top of Heikki's version (all the changes in which I've otherwise kept): 1. Changed TupleDescCopyEntry parameter order to be (dest,src) for better consistency with TupleDescInitEntry and general C style. 2. Removed CreateTupleDescCopyExtend which is now dead code 3. Some small cleanups in the building of tupdescs in executor init 4. Refactored the grammar further to reinstant multi-arg unnest inside table(), and added regression tests for that 5. comment and doc changes requested in Heikki's message -- Andrew (irc:RhodiumToad)
Attachment
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > Here is a new patch with the following changes on top of Heikki's > version (all the changes in which I've otherwise kept): Here is an updated version: 1. Rebased against HEAD. 2. New code is pgindent'ed (mainly because most of the rebasing pain was because the previous ORDINALITY patch hadn't been pgindented before committing) and is now git diff --check clean. 3. Fixed build failure due to recent change of make_pathkey_from_sortinfo's API. I don't think that what I did here is actually right --- if we're keeping build_expression_pathkey, then probably it needs to expose nullable_relids as a parameter. But this will at least make the thing compilable pending review of that point. This compiles clean and passes regression tests, but I've not done any actual reviewing yet. regards, tom lane diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c index ea930af..8e37ecd 100644 *** a/contrib/pg_stat_statements/pg_stat_statements.c --- b/contrib/pg_stat_statements/pg_stat_statements.c *************** JumbleRangeTable(pgssJumbleState *jstate *** 1456,1462 **** APP_JUMB(rte->jointype); break; case RTE_FUNCTION: ! JumbleExpr(jstate, rte->funcexpr); break; case RTE_VALUES: JumbleExpr(jstate, (Node *) rte->values_lists); --- 1456,1462 ---- APP_JUMB(rte->jointype); break; case RTE_FUNCTION: ! JumbleExpr(jstate, (Node *) rte->funcexprs); break; case RTE_VALUES: JumbleExpr(jstate, (Node *) rte->values_lists); diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index a5c808e..d3f773d 100644 *** a/doc/src/sgml/func.sgml --- b/doc/src/sgml/func.sgml *************** SELECT NULLIF(value, '(none)') ... *** 11185,11190 **** --- 11185,11206 ---- <entry><literallayout class="monospaced">1 2</literallayout>(2 rows)</entry> </row> + <row> + <entry> + <literal> + <function>unnest</function>(<type>anyarray</type>, <type>anyarray</type>[,...]) + </literal> + </entry> + <entry><type>setof anyelement, anyelement[,...]</type></entry> + <entry>expand more than one array to a set of rows. This is + only allowed in the FROM clause, and the input arrays can be + of different types. For more on table(), which forms the basis of <function>unnest</function> + with multiple arrays, see <xref linkend="queries-tablefunctions"></entry> + <entry><literal>unnest(ARRAY[1,2],ARRAY['foo','bar','baz'])</literal></entry> + <entry><literallayout class="monospaced">1 foo + 2 bar + NULL baz</literallayout>(3 rows)</entry> + </row> </tbody> </tgroup> </table> *************** AND *** 13294,13300 **** The most widely used functions in this class are series generating functions, as detailed in <xref linkend="functions-srf-series"> and <xref linkend="functions-srf-subscripts">. Other, more specialized ! set-returning functions are described elsewhere in this manual. </para> <table id="functions-srf-series"> --- 13310,13317 ---- The most widely used functions in this class are series generating functions, as detailed in <xref linkend="functions-srf-series"> and <xref linkend="functions-srf-subscripts">. Other, more specialized ! set-returning functions are described elsewhere in this manual. See <xref ! linkend="queries-tablefunctions"> for combining multiple set-returning functions. </para> <table id="functions-srf-series"> diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml index c32c857..1ccf753 100644 *** a/doc/src/sgml/queries.sgml --- b/doc/src/sgml/queries.sgml *************** FROM (VALUES ('anne', 'smith'), ('bob', *** 647,663 **** </para> <para> ! If a table function returns a base data type, the single result ! column name matches the function name. If the function returns a ! composite type, the result columns get the same names as the ! individual attributes of the type. </para> <para> ! A table function can be aliased in the <literal>FROM</> clause, ! but it also can be left unaliased. If a function is used in the ! <literal>FROM</> clause with no alias, the function name is used ! as the resulting table name. </para> <para> --- 647,706 ---- </para> <para> ! Table functions may also be combined using the <literal>TABLE</literal> ! syntax, with the results returned in parallel columns; the number of ! result rows in this case is that of the largest function result, with ! smaller results padded with NULLs to match. </para> <para> ! If the <literal>WITH ORDINALITY</literal> clause is specified, an ! additional column of type <type>bigint</type> will be appended to the ! function result columns, numbering the rows of the function result set ! starting from 1. (This is a generalization of the SQL-standard syntax ! for <literal>UNNEST ... WITH ORDINALITY</literal>.) The ordinal column ! may be named using a column alias as part of a ! following <literal>AS</literal> clause. ! </para> ! ! <synopsis> ! <replaceable>function_call</replaceable> <optional>WITH ORDINALITY</optional> <optional><optional>AS</optional> <replaceable>alias</replaceable><optional>(<replaceable>column_alias</replaceable> <optional>, <replaceable>column_alias</replaceable>... </optional>)</optional></optional> ! TABLE( <replaceable>function_call</replaceable> <optional>, <replaceable>function_call</replaceable> ... </optional> )<optional>WITH ORDINALITY</optional> <optional><optional>AS</optional> <replaceable>alias</replaceable> <optional>(<replaceable>column_alias</replaceable><optional>, <replaceable>column_alias</replaceable> ... </optional>)</optional></optional> ! </synopsis> ! ! <para> ! The special table function <literal>UNNEST</literal> may be called with ! any number of array parameters, and returns a corresponding number of ! columns, as if <literal>UNNEST</literal> ! (see <xref linkend="functions-array">) had been called on each parameter ! separately and combined using the <literal>TABLE</literal> construct. The ! number of result columns is determined by the sum of the number of columns ! in each individual array element type; that is to say, any array of ! composite type is expanded into separate result columns for each field of ! the type. Likewise, the number of rows returned is determined by the ! largest array parameter, with smaller values padded with NULLs. ! </para> ! ! <synopsis> ! UNNEST( <replaceable>array_parameter</replaceable> <optional>, <replaceable>array_parameter</replaceable> ... </optional>) <optional>WITH ORDINALITY</optional> <optional><optional>AS</optional> <replaceable>alias</replaceable> <optional>(<replaceable>column_alias</replaceable><optional>, <replaceable>column_alias</replaceable> ... </optional>)</optional></optional> ! </synopsis> ! ! <para> ! If a table function returns a base data type, the single result column ! name matches the function name. If the function returns a composite type, ! the result columns get the same names as the individual attributes of the ! type. These rules still apply if multiple functions are used with ! the <literal>TABLE</literal> construct. ! </para> ! ! <para> ! A table function can be aliased in the <literal>FROM</> clause, but it ! also can be left unaliased. If a function is used alone in the ! <literal>FROM</> clause with no alias, the function name is used as the ! resulting table name. If the <literal>TABLE</literal> construct is used ! without an alias, the resulting table name is ! <literal>"table"</literal>. The use of explicit aliases is strongly ! encouraged in this case. </para> <para> *************** SELECT * FROM vw_getfoo; *** 691,697 **** the pseudotype <type>record</>. When such a function is used in a query, the expected row structure must be specified in the query itself, so that the system can know how to parse and plan ! the query. Consider this example: <programlisting> SELECT * FROM dblink('dbname=mydb', 'SELECT proname, prosrc FROM pg_proc') --- 734,750 ---- the pseudotype <type>record</>. When such a function is used in a query, the expected row structure must be specified in the query itself, so that the system can know how to parse and plan ! the query. ! </para> ! ! <synopsis> ! <replaceable>function_call</replaceable> <optional>AS</optional> <replaceable>alias</replaceable> (<replaceable>column_definition</replaceable><optional>, <replaceable>column_definition</replaceable> ... </optional>) ! <replaceable>function_call</replaceable> AS <optional><replaceable>alias</replaceable></optional> (<replaceable>column_definition</replaceable><optional>, <replaceable>column_definition</replaceable> ... </optional>) ! TABLE( <replaceable>function_call</replaceable> AS (<replaceable>column_definition</replaceable> <optional>, <replaceable>column_definition</replaceable>... </optional>) <optional>, <replaceable>function_call</replaceable> AS (<replaceable>column_definition</replaceable><optional>, <replaceable>column_definition</replaceable> ... </optional>) ...</optional> ) <optional>WITH ORDINALITY</optional> <optional><optional>AS</optional> <replaceable>alias</replaceable><optional>(<replaceable>column_alias</replaceable> <optional>, <replaceable>column_alias</replaceable>... </optional>)</optional></optional> ! </synopsis> ! ! <para> ! Consider this example: <programlisting> SELECT * FROM dblink('dbname=mydb', 'SELECT proname, prosrc FROM pg_proc') diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index e603b76..ba299a1 100644 *** a/doc/src/sgml/ref/select.sgml --- b/doc/src/sgml/ref/select.sgml *************** SELECT [ ALL | DISTINCT [ ON ( <replacea *** 55,60 **** --- 55,63 ---- [ LATERAL ] <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable>[, ...] ] ) [ WITH ORDINALITY ] [ [ AS ] <replaceable class="parameter">alias</replaceable>[ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ] ] [ LATERAL ] <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable>[, ...] ] ) [ AS ] <replaceable class="parameter">alias</replaceable> ( <replaceableclass="parameter">column_definition</replaceable> [, ...] ) [ LATERAL ] <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable>[, ...] ] ) AS ( <replaceable class="parameter">column_definition</replaceable> [,...] ) + [ LATERAL ] TABLE( <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable>[, ...] ] ) [ AS ( <replaceable class="parameter">column_definition</replaceable>[, ...] ) ] + [, <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable>[, ...] ] ) [ AS ( <replaceable class="parameter">column_definition</replaceable>[, ...] ) ] [, ...] ] ) + [ WITH ORDINALITY ] [ [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable>[, ...] ) ] ] <replaceable class="parameter">from_item</replaceable> [ NATURAL ] <replaceable class="parameter">join_type</replaceable><replaceable class="parameter">from_item</replaceable> [ ON <replaceable class="parameter">join_condition</replaceable>| USING ( <replaceable class="parameter">join_column</replaceable> [, ...]) ] <phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase> diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c index bf47640..fecd10c 100644 *** a/src/backend/access/common/tupdesc.c --- b/src/backend/access/common/tupdesc.c *************** CreateTupleDescCopy(TupleDesc tupdesc) *** 158,197 **** } /* - * CreateTupleDescCopyExtend - * This function creates a new TupleDesc by copying from an existing - * TupleDesc, but adding space for more columns. The new tupdesc is - * not regarded as the same record type as the old one (and therefore - * does not inherit its typeid/typmod, which instead are left as an - * anonymous record type). - * - * The additional column slots are not initialized in any way; - * callers must do their own TupleDescInitEntry on each. - * - * !!! Constraints and defaults are not copied !!! - */ - TupleDesc - CreateTupleDescCopyExtend(TupleDesc tupdesc, int moreatts) - { - TupleDesc desc; - int i; - int src_natts = tupdesc->natts; - - Assert(moreatts >= 0); - - desc = CreateTemplateTupleDesc(src_natts + moreatts, tupdesc->tdhasoid); - - for (i = 0; i < src_natts; i++) - { - memcpy(desc->attrs[i], tupdesc->attrs[i], ATTRIBUTE_FIXED_PART_SIZE); - desc->attrs[i]->attnotnull = false; - desc->attrs[i]->atthasdef = false; - } - - return desc; - } - - /* * CreateTupleDescCopyConstr * This function creates a new TupleDesc by copying from an existing * TupleDesc (including its constraints and defaults). --- 158,163 ---- *************** TupleDescInitEntryCollation(TupleDesc de *** 564,569 **** --- 530,563 ---- desc->attrs[attributeNumber - 1]->attcollation = collationid; } + /* + * TupleDescCopyEntry + * This function copies a single attribute structure from one tuple + * descriptor to another. + * + * * !!! Constraints and defaults are not copied !!! + */ + void + TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno, + const TupleDesc src, AttrNumber srcAttno) + { + /* + * sanity checks + */ + AssertArg(PointerIsValid(src)); + AssertArg(PointerIsValid(dst)); + AssertArg(srcAttno >= 1); + AssertArg(srcAttno <= src->natts); + AssertArg(dstAttno >= 1); + AssertArg(dstAttno <= dst->natts); + + memcpy(dst->attrs[dstAttno - 1], src->attrs[srcAttno - 1], + ATTRIBUTE_FIXED_PART_SIZE); + dst->attrs[dstAttno - 1]->attnum = dstAttno; + dst->attrs[dstAttno - 1]->attcacheoff = -1; + dst->attrs[dstAttno - 1]->attnotnull = false; + dst->attrs[dstAttno - 1]->atthasdef = false; + } /* * BuildDescForRelation diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index fe17c96..2f0ab82 100644 *** a/src/backend/catalog/dependency.c --- b/src/backend/catalog/dependency.c *************** find_expr_references_walker(Node *node, *** 1601,1609 **** --- 1601,1630 ---- else if (IsA(node, FuncExpr)) { FuncExpr *funcexpr = (FuncExpr *) node; + ListCell *ct; add_object_address(OCLASS_PROC, funcexpr->funcid, 0, context->addrs); + + /* + * FuncExpr in a function RTE may have a column definition list, in + * which case deal with its types and collations + */ + foreach(ct, funcexpr->funccoltypes) + { + add_object_address(OCLASS_TYPE, lfirst_oid(ct), 0, + context->addrs); + } + foreach(ct, funcexpr->funccolcollations) + { + Oid collid = lfirst_oid(ct); + + if (OidIsValid(collid) && + collid != DEFAULT_COLLATION_OID) + add_object_address(OCLASS_COLLATION, collid, 0, + context->addrs); + } + /* fall through to examine arguments */ } else if (IsA(node, OpExpr)) *************** find_expr_references_walker(Node *node, *** 1757,1764 **** /* * Add whole-relation refs for each plain relation mentioned in the ! * subquery's rtable, as well as refs for any datatypes and collations ! * used in a RECORD function's output. * * Note: query_tree_walker takes care of recursing into RTE_FUNCTION * RTEs, subqueries, etc, so no need to do that here. But keep it --- 1778,1786 ---- /* * Add whole-relation refs for each plain relation mentioned in the ! * subquery's rtable. (Refs for any datatypes and collations used in ! * RECORD function column definitions lists are handled under ! * FuncExpr.) * * Note: query_tree_walker takes care of recursing into RTE_FUNCTION * RTEs, subqueries, etc, so no need to do that here. But keep it *************** find_expr_references_walker(Node *node, *** 1771,1777 **** foreach(lc, query->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); - ListCell *ct; switch (rte->rtekind) { --- 1793,1798 ---- *************** find_expr_references_walker(Node *node, *** 1779,1800 **** add_object_address(OCLASS_CLASS, rte->relid, 0, context->addrs); break; - case RTE_FUNCTION: - foreach(ct, rte->funccoltypes) - { - add_object_address(OCLASS_TYPE, lfirst_oid(ct), 0, - context->addrs); - } - foreach(ct, rte->funccolcollations) - { - Oid collid = lfirst_oid(ct); - - if (OidIsValid(collid) && - collid != DEFAULT_COLLATION_OID) - add_object_address(OCLASS_COLLATION, collid, 0, - context->addrs); - } - break; default: break; } --- 1800,1805 ---- diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 4e93df2..851d43a 100644 *** a/src/backend/commands/explain.c --- b/src/backend/commands/explain.c *************** ExplainNode(PlanState *planstate, List * *** 1259,1265 **** break; case T_FunctionScan: if (es->verbose) ! show_expression(((FunctionScan *) plan)->funcexpr, "Function Call", planstate, ancestors, es->verbose, es); show_scan_qual(plan->qual, "Filter", planstate, ancestors, es); --- 1259,1265 ---- break; case T_FunctionScan: if (es->verbose) ! show_expression((Node *) ((FunctionScan *) plan)->funcexprs, "Function Call", planstate, ancestors, es->verbose, es); show_scan_qual(plan->qual, "Filter", planstate, ancestors, es); *************** ExplainTargetRel(Plan *plan, Index rti, *** 1984,2004 **** break; case T_FunctionScan: { ! Node *funcexpr; /* Assert it's on a RangeFunction */ Assert(rte->rtekind == RTE_FUNCTION); /* ! * If the expression is still a function call, we can get the ! * real name of the function. Otherwise, punt (this can ! * happen if the optimizer simplified away the function call, ! * for example). */ ! funcexpr = ((FunctionScan *) plan)->funcexpr; ! if (funcexpr && IsA(funcexpr, FuncExpr)) { ! Oid funcid = ((FuncExpr *) funcexpr)->funcid; objectname = get_func_name(funcid); if (es->verbose) --- 1984,2005 ---- break; case T_FunctionScan: { ! FunctionScan *fscan = (FunctionScan *) plan; /* Assert it's on a RangeFunction */ Assert(rte->rtekind == RTE_FUNCTION); /* ! * If the expression is still a function call of a single ! * function, we can get the real name of the function. ! * Otherwise, punt. (even if it was a single function call ! * originally, the optimizer could have simplified it away) */ ! if (fscan->funcexprs && list_length(fscan->funcexprs) == 1 && ! IsA(linitial(fscan->funcexprs), FuncExpr)) { ! FuncExpr *funcexpr = linitial(fscan->funcexprs); ! Oid funcid = funcexpr->funcid; objectname = get_func_name(funcid); if (es->verbose) diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c index ff6a123..f216cd4 100644 *** a/src/backend/executor/functions.c --- b/src/backend/executor/functions.c *************** sql_fn_post_column_ref(ParseState *pstat *** 380,387 **** param = ParseFuncOrColumn(pstate, list_make1(subfield), list_make1(param), ! NIL, NULL, false, false, false, ! NULL, true, cref->location); } return param; --- 380,387 ---- param = ParseFuncOrColumn(pstate, list_make1(subfield), list_make1(param), ! cref->location, ! NULL); } return param; diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c index 423e02f..26f6c30 100644 *** a/src/backend/executor/nodeFunctionscan.c --- b/src/backend/executor/nodeFunctionscan.c *************** *** 27,32 **** --- 27,44 ---- #include "nodes/nodeFuncs.h" #include "catalog/pg_type.h" + /* + * Runtime data for each function being scanned. + */ + typedef struct FunctionScanPerFuncState + { + ExprState *funcexpr; /* state of the FuncExpr being evaluated */ + TupleDesc tupdesc; /* desc of the function result type */ + Tuplestorestate *tstore; /* holds the function result set */ + int64 rowcount; /* number of rows in tstore, -1 if not known */ + TupleTableSlot *func_slot; /* function result slot (or NULL) */ + } FunctionScanPerFuncState; + static TupleTableSlot *FunctionNext(FunctionScanState *node); /* ---------------------------------------------------------------- *************** FunctionNext(FunctionScanState *node) *** 44,119 **** { EState *estate; ScanDirection direction; - Tuplestorestate *tuplestorestate; TupleTableSlot *scanslot; ! TupleTableSlot *funcslot; ! ! if (node->func_slot) ! { ! /* ! * ORDINALITY case: ! * ! * We fetch the function result into FUNCSLOT (which matches the ! * function return type), and then copy the values to SCANSLOT ! * (which matches the scan result type), setting the ordinal ! * column in the process. ! */ ! ! funcslot = node->func_slot; ! scanslot = node->ss.ss_ScanTupleSlot; ! } ! else ! { ! /* ! * non-ORDINALITY case: the function return type and scan result ! * type are the same, so we fetch the function result straight ! * into the scan result slot. ! */ ! ! funcslot = node->ss.ss_ScanTupleSlot; ! scanslot = NULL; ! } /* * get information from the estate and scan state */ estate = node->ss.ps.state; direction = estate->es_direction; ! tuplestorestate = node->tuplestorestate; ! ! /* ! * If first time through, read all tuples from function and put them in a ! * tuplestore. Subsequent calls just fetch tuples from tuplestore. ! */ ! if (tuplestorestate == NULL) { ! node->tuplestorestate = tuplestorestate = ! ExecMakeTableFunctionResult(node->funcexpr, ! node->ss.ps.ps_ExprContext, ! node->func_tupdesc, ! node->eflags & EXEC_FLAG_BACKWARD); ! } ! ! /* ! * Get the next tuple from tuplestore. Return NULL if no more tuples. ! */ ! (void) tuplestore_gettupleslot(tuplestorestate, ! ScanDirectionIsForward(direction), ! false, ! funcslot); ! if (!scanslot) ! return funcslot; ! /* ! * we're doing ordinality, so we copy the values from the function return ! * slot to the (distinct) scan slot. We can do this because the lifetimes ! * of the values in each slot are the same; until we reset the scan or ! * fetch the next tuple, both will be valid. ! */ ! ExecClearTuple(scanslot); /* * increment or decrement before checking for end-of-data, so that we can --- 56,118 ---- { EState *estate; ScanDirection direction; TupleTableSlot *scanslot; ! bool alldone; ! int64 oldpos; ! int funcno; ! int att; /* * get information from the estate and scan state */ estate = node->ss.ps.state; direction = estate->es_direction; + scanslot = node->ss.ss_ScanTupleSlot; ! if (node->simple) { ! /* ! * Fast path for the trivial case: the function return type and scan ! * result type are the same, so we fetch the function result straight ! * into the scan result slot. No need to update ordinality or ! * rowcounts either. ! */ ! Tuplestorestate *tstore = node->funcstates[0].tstore; ! /* ! * If first time through, read all tuples from function and put them ! * in a tuplestore. Subsequent calls just fetch tuples from ! * tuplestore. ! */ ! if (tstore == NULL) ! { ! node->funcstates[0].tstore = tstore = ! ExecMakeTableFunctionResult(node->funcstates[0].funcexpr, ! node->ss.ps.ps_ExprContext, ! node->funcstates[0].tupdesc, ! node->eflags & EXEC_FLAG_BACKWARD); ! /* ! * paranoia - cope if the function, which may have constructed the ! * tuplestore itself, didn't leave it pointing at the start. This ! * call is fast, so the overhead shouldn't be an issue. ! */ ! tuplestore_rescan(tstore); ! } ! /* ! * Get the next tuple from tuplestore. ! * ! * If we have a rowcount for the function, and we know the previous ! * read position was out of bounds, don't try the read. This allows ! * backward scan to work when there are mixed row counts present. ! */ ! (void) tuplestore_gettupleslot(tstore, ! ScanDirectionIsForward(direction), ! false, ! scanslot); ! return scanslot; ! } /* * increment or decrement before checking for end-of-data, so that we can *************** FunctionNext(FunctionScanState *node) *** 121,151 **** * losing correct count. See PortalRunSelect for why we assume that we * won't be called repeatedly in the end-of-data state. */ ! if (ScanDirectionIsForward(direction)) ! node->ordinal++; else ! node->ordinal--; ! if (!TupIsNull(funcslot)) { ! int natts = funcslot->tts_tupleDescriptor->natts; ! int i; ! slot_getallattrs(funcslot); ! for (i = 0; i < natts; ++i) { ! scanslot->tts_values[i] = funcslot->tts_values[i]; ! scanslot->tts_isnull[i] = funcslot->tts_isnull[i]; } ! scanslot->tts_values[natts] = Int64GetDatumFast(node->ordinal); ! scanslot->tts_isnull[natts] = false; ! ExecStoreVirtualTuple(scanslot); } return scanslot; } --- 120,239 ---- * losing correct count. See PortalRunSelect for why we assume that we * won't be called repeatedly in the end-of-data state. */ ! oldpos = node->ordinal; if (ScanDirectionIsForward(direction)) ! oldpos = node->ordinal++; else ! oldpos = node->ordinal--; ! /* ! * Main loop over functions. ! * ! * We fetch the function results into FUNCSLOTs (which match the function ! * return types), and then copy the values to SCANSLOT (which matches the ! * scan result type), setting the ordinal column (if any) in the process. ! */ ! att = 0; ! alldone = true; ! ExecClearTuple(scanslot); ! for (funcno = 0; funcno < node->nfuncs; funcno++) { ! FunctionScanPerFuncState *fs = &node->funcstates[funcno]; ! int i; ! /* ! * If first time through, read all tuples from function and put them ! * in a tuplestore. Subsequent calls just fetch tuples from ! * tuplestore. ! */ ! if (fs->tstore == NULL) ! { ! fs->tstore = ! ExecMakeTableFunctionResult(fs->funcexpr, ! node->ss.ps.ps_ExprContext, ! fs->tupdesc, ! node->eflags & EXEC_FLAG_BACKWARD); ! /* ! * paranoia - cope if the function, which may have constructed the ! * tuplestore itself, didn't leave it pointing at the start. This ! * call is fast, so the overhead shouldn't be an issue. ! */ ! tuplestore_rescan(fs->tstore); ! } ! ! /* ! * Get the next tuple from tuplestore. ! * ! * If we have a rowcount for the function, and we know the previous ! * read position was out of bounds, don't try the read. This allows ! * backward scan to work when there are mixed row counts present. ! */ ! if (fs->rowcount != -1 && fs->rowcount < oldpos) ! ExecClearTuple(fs->func_slot); ! else ! (void) tuplestore_gettupleslot(fs->tstore, ! ScanDirectionIsForward(direction), ! false, ! fs->func_slot); ! ! if (TupIsNull(fs->func_slot)) { ! /* ! * If we ran out of data for this function in the forward ! * direction then we now know how many rows it returned. We need ! * to know this in order to handle backwards scans. The row count ! * we store is actually 1+ the actual number, because we have to ! * position the tuplestore 1 off its end sometimes. ! */ ! if (ScanDirectionIsForward(direction) && fs->rowcount == -1) ! fs->rowcount = node->ordinal; ! ! /* ! * populate our result cols with null ! */ ! for (i = 0; i < fs->tupdesc->natts; i++) ! { ! scanslot->tts_values[att] = (Datum) 0; ! scanslot->tts_isnull[att] = true; ! att++; ! } } + else + { + /* + * we have a result, so just copy it to the result cols. + */ ! slot_getallattrs(fs->func_slot); ! for (i = 0; i < fs->tupdesc->natts; i++) ! { ! scanslot->tts_values[att] = fs->func_slot->tts_values[i]; ! scanslot->tts_isnull[att] = fs->func_slot->tts_isnull[i]; ! att++; ! } ! ! /* ! * We're not done until every function result is exhausted; we pad ! * the shorter results with nulls until then. ! */ ! alldone = false; ! } ! } ! ! /* ! * ordinal col is always last, per spec. ! */ ! if (node->ordinality) ! { ! scanslot->tts_values[att] = Int64GetDatumFast(node->ordinal); ! scanslot->tts_isnull[att] = false; } + if (!alldone) + ExecStoreVirtualTuple(scanslot); + return scanslot; } *************** ExecInitFunctionScan(FunctionScan *node, *** 186,193 **** FunctionScanState *scanstate; Oid funcrettype; TypeFuncClass functypclass; - TupleDesc func_tupdesc = NULL; TupleDesc scan_tupdesc = NULL; /* check for unsupported flags */ Assert(!(eflags & EXEC_FLAG_MARK)); --- 274,284 ---- FunctionScanState *scanstate; Oid funcrettype; TypeFuncClass functypclass; TupleDesc scan_tupdesc = NULL; + int nfuncs = list_length(node->funcexprs); + int i, + natts; + ListCell *lc; /* check for unsupported flags */ Assert(!(eflags & EXEC_FLAG_MARK)); *************** ExecInitFunctionScan(FunctionScan *node, *** 207,212 **** --- 298,326 ---- scanstate->eflags = eflags; /* + * are we adding an ordinality column? + */ + scanstate->ordinality = node->funcordinality; + + scanstate->nfuncs = nfuncs; + if (nfuncs == 1 && !node->funcordinality) + scanstate->simple = true; + else + scanstate->simple = false; + + /* + * Ordinal 0 represents the "before the first row" position. + * + * We need to track ordinal position even when not adding an ordinality + * column to the result, in order to handle backwards scanning properly + * with multiple functions with different result sizes. (We can't position + * any individual function's tuplestore any more than 1 place beyond its + * end, so when scanning backwards, we need to know when to start + * including the function in the scan again.) + */ + scanstate->ordinal = 0; + + /* * Miscellaneous initialization * * create expression context for node *************** ExecInitFunctionScan(FunctionScan *node, *** 220,235 **** ExecInitScanTupleSlot(estate, &scanstate->ss); /* - * We only need a separate slot for the function result if we are doing - * ordinality; otherwise, we fetch function results directly into the - * scan slot. - */ - if (node->funcordinality) - scanstate->func_slot = ExecInitExtraTupleSlot(estate); - else - scanstate->func_slot = NULL; - - /* * initialize child expressions */ scanstate->ss.ps.targetlist = (List *) --- 334,339 ---- *************** ExecInitFunctionScan(FunctionScan *node, *** 239,351 **** ExecInitExpr((Expr *) node->scan.plan.qual, (PlanState *) scanstate); ! /* ! * Now determine if the function returns a simple or composite ! * type, and build an appropriate tupdesc. This tupdesc ! * (func_tupdesc) is the one that matches the shape of the ! * function result, no extra columns. ! */ ! functypclass = get_expr_result_type(node->funcexpr, ! &funcrettype, ! &func_tupdesc); ! if (functypclass == TYPEFUNC_COMPOSITE) { ! /* Composite data type, e.g. a table's row type */ ! Assert(func_tupdesc); /* ! * XXX ! * Existing behaviour is a bit inconsistent with regard to aliases and ! * whole-row Vars of the function result. If the function returns a ! * composite type, then the whole-row Var will refer to this tupdesc, ! * which has the type's own column names rather than the alias column ! * names given in the query. This affects the output of constructs like ! * row_to_json which read the column names from the passed-in values. */ ! /* Must copy it out of typcache for safety */ ! func_tupdesc = CreateTupleDescCopy(func_tupdesc); ! } ! else if (functypclass == TYPEFUNC_SCALAR) ! { ! /* Base data type, i.e. scalar */ ! char *attname = strVal(linitial(node->funccolnames)); ! func_tupdesc = CreateTemplateTupleDesc(1, false); ! TupleDescInitEntry(func_tupdesc, ! (AttrNumber) 1, ! attname, ! funcrettype, ! -1, ! 0); ! TupleDescInitEntryCollation(func_tupdesc, ! (AttrNumber) 1, ! exprCollation(node->funcexpr)); ! } ! else if (functypclass == TYPEFUNC_RECORD) ! { ! func_tupdesc = BuildDescFromLists(node->funccolnames, ! node->funccoltypes, ! node->funccoltypmods, ! node->funccolcollations); ! } ! else ! { ! /* crummy error message, but parser should have caught this */ ! elog(ERROR, "function in FROM has unsupported return type"); ! } ! /* ! * For RECORD results, make sure a typmod has been assigned. (The ! * function should do this for itself, but let's cover things in case it ! * doesn't.) ! */ ! BlessTupleDesc(func_tupdesc); /* ! * If doing ordinality, we need a new tupdesc with one additional column ! * tacked on, always of type "bigint". The name to use has already been ! * recorded by the parser as the last element of funccolnames. * ! * Without ordinality, the scan result tupdesc is the same as the ! * function result tupdesc. (No need to make a copy.) */ ! if (node->funcordinality) { ! int natts = func_tupdesc->natts; ! scan_tupdesc = CreateTupleDescCopyExtend(func_tupdesc, 1); ! TupleDescInitEntry(scan_tupdesc, ! natts + 1, ! strVal(llast(node->funccolnames)), ! INT8OID, ! -1, ! 0); ! BlessTupleDesc(scan_tupdesc); ! } ! else ! scan_tupdesc = func_tupdesc; ! scanstate->scan_tupdesc = scan_tupdesc; ! scanstate->func_tupdesc = func_tupdesc; ! ExecAssignScanType(&scanstate->ss, scan_tupdesc); ! if (scanstate->func_slot) ! ExecSetSlotDescriptor(scanstate->func_slot, func_tupdesc); /* ! * Other node-specific setup */ ! scanstate->ordinal = 0; ! scanstate->tuplestorestate = NULL; ! scanstate->funcexpr = ExecInitExpr((Expr *) node->funcexpr, ! (PlanState *) scanstate); ! scanstate->ss.ps.ps_TupFromTlist = false; /* * Initialize result tuple type and projection info. --- 343,495 ---- ExecInitExpr((Expr *) node->scan.plan.qual, (PlanState *) scanstate); ! scanstate->funcstates = palloc(nfuncs * sizeof(FunctionScanPerFuncState)); ! i = 0; ! natts = 0; ! ! foreach(lc, node->funcexprs) { ! Expr *funcexpr = (Expr *) lfirst(lc); ! FunctionScanPerFuncState *fs = &scanstate->funcstates[i]; ! TupleDesc tupdesc; ! ! fs->funcexpr = ExecInitExpr(funcexpr, (PlanState *) scanstate); /* ! * don't allocate the tuplestores; the actual call to the function ! * does that. NULL flags that we have not called the function yet (or ! * need to call it again after a rescan). */ + fs->tstore = NULL; + fs->rowcount = -1; ! /* ! * Determine if this function returns a simple or composite type, and ! * build an appropriate tupdesc. This tupdesc is the one that matches ! * the shape of the function result, no extra columns. ! */ ! functypclass = get_expr_result_type((Node *) funcexpr, ! &funcrettype, ! &tupdesc); ! if (functypclass == TYPEFUNC_COMPOSITE) ! { ! /* Composite data type, e.g. a table's row type */ ! Assert(tupdesc); ! /* ! * XXX Existing behaviour is a bit inconsistent with regard to ! * aliases and whole-row Vars of the function result. If the ! * function returns a composite type, then the whole-row Var will ! * refer to this tupdesc, which has the type's own column names ! * rather than the alias column names given in the query. This ! * affects the output of constructs like row_to_json which read ! * the column names from the passed-in values. ! */ ! ! /* Must copy it out of typcache for safety (?) */ ! tupdesc = CreateTupleDescCopy(tupdesc); ! ! natts += tupdesc->natts; ! } ! else if (functypclass == TYPEFUNC_SCALAR) ! { ! /* Base data type, i.e. scalar */ ! char *attname = strVal(list_nth(node->funccolnames, natts)); ! ! tupdesc = CreateTemplateTupleDesc(1, false); ! TupleDescInitEntry(tupdesc, ! (AttrNumber) 1, ! attname, ! funcrettype, ! -1, ! 0); ! TupleDescInitEntryCollation(tupdesc, ! (AttrNumber) 1, ! exprCollation((Node *) funcexpr)); ! natts++; ! } ! else ! { ! /* crummy error message, but parser should have caught this */ ! elog(ERROR, "function in FROM has unsupported return type"); ! } ! fs->tupdesc = tupdesc; ! ! /* ! * We only need separate slots for the function results if we are ! * doing ordinality or multiple functions; otherwise, we fetch ! * function results directly into the scan slot. ! */ ! if (!scanstate->simple) ! { ! fs->func_slot = ExecInitExtraTupleSlot(estate); ! ExecSetSlotDescriptor(fs->func_slot, fs->tupdesc); ! } ! else ! fs->func_slot = NULL; ! ! i++; ! } /* ! * Create the combined TupleDesc * ! * If there is just one function without ordinality, the scan result ! * tupdesc is the same as the function result tupdesc. (No need to make a ! * copy.) */ ! if (scanstate->simple) { ! scan_tupdesc = scanstate->funcstates[0].tupdesc; ! } ! else ! { ! AttrNumber attno = 0; ! if (node->funcordinality) ! natts++; ! scan_tupdesc = CreateTemplateTupleDesc(natts, false); ! for (i = 0; i < nfuncs; i++) ! { ! TupleDesc tupdesc = scanstate->funcstates[i].tupdesc; ! int j; ! for (j = 1; j <= tupdesc->natts; j++) ! TupleDescCopyEntry(scan_tupdesc, ++attno, tupdesc, j); ! } ! if (node->funcordinality) ! { ! /* ! * If doing ordinality, add a column of type "bigint" at the end. ! * The column name to use has already been recorded by the parser ! * as the last element of funccolnames. ! */ ! TupleDescInitEntry(scan_tupdesc, ! ++attno, ! strVal(llast(node->funccolnames)), ! INT8OID, ! -1, ! 0); ! } ! ! Assert(attno == natts); ! } /* ! * We didn't necessarily bless all the individual function tupdescs, but ! * we have to ensure that the scan result tupdesc is, regardless of where ! * it came from. */ ! BlessTupleDesc(scan_tupdesc); ! scanstate->scan_tupdesc = scan_tupdesc; ! ExecAssignScanType(&scanstate->ss, scan_tupdesc); /* * Initialize result tuple type and projection info. *************** ExecInitFunctionScan(FunctionScan *node, *** 353,358 **** --- 497,504 ---- ExecAssignResultTypeFromTL(&scanstate->ss.ps); ExecAssignScanProjectionInfo(&scanstate->ss); + scanstate->ss.ps.ps_TupFromTlist = false; + return scanstate; } *************** ExecInitFunctionScan(FunctionScan *node, *** 365,370 **** --- 511,518 ---- void ExecEndFunctionScan(FunctionScanState *node) { + int i; + /* * Free the exprcontext */ *************** ExecEndFunctionScan(FunctionScanState *n *** 375,389 **** */ ExecClearTuple(node->ss.ps.ps_ResultTupleSlot); ExecClearTuple(node->ss.ss_ScanTupleSlot); - if (node->func_slot) - ExecClearTuple(node->func_slot); /* ! * Release tuplestore resources */ ! if (node->tuplestorestate != NULL) ! tuplestore_end(node->tuplestorestate); ! node->tuplestorestate = NULL; } /* ---------------------------------------------------------------- --- 523,545 ---- */ ExecClearTuple(node->ss.ps.ps_ResultTupleSlot); ExecClearTuple(node->ss.ss_ScanTupleSlot); /* ! * Release slots and tuplestore resources */ ! for (i = 0; i < node->nfuncs; i++) ! { ! FunctionScanPerFuncState *fs = &node->funcstates[i]; ! ! if (fs->func_slot) ! ExecClearTuple(fs->func_slot); ! ! if (fs->tstore != NULL) ! { ! tuplestore_end(node->funcstates[i].tstore); ! fs->tstore = NULL; ! } ! } } /* ---------------------------------------------------------------- *************** ExecEndFunctionScan(FunctionScanState *n *** 395,425 **** void ExecReScanFunctionScan(FunctionScanState *node) { ExecClearTuple(node->ss.ps.ps_ResultTupleSlot); ! if (node->func_slot) ! ExecClearTuple(node->func_slot); ExecScanReScan(&node->ss); node->ordinal = 0; /* ! * If we haven't materialized yet, just return. */ ! if (!node->tuplestorestate) ! return; ! /* ! * Here we have a choice whether to drop the tuplestore (and recompute the ! * function outputs) or just rescan it. We must recompute if the ! * expression contains parameters, else we rescan. XXX maybe we should ! * recompute if the function is volatile? ! */ ! if (node->ss.ps.chgParam != NULL) { ! tuplestore_end(node->tuplestorestate); ! node->tuplestorestate = NULL; } - else - tuplestore_rescan(node->tuplestorestate); } --- 551,607 ---- void ExecReScanFunctionScan(FunctionScanState *node) { + FunctionScan *scan = (FunctionScan *) node->ss.ps.plan; + int i; + Bitmapset *chgparam = node->ss.ps.chgParam; + ExecClearTuple(node->ss.ps.ps_ResultTupleSlot); ! for (i = 0; i < node->nfuncs; ++i) ! { ! FunctionScanPerFuncState *fs = &node->funcstates[i]; ! ! if (fs->func_slot) ! ExecClearTuple(fs->func_slot); ! } ExecScanReScan(&node->ss); node->ordinal = 0; /* ! * Here we have a choice whether to drop the tuplestores (and recompute ! * the function outputs) or just rescan them. We must recompute if the ! * expression contains changed parameters, else we rescan. ! * ! * Note that if chgparam is NULL, it's possible that the funcparams list ! * may be empty (if there never were any params and so finalize_plan was ! * never called), so we have to be careful about iterating or referencing ! * it. ! * ! * XXX maybe we should recompute if the function is volatile? */ ! if (chgparam) ! { ! ListCell *lc; ! i = 0; ! foreach(lc, scan->funcparams) ! { ! if (bms_overlap(chgparam, lfirst(lc))) ! { ! if (node->funcstates[i].tstore != NULL) ! { ! tuplestore_end(node->funcstates[i].tstore); ! node->funcstates[i].tstore = NULL; ! } ! node->funcstates[i].rowcount = -1; ! } ! i++; ! } ! } ! for (i = 0; i < node->nfuncs; ++i) { ! if (node->funcstates[i].tstore != NULL) ! tuplestore_rescan(node->funcstates[i].tstore); } } diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 1733da6..2859d51 100644 *** a/src/backend/nodes/copyfuncs.c --- b/src/backend/nodes/copyfuncs.c *************** static FunctionScan * *** 495,500 **** --- 495,501 ---- _copyFunctionScan(const FunctionScan *from) { FunctionScan *newnode = makeNode(FunctionScan); + ListCell *lc; /* * copy node superclass fields *************** _copyFunctionScan(const FunctionScan *fr *** 504,516 **** /* * copy remainder of node */ ! COPY_NODE_FIELD(funcexpr); COPY_NODE_FIELD(funccolnames); - COPY_NODE_FIELD(funccoltypes); - COPY_NODE_FIELD(funccoltypmods); - COPY_NODE_FIELD(funccolcollations); COPY_SCALAR_FIELD(funcordinality); return newnode; } --- 505,522 ---- /* * copy remainder of node */ ! COPY_NODE_FIELD(funcexprs); COPY_NODE_FIELD(funccolnames); COPY_SCALAR_FIELD(funcordinality); + /* + * copy the param bitmap list by shallow-copying the list structure, then + * replacing the values with copies + */ + newnode->funcparams = list_copy(from->funcparams); + foreach(lc, newnode->funcparams) + lfirst(lc) = bms_copy(lfirst(lc)); + return newnode; } *************** _copyFuncExpr(const FuncExpr *from) *** 1206,1211 **** --- 1212,1221 ---- COPY_SCALAR_FIELD(funccollid); COPY_SCALAR_FIELD(inputcollid); COPY_NODE_FIELD(args); + COPY_NODE_FIELD(funccolnames); + COPY_NODE_FIELD(funccoltypes); + COPY_NODE_FIELD(funccoltypmods); + COPY_NODE_FIELD(funccolcollations); COPY_LOCATION_FIELD(location); return newnode; *************** _copyRangeTblEntry(const RangeTblEntry * *** 1981,1990 **** COPY_SCALAR_FIELD(security_barrier); COPY_SCALAR_FIELD(jointype); COPY_NODE_FIELD(joinaliasvars); ! COPY_NODE_FIELD(funcexpr); ! COPY_NODE_FIELD(funccoltypes); ! COPY_NODE_FIELD(funccoltypmods); ! COPY_NODE_FIELD(funccolcollations); COPY_SCALAR_FIELD(funcordinality); COPY_NODE_FIELD(values_lists); COPY_NODE_FIELD(values_collations); --- 1991,1997 ---- COPY_SCALAR_FIELD(security_barrier); COPY_SCALAR_FIELD(jointype); COPY_NODE_FIELD(joinaliasvars); ! COPY_NODE_FIELD(funcexprs); COPY_SCALAR_FIELD(funcordinality); COPY_NODE_FIELD(values_lists); COPY_NODE_FIELD(values_collations); *************** _copyFuncCall(const FuncCall *from) *** 2175,2180 **** --- 2182,2188 ---- COPY_SCALAR_FIELD(agg_distinct); COPY_SCALAR_FIELD(func_variadic); COPY_NODE_FIELD(over); + COPY_NODE_FIELD(coldeflist); COPY_LOCATION_FIELD(location); return newnode; *************** _copyRangeFunction(const RangeFunction * *** 2301,2309 **** COPY_SCALAR_FIELD(ordinality); COPY_SCALAR_FIELD(lateral); ! COPY_NODE_FIELD(funccallnode); COPY_NODE_FIELD(alias); - COPY_NODE_FIELD(coldeflist); return newnode; } --- 2309,2317 ---- COPY_SCALAR_FIELD(ordinality); COPY_SCALAR_FIELD(lateral); ! COPY_SCALAR_FIELD(is_table); ! COPY_NODE_FIELD(funccallnodes); COPY_NODE_FIELD(alias); return newnode; } diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 7b29812..ef77f97 100644 *** a/src/backend/nodes/equalfuncs.c --- b/src/backend/nodes/equalfuncs.c *************** _equalFuncExpr(const FuncExpr *a, const *** 248,253 **** --- 248,257 ---- COMPARE_SCALAR_FIELD(funccollid); COMPARE_SCALAR_FIELD(inputcollid); COMPARE_NODE_FIELD(args); + COMPARE_NODE_FIELD(funccolnames); + COMPARE_NODE_FIELD(funccoltypes); + COMPARE_NODE_FIELD(funccoltypmods); + COMPARE_NODE_FIELD(funccolcollations); COMPARE_LOCATION_FIELD(location); return true; *************** _equalFuncCall(const FuncCall *a, const *** 2016,2021 **** --- 2020,2026 ---- COMPARE_SCALAR_FIELD(agg_distinct); COMPARE_SCALAR_FIELD(func_variadic); COMPARE_NODE_FIELD(over); + COMPARE_NODE_FIELD(coldeflist); COMPARE_LOCATION_FIELD(location); return true; *************** _equalRangeFunction(const RangeFunction *** 2142,2150 **** { COMPARE_SCALAR_FIELD(ordinality); COMPARE_SCALAR_FIELD(lateral); ! COMPARE_NODE_FIELD(funccallnode); COMPARE_NODE_FIELD(alias); - COMPARE_NODE_FIELD(coldeflist); return true; } --- 2147,2155 ---- { COMPARE_SCALAR_FIELD(ordinality); COMPARE_SCALAR_FIELD(lateral); ! COMPARE_SCALAR_FIELD(is_table); ! COMPARE_NODE_FIELD(funccallnodes); COMPARE_NODE_FIELD(alias); return true; } *************** _equalRangeTblEntry(const RangeTblEntry *** 2245,2254 **** COMPARE_SCALAR_FIELD(security_barrier); COMPARE_SCALAR_FIELD(jointype); COMPARE_NODE_FIELD(joinaliasvars); ! COMPARE_NODE_FIELD(funcexpr); ! COMPARE_NODE_FIELD(funccoltypes); ! COMPARE_NODE_FIELD(funccoltypmods); ! COMPARE_NODE_FIELD(funccolcollations); COMPARE_SCALAR_FIELD(funcordinality); COMPARE_NODE_FIELD(values_lists); COMPARE_NODE_FIELD(values_collations); --- 2250,2256 ---- COMPARE_SCALAR_FIELD(security_barrier); COMPARE_SCALAR_FIELD(jointype); COMPARE_NODE_FIELD(joinaliasvars); ! COMPARE_NODE_FIELD(funcexprs); COMPARE_SCALAR_FIELD(funcordinality); COMPARE_NODE_FIELD(values_lists); COMPARE_NODE_FIELD(values_collations); diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 4a7e793..90beca2 100644 *** a/src/backend/nodes/makefuncs.c --- b/src/backend/nodes/makefuncs.c *************** makeVarFromTargetEntry(Index varno, *** 127,135 **** * the function's result directly, instead of the single-column composite * value that the whole-row notation might otherwise suggest. * ! * We also handle the specific case of function RTEs with ordinality, ! * where the additional column has to be added. This forces the result ! * to be composite and RECORD type. */ Var * makeWholeRowVar(RangeTblEntry *rte, --- 127,135 ---- * the function's result directly, instead of the single-column composite * value that the whole-row notation might otherwise suggest. * ! * We also handle the specific case of function RTEs with ordinality or ! * multiple function calls. This forces the result to be composite and RECORD ! * type. */ Var * makeWholeRowVar(RangeTblEntry *rte, *************** makeWholeRowVar(RangeTblEntry *rte, *** 157,179 **** break; case RTE_FUNCTION: /* ! * RTE is a function with or without ordinality. We map the ! * cases as follows: * ! * If ordinality is set, we return a composite var even if ! * the function is a scalar. This var is always of RECORD type. * ! * If ordinality is not set but the function returns a row, ! * we keep the function's return type. * * If the function is a scalar, we do what allowScalar requests. */ ! toid = exprType(rte->funcexpr); ! if (rte->funcordinality) { ! /* ORDINALITY always produces an anonymous RECORD result */ result = makeVar(varno, InvalidAttrNumber, RECORDOID, --- 157,183 ---- break; case RTE_FUNCTION: + /* ! * RTE is a function with or without ordinality. We map the cases ! * as follows: * ! * If ordinality is set, we return a composite var even if the ! * function is a scalar. This var is always of RECORD type. * ! * If the RTE has more than one function, we return a composite ! * var of record type. ! * ! * If ordinality is not set but the function returns a row, we ! * keep the function's return type. * * If the function is a scalar, we do what allowScalar requests. */ ! toid = exprType(linitial(rte->funcexprs)); ! if (rte->funcordinality || list_length(rte->funcexprs) > 1) { ! /* always produces an anonymous RECORD result */ result = makeVar(varno, InvalidAttrNumber, RECORDOID, *************** makeWholeRowVar(RangeTblEntry *rte, *** 198,204 **** 1, toid, -1, ! exprCollation(rte->funcexpr), varlevelsup); } else --- 202,208 ---- 1, toid, -1, ! exprCollation(linitial(rte->funcexprs)), varlevelsup); } else *************** makeFuncExpr(Oid funcid, Oid rettype, Li *** 494,499 **** --- 498,507 ---- funcexpr->funccollid = funccollid; funcexpr->inputcollid = inputcollid; funcexpr->args = args; + funcexpr->funccolnames = NIL; + funcexpr->funccoltypes = NIL; + funcexpr->funccoltypmods = NIL; + funcexpr->funccolcollations = NIL; funcexpr->location = -1; return funcexpr; *************** makeDefElemExtended(char *nameSpace, cha *** 549,555 **** FuncCall * makeFuncCall(List *name, List *args, int location) { ! FuncCall *n = makeNode(FuncCall); n->funcname = name; n->args = args; n->location = location; --- 557,564 ---- FuncCall * makeFuncCall(List *name, List *args, int location) { ! FuncCall *n = makeNode(FuncCall); ! n->funcname = name; n->args = args; n->location = location; *************** makeFuncCall(List *name, List *args, int *** 559,563 **** --- 568,573 ---- n->agg_distinct = FALSE; n->func_variadic = FALSE; n->over = NULL; + n->coldeflist = NULL; return n; } diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 908f397..e0113ef 100644 *** a/src/backend/nodes/nodeFuncs.c --- b/src/backend/nodes/nodeFuncs.c *************** range_table_walker(List *rtable, *** 2000,2006 **** return true; break; case RTE_FUNCTION: ! if (walker(rte->funcexpr, context)) return true; break; case RTE_VALUES: --- 2000,2006 ---- return true; break; case RTE_FUNCTION: ! if (walker(rte->funcexprs, context)) return true; break; case RTE_VALUES: *************** range_table_mutator(List *rtable, *** 2725,2731 **** } break; case RTE_FUNCTION: ! MUTATE(newrte->funcexpr, rte->funcexpr, Node *); break; case RTE_VALUES: MUTATE(newrte->values_lists, rte->values_lists, List *); --- 2725,2731 ---- } break; case RTE_FUNCTION: ! MUTATE(newrte->funcexprs, rte->funcexprs, List *); break; case RTE_VALUES: MUTATE(newrte->values_lists, rte->values_lists, List *); *************** raw_expression_tree_walker(Node *node, *** 3035,3040 **** --- 3035,3045 ---- if (walker(fcall->over, context)) return true; /* function name is deemed uninteresting */ + + /* + * RangeFunction doesn't recurse into coldeflist so we don't + * either + */ } break; case T_NamedArgExpr: *************** raw_expression_tree_walker(Node *node, *** 3113,3119 **** { RangeFunction *rf = (RangeFunction *) node; ! if (walker(rf->funccallnode, context)) return true; if (walker(rf->alias, context)) return true; --- 3118,3124 ---- { RangeFunction *rf = (RangeFunction *) node; ! if (walker(rf->funccallnodes, context)) return true; if (walker(rf->alias, context)) return true; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index b39927e..30156b0 100644 *** a/src/backend/nodes/outfuncs.c --- b/src/backend/nodes/outfuncs.c *************** _outSubqueryScan(StringInfo str, const S *** 512,527 **** static void _outFunctionScan(StringInfo str, const FunctionScan *node) { WRITE_NODE_TYPE("FUNCTIONSCAN"); _outScanInfo(str, (const Scan *) node); ! WRITE_NODE_FIELD(funcexpr); WRITE_NODE_FIELD(funccolnames); - WRITE_NODE_FIELD(funccoltypes); - WRITE_NODE_FIELD(funccoltypmods); - WRITE_NODE_FIELD(funccolcollations); WRITE_BOOL_FIELD(funcordinality); } static void --- 512,533 ---- static void _outFunctionScan(StringInfo str, const FunctionScan *node) { + ListCell *lc; + WRITE_NODE_TYPE("FUNCTIONSCAN"); _outScanInfo(str, (const Scan *) node); ! WRITE_NODE_FIELD(funcexprs); WRITE_NODE_FIELD(funccolnames); WRITE_BOOL_FIELD(funcordinality); + + appendStringInfoString(str, " :funcparams"); + foreach(lc, node->funcparams) + { + appendStringInfoChar(str, ' '); + _outBitmapset(str, lfirst(lc)); + } } static void *************** _outFuncExpr(StringInfo str, const FuncE *** 1012,1017 **** --- 1018,1027 ---- WRITE_OID_FIELD(funccollid); WRITE_OID_FIELD(inputcollid); WRITE_NODE_FIELD(args); + WRITE_NODE_FIELD(funccolnames); + WRITE_NODE_FIELD(funccoltypes); + WRITE_NODE_FIELD(funccoltypmods); + WRITE_NODE_FIELD(funccolcollations); WRITE_LOCATION_FIELD(location); } *************** _outFuncCall(StringInfo str, const FuncC *** 2092,2097 **** --- 2102,2108 ---- WRITE_BOOL_FIELD(agg_distinct); WRITE_BOOL_FIELD(func_variadic); WRITE_NODE_FIELD(over); + WRITE_NODE_FIELD(coldeflist); WRITE_LOCATION_FIELD(location); } *************** _outRangeTblEntry(StringInfo str, const *** 2382,2391 **** WRITE_NODE_FIELD(joinaliasvars); break; case RTE_FUNCTION: ! WRITE_NODE_FIELD(funcexpr); ! WRITE_NODE_FIELD(funccoltypes); ! WRITE_NODE_FIELD(funccoltypmods); ! WRITE_NODE_FIELD(funccolcollations); WRITE_BOOL_FIELD(funcordinality); break; case RTE_VALUES: --- 2393,2399 ---- WRITE_NODE_FIELD(joinaliasvars); break; case RTE_FUNCTION: ! WRITE_NODE_FIELD(funcexprs); WRITE_BOOL_FIELD(funcordinality); break; case RTE_VALUES: *************** _outRangeFunction(StringInfo str, const *** 2621,2629 **** WRITE_BOOL_FIELD(ordinality); WRITE_BOOL_FIELD(lateral); ! WRITE_NODE_FIELD(funccallnode); WRITE_NODE_FIELD(alias); - WRITE_NODE_FIELD(coldeflist); } static void --- 2629,2637 ---- WRITE_BOOL_FIELD(ordinality); WRITE_BOOL_FIELD(lateral); ! WRITE_BOOL_FIELD(is_table); ! WRITE_NODE_FIELD(funccallnodes); WRITE_NODE_FIELD(alias); } static void diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index d325bb3..c0f16bf 100644 *** a/src/backend/nodes/readfuncs.c --- b/src/backend/nodes/readfuncs.c *************** _readFuncExpr(void) *** 562,567 **** --- 562,571 ---- READ_OID_FIELD(funccollid); READ_OID_FIELD(inputcollid); READ_NODE_FIELD(args); + READ_NODE_FIELD(funccolnames); + READ_NODE_FIELD(funccoltypes); + READ_NODE_FIELD(funccoltypmods); + READ_NODE_FIELD(funccolcollations); READ_LOCATION_FIELD(location); READ_DONE(); *************** _readRangeTblEntry(void) *** 1220,1229 **** READ_NODE_FIELD(joinaliasvars); break; case RTE_FUNCTION: ! READ_NODE_FIELD(funcexpr); ! READ_NODE_FIELD(funccoltypes); ! READ_NODE_FIELD(funccoltypmods); ! READ_NODE_FIELD(funccolcollations); READ_BOOL_FIELD(funcordinality); break; case RTE_VALUES: --- 1224,1230 ---- READ_NODE_FIELD(joinaliasvars); break; case RTE_FUNCTION: ! READ_NODE_FIELD(funcexprs); READ_BOOL_FIELD(funcordinality); break; case RTE_VALUES: diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index bfd3809..a22dac0 100644 *** a/src/backend/optimizer/path/allpaths.c --- b/src/backend/optimizer/path/allpaths.c *************** *** 37,42 **** --- 37,43 ---- #include "parser/parsetree.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" + #include "catalog/pg_opfamily.h" /* These parameters are set by GUC */ *************** static void *** 1258,1263 **** --- 1259,1265 ---- set_function_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte) { Relids required_outer; + List *pathkeys = NIL; /* * We don't support pushing join clauses into the quals of a function *************** set_function_pathlist(PlannerInfo *root, *** 1266,1273 **** */ required_outer = rel->lateral_relids; /* Generate appropriate path */ ! add_path(rel, create_functionscan_path(root, rel, required_outer)); /* Select cheapest path (pretty easy in this case...) */ set_cheapest(rel); --- 1268,1325 ---- */ required_outer = rel->lateral_relids; + /* + * The result is treated as unordered unless ORDINALITY was used, in which + * case it is ordered by the ordinal column (last). + */ + if (rte->funcordinality) + { + ListCell *lc; + Var *var = NULL; + AttrNumber ordattno = list_length(rte->eref->colnames); + + /* + * Find corresponding Var in our tlist by searching for matching + * attno. + */ + + foreach(lc, rel->reltargetlist) + { + Var *node = lfirst(lc); + + if (IsA(node, Var) + &&node->varno == rel->relid + && node->varattno == ordattno + && node->varlevelsup == 0) + { + var = node; + break; + } + } + + /* + * The Var might not be found in the tlist, but that should only + * happen if the ordinality column is never referenced anywhere in the + * query - in which case nobody can possibly care about the ordering + * of it. So just leave the pathkeys NIL in that case. + * + * Also, build_expression_pathkey will only build the pathkey if + * there's already an equivalence class; if there isn't, it indicates + * that nothing cares about the ordering. + */ + + if (var) + { + Oid opno = get_opfamily_member(INTEGER_BTREE_FAM_OID, + var->vartype, var->vartype, + BTLessStrategyNumber); + + pathkeys = build_expression_pathkey(root, rel, (Expr *) var, opno, false); + } + } + /* Generate appropriate path */ ! add_path(rel, create_functionscan_path(root, rel, pathkeys, required_outer)); /* Select cheapest path (pretty easy in this case...) */ set_cheapest(rel); diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index e7f8cec..9a1588e 100644 *** a/src/backend/optimizer/path/costsize.c --- b/src/backend/optimizer/path/costsize.c *************** cost_functionscan(Path *path, PlannerInf *** 1088,1094 **** * estimates for functions tend to be, there's not a lot of point in that * refinement right now. */ ! cost_qual_eval_node(&exprcost, rte->funcexpr, root); startup_cost += exprcost.startup + exprcost.per_tuple; --- 1088,1094 ---- * estimates for functions tend to be, there's not a lot of point in that * refinement right now. */ ! cost_qual_eval_node(&exprcost, (Node *) rte->funcexprs, root); startup_cost += exprcost.startup + exprcost.per_tuple; *************** void *** 3845,3858 **** set_function_size_estimates(PlannerInfo *root, RelOptInfo *rel) { RangeTblEntry *rte; /* Should only be applied to base relations that are functions */ Assert(rel->relid > 0); rte = planner_rt_fetch(rel->relid, root); Assert(rte->rtekind == RTE_FUNCTION); ! /* Estimate number of rows the function itself will return */ ! rel->tuples = expression_returns_set_rows(rte->funcexpr); /* Now estimate number of output rows, etc */ set_baserel_size_estimates(root, rel); --- 3845,3870 ---- set_function_size_estimates(PlannerInfo *root, RelOptInfo *rel) { RangeTblEntry *rte; + ListCell *lc; /* Should only be applied to base relations that are functions */ Assert(rel->relid > 0); rte = planner_rt_fetch(rel->relid, root); Assert(rte->rtekind == RTE_FUNCTION); ! rel->tuples = 0; ! ! /* ! * Estimate number of rows the functions will return. The rowcount of ! * result of the node is that of the largest function result. ! */ ! foreach(lc, rte->funcexprs) ! { ! double ntup = expression_returns_set_rows(lfirst(lc)); ! ! if (ntup > rel->tuples) ! rel->tuples = ntup; ! } /* Now estimate number of output rows, etc */ set_baserel_size_estimates(root, rel); diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 032b2cd..9f9ec7d 100644 *** a/src/backend/optimizer/path/pathkeys.c --- b/src/backend/optimizer/path/pathkeys.c *************** build_index_pathkeys(PlannerInfo *root, *** 501,506 **** --- 501,557 ---- return retval; } + + /* + * build_expression_pathkey + * Build a pathkeys list (empty or 1 element) that describes an ordering + * of a single expression using a given operator + * + * The result is empty if the expression isn't already in some equivalence + * class. + * + * The main use for this is in declaring the ordering of ordinality + * columns in FunctionScan, but it's written this way to avoid making any + * assumptions about type. + */ + + List * + build_expression_pathkey(PlannerInfo *root, + RelOptInfo *rel, + Expr *expr, + Oid opno, + bool nulls_first) + { + List *pathkeys = NIL; + Oid opfamily, + opcintype; + int16 strategy; + PathKey *cpathkey; + + /* Find the operator in pg_amop --- failure shouldn't happen */ + if (!get_ordering_op_properties(opno, + &opfamily, &opcintype, &strategy)) + elog(ERROR, "operator %u is not a valid ordering operator", + opno); + + cpathkey = make_pathkey_from_sortinfo(root, + expr, + NULL, /* XXX likely wrong */ + opfamily, + opcintype, + exprCollation((Node *) expr), + (strategy == BTGreaterStrategyNumber), + nulls_first, + 0, + rel->relids, + false); + + if (cpathkey) + pathkeys = lappend(pathkeys, cpathkey); + + return pathkeys; + } + /* * convert_subquery_pathkeys * Build a pathkeys list that describes the ordering of a subquery's diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 5947e5b..622fb37 100644 *** a/src/backend/optimizer/plan/createplan.c --- b/src/backend/optimizer/plan/createplan.c *************** static BitmapHeapScan *make_bitmap_heaps *** 115,123 **** static TidScan *make_tidscan(List *qptlist, List *qpqual, Index scanrelid, List *tidquals); static FunctionScan *make_functionscan(List *qptlist, List *qpqual, ! Index scanrelid, Node *funcexpr, bool ordinality, ! List *funccolnames, List *funccoltypes, List *funccoltypmods, ! List *funccolcollations); static ValuesScan *make_valuesscan(List *qptlist, List *qpqual, Index scanrelid, List *values_lists); static CteScan *make_ctescan(List *qptlist, List *qpqual, --- 115,122 ---- static TidScan *make_tidscan(List *qptlist, List *qpqual, Index scanrelid, List *tidquals); static FunctionScan *make_functionscan(List *qptlist, List *qpqual, ! Index scanrelid, List *funcexprs, bool ordinality, ! List *funccolnames); static ValuesScan *make_valuesscan(List *qptlist, List *qpqual, Index scanrelid, List *values_lists); static CteScan *make_ctescan(List *qptlist, List *qpqual, *************** create_functionscan_plan(PlannerInfo *ro *** 1709,1721 **** FunctionScan *scan_plan; Index scan_relid = best_path->parent->relid; RangeTblEntry *rte; ! Node *funcexpr; /* it should be a function base rel... */ Assert(scan_relid > 0); rte = planner_rt_fetch(scan_relid, root); Assert(rte->rtekind == RTE_FUNCTION); ! funcexpr = rte->funcexpr; /* Sort clauses into best execution order */ scan_clauses = order_qual_clauses(root, scan_clauses); --- 1708,1720 ---- FunctionScan *scan_plan; Index scan_relid = best_path->parent->relid; RangeTblEntry *rte; ! List *funcexprs; /* it should be a function base rel... */ Assert(scan_relid > 0); rte = planner_rt_fetch(scan_relid, root); Assert(rte->rtekind == RTE_FUNCTION); ! funcexprs = rte->funcexprs; /* Sort clauses into best execution order */ scan_clauses = order_qual_clauses(root, scan_clauses); *************** create_functionscan_plan(PlannerInfo *ro *** 1729,1744 **** scan_clauses = (List *) replace_nestloop_params(root, (Node *) scan_clauses); /* The func expression itself could contain nestloop params, too */ ! funcexpr = replace_nestloop_params(root, funcexpr); } scan_plan = make_functionscan(tlist, scan_clauses, scan_relid, ! funcexpr, rte->funcordinality, ! rte->eref->colnames, ! rte->funccoltypes, ! rte->funccoltypmods, ! rte->funccolcollations); copy_path_costsize(&scan_plan->scan.plan, best_path); --- 1728,1740 ---- scan_clauses = (List *) replace_nestloop_params(root, (Node *) scan_clauses); /* The func expression itself could contain nestloop params, too */ ! funcexprs = (List *) replace_nestloop_params(root, (Node *) funcexprs); } scan_plan = make_functionscan(tlist, scan_clauses, scan_relid, ! funcexprs, rte->funcordinality, ! rte->eref->colnames); copy_path_costsize(&scan_plan->scan.plan, best_path); *************** static FunctionScan * *** 3388,3399 **** make_functionscan(List *qptlist, List *qpqual, Index scanrelid, ! Node *funcexpr, bool ordinality, ! List *funccolnames, ! List *funccoltypes, ! List *funccoltypmods, ! List *funccolcollations) { FunctionScan *node = makeNode(FunctionScan); Plan *plan = &node->scan.plan; --- 3384,3392 ---- make_functionscan(List *qptlist, List *qpqual, Index scanrelid, ! List *funcexprs, bool ordinality, ! List *funccolnames) { FunctionScan *node = makeNode(FunctionScan); Plan *plan = &node->scan.plan; *************** make_functionscan(List *qptlist, *** 3404,3415 **** plan->lefttree = NULL; plan->righttree = NULL; node->scan.scanrelid = scanrelid; ! node->funcexpr = funcexpr; node->funcordinality = ordinality; node->funccolnames = funccolnames; ! node->funccoltypes = funccoltypes; ! node->funccoltypmods = funccoltypmods; ! node->funccolcollations = funccolcollations; return node; } --- 3397,3407 ---- plan->lefttree = NULL; plan->righttree = NULL; node->scan.scanrelid = scanrelid; ! node->funcexprs = funcexprs; node->funcordinality = ordinality; node->funccolnames = funccolnames; ! /* finalize_plan will fill this in if need be */ ! node->funcparams = NIL; return node; } diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 04a399e..633fffa 100644 *** a/src/backend/optimizer/plan/initsplan.c --- b/src/backend/optimizer/plan/initsplan.c *************** extract_lateral_references(PlannerInfo * *** 307,313 **** if (rte->rtekind == RTE_SUBQUERY) vars = pull_vars_of_level((Node *) rte->subquery, 1); else if (rte->rtekind == RTE_FUNCTION) ! vars = pull_vars_of_level(rte->funcexpr, 0); else if (rte->rtekind == RTE_VALUES) vars = pull_vars_of_level((Node *) rte->values_lists, 0); else --- 307,313 ---- if (rte->rtekind == RTE_SUBQUERY) vars = pull_vars_of_level((Node *) rte->subquery, 1); else if (rte->rtekind == RTE_FUNCTION) ! vars = pull_vars_of_level((Node *) rte->funcexprs, 0); else if (rte->rtekind == RTE_VALUES) vars = pull_vars_of_level((Node *) rte->values_lists, 0); else diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index d8aa35d..7f39d13 100644 *** a/src/backend/optimizer/plan/planner.c --- b/src/backend/optimizer/plan/planner.c *************** subquery_planner(PlannerGlobal *glob, Qu *** 487,493 **** { /* Preprocess the function expression fully */ kind = rte->lateral ? EXPRKIND_RTFUNC_LATERAL : EXPRKIND_RTFUNC; ! rte->funcexpr = preprocess_expression(root, rte->funcexpr, kind); } else if (rte->rtekind == RTE_VALUES) { --- 487,493 ---- { /* Preprocess the function expression fully */ kind = rte->lateral ? EXPRKIND_RTFUNC_LATERAL : EXPRKIND_RTFUNC; ! rte->funcexprs = (List *) preprocess_expression(root, (Node *) rte->funcexprs, kind); } else if (rte->rtekind == RTE_VALUES) { diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index b78d727..ae72a7c 100644 *** a/src/backend/optimizer/plan/setrefs.c --- b/src/backend/optimizer/plan/setrefs.c *************** add_rte_to_flat_rtable(PlannerGlobal *gl *** 381,390 **** /* zap unneeded sub-structure */ newrte->subquery = NULL; newrte->joinaliasvars = NIL; ! newrte->funcexpr = NULL; ! newrte->funccoltypes = NIL; ! newrte->funccoltypmods = NIL; ! newrte->funccolcollations = NIL; newrte->values_lists = NIL; newrte->values_collations = NIL; newrte->ctecoltypes = NIL; --- 381,387 ---- /* zap unneeded sub-structure */ newrte->subquery = NULL; newrte->joinaliasvars = NIL; ! newrte->funcexprs = NULL; newrte->values_lists = NIL; newrte->values_collations = NIL; newrte->ctecoltypes = NIL; *************** set_plan_refs(PlannerInfo *root, Plan *p *** 525,532 **** fix_scan_list(root, splan->scan.plan.targetlist, rtoffset); splan->scan.plan.qual = fix_scan_list(root, splan->scan.plan.qual, rtoffset); ! splan->funcexpr = ! fix_scan_expr(root, splan->funcexpr, rtoffset); } break; case T_ValuesScan: --- 522,529 ---- fix_scan_list(root, splan->scan.plan.targetlist, rtoffset); splan->scan.plan.qual = fix_scan_list(root, splan->scan.plan.qual, rtoffset); ! splan->funcexprs = ! fix_scan_list(root, splan->funcexprs, rtoffset); } break; case T_ValuesScan: diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index 0df70c4..64b1519 100644 *** a/src/backend/optimizer/plan/subselect.c --- b/src/backend/optimizer/plan/subselect.c *************** finalize_plan(PlannerInfo *root, Plan *p *** 2135,2143 **** break; case T_FunctionScan: ! finalize_primnode(((FunctionScan *) plan)->funcexpr, ! &context); ! context.paramids = bms_add_members(context.paramids, scan_params); break; case T_ValuesScan: --- 2135,2173 ---- break; case T_FunctionScan: ! { ! finalize_primnode_context funccontext; ! FunctionScan *fscan = (FunctionScan *) plan; ! ListCell *lc; ! ! /* ! * Call finalize_primnode independently on each funcexpr so ! * that we can record which params are referenced in each, in ! * order to decide which need re-evaluating. ! */ ! ! funccontext = context; ! ! Assert(fscan->funcparams == NIL); ! ! foreach(lc, fscan->funcexprs) ! { ! funccontext.paramids = NULL; ! ! finalize_primnode(lfirst(lc), &funccontext); ! ! /* add the function's params to the overall set */ ! context.paramids = bms_add_members(context.paramids, ! funccontext.paramids); ! ! /* hand off the function's params to the node's list */ ! fscan->funcparams = lappend(fscan->funcparams, ! funccontext.paramids); ! } ! ! context.paramids = bms_add_members(context.paramids, ! scan_params); ! } break; case T_ValuesScan: diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index c742cc9..ec0a852 100644 *** a/src/backend/optimizer/prep/prepjointree.c --- b/src/backend/optimizer/prep/prepjointree.c *************** inline_set_returning_functions(PlannerIn *** 580,589 **** /* Successful expansion, replace the rtable entry */ rte->rtekind = RTE_SUBQUERY; rte->subquery = funcquery; ! rte->funcexpr = NULL; ! rte->funccoltypes = NIL; ! rte->funccoltypmods = NIL; ! rte->funccolcollations = NIL; } } } --- 580,586 ---- /* Successful expansion, replace the rtable entry */ rte->rtekind = RTE_SUBQUERY; rte->subquery = funcquery; ! rte->funcexprs = NULL; } } } *************** replace_vars_in_jointree(Node *jtnode, *** 1623,1630 **** context); break; case RTE_FUNCTION: ! rte->funcexpr = ! pullup_replace_vars(rte->funcexpr, context); break; case RTE_VALUES: --- 1620,1627 ---- context); break; case RTE_FUNCTION: ! rte->funcexprs = (List *) ! pullup_replace_vars((Node *) rte->funcexprs, context); break; case RTE_VALUES: diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7ce8a9d..54e295e 100644 *** a/src/backend/optimizer/util/clauses.c --- b/src/backend/optimizer/util/clauses.c *************** static Expr *simplify_function(Oid funci *** 111,116 **** --- 111,117 ---- Oid result_type, int32 result_typmod, Oid result_collid, Oid input_collid, List **args_p, bool funcvariadic, bool process_args, bool allow_non_const, + FuncExpr *orig_funcexpr, eval_const_expressions_context *context); static List *expand_function_arguments(List *args, Oid result_type, HeapTuple func_tuple); *************** eval_const_expressions_mutator(Node *nod *** 2359,2364 **** --- 2360,2366 ---- expr->funcvariadic, true, true, + expr, context); if (simple) /* successfully simplified it */ return (Node *) simple; *************** eval_const_expressions_mutator(Node *nod *** 2378,2383 **** --- 2380,2389 ---- newexpr->funccollid = expr->funccollid; newexpr->inputcollid = expr->inputcollid; newexpr->args = args; + newexpr->funccolnames = expr->funccolnames; + newexpr->funccoltypes = expr->funccoltypes; + newexpr->funccoltypmods = expr->funccoltypmods; + newexpr->funccolcollations = expr->funccolcollations; newexpr->location = expr->location; return (Node *) newexpr; } *************** eval_const_expressions_mutator(Node *nod *** 2406,2411 **** --- 2412,2418 ---- false, true, true, + NULL, context); if (simple) /* successfully simplified it */ return (Node *) simple; *************** eval_const_expressions_mutator(Node *nod *** 2510,2515 **** --- 2517,2523 ---- false, false, false, + NULL, context); if (simple) /* successfully simplified it */ { *************** eval_const_expressions_mutator(Node *nod *** 2714,2719 **** --- 2722,2728 ---- false, true, true, + NULL, context); if (simple) /* successfully simplified output fn */ { *************** eval_const_expressions_mutator(Node *nod *** 2746,2751 **** --- 2755,2761 ---- false, false, true, + NULL, context); if (simple) /* successfully simplified input fn */ return (Node *) simple; *************** simplify_boolean_equality(Oid opno, List *** 3597,3603 **** * polymorphic functions), result typmod, result collation, the input * collation to use for the function, the original argument list (not * const-simplified yet, unless process_args is false), and some flags; ! * also the context data for eval_const_expressions. * * Returns a simplified expression if successful, or NULL if cannot * simplify the function call. --- 3607,3616 ---- * polymorphic functions), result typmod, result collation, the input * collation to use for the function, the original argument list (not * const-simplified yet, unless process_args is false), and some flags; ! * also the context data for eval_const_expressions. The original funcexpr, ! * if there was one, is passed in too so that fields of FuncExpr which are ! * not interesting for simplification are nonetheless available to the ! * transform function; currently that applies to the coldef list fields. * * Returns a simplified expression if successful, or NULL if cannot * simplify the function call. *************** static Expr * *** 3614,3619 **** --- 3627,3633 ---- simplify_function(Oid funcid, Oid result_type, int32 result_typmod, Oid result_collid, Oid input_collid, List **args_p, bool funcvariadic, bool process_args, bool allow_non_const, + FuncExpr *orig_funcexpr, eval_const_expressions_context *context) { List *args = *args_p; *************** simplify_function(Oid funcid, Oid result *** 3679,3684 **** --- 3693,3712 ---- fexpr.funccollid = result_collid; fexpr.inputcollid = input_collid; fexpr.args = args; + if (orig_funcexpr) + { + fexpr.funccolnames = orig_funcexpr->funccolnames; + fexpr.funccoltypes = orig_funcexpr->funccoltypes; + fexpr.funccoltypmods = orig_funcexpr->funccoltypmods; + fexpr.funccolcollations = orig_funcexpr->funccolcollations; + } + else + { + fexpr.funccolnames = NIL; + fexpr.funccoltypes = NIL; + fexpr.funccoltypmods = NIL; + fexpr.funccolcollations = NIL; + } fexpr.location = -1; newexpr = (Expr *) *************** inline_set_returning_function(PlannerInf *** 4541,4551 **** if (rte->funcordinality) return NULL; ! /* Fail if FROM item isn't a simple FuncExpr */ ! fexpr = (FuncExpr *) rte->funcexpr; ! if (fexpr == NULL || !IsA(fexpr, FuncExpr)) return NULL; func_oid = fexpr->funcid; /* --- 4569,4581 ---- if (rte->funcordinality) return NULL; ! /* Fail if FROM item isn't a simple, single, FuncExpr */ ! if (list_length(rte->funcexprs) != 1 ! || !IsA(linitial(rte->funcexprs), FuncExpr)) return NULL; + fexpr = (FuncExpr *) linitial(rte->funcexprs); + func_oid = fexpr->funcid; /* *************** inline_set_returning_function(PlannerInf *** 4726,4740 **** /* * If it returns RECORD, we have to check against the column type list ! * provided in the RTE; check_sql_fn_retval can't do that. (If no match, ! * we just fail to inline, rather than complaining; see notes for ! * tlist_matches_coltypelist.) We don't have to do this for functions ! * with declared OUT parameters, even though their funcresulttype is ! * RECORDOID, so check get_func_result_type too. */ if (fexpr->funcresulttype == RECORDOID && get_func_result_type(func_oid, NULL, NULL) == TYPEFUNC_RECORD && ! !tlist_matches_coltypelist(querytree->targetList, rte->funccoltypes)) goto fail; /* --- 4756,4770 ---- /* * If it returns RECORD, we have to check against the column type list ! * provided in the FuncExpr (used to be in the RTE); check_sql_fn_retval ! * can't do that. (If no match, we just fail to inline, rather than ! * complaining; see notes for tlist_matches_coltypelist.) We don't have to ! * do this for functions with declared OUT parameters, even though their ! * funcresulttype is RECORDOID, so check get_func_result_type too. */ if (fexpr->funcresulttype == RECORDOID && get_func_result_type(func_oid, NULL, NULL) == TYPEFUNC_RECORD && ! !tlist_matches_coltypelist(querytree->targetList, fexpr->funccoltypes)) goto fail; /* diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 64b1705..55b9f56 100644 *** a/src/backend/optimizer/util/pathnode.c --- b/src/backend/optimizer/util/pathnode.c *************** create_subqueryscan_path(PlannerInfo *ro *** 1623,1628 **** --- 1623,1629 ---- */ Path * create_functionscan_path(PlannerInfo *root, RelOptInfo *rel, + List *pathkeys, Relids required_outer) { Path *pathnode = makeNode(Path); *************** create_functionscan_path(PlannerInfo *ro *** 1631,1637 **** pathnode->parent = rel; pathnode->param_info = get_baserel_parampathinfo(root, rel, required_outer); ! pathnode->pathkeys = NIL; /* for now, assume unordered result */ cost_functionscan(pathnode, root, rel, pathnode->param_info); --- 1632,1638 ---- pathnode->parent = rel; pathnode->param_info = get_baserel_parampathinfo(root, rel, required_outer); ! pathnode->pathkeys = pathkeys; cost_functionscan(pathnode, root, rel, pathnode->param_info); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 11f6291..8906b44 100644 *** a/src/backend/parser/gram.y --- b/src/backend/parser/gram.y *************** static void doNegateFloat(Value *v); *** 153,158 **** --- 153,159 ---- static Node *makeAArrayExpr(List *elements, int location); static Node *makeXmlExpr(XmlExprOp op, char *name, List *named_args, List *args, int location); + static void processTableFuncColdef(RangeFunction *n, List *coldeflist, int location, core_yyscan_t yyscanner); static List *mergeTableFuncParameters(List *func_args, List *columns); static TypeName *TableFuncTypeName(List *columns); static RangeVar *makeRangeVarFromAnyName(List *names, int position, core_yyscan_t yyscanner); *************** static Node *makeRecursiveViewSelect(cha *** 404,411 **** %type <defelt> def_elem reloption_elem old_aggr_elem %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el ! columnref in_expr having_clause func_table array_expr ExclusionWhereClause %type <list> ExclusionConstraintList ExclusionConstraintElem %type <list> func_arg_list %type <node> func_arg_expr --- 405,417 ---- %type <defelt> def_elem reloption_elem old_aggr_elem %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el ! columnref in_expr having_clause array_expr ExclusionWhereClause + %type <list> func_table_def + %type <list> func_table_list + %type <list> func_table_single + %type <list> opt_col_def_list + %type <boolean> opt_ordinality %type <list> ExclusionConstraintList ExclusionConstraintElem %type <list> func_arg_list %type <node> func_arg_expr *************** static Node *makeRecursiveViewSelect(cha *** 420,426 **** %type <list> func_alias_clause %type <sortby> sortby %type <ielem> index_elem ! %type <node> table_ref %type <jexpr> joined_table %type <range> relation_expr %type <range> relation_expr_opt_alias --- 426,432 ---- %type <list> func_alias_clause %type <sortby> sortby %type <ielem> index_elem ! %type <node> table_ref func_table_ref %type <jexpr> joined_table %type <range> relation_expr %type <range> relation_expr_opt_alias *************** from_list: *** 9638,9690 **** | from_list ',' table_ref { $$ = lappend($1, $3); } ; /* * table_ref is where an alias clause can be attached. */ table_ref: relation_expr opt_alias_clause { $1->alias = $2; $$ = (Node *) $1; } ! | func_table func_alias_clause ! { ! RangeFunction *n = makeNode(RangeFunction); ! n->lateral = false; ! n->ordinality = false; ! n->funccallnode = $1; ! n->alias = linitial($2); ! n->coldeflist = lsecond($2); ! $$ = (Node *) n; ! } ! | func_table WITH_ORDINALITY func_alias_clause ! { ! RangeFunction *n = makeNode(RangeFunction); ! n->lateral = false; ! n->ordinality = true; ! n->funccallnode = $1; ! n->alias = linitial($3); ! n->coldeflist = lsecond($3); ! $$ = (Node *) n; ! } ! | LATERAL_P func_table func_alias_clause { ! RangeFunction *n = makeNode(RangeFunction); ! n->lateral = true; ! n->ordinality = false; ! n->funccallnode = $2; ! n->alias = linitial($3); ! n->coldeflist = lsecond($3); ! $$ = (Node *) n; } ! | LATERAL_P func_table WITH_ORDINALITY func_alias_clause { ! RangeFunction *n = makeNode(RangeFunction); ! n->lateral = true; ! n->ordinality = true; ! n->funccallnode = $2; ! n->alias = linitial($4); ! n->coldeflist = lsecond($4); ! $$ = (Node *) n; } | select_with_parens opt_alias_clause { --- 9644,9672 ---- | from_list ',' table_ref { $$ = lappend($1, $3); } ; + opt_ordinality: WITH_ORDINALITY { $$ = true; } + | /*EMPTY*/ { $$ = false; } + ; + /* * table_ref is where an alias clause can be attached. + * + * func_table is a list whose first element is a list of FuncCall nodes, + * and which has a second element iff the TABLE() syntax was used. */ table_ref: relation_expr opt_alias_clause { $1->alias = $2; $$ = (Node *) $1; } ! | func_table_ref { ! $$ = (Node *) $1; } ! | LATERAL_P func_table_ref { ! ((RangeFunction *) $2)->lateral = true; ! $$ = $2; } | select_with_parens opt_alias_clause { *************** table_ref: relation_expr opt_alias_claus *** 9757,9762 **** --- 9739,9793 ---- } ; + /*---------- + * func_table_ref represents a function invocation in FROM list. It can be a + * plain function call, like "FROM foo(...)", or a TABLE expression with one + * or more function calls, "FROM TABLE (foo(...), bar(...))". + * + * In the TABLE syntax, a column definition list can be given for each + * function. For example: + * + * FROM TABLE (foo() AS (foo_res_a text, foo_res_b text), + * bar() AS (bar_res_a text, bar_res_b text)) AS alias + * + * In the plain function call syntax, a column definition list can be given + * like this: + * + * FROM foo() AS alias (foo_res_a text, foo_res_b text) + * + * For backwards-compatibility, that is also allowed with the TABLE syntax, + * if the TABLE-expression contains only one function call: + * + * FROM TABLE(foo()) AS alias (foo_res_a text, foo_res_b text)" + * + *---------- + */ + func_table_ref: func_table_single opt_ordinality func_alias_clause + { + RangeFunction *n = makeNode(RangeFunction); + + n->ordinality = $2; + n->is_table = false; + n->funccallnodes = $1; + n->alias = linitial($3); + + processTableFuncColdef(n, lsecond($3), @3, yyscanner); + + $$ = (Node *) n; + } + | TABLE '(' func_table_list ')' opt_ordinality func_alias_clause + { + RangeFunction *n = makeNode(RangeFunction); + n->ordinality = $5; + n->is_table = true; + n->funccallnodes = $3; + n->alias = linitial($6); + + processTableFuncColdef(n, lsecond($6), @3, yyscanner); + + $$ = (Node *) n; + } + ; /* * It may seem silly to separate joined_table from table_ref, but there is *************** relation_expr_opt_alias: relation_expr *** 9996,10004 **** } ; ! func_table: func_expr_windowless { $$ = $1; } ; where_clause: WHERE a_expr { $$ = $2; } --- 10027,10127 ---- } ; ! func_table_list: func_table_def { $$ = $1; } ! | func_table_list ',' func_table_def { $$ = list_concat($1, $3); } ! ; ! ! func_table_def: func_table_single opt_col_def_list ! { ! if (list_length($2) > 0) ! { ! List *l = $1; ! ! if (list_length(l) > 1) ! ereport(ERROR, ! (errcode(ERRCODE_SYNTAX_ERROR), ! errmsg("a column definition list is not allowed for unnest with multiple arguments"), ! errhint("Use separate unnest calls with one argument each"), ! parser_errposition(@2))); ! ! /* ! * A column definition list is only allowed for functions ! * returning 'record', but that is checked in later, in ! * parse analysis, as we don't know the result type yet. ! * We do check that it's a regular FuncCall, though. ! * (it could be some other expression that looks like ! * a function call but is handled as a separate expression ! * type, e.g COALESCE) ! */ ! if (!IsA(linitial(l), FuncCall)) ! ereport(ERROR, ! (errcode(ERRCODE_SYNTAX_ERROR), ! errmsg("a column definition list is not allowed for this expression"), ! parser_errposition(@2))); ! ! ((FuncCall *) linitial(l))->coldeflist = $2; ! } ! $$ = $1; ! } ; + /* + * All table function calls in FROM come through here so that we can do the + * expansion of unnest(). + */ + func_table_single: func_expr_windowless + { + /*---------- + * Handle the spec's UNNEST syntax, by transforming + * + * UNNEST(a, b, ...) + * + * into + * + * TABLE (UNNEST(a), UNNEST(b), ...) + * + * We handle this here, rather than directly as grammar + * rules, to avoid interfering with the use of unnest() + * as a plain SRF in other contexts. Ugly, but effective. + * + * Note, strcmp not pg_strcasecmp, identifiers have + * already been casefolded. + *---------- + */ + if (IsA($1, FuncCall) && + list_length(((FuncCall *) $1)->funcname) == 1 && + strcmp(strVal(linitial(((FuncCall *) $1)->funcname)), "unnest") == 0) + { + FuncCall *fc = (FuncCall *) $1; + List *funccalls = NIL; + ListCell *lc; + + if (fc->agg_order != NIL || fc->func_variadic || fc->agg_star || fc->agg_distinct) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid syntax for unnest()"), + parser_errposition(@1))); + + foreach(lc, fc->args) + { + funccalls = lappend(funccalls, + makeFuncCall(SystemFuncName("unnest"), + list_make1(lfirst(lc)), + fc->location)); + } + + $$ = funccalls; + } + else + { + $$ = list_make1($1); + } + } + ; + + opt_col_def_list: AS '(' TableFuncElementList ')' { $$ = $3; } + | /*EMPTY*/ { $$ = NIL; } + ; where_clause: WHERE a_expr { $$ = $2; } *************** makeXmlExpr(XmlExprOp op, char *name, Li *** 13396,13401 **** --- 13519,13569 ---- } /* + * Support the old column definition list syntax, either when TABLE() was + * not used, or when TABLE(func()) was used with only one function. + * + * This handles pushing the coldeflist down into the function call node. + */ + static void + processTableFuncColdef(RangeFunction *n, List *coldeflist, + int location, core_yyscan_t yyscanner) + { + /* + * coldeflist is allowed only for exactly one function; if more than one, + * then the coldeflist must be applied inside TABLE() not outside. + */ + if (coldeflist != NIL) + { + FuncCall *fn = linitial(n->funccallnodes); + + if (list_length(n->funccallnodes) > 1) + { + if (n->is_table) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("a column definition list is not allowed for TABLE() with multiple functions"), + errhint("Give column definition lists for individual functions inside TABLE()"), + parser_errposition(location))); + } + + if (!IsA(fn, FuncCall)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("a column definition list is not allowed for this expression"), + parser_errposition(location))); + + if (fn->coldeflist != NIL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("multiple column definition lists are not allowed for the same function"), + errhint("remove one of the definition lists"), + parser_errposition(location))); + + fn->coldeflist = coldeflist; + } + } + + /* * Merge the input and output parameters of a table function. */ static List * diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 7a1261d..64b69bc 100644 *** a/src/backend/parser/parse_clause.c --- b/src/backend/parser/parse_clause.c *************** *** 24,29 **** --- 24,30 ---- #include "optimizer/tlist.h" #include "parser/analyze.h" #include "parser/parsetree.h" + #include "parser/parser.h" #include "parser/parse_clause.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" *************** transformRangeSubselect(ParseState *psta *** 515,532 **** static RangeTblEntry * transformRangeFunction(ParseState *pstate, RangeFunction *r) { ! Node *funcexpr; ! char *funcname; bool is_lateral; RangeTblEntry *rte; ! ! /* ! * Get function name for possible use as alias. We use the same ! * transformation rules as for a SELECT output expression. For a FuncCall ! * node, the result will be the function name, but it is possible for the ! * grammar to hand back other node types. ! */ ! funcname = FigureColname(r->funccallnode); /* * We make lateral_only names of this level visible, whether or not the --- 516,526 ---- static RangeTblEntry * transformRangeFunction(ParseState *pstate, RangeFunction *r) { ! List *funcexprs = NIL; ! List *funcnames = NIL; bool is_lateral; RangeTblEntry *rte; ! ListCell *lc; /* * We make lateral_only names of this level visible, whether or not the *************** transformRangeFunction(ParseState *pstat *** 541,586 **** pstate->p_lateral_active = true; /* ! * Transform the raw expression. */ ! funcexpr = transformExpr(pstate, r->funccallnode, EXPR_KIND_FROM_FUNCTION); pstate->p_lateral_active = false; /* ! * We must assign collations now so that we can fill funccolcollations. */ ! assign_expr_collations(pstate, funcexpr); /* * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if * there are any lateral cross-references in it. */ ! is_lateral = r->lateral || contain_vars_of_level(funcexpr, 0); /* * OK, build an RTE for the function. */ ! rte = addRangeTableEntryForFunction(pstate, funcname, funcexpr, r, is_lateral, true); - /* - * If a coldeflist was supplied, ensure it defines a legal set of names - * (no duplicates) and datatypes (no pseudo-types, for instance). - * addRangeTableEntryForFunction looked up the type names but didn't check - * them further than that. - */ - if (r->coldeflist) - { - TupleDesc tupdesc; - - tupdesc = BuildDescFromLists(rte->eref->colnames, - rte->funccoltypes, - rte->funccoltypmods, - rte->funccolcollations); - CheckAttributeNamesTypes(tupdesc, RELKIND_COMPOSITE_TYPE, false); - } - return rte; } --- 535,586 ---- pstate->p_lateral_active = true; /* ! * Transform the raw expressions. ! * ! * While transforming, get function names for possible use as alias and ! * column names. We use the same transformation rules as for a SELECT ! * output expression. For a FuncCall node, the result will be the function ! * name, but it is possible for the grammar to hand back other node types. ! * ! * Have to get this info now, because FigureColname only works on raw ! * parsetree. Actually deciding what to do with the names is left up to ! * addRangeTableEntryForFunction (which does not see the raw parsenodes). */ ! ! foreach(lc, r->funccallnodes) ! { ! Node *node = lfirst(lc); ! ! funcexprs = lappend(funcexprs, ! transformExpr(pstate, node, EXPR_KIND_FROM_FUNCTION)); ! ! funcnames = lappend(funcnames, makeString(FigureColname(node))); ! } pstate->p_lateral_active = false; /* ! * Assign collations now. ! * ! * This comment used to say that this was required to fill in ! * funccolcollations, but that does not appear to have been the case ! * (assignment of funccolcollations was since moved to the Expr handling ! * above) */ ! assign_list_collations(pstate, funcexprs); /* * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if * there are any lateral cross-references in it. */ ! is_lateral = r->lateral || contain_vars_of_level((Node *) funcexprs, 0); /* * OK, build an RTE for the function. */ ! rte = addRangeTableEntryForFunction(pstate, funcnames, funcexprs, r, is_lateral, true); return rte; } diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 68b711d..5544f4a 100644 *** a/src/backend/parser/parse_expr.c --- b/src/backend/parser/parse_expr.c *************** transformIndirection(ParseState *pstate, *** 463,470 **** newresult = ParseFuncOrColumn(pstate, list_make1(n), list_make1(result), ! NIL, NULL, false, false, false, ! NULL, true, location); if (newresult == NULL) unknown_attribute(pstate, result, strVal(n), location); result = newresult; --- 463,470 ---- newresult = ParseFuncOrColumn(pstate, list_make1(n), list_make1(result), ! location, ! NULL); if (newresult == NULL) unknown_attribute(pstate, result, strVal(n), location); result = newresult; *************** transformColumnRef(ParseState *pstate, C *** 631,638 **** node = ParseFuncOrColumn(pstate, list_make1(makeString(colname)), list_make1(node), ! NIL, NULL, false, false, false, ! NULL, true, cref->location); } break; } --- 631,637 ---- node = ParseFuncOrColumn(pstate, list_make1(makeString(colname)), list_make1(node), ! cref->location, NULL); } break; } *************** transformColumnRef(ParseState *pstate, C *** 676,683 **** node = ParseFuncOrColumn(pstate, list_make1(makeString(colname)), list_make1(node), ! NIL, NULL, false, false, false, ! NULL, true, cref->location); } break; } --- 675,681 ---- node = ParseFuncOrColumn(pstate, list_make1(makeString(colname)), list_make1(node), ! cref->location, NULL); } break; } *************** transformColumnRef(ParseState *pstate, C *** 734,741 **** node = ParseFuncOrColumn(pstate, list_make1(makeString(colname)), list_make1(node), ! NIL, NULL, false, false, false, ! NULL, true, cref->location); } break; } --- 732,738 ---- node = ParseFuncOrColumn(pstate, list_make1(makeString(colname)), list_make1(node), ! cref->location, NULL); } break; } *************** transformFuncCall(ParseState *pstate, Fu *** 1242,1248 **** { List *targs; ListCell *args; - Expr *tagg_filter; /* Transform the list of arguments ... */ targs = NIL; --- 1239,1244 ---- *************** transformFuncCall(ParseState *pstate, Fu *** 1252,1279 **** (Node *) lfirst(args))); } - /* - * Transform the aggregate filter using transformWhereClause(), to which - * FILTER is virtually identical... - */ - tagg_filter = NULL; - if (fn->agg_filter != NULL) - tagg_filter = (Expr *) - transformWhereClause(pstate, (Node *) fn->agg_filter, - EXPR_KIND_FILTER, "FILTER"); - /* ... and hand off to ParseFuncOrColumn */ return ParseFuncOrColumn(pstate, fn->funcname, targs, ! fn->agg_order, ! tagg_filter, ! fn->agg_star, ! fn->agg_distinct, ! fn->func_variadic, ! fn->over, ! false, ! fn->location); } static Node * --- 1248,1259 ---- (Node *) lfirst(args))); } /* ... and hand off to ParseFuncOrColumn */ return ParseFuncOrColumn(pstate, fn->funcname, targs, ! fn->location, ! fn); } static Node * diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index ede36d1..ef20f9f 100644 *** a/src/backend/parser/parse_func.c --- b/src/backend/parser/parse_func.c *************** *** 15,20 **** --- 15,22 ---- #include "postgres.h" #include "access/htup_details.h" + #include "catalog/heap.h" + #include "catalog/pg_class.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "funcapi.h" *************** *** 22,27 **** --- 24,30 ---- #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/parse_agg.h" + #include "parser/parse_clause.h" #include "parser/parse_coerce.h" #include "parser/parse_func.h" #include "parser/parse_relation.h" *************** static Node *ParseComplexProjection(Pars *** 61,70 **** */ Node * ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, ! List *agg_order, Expr *agg_filter, ! bool agg_star, bool agg_distinct, bool func_variadic, ! WindowDef *over, bool is_column, int location) { Oid rettype; Oid funcid; ListCell *l; --- 64,79 ---- */ Node * ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, ! int location, FuncCall *fn) { + List *agg_order = (fn ? fn->agg_order : NIL); + Expr *agg_filter = NULL; + bool agg_star = (fn ? fn->agg_star : false); + bool agg_distinct = (fn ? fn->agg_distinct : false); + bool func_variadic = (fn ? fn->func_variadic : false); + WindowDef *over = (fn ? fn->over : NULL); + List *coldeflist = (fn ? fn->coldeflist : NIL); + bool is_column = (fn == NULL); Oid rettype; Oid funcid; ListCell *l; *************** ParseFuncOrColumn(ParseState *pstate, Li *** 98,103 **** --- 107,121 ---- parser_errposition(pstate, location))); /* + * Transform the aggregate filter using transformWhereClause(), to which + * FILTER is virtually identical... + */ + if (fn && fn->agg_filter != NULL) + agg_filter = (Expr *) + transformWhereClause(pstate, (Node *) fn->agg_filter, + EXPR_KIND_FILTER, "FILTER"); + + /* * Extract arg type info in preparation for function lookup. * * If any arguments are Param markers of type VOID, we discard them from *************** ParseFuncOrColumn(ParseState *pstate, Li *** 414,419 **** --- 432,499 ---- funcexpr->args = fargs; funcexpr->location = location; + /* + * If we're called in the FROM-clause, we might have a column + * definition list if we return RECORD. The grammar should prevent + * supplying a list in other contexts. Missing coldeflists are checked + * for in parse_relation.c + */ + if (coldeflist != NIL) + { + TypeFuncClass functypclass; + ListCell *col; + TupleDesc tupdesc; + + Assert(pstate->p_expr_kind == EXPR_KIND_FROM_FUNCTION); + + functypclass = get_func_result_type(funcid, NULL, NULL); + + if (functypclass != TYPEFUNC_RECORD) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("a column definition list is only allowed for functions returning \"record\""), + parser_errposition(pstate, location))); + + /* + * Use the column definition list to form the + * funccolnames/funccoltypes/funccoltypmods/funccolcollations + * lists. + */ + foreach(col, coldeflist) + { + ColumnDef *n = (ColumnDef *) lfirst(col); + char *attrname; + Oid attrtype; + int32 attrtypmod; + Oid attrcollation; + + attrname = pstrdup(n->colname); + if (n->typeName->setof) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" cannot be declared SETOF", + attrname), + parser_errposition(pstate, n->typeName->location))); + typenameTypeIdAndMod(pstate, n->typeName, &attrtype, &attrtypmod); + attrcollation = GetColumnDefCollation(pstate, n, attrtype); + funcexpr->funccolnames = lappend(funcexpr->funccolnames, makeString(attrname)); + funcexpr->funccoltypes = lappend_oid(funcexpr->funccoltypes, attrtype); + funcexpr->funccoltypmods = lappend_int(funcexpr->funccoltypmods, attrtypmod); + funcexpr->funccolcollations = lappend_oid(funcexpr->funccolcollations, + attrcollation); + } + + /* + * Ensure it defines a legal set of names (no duplicates) and + * datatypes (no pseudo-types, for instance). + */ + tupdesc = BuildDescFromLists(funcexpr->funccolnames, + funcexpr->funccoltypes, + funcexpr->funccoltypmods, + funcexpr->funccolcollations); + CheckAttributeNamesTypes(tupdesc, RELKIND_COMPOSITE_TYPE, false); + } + retval = (Node *) funcexpr; } else if (fdresult == FUNCDETAIL_AGGREGATE && !over) diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 0052d21..1bc784c 100644 *** a/src/backend/parser/parse_relation.c --- b/src/backend/parser/parse_relation.c *************** *** 27,32 **** --- 27,33 ---- #include "parser/parsetree.h" #include "parser/parse_relation.h" #include "parser/parse_type.h" + #include "parser/parse_target.h" #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/rel.h" *************** static void expandRelation(Oid relid, Al *** 43,49 **** int rtindex, int sublevels_up, int location, bool include_dropped, List **colnames, List **colvars); ! static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, int location, bool include_dropped, List **colnames, List **colvars); --- 44,50 ---- int rtindex, int sublevels_up, int location, bool include_dropped, List **colnames, List **colvars); ! static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int atts_done, int rtindex, int sublevels_up, int location, bool include_dropped, List **colnames, List **colvars); *************** buildRelationAliases(TupleDesc tupdesc, *** 880,886 **** /* tack on the ordinality column at the end */ if (ordinality) { ! Value *attrname; if (aliaslc) { --- 881,887 ---- /* tack on the ordinality column at the end */ if (ordinality) { ! Value *attrname; if (aliaslc) { *************** buildRelationAliases(TupleDesc tupdesc, *** 912,933 **** * when the function returns a scalar type (not composite or RECORD). * * funcexpr: transformed expression tree for the function call ! * funcname: function name (used only for error message) * alias: the user-supplied alias, or NULL if none * eref: the eref Alias to store column names in * ordinality: whether to add an ordinality column * * eref->colnames is filled in. * ! * The caller must have previously filled in eref->aliasname, which will ! * be used as the result column name if no alias is given. * * A user-supplied Alias can contain up to two column alias names; one for * the function result, and one for the ordinality column; it is an error * to specify more aliases than required. */ static void ! buildScalarFunctionAlias(Node *funcexpr, char *funcname, Alias *alias, Alias *eref, bool ordinality) { Assert(eref->colnames == NIL); --- 913,942 ---- * when the function returns a scalar type (not composite or RECORD). * * funcexpr: transformed expression tree for the function call ! * funcname: function name ! * prefer_funcname: prefer to use funcname rather than eref->aliasname * alias: the user-supplied alias, or NULL if none * eref: the eref Alias to store column names in * ordinality: whether to add an ordinality column * * eref->colnames is filled in. * ! * The caller must have previously filled in eref->aliasname, which will be ! * used as the result column name if no column alias is given, no column name ! * is provided by the function, and prefer_funcname is false. (This makes FROM ! * func() AS foo use "foo" as the column name as well as the table alias.) ! * ! * prefer_funcname is true for the TABLE(func()) case, where calling the ! * resulting column "table" would be silly, and using the function name as ! * eref->aliasname would be inconsistent with TABLE(func1(),func2()). This ! * isn't ideal, but seems to be the least surprising behaviour. * * A user-supplied Alias can contain up to two column alias names; one for * the function result, and one for the ordinality column; it is an error * to specify more aliases than required. */ static void ! buildScalarFunctionAlias(Node *funcexpr, char *funcname, bool prefer_funcname, Alias *alias, Alias *eref, bool ordinality) { Assert(eref->colnames == NIL); *************** buildScalarFunctionAlias(Node *funcexpr, *** 959,970 **** * caller (which is not necessarily the function name!) */ if (!pname) ! pname = eref->aliasname; eref->colnames = list_make1(makeString(pname)); } ! /* If we don't have a name for the ordinality column yet, supply a default. */ if (ordinality && list_length(eref->colnames) < 2) eref->colnames = lappend(eref->colnames, makeString(pstrdup("ordinality"))); --- 968,982 ---- * caller (which is not necessarily the function name!) */ if (!pname) ! pname = (prefer_funcname ? funcname : eref->aliasname); eref->colnames = list_make1(makeString(pname)); } ! /* ! * If we don't have a name for the ordinality column yet, supply a ! * default. ! */ if (ordinality && list_length(eref->colnames) < 2) eref->colnames = lappend(eref->colnames, makeString(pstrdup("ordinality"))); *************** addRangeTableEntryForSubquery(ParseState *** 1230,1243 **** } /* ! * Add an entry for a function to the pstate's range table (p_rtable). * * This is just like addRangeTableEntry() except that it makes a function RTE. */ RangeTblEntry * addRangeTableEntryForFunction(ParseState *pstate, ! char *funcname, ! Node *funcexpr, RangeFunction *rangefunc, bool lateral, bool inFromCl) --- 1242,1256 ---- } /* ! * Add an entry for a function (or functions) to the pstate's range table ! * (p_rtable). * * This is just like addRangeTableEntry() except that it makes a function RTE. */ RangeTblEntry * addRangeTableEntryForFunction(ParseState *pstate, ! List *funcnames, ! List *funcexprs, RangeFunction *rangefunc, bool lateral, bool inFromCl) *************** addRangeTableEntryForFunction(ParseState *** 1247,1293 **** Oid funcrettype; TupleDesc tupdesc; Alias *alias = rangefunc->alias; - List *coldeflist = rangefunc->coldeflist; Alias *eref; rte->rtekind = RTE_FUNCTION; rte->relid = InvalidOid; rte->subquery = NULL; ! rte->funcexpr = funcexpr; ! rte->funccoltypes = NIL; ! rte->funccoltypmods = NIL; ! rte->funccolcollations = NIL; rte->alias = alias; ! eref = makeAlias(alias ? alias->aliasname : funcname, NIL); rte->eref = eref; /* * Now determine if the function returns a simple or composite type. */ ! functypclass = get_expr_result_type(funcexpr, ! &funcrettype, ! &tupdesc); ! ! /* ! * A coldeflist is required if the function returns RECORD and hasn't got ! * a predetermined record type, and is prohibited otherwise. ! */ ! if (coldeflist != NIL) { ! if (functypclass != TYPEFUNC_RECORD) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), ! errmsg("a column definition list is only allowed for functions returning \"record\""), ! parser_errposition(pstate, exprLocation(funcexpr)))); } else { ! if (functypclass == TYPEFUNC_RECORD) ! ereport(ERROR, ! (errcode(ERRCODE_SYNTAX_ERROR), ! errmsg("a column definition list is required for functions returning \"record\""), ! parser_errposition(pstate, exprLocation(funcexpr)))); } if (functypclass == TYPEFUNC_COMPOSITE) --- 1260,1420 ---- Oid funcrettype; TupleDesc tupdesc; Alias *alias = rangefunc->alias; Alias *eref; + char *aliasname; + Oid *funcrettypes = NULL; + TupleDesc *functupdescs = NULL; + int nfuncs = list_length(funcexprs); + ListCell *lc, + *lc2; + int i; + int j; + int natts; rte->rtekind = RTE_FUNCTION; rte->relid = InvalidOid; rte->subquery = NULL; ! rte->funcexprs = funcexprs; rte->alias = alias; ! /*---------- ! * Choose the RTE alias name. ! * ! * We punt to "table" if the list results from explicit TABLE() syntax ! * regardless of number of functions. Otherwise, use the first function, ! * since either there is only one, or it was an unnest() which got ! * expanded. We don't currently need to record this fact in the ! * transformed node, since deparse always emits an alias for table ! * functions, and ! * ... FROM unnest(a,b) ! * and ! * ... FROM TABLE(unnest(a),unnest(b)) AS "unnest" ! * are equivalent enough for our purposes. ! *---------- ! */ ! if (alias) ! aliasname = alias->aliasname; ! else if (rangefunc->is_table) ! aliasname = "table"; ! else ! aliasname = strVal(linitial(funcnames)); ! ! eref = makeAlias(aliasname, NIL); rte->eref = eref; /* * Now determine if the function returns a simple or composite type. + * + * If there's more than one function, the result must be composite, and we + * have to produce a merged tupdesc. */ ! if (nfuncs == 1) { ! functypclass = get_expr_result_type(linitial(funcexprs), ! &funcrettype, ! &tupdesc); ! ! /* ! * TYPEFUNC_RECORD is only possible here if a column definition list ! * was not supplied. ! */ ! if (functypclass == TYPEFUNC_RECORD) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), ! errmsg("a column definition list is required for functions returning \"record\""), ! parser_errposition(pstate, exprLocation(linitial(funcexprs))))); } else { ! /* ! * Produce a flattened TupleDesc with all the constituent columns from ! * all functions. We're only going to use this for assigning aliases, ! * so we don't need collations and so on. ! * ! * This would be less painful if there was a reasonable way to insert ! * dropped columns into a tupdesc. ! */ ! ! funcrettypes = palloc(nfuncs * sizeof(Oid)); ! functupdescs = palloc(nfuncs * sizeof(TupleDesc)); ! ! i = 0; ! natts = 0; ! forboth(lc, funcexprs, lc2, funcnames) ! { ! functypclass = get_expr_result_type(lfirst(lc), ! &funcrettypes[i], ! &functupdescs[i]); ! ! switch (functypclass) ! { ! case TYPEFUNC_RECORD: ! ! /* ! * Only gets here if no column definition list was ! * supplied for a function returning an unspecified ! * RECORD. ! */ ! ereport(ERROR, ! (errcode(ERRCODE_SYNTAX_ERROR), ! errmsg("a column definition list is required for functions returning \"record\""), ! parser_errposition(pstate, exprLocation(lfirst(lc))))); ! ! case TYPEFUNC_SCALAR: ! { ! FuncExpr *funcexpr = lfirst(lc); ! char *pname = NULL; ! ! /* ! * Function might have its own idea what the result ! * column name should be. Prefer that (since ! * buildScalarFunctionAlias does too) ! */ ! if (IsA(funcexpr, FuncExpr)) ! pname = get_func_result_name(funcexpr->funcid); ! ! /* ! * If not, use the function name as the column name. ! */ ! if (!pname) ! pname = strVal(lfirst(lc2)); ! ! functupdescs[i] = CreateTemplateTupleDesc(1, false); ! TupleDescInitEntry(functupdescs[i], ! (AttrNumber) 1, ! pname, ! funcrettypes[i], ! -1, ! 0); ! break; ! } ! ! case TYPEFUNC_COMPOSITE: ! break; ! ! default: ! ereport(ERROR, ! (errcode(ERRCODE_DATATYPE_MISMATCH), ! errmsg("function \"%s\" in FROM has unsupported return type %s", ! strVal(lfirst(lc2)), format_type_be(funcrettype)), ! parser_errposition(pstate, exprLocation(lfirst(lc))))); ! } ! natts += functupdescs[i]->natts; ! ! i++; ! } ! ! functypclass = TYPEFUNC_COMPOSITE; ! funcrettype = RECORDOID; ! ! /* Merge the tuple descs of each function into a composite one */ ! tupdesc = CreateTemplateTupleDesc(natts, false); ! natts = 0; ! for (i = 0; i < nfuncs; i++) ! { ! for (j = 1; j <= functupdescs[i]->natts; j++) ! TupleDescCopyEntry(tupdesc, ++natts, functupdescs[i], j); ! } } if (functypclass == TYPEFUNC_COMPOSITE) *************** addRangeTableEntryForFunction(ParseState *** 1300,1351 **** else if (functypclass == TYPEFUNC_SCALAR) { /* Base data type, i.e. scalar */ ! buildScalarFunctionAlias(funcexpr, funcname, alias, eref, rangefunc->ordinality); ! } ! else if (functypclass == TYPEFUNC_RECORD) ! { ! ListCell *col; ! ! if (rangefunc->ordinality) ! ereport(ERROR, ! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ! errmsg("WITH ORDINALITY is not supported for functions returning \"record\""), ! parser_errposition(pstate, exprLocation(funcexpr)))); ! ! /* ! * Use the column definition list to form the alias list and ! * funccoltypes/funccoltypmods/funccolcollations lists. ! */ ! foreach(col, coldeflist) ! { ! ColumnDef *n = (ColumnDef *) lfirst(col); ! char *attrname; ! Oid attrtype; ! int32 attrtypmod; ! Oid attrcollation; ! ! attrname = pstrdup(n->colname); ! if (n->typeName->setof) ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_TABLE_DEFINITION), ! errmsg("column \"%s\" cannot be declared SETOF", ! attrname), ! parser_errposition(pstate, n->typeName->location))); ! typenameTypeIdAndMod(pstate, n->typeName, &attrtype, &attrtypmod); ! attrcollation = GetColumnDefCollation(pstate, n, attrtype); ! eref->colnames = lappend(eref->colnames, makeString(attrname)); ! rte->funccoltypes = lappend_oid(rte->funccoltypes, attrtype); ! rte->funccoltypmods = lappend_int(rte->funccoltypmods, attrtypmod); ! rte->funccolcollations = lappend_oid(rte->funccolcollations, ! attrcollation); ! } } else ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("function \"%s\" in FROM has unsupported return type %s", ! funcname, format_type_be(funcrettype)), ! parser_errposition(pstate, exprLocation(funcexpr)))); /* * Set flags and access permissions. --- 1427,1442 ---- else if (functypclass == TYPEFUNC_SCALAR) { /* Base data type, i.e. scalar */ ! buildScalarFunctionAlias(linitial(funcexprs), ! strVal(linitial(funcnames)), rangefunc->is_table, ! alias, eref, rangefunc->ordinality); } else ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("function \"%s\" in FROM has unsupported return type %s", ! strVal(linitial(funcnames)), format_type_be(funcrettype)), ! parser_errposition(pstate, exprLocation(linitial(funcexprs))))); /* * Set flags and access permissions. *************** expandRTE(RangeTblEntry *rte, int rtinde *** 1783,1886 **** TypeFuncClass functypclass; Oid funcrettype; TupleDesc tupdesc; ! int ordinality_attno = 0; ! ! functypclass = get_expr_result_type(rte->funcexpr, ! &funcrettype, ! &tupdesc); ! if (functypclass == TYPEFUNC_COMPOSITE) ! { ! /* Composite data type, e.g. a table's row type */ ! Assert(tupdesc); ! ! /* ! * we rely here on the fact that expandTupleDesc doesn't ! * care about being passed more aliases than it needs. ! */ ! expandTupleDesc(tupdesc, rte->eref, ! rtindex, sublevels_up, location, ! include_dropped, colnames, colvars); ! ordinality_attno = tupdesc->natts + 1; ! } ! else if (functypclass == TYPEFUNC_SCALAR) { ! /* Base data type, i.e. scalar */ ! if (colnames) ! *colnames = lappend(*colnames, ! linitial(rte->eref->colnames)); ! ! if (colvars) { ! Var *varnode; ! varnode = makeVar(rtindex, 1, ! funcrettype, -1, ! exprCollation(rte->funcexpr), ! sublevels_up); ! varnode->location = location; ! *colvars = lappend(*colvars, varnode); } ! ! ordinality_attno = 2; ! } ! else if (functypclass == TYPEFUNC_RECORD) ! { ! if (colnames) ! *colnames = copyObject(rte->eref->colnames); ! if (colvars) { ! ListCell *l1; ! ListCell *l2; ! ListCell *l3; ! int attnum = 0; ! forthree(l1, rte->funccoltypes, ! l2, rte->funccoltypmods, ! l3, rte->funccolcollations) { - Oid attrtype = lfirst_oid(l1); - int32 attrtypmod = lfirst_int(l2); - Oid attrcollation = lfirst_oid(l3); Var *varnode; ! attnum++; ! varnode = makeVar(rtindex, ! attnum, ! attrtype, ! attrtypmod, ! attrcollation, sublevels_up); varnode->location = location; *colvars = lappend(*colvars, varnode); } - } ! /* note, ordinality is not allowed in this case */ ! } ! else ! { ! /* addRangeTableEntryForFunction should've caught this */ ! elog(ERROR, "function in FROM has unsupported return type"); } /* tack on the extra ordinality column if present */ if (rte->funcordinality) { ! Assert(ordinality_attno > 0); if (colnames) *colnames = lappend(*colnames, llast(rte->eref->colnames)); if (colvars) { ! Var *varnode = makeVar(rtindex, ! ordinality_attno, ! INT8OID, ! -1, ! InvalidOid, ! sublevels_up); *colvars = lappend(*colvars, varnode); } } --- 1874,1956 ---- TypeFuncClass functypclass; Oid funcrettype; TupleDesc tupdesc; ! int atts_done = 0; ! ListCell *lc; ! /* ! * Loop over functions to assemble result. ! * ! * atts_done is the number of attributes (including dropped ! * cols) constructed so far; it's used as an index offset in ! * various places. ! */ ! foreach(lc, rte->funcexprs) { ! functypclass = get_expr_result_type(lfirst(lc), ! &funcrettype, ! &tupdesc); ! if (functypclass == TYPEFUNC_COMPOSITE) { ! /* Composite data type, e.g. a table's row type */ ! Assert(tupdesc); ! /* ! * we rely here on the fact that expandTupleDesc ! * doesn't care about being passed more aliases than ! * it needs. ! */ ! expandTupleDesc(tupdesc, rte->eref, atts_done, ! rtindex, sublevels_up, location, ! include_dropped, colnames, colvars); ! atts_done += tupdesc->natts; } ! else if (functypclass == TYPEFUNC_SCALAR) { ! /* Base data type, i.e. scalar */ ! if (colnames) ! *colnames = lappend(*colnames, ! list_nth(rte->eref->colnames, atts_done)); ! if (colvars) { Var *varnode; ! varnode = makeVar(rtindex, atts_done + 1, ! funcrettype, -1, ! exprCollation(lfirst(lc)), sublevels_up); varnode->location = location; + *colvars = lappend(*colvars, varnode); } ! ++atts_done; ! } ! else ! { ! /* addRangeTableEntryForFunction should've caught this */ ! elog(ERROR, "function in FROM has unsupported return type"); ! } } /* tack on the extra ordinality column if present */ if (rte->funcordinality) { ! Assert(atts_done > 0); if (colnames) *colnames = lappend(*colnames, llast(rte->eref->colnames)); if (colvars) { ! Var *varnode = makeVar(rtindex, ! atts_done + 1, ! INT8OID, ! -1, ! InvalidOid, ! sublevels_up); ! *colvars = lappend(*colvars, varnode); } } *************** expandRelation(Oid relid, Alias *eref, i *** 2051,2057 **** /* Get the tupledesc and turn it over to expandTupleDesc */ rel = relation_open(relid, AccessShareLock); ! expandTupleDesc(rel->rd_att, eref, rtindex, sublevels_up, location, include_dropped, colnames, colvars); relation_close(rel, AccessShareLock); --- 2121,2127 ---- /* Get the tupledesc and turn it over to expandTupleDesc */ rel = relation_open(relid, AccessShareLock); ! expandTupleDesc(rel->rd_att, eref, 0, rtindex, sublevels_up, location, include_dropped, colnames, colvars); relation_close(rel, AccessShareLock); *************** expandRelation(Oid relid, Alias *eref, i *** 2060,2076 **** /* * expandTupleDesc -- expandRTE subroutine * ! * Only the required number of column names are used from the Alias; ! * it is not an error to supply too many. (ordinality depends on this) */ static void ! expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, int location, bool include_dropped, List **colnames, List **colvars) { int maxattrs = tupdesc->natts; ! int numaliases = list_length(eref->colnames); int varattno; for (varattno = 0; varattno < maxattrs; varattno++) --- 2130,2149 ---- /* * expandTupleDesc -- expandRTE subroutine * ! * Only the required number of column names are used from the Alias; it is not ! * an error to supply too many. (ordinality depends on this) ! * ! * atts_done offsets the resulting column numbers, for the function case when ! * we merge multiple tupdescs into one list. */ static void ! expandTupleDesc(TupleDesc tupdesc, Alias *eref, int atts_done, int rtindex, int sublevels_up, int location, bool include_dropped, List **colnames, List **colvars) { int maxattrs = tupdesc->natts; ! int numaliases = list_length(eref->colnames) - atts_done; int varattno; for (varattno = 0; varattno < maxattrs; varattno++) *************** expandTupleDesc(TupleDesc tupdesc, Alias *** 2101,2107 **** char *label; if (varattno < numaliases) ! label = strVal(list_nth(eref->colnames, varattno)); else label = NameStr(attr->attname); *colnames = lappend(*colnames, makeString(pstrdup(label))); --- 2174,2180 ---- char *label; if (varattno < numaliases) ! label = strVal(list_nth(eref->colnames, varattno + atts_done)); else label = NameStr(attr->attname); *colnames = lappend(*colnames, makeString(pstrdup(label))); *************** expandTupleDesc(TupleDesc tupdesc, Alias *** 2111,2117 **** { Var *varnode; ! varnode = makeVar(rtindex, attr->attnum, attr->atttypid, attr->atttypmod, attr->attcollation, sublevels_up); --- 2184,2190 ---- { Var *varnode; ! varnode = makeVar(rtindex, attr->attnum + atts_done, attr->atttypid, attr->atttypmod, attr->attcollation, sublevels_up); *************** get_rte_attribute_type(RangeTblEntry *rt *** 2281,2291 **** TypeFuncClass functypclass; Oid funcrettype; TupleDesc tupdesc; /* ! * if ordinality, then a reference to the last column ! * in the name list must be referring to the ! * ordinality column */ if (rte->funcordinality && attnum == list_length(rte->eref->colnames)) --- 2354,2365 ---- TypeFuncClass functypclass; Oid funcrettype; TupleDesc tupdesc; + ListCell *lc; + int atts_done = 0; /* ! * if ordinality, then a reference to the last column in the ! * name list must be referring to the ordinality column */ if (rte->funcordinality && attnum == list_length(rte->eref->colnames)) *************** get_rte_attribute_type(RangeTblEntry *rt *** 2296,2356 **** break; } ! functypclass = get_expr_result_type(rte->funcexpr, ! &funcrettype, ! &tupdesc); ! ! if (functypclass == TYPEFUNC_COMPOSITE) { ! /* Composite data type, e.g. a table's row type */ ! Form_pg_attribute att_tup; ! Assert(tupdesc); ! /* this is probably a can't-happen case */ ! if (attnum < 1 || attnum > tupdesc->natts) ! ereport(ERROR, ! (errcode(ERRCODE_UNDEFINED_COLUMN), ! errmsg("column %d of relation \"%s\" does not exist", ! attnum, ! rte->eref->aliasname))); ! att_tup = tupdesc->attrs[attnum - 1]; ! /* ! * If dropped column, pretend it ain't there. See notes ! * in scanRTEForColumn. ! */ ! if (att_tup->attisdropped) ! ereport(ERROR, ! (errcode(ERRCODE_UNDEFINED_COLUMN), ! errmsg("column \"%s\" of relation \"%s\" does not exist", ! NameStr(att_tup->attname), ! rte->eref->aliasname))); ! *vartype = att_tup->atttypid; ! *vartypmod = att_tup->atttypmod; ! *varcollid = att_tup->attcollation; ! } ! else if (functypclass == TYPEFUNC_SCALAR) ! { ! Assert(attnum == 1); ! /* Base data type, i.e. scalar */ ! *vartype = funcrettype; ! *vartypmod = -1; ! *varcollid = exprCollation(rte->funcexpr); ! } ! else if (functypclass == TYPEFUNC_RECORD) ! { ! *vartype = list_nth_oid(rte->funccoltypes, attnum - 1); ! *vartypmod = list_nth_int(rte->funccoltypmods, attnum - 1); ! *varcollid = list_nth_oid(rte->funccolcollations, attnum - 1); ! } ! else ! { ! /* addRangeTableEntryForFunction should've caught this */ ! elog(ERROR, "function in FROM has unsupported return type"); } } break; case RTE_VALUES: --- 2370,2441 ---- break; } ! /* ! * Loop over funcs until we find the one that covers the ! * requested column. ! */ ! foreach(lc, rte->funcexprs) { ! functypclass = get_expr_result_type(lfirst(lc), ! &funcrettype, ! &tupdesc); ! if (functypclass == TYPEFUNC_COMPOSITE) ! { ! /* Composite data type, e.g. a table's row type */ ! Form_pg_attribute att_tup; ! Assert(tupdesc); ! if (attnum > atts_done ! && attnum <= atts_done + tupdesc->natts) ! { ! att_tup = tupdesc->attrs[attnum - atts_done - 1]; ! /* ! * If dropped column, pretend it ain't there. See ! * notes in scanRTEForColumn. ! */ ! if (att_tup->attisdropped) ! ereport(ERROR, ! (errcode(ERRCODE_UNDEFINED_COLUMN), ! errmsg("column \"%s\" of relation \"%s\" does not exist", ! NameStr(att_tup->attname), ! rte->eref->aliasname))); ! *vartype = att_tup->atttypid; ! *vartypmod = att_tup->atttypmod; ! *varcollid = att_tup->attcollation; ! return; ! } ! atts_done += tupdesc->natts; ! } ! else if (functypclass == TYPEFUNC_SCALAR) ! { ! if (attnum == atts_done + 1) ! { ! /* Base data type, i.e. scalar */ ! *vartype = funcrettype; ! *vartypmod = -1; ! *varcollid = exprCollation(lfirst(lc)); ! return; ! } ! ! ++atts_done; ! } ! else ! { ! /* addRangeTableEntryForFunction should've caught this */ ! elog(ERROR, "function in FROM has unsupported return type"); ! } } + + /* this is probably a can't-happen case */ + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column %d of relation \"%s\" does not exist", + attnum, + rte->eref->aliasname))); } break; case RTE_VALUES: *************** get_rte_attribute_is_dropped(RangeTblEnt *** 2456,2500 **** case RTE_FUNCTION: { /* Function RTE */ ! Oid funcrettype = exprType(rte->funcexpr); ! Oid funcrelid = typeidTypeRelid(funcrettype); /* ! * if ordinality, then a reference to the last column ! * in the name list must be referring to the ! * ordinality column, which is not dropped */ if (rte->funcordinality && attnum == list_length(rte->eref->colnames)) ! { ! result = false; ! } ! else if (OidIsValid(funcrelid)) ! { ! /* ! * Composite data type, i.e. a table's row type ! * ! * Same as ordinary relation RTE ! */ ! HeapTuple tp; ! Form_pg_attribute att_tup; ! tp = SearchSysCache2(ATTNUM, ! ObjectIdGetDatum(funcrelid), ! Int16GetDatum(attnum)); ! if (!HeapTupleIsValid(tp)) /* shouldn't happen */ ! elog(ERROR, "cache lookup failed for attribute %d of relation %u", ! attnum, funcrelid); ! att_tup = (Form_pg_attribute) GETSTRUCT(tp); ! result = att_tup->attisdropped; ! ReleaseSysCache(tp); ! } ! else { ! /* ! * Must be a base data type, i.e. scalar ! */ ! result = false; } } break; --- 2541,2607 ---- case RTE_FUNCTION: { /* Function RTE */ ! TypeFuncClass functypclass; ! Oid funcrettype; ! TupleDesc tupdesc; ! ListCell *lc; ! int atts_done = 0; ! ! result = false; /* ! * if ordinality, then a reference to the last column in the ! * name list must be referring to the ordinality column, which ! * is not dropped */ if (rte->funcordinality && attnum == list_length(rte->eref->colnames)) ! break; ! /* ! * Loop over funcs until we find the one that covers the ! * requested column. ! */ ! foreach(lc, rte->funcexprs) { ! functypclass = get_expr_result_type(lfirst(lc), ! &funcrettype, ! &tupdesc); ! ! if (functypclass == TYPEFUNC_COMPOSITE) ! { ! /* Composite data type, e.g. a table's row type */ ! Form_pg_attribute att_tup; ! ! Assert(tupdesc); ! ! if (attnum > atts_done ! && attnum <= atts_done + tupdesc->natts) ! { ! att_tup = tupdesc->attrs[attnum - atts_done - 1]; ! ! result = att_tup->attisdropped; ! break; ! } ! ! atts_done += tupdesc->natts; ! } ! else if (functypclass == TYPEFUNC_SCALAR) ! { ! if (attnum == atts_done + 1) ! { ! /* Base data type, i.e. scalar */ ! result = false; ! break; ! } ! ! ++atts_done; ! } ! else ! { ! /* addRangeTableEntryForFunction should've caught this */ ! elog(ERROR, "function in FROM has unsupported return type"); ! } } } break; diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index c52a374..94f3cf5 100644 *** a/src/backend/rewrite/rewriteHandler.c --- b/src/backend/rewrite/rewriteHandler.c *************** rewriteRuleAction(Query *parsetree, *** 390,396 **** { case RTE_FUNCTION: sub_action->hasSubLinks = ! checkExprHasSubLink(rte->funcexpr); break; case RTE_VALUES: sub_action->hasSubLinks = --- 390,396 ---- { case RTE_FUNCTION: sub_action->hasSubLinks = ! checkExprHasSubLink((Node *) rte->funcexprs); break; case RTE_VALUES: sub_action->hasSubLinks = diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 5ffce68..6fe0075 100644 *** a/src/backend/utils/adt/ruleutils.c --- b/src/backend/utils/adt/ruleutils.c *************** *** 27,32 **** --- 27,33 ---- #include "catalog/pg_constraint.h" #include "catalog/pg_depend.h" #include "catalog/pg_language.h" + #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" *************** static void get_from_clause_item(Node *j *** 387,393 **** deparse_context *context); static void get_column_alias_list(deparse_columns *colinfo, deparse_context *context); ! static void get_from_clause_coldeflist(deparse_columns *colinfo, List *types, List *typmods, List *collations, deparse_context *context); static void get_opclass_name(Oid opclass, Oid actual_datatype, --- 388,394 ---- deparse_context *context); static void get_column_alias_list(deparse_columns *colinfo, deparse_context *context); ! static void get_from_clause_coldeflist(deparse_columns *colinfo, List *names, List *types, List *typmods, List *collations, deparse_context *context); static void get_opclass_name(Oid opclass, Oid actual_datatype, *************** get_from_clause_item(Node *jtnode, Query *** 8013,8018 **** --- 8014,8020 ---- char *refname = get_rtable_name(varno, context); deparse_columns *colinfo = deparse_columns_fetch(varno, dpns); bool printalias; + FuncExpr *func_coldef = NULL; if (rte->lateral) appendStringInfoString(buf, "LATERAL "); *************** get_from_clause_item(Node *jtnode, Query *** 8037,8043 **** break; case RTE_FUNCTION: /* Function RTE */ ! get_rule_expr(rte->funcexpr, context, true); if (rte->funcordinality) appendStringInfoString(buf, " WITH ORDINALITY"); break; --- 8039,8139 ---- break; case RTE_FUNCTION: /* Function RTE */ ! ! /* ! * The simple case of omitting TABLE() for one function only ! * works if either there's no ordinality, or the function does ! * not need a column definition list. ! */ ! if (list_length(rte->funcexprs) == 1 ! && (!rte->funcordinality ! || !IsA(linitial(rte->funcexprs), FuncExpr) ! ||((FuncExpr *) linitial(rte->funcexprs))->funccoltypes == NIL)) ! { ! get_rule_expr(linitial(rte->funcexprs), context, true); ! func_coldef = linitial(rte->funcexprs); ! } ! else ! { ! ListCell *lc = list_head(rte->funcexprs); ! Oid unnest_oid = InvalidOid; ! ! /* ! * If all the function calls in the list are to ! * pg_catalog.unnest, then collapse the list back down to ! * UNNEST(args). Since there's currently only one unnest, ! * we check by oid after the first one. ! */ ! ! if (IsA(lfirst(lc), FuncExpr)) ! { ! unnest_oid = ((FuncExpr *) lfirst(lc))->funcid; ! ! if (get_func_namespace(unnest_oid) != PG_CATALOG_NAMESPACE ! || strcmp(get_func_name(unnest_oid), "unnest") != 0) ! unnest_oid = InvalidOid; ! } ! ! while (OidIsValid(unnest_oid)) ! { ! FuncExpr *fn; ! ! lc = lnext(lc); ! if (!lc) ! break; ! ! fn = lfirst(lc); ! if (!IsA(fn, FuncExpr) ! ||fn->funcid != unnest_oid ! || fn->funccoltypes != NIL) ! unnest_oid = InvalidOid; ! } ! ! if (OidIsValid(unnest_oid)) ! { ! List *allargs = NIL; ! ! foreach(lc, rte->funcexprs) ! { ! List *args = ((FuncExpr *) lfirst(lc))->args; ! ! allargs = list_concat(allargs, list_copy(args)); ! } ! ! appendStringInfoString(buf, "unnest("); ! get_rule_expr((Node *) allargs, context, true); ! } ! else ! { ! appendStringInfoString(buf, "TABLE("); ! ! foreach(lc, rte->funcexprs) ! { ! FuncExpr *fn = lfirst(lc); ! ! get_rule_expr((Node *) fn, context, true); ! ! if (IsA(fn, FuncExpr) &&fn->funccoltypes != NIL) ! { ! /* ! * Function returning RECORD, reconstruct the ! * columndefs ! */ ! appendStringInfoString(buf, " AS "); ! get_from_clause_coldeflist(NULL, ! fn->funccolnames, ! fn->funccoltypes, ! fn->funccoltypmods, ! fn->funccolcollations, ! context); ! } ! ! if (lnext(lc)) ! appendStringInfoString(buf, ", "); ! } ! } ! appendStringInfoChar(buf, ')'); ! } if (rte->funcordinality) appendStringInfoString(buf, " WITH ORDINALITY"); break; *************** get_from_clause_item(Node *jtnode, Query *** 8099,8111 **** appendStringInfo(buf, " %s", quote_identifier(refname)); /* Print the column definitions or aliases, if needed */ ! if (rte->rtekind == RTE_FUNCTION && rte->funccoltypes != NIL) { /* Function returning RECORD, reconstruct the columndefs */ get_from_clause_coldeflist(colinfo, ! rte->funccoltypes, ! rte->funccoltypmods, ! rte->funccolcollations, context); } else --- 8195,8208 ---- appendStringInfo(buf, " %s", quote_identifier(refname)); /* Print the column definitions or aliases, if needed */ ! if (rte->rtekind == RTE_FUNCTION && func_coldef && func_coldef->funccoltypes != NIL) { /* Function returning RECORD, reconstruct the columndefs */ get_from_clause_coldeflist(colinfo, ! NIL, ! func_coldef->funccoltypes, ! func_coldef->funccoltypmods, ! func_coldef->funccolcollations, context); } else *************** get_column_alias_list(deparse_columns *c *** 8254,8260 **** * responsible for ensuring that an alias or AS is present before it. */ static void ! get_from_clause_coldeflist(deparse_columns *colinfo, List *types, List *typmods, List *collations, deparse_context *context) { --- 8351,8357 ---- * responsible for ensuring that an alias or AS is present before it. */ static void ! get_from_clause_coldeflist(deparse_columns *colinfo, List *names, List *types, List *typmods, List *collations, deparse_context *context) { *************** get_from_clause_coldeflist(deparse_colum *** 8262,8267 **** --- 8359,8365 ---- ListCell *l1; ListCell *l2; ListCell *l3; + ListCell *l4 = (names ? list_head(names) : NULL); int i; appendStringInfoChar(buf, '('); *************** get_from_clause_coldeflist(deparse_colum *** 8269,8275 **** i = 0; forthree(l1, types, l2, typmods, l3, collations) { ! char *attname = colinfo->colnames[i]; Oid atttypid = lfirst_oid(l1); int32 atttypmod = lfirst_int(l2); Oid attcollation = lfirst_oid(l3); --- 8367,8373 ---- i = 0; forthree(l1, types, l2, typmods, l3, collations) { ! char *attname = (colinfo ? colinfo->colnames[i] : strVal(lfirst(l4))); Oid atttypid = lfirst_oid(l1); int32 atttypmod = lfirst_int(l2); Oid attcollation = lfirst_oid(l3); *************** get_from_clause_coldeflist(deparse_colum *** 8285,8290 **** --- 8383,8391 ---- attcollation != get_typcollation(atttypid)) appendStringInfo(buf, " COLLATE %s", generate_collation_name(attcollation)); + + if (!colinfo) + l4 = lnext(l4); i++; } diff --git a/src/backend/utils/fmgr/funcapi.c b/src/backend/utils/fmgr/funcapi.c index 6347a8f..73f9f04 100644 *** a/src/backend/utils/fmgr/funcapi.c --- b/src/backend/utils/fmgr/funcapi.c *************** internal_get_result_type(Oid funcid, *** 375,381 **** case TYPEFUNC_SCALAR: break; case TYPEFUNC_RECORD: ! /* We must get the tupledesc from call context */ if (rsinfo && IsA(rsinfo, ReturnSetInfo) && rsinfo->expectedDesc != NULL) { --- 375,386 ---- case TYPEFUNC_SCALAR: break; case TYPEFUNC_RECORD: ! ! /* ! * We prefer to get the tupledesc from the call context since that ! * is already built. If there isn't one, we try and cons it up ! * from the funccol* fields of FuncExpr. ! */ if (rsinfo && IsA(rsinfo, ReturnSetInfo) && rsinfo->expectedDesc != NULL) { *************** internal_get_result_type(Oid funcid, *** 384,389 **** --- 389,410 ---- *resultTupleDesc = rsinfo->expectedDesc; /* Assume no polymorphic columns here, either */ } + else if (call_expr && IsA(call_expr, FuncExpr)) + { + FuncExpr *func = (FuncExpr *) call_expr; + + if (func->funccoltypes != NIL) + { + tupdesc = BuildDescFromLists(func->funccolnames, + func->funccoltypes, + func->funccoltypmods, + func->funccolcollations); + result = TYPEFUNC_COMPOSITE; + if (resultTupleDesc) + *resultTupleDesc = tupdesc; + /* Assume no polymorphic columns here, either */ + } + } break; default: break; diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h index 49226b7..6bf3b35 100644 *** a/src/include/access/tupdesc.h --- b/src/include/access/tupdesc.h *************** extern TupleDesc CreateTupleDesc(int nat *** 87,93 **** Form_pg_attribute *attrs); extern TupleDesc CreateTupleDescCopy(TupleDesc tupdesc); - extern TupleDesc CreateTupleDescCopyExtend(TupleDesc tupdesc, int moreatts); extern TupleDesc CreateTupleDescCopyConstr(TupleDesc tupdesc); --- 87,92 ---- *************** extern void TupleDescInitEntryCollation( *** 121,126 **** --- 120,128 ---- AttrNumber attributeNumber, Oid collationid); + extern void TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno, + const TupleDesc src, AttrNumber srcAttno); + extern TupleDesc BuildDescForRelation(List *schema); extern TupleDesc BuildDescFromLists(List *names, List *types, List *typmods, List *collations); diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index bedcf04..dcc0a5e 100644 *** a/src/include/nodes/execnodes.h --- b/src/include/nodes/execnodes.h *************** typedef struct SubqueryScanState *** 1395,1418 **** * function appearing in FROM (typically a function returning set). * * eflags node's capability flags ! * ordinal column value for WITH ORDINALITY * scan_tupdesc scan tuple descriptor ! * func_tupdesc function tuple descriptor ! * func_slot function result slot, or null ! * tuplestorestate private state of tuplestore.c ! * funcexpr state for function expression being evaluated * ---------------- */ typedef struct FunctionScanState { ScanState ss; /* its first field is NodeTag */ int eflags; ! int64 ordinal; TupleDesc scan_tupdesc; ! TupleDesc func_tupdesc; ! TupleTableSlot *func_slot; ! Tuplestorestate *tuplestorestate; ! ExprState *funcexpr; } FunctionScanState; /* ---------------- --- 1395,1421 ---- * function appearing in FROM (typically a function returning set). * * eflags node's capability flags ! * ordinality is this scan WITH ORDINALITY? ! * ordinal current ordinal column value * scan_tupdesc scan tuple descriptor ! * nfuncs number of functions being executed ! * funcstates per-function execution states (private in ! * nodeFunctionscan.c) * ---------------- */ + struct FunctionScanPerFuncState; + typedef struct FunctionScanState { ScanState ss; /* its first field is NodeTag */ int eflags; ! bool ordinality; ! bool simple; ! int64 ordinal; TupleDesc scan_tupdesc; ! int nfuncs; ! struct FunctionScanPerFuncState *funcstates; /* array of length ! * nfuncs */ } FunctionScanState; /* ---------------- diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 55524b4..e881795 100644 *** a/src/include/nodes/parsenodes.h --- b/src/include/nodes/parsenodes.h *************** typedef struct FuncCall *** 304,309 **** --- 304,310 ---- bool agg_distinct; /* arguments were labeled DISTINCT */ bool func_variadic; /* last argument was labeled VARIADIC */ struct WindowDef *over; /* OVER clause, if any */ + List *coldeflist; /* column definition list for record funcs */ int location; /* token location, or -1 if unknown */ } FuncCall; *************** typedef struct RangeSubselect *** 466,481 **** /* * RangeFunction - function call appearing in a FROM clause */ typedef struct RangeFunction { NodeTag type; bool lateral; /* does it have LATERAL prefix? */ bool ordinality; /* does it have WITH ORDINALITY suffix? */ ! Node *funccallnode; /* untransformed function call tree */ Alias *alias; /* table alias & optional column aliases */ - List *coldeflist; /* list of ColumnDef nodes to describe result - * of function returning RECORD */ } RangeFunction; /* --- 467,484 ---- /* * RangeFunction - function call appearing in a FROM clause + * + * funccallnodes is a list because we use this to represent the construct + * TABLE(func1(...),func2(...),...) AS ... */ typedef struct RangeFunction { NodeTag type; bool lateral; /* does it have LATERAL prefix? */ bool ordinality; /* does it have WITH ORDINALITY suffix? */ ! bool is_table; /* result of TABLE() syntax */ ! List *funccallnodes; /* untransformed function call trees */ Alias *alias; /* table alias & optional column aliases */ } RangeFunction; /* *************** typedef struct XmlSerialize *** 653,664 **** * colnames for columns dropped since the rule was created (and for that * matter the colnames might be out of date due to column renamings). * ! * The same comments apply to FUNCTION RTEs when the function's return type * is a named composite type. In addition, for all return types, FUNCTION ! * RTEs with ORDINALITY must always have the last colname entry being the ! * one for the ordinal column; this is enforced when constructing the RTE. ! * Thus when ORDINALITY is used, there will be exactly one more colname ! * than would have been present otherwise. * * In JOIN RTEs, the colnames in both alias and eref are one-to-one with * joinaliasvars entries. A JOIN RTE will omit columns of its inputs when --- 656,667 ---- * colnames for columns dropped since the rule was created (and for that * matter the colnames might be out of date due to column renamings). * ! * The same comments apply to FUNCTION RTEs when a function's return type * is a named composite type. In addition, for all return types, FUNCTION ! * RTEs with ORDINALITY must always have the last colname entry being the ! * one for the ordinal column; this is enforced when constructing the RTE. ! * Thus when ORDINALITY is used, there will be exactly one more colname ! * than would have been present otherwise. * * In JOIN RTEs, the colnames in both alias and eref are one-to-one with * joinaliasvars entries. A JOIN RTE will omit columns of its inputs when *************** typedef struct RangeTblEntry *** 757,777 **** /* * Fields valid for a function RTE (else NULL): * ! * If the function returns an otherwise-unspecified RECORD, funccoltypes ! * lists the column types declared in the RTE's column type specification, ! * funccoltypmods lists their declared typmods, funccolcollations their ! * collations. Note that in this case, ORDINALITY is not permitted, so ! * there is no extra ordinal column to be allowed for. * ! * Otherwise, those fields are NIL, and the result column types must be ! * derived from the funcexpr while treating the ordinal column, if ! * present, as a special case. (see get_rte_attribute_*) */ ! Node *funcexpr; /* expression tree for func call */ ! List *funccoltypes; /* OID list of column type OIDs */ ! List *funccoltypmods; /* integer list of column typmods */ ! List *funccolcollations; /* OID list of column collation OIDs */ ! bool funcordinality; /* is this called WITH ORDINALITY? */ /* * Fields valid for a values RTE (else NIL): --- 760,775 ---- /* * Fields valid for a function RTE (else NULL): * ! * If the function returns an otherwise-unspecified RECORD, we used to ! * store type lists here; we now push those down to the individual ! * FuncExpr nodes, so that we can handle multiple RECORD functions and/or ! * RECORD functions with ordinality. * ! * So, in all cases the result column types can be determined from the ! * funcexprs, with the ordinality column, if present, appended to the end. */ ! List *funcexprs; /* expression trees for func calls */ ! bool funcordinality; /* is this called WITH ORDINALITY? */ /* * Fields valid for a values RTE (else NIL): diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 44ea0b7..b5df47c 100644 *** a/src/include/nodes/plannodes.h --- b/src/include/nodes/plannodes.h *************** typedef struct SubqueryScan *** 424,435 **** typedef struct FunctionScan { Scan scan; ! Node *funcexpr; /* expression tree for func call */ ! bool funcordinality; /* WITH ORDINALITY */ ! List *funccolnames; /* output column names (string Value nodes) */ ! List *funccoltypes; /* OID list of column type OIDs */ ! List *funccoltypmods; /* integer list of column typmods */ ! List *funccolcollations; /* OID list of column collation OIDs */ } FunctionScan; /* ---------------- --- 424,434 ---- typedef struct FunctionScan { Scan scan; ! List *funcexprs; /* expression trees for func calls */ ! List *funccolnames; /* result column names */ ! bool funcordinality; /* WITH ORDINALITY */ ! /* keep this last due to nonstandard output */ ! List *funcparams; /* Bitmapsets for params used by each func */ } FunctionScan; /* ---------------- diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7918537..ba97f00 100644 *** a/src/include/nodes/primnodes.h --- b/src/include/nodes/primnodes.h *************** typedef struct FuncExpr *** 354,359 **** --- 354,365 ---- Oid funccollid; /* OID of collation of result */ Oid inputcollid; /* OID of collation that function should use */ List *args; /* arguments to the function */ + /* These func* fields are used only for table functions returning RECORD */ + List *funccolnames; /* result colnames for RECORD rangefuncs */ + List *funccoltypes; /* result coltypes for RECORD rangefuncs */ + List *funccoltypmods; /* result coltypmods for RECORD rangefuncs */ + List *funccolcollations; /* result colcollations for RECORD + * rangefuncs */ int location; /* token location, or -1 if unknown */ } FuncExpr; diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index 9686229..0033a3c 100644 *** a/src/include/optimizer/pathnode.h --- b/src/include/optimizer/pathnode.h *************** extern UniquePath *create_unique_path(Pl *** 70,76 **** extern Path *create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, List *pathkeys, Relids required_outer); extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel, ! Relids required_outer); extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel, Relids required_outer); extern Path *create_ctescan_path(PlannerInfo *root, RelOptInfo *rel, --- 70,76 ---- extern Path *create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, List *pathkeys, Relids required_outer); extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel, ! List *pathkeys, Relids required_outer); extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel, Relids required_outer); extern Path *create_ctescan_path(PlannerInfo *root, RelOptInfo *rel, diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 96ffdb1..faccbda 100644 *** a/src/include/optimizer/paths.h --- b/src/include/optimizer/paths.h *************** extern Path *get_cheapest_fractional_pat *** 166,171 **** --- 166,173 ---- double fraction); extern List *build_index_pathkeys(PlannerInfo *root, IndexOptInfo *index, ScanDirection scandir); + extern List *build_expression_pathkey(PlannerInfo *root, RelOptInfo *rel, + Expr *expr, Oid opno, bool nulls_first); extern List *convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel, List *subquery_pathkeys); extern List *build_join_pathkeys(PlannerInfo *root, diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h index d33eef3..867d1ff 100644 *** a/src/include/parser/parse_func.h --- b/src/include/parser/parse_func.h *************** typedef enum *** 43,51 **** extern Node *ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, ! List *agg_order, Expr *agg_filter, ! bool agg_star, bool agg_distinct, bool func_variadic, ! WindowDef *over, bool is_column, int location); extern FuncDetailCode func_get_detail(List *funcname, List *fargs, List *fargnames, --- 43,49 ---- extern Node *ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, ! int location, FuncCall *fn); extern FuncDetailCode func_get_detail(List *funcname, List *fargs, List *fargnames, diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index 83c9131..808d341 100644 *** a/src/include/parser/parse_relation.h --- b/src/include/parser/parse_relation.h *************** extern RangeTblEntry *addRangeTableEntry *** 58,65 **** bool lateral, bool inFromCl); extern RangeTblEntry *addRangeTableEntryForFunction(ParseState *pstate, ! char *funcname, ! Node *funcexpr, RangeFunction *rangefunc, bool lateral, bool inFromCl); --- 58,65 ---- bool lateral, bool inFromCl); extern RangeTblEntry *addRangeTableEntryForFunction(ParseState *pstate, ! List *funcnames, ! List *funcexprs, RangeFunction *rangefunc, bool lateral, bool inFromCl); diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out index 37391dc..fe6550e 100644 *** a/src/test/regress/expected/rangefuncs.out --- b/src/test/regress/expected/rangefuncs.out *************** select definition from pg_views where vi *** 87,145 **** (1 row) drop view vw_ord; ! -- ordinality vs. rewind and reverse scan begin; ! declare foo scroll cursor for select * from generate_series(1,5) with ordinality as g(i,o); fetch all from foo; ! i | o ! ---+--- ! 1 | 1 ! 2 | 2 ! 3 | 3 ! 4 | 4 ! 5 | 5 (5 rows) fetch backward all from foo; ! i | o ! ---+--- ! 5 | 5 ! 4 | 4 ! 3 | 3 ! 2 | 2 ! 1 | 1 (5 rows) fetch all from foo; ! i | o ! ---+--- ! 1 | 1 ! 2 | 2 ! 3 | 3 ! 4 | 4 ! 5 | 5 (5 rows) fetch next from foo; ! i | o ! ---+--- (0 rows) fetch next from foo; ! i | o ! ---+--- (0 rows) fetch prior from foo; ! i | o ! ---+--- ! 5 | 5 (1 row) fetch absolute 1 from foo; ! i | o ! ---+--- ! 1 | 1 (1 row) commit; --- 87,293 ---- (1 row) drop view vw_ord; ! -- multiple functions ! select * from table(foot(1),foot(2)) with ordinality as z(a,b,c,d,ord); ! a | b | c | d | ord ! ---+-----+---+----+----- ! 1 | 11 | 2 | 22 | 1 ! 1 | 111 | | | 2 ! (2 rows) ! ! create temporary view vw_ord as select * from (values (1)) v(n) join table(foot(1),foot(2)) with ordinality as z(a,b,c,d,ord)on (n=ord); ! select * from vw_ord; ! n | a | b | c | d | ord ! ---+---+----+---+----+----- ! 1 | 1 | 11 | 2 | 22 | 1 ! (1 row) ! ! select definition from pg_views where viewname='vw_ord'; ! definition ! ----------------------------------------------------------------------------------------- ! SELECT v.n, + ! z.a, + ! z.b, + ! z.c, + ! z.d, + ! z.ord + ! FROM (( VALUES (1)) v(n) + ! JOIN TABLE(foot(1), foot(2)) WITH ORDINALITY z(a, b, c, d, ord) ON ((v.n = z.ord))); ! (1 row) ! ! drop view vw_ord; ! -- expansions of unnest() ! select * from unnest(array[10,20],array['foo','bar'],array[1.0]); ! unnest | unnest | unnest ! --------+--------+-------- ! 10 | foo | 1.0 ! 20 | bar | ! (2 rows) ! ! select * from unnest(array[10,20],array['foo','bar'],array[1.0]) with ordinality as z(a,b,c,ord); ! a | b | c | ord ! ----+-----+-----+----- ! 10 | foo | 1.0 | 1 ! 20 | bar | | 2 ! (2 rows) ! ! select * from table(unnest(array[10,20],array['foo','bar'],array[1.0])) with ordinality as z(a,b,c,ord); ! a | b | c | ord ! ----+-----+-----+----- ! 10 | foo | 1.0 | 1 ! 20 | bar | | 2 ! (2 rows) ! ! select * from table(unnest(array[10,20],array['foo','bar']), generate_series(101,102)) with ordinality as z(a,b,c,ord); ! a | b | c | ord ! ----+-----+-----+----- ! 10 | foo | 101 | 1 ! 20 | bar | 102 | 2 ! (2 rows) ! ! create temporary view vw_ord as select * from unnest(array[10,20],array['foo','bar'],array[1.0]) as z(a,b,c); ! select * from vw_ord; ! a | b | c ! ----+-----+----- ! 10 | foo | 1.0 ! 20 | bar | ! (2 rows) ! ! select definition from pg_views where viewname='vw_ord'; ! definition ! ---------------------------------------------------------------------------------------- ! SELECT z.a, + ! z.b, + ! z.c + ! FROM unnest(ARRAY[10, 20], ARRAY['foo'::text, 'bar'::text], ARRAY[1.0]) z(a, b, c); ! (1 row) ! ! drop view vw_ord; ! create temporary view vw_ord as select * from table(unnest(array[10,20],array['foo','bar'],array[1.0])) as z(a,b,c); ! select * from vw_ord; ! a | b | c ! ----+-----+----- ! 10 | foo | 1.0 ! 20 | bar | ! (2 rows) ! ! select definition from pg_views where viewname='vw_ord'; ! definition ! ---------------------------------------------------------------------------------------- ! SELECT z.a, + ! z.b, + ! z.c + ! FROM unnest(ARRAY[10, 20], ARRAY['foo'::text, 'bar'::text], ARRAY[1.0]) z(a, b, c); ! (1 row) ! ! drop view vw_ord; ! create temporary view vw_ord as select * from table(unnest(array[10,20],array['foo','bar']), generate_series(1,2)) as z(a,b,c); ! select * from vw_ord; ! a | b | c ! ----+-----+--- ! 10 | foo | 1 ! 20 | bar | 2 ! (2 rows) ! ! select definition from pg_views where viewname='vw_ord'; ! definition ! ------------------------------------------------------------------------------------------------------------------ ! SELECT z.a, + ! z.b, + ! z.c + ! FROM TABLE(unnest(ARRAY[10, 20]), unnest(ARRAY['foo'::text, 'bar'::text]), generate_series(1, 2)) z(a, b, c); ! (1 row) ! ! drop view vw_ord; ! -- ordinality and multiple functions vs. rewind and reverse scan begin; ! declare foo scroll cursor for select * from table(generate_series(1,5),generate_series(1,2)) with ordinality as g(i,j,o); fetch all from foo; ! i | j | o ! ---+---+--- ! 1 | 1 | 1 ! 2 | 2 | 2 ! 3 | | 3 ! 4 | | 4 ! 5 | | 5 (5 rows) fetch backward all from foo; ! i | j | o ! ---+---+--- ! 5 | | 5 ! 4 | | 4 ! 3 | | 3 ! 2 | 2 | 2 ! 1 | 1 | 1 (5 rows) fetch all from foo; ! i | j | o ! ---+---+--- ! 1 | 1 | 1 ! 2 | 2 | 2 ! 3 | | 3 ! 4 | | 4 ! 5 | | 5 (5 rows) fetch next from foo; ! i | j | o ! ---+---+--- (0 rows) fetch next from foo; ! i | j | o ! ---+---+--- (0 rows) fetch prior from foo; ! i | j | o ! ---+---+--- ! 5 | | 5 (1 row) fetch absolute 1 from foo; ! i | j | o ! ---+---+--- ! 1 | 1 | 1 ! (1 row) ! ! fetch next from foo; ! i | j | o ! ---+---+--- ! 2 | 2 | 2 ! (1 row) ! ! fetch next from foo; ! i | j | o ! ---+---+--- ! 3 | | 3 ! (1 row) ! ! fetch next from foo; ! i | j | o ! ---+---+--- ! 4 | | 4 ! (1 row) ! ! fetch prior from foo; ! i | j | o ! ---+---+--- ! 3 | | 3 ! (1 row) ! ! fetch prior from foo; ! i | j | o ! ---+---+--- ! 2 | 2 | 2 ! (1 row) ! ! fetch prior from foo; ! i | j | o ! ---+---+--- ! 1 | 1 | 1 (1 row) commit; *************** INSERT INTO foo VALUES(1,1,'Joe'); *** 199,260 **** INSERT INTO foo VALUES(1,2,'Ed'); INSERT INTO foo VALUES(2,1,'Mary'); -- sql, proretset = f, prorettype = b ! CREATE FUNCTION getfoo(int) RETURNS int AS 'SELECT $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; t1 ---- 1 (1 row) ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); v | o ---+--- 1 | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; ! getfoo ! -------- ! 1 (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY as t1(v,o); SELECT * FROM vw_getfoo; v | o ---+--- 1 | 1 (1 row) - -- sql, proretset = t, prorettype = b DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof int AS 'SELECT fooid FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; t1 ---- 1 1 (2 rows) ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); v | o ---+--- 1 | 1 1 | 2 (2 rows) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; ! getfoo ! -------- ! 1 ! 1 (2 rows) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; v | o ---+--- --- 347,407 ---- INSERT INTO foo VALUES(1,2,'Ed'); INSERT INTO foo VALUES(2,1,'Mary'); -- sql, proretset = f, prorettype = b ! CREATE FUNCTION getfoo1(int) RETURNS int AS 'SELECT $1;' LANGUAGE SQL; ! SELECT * FROM getfoo1(1) AS t1; t1 ---- 1 (1 row) ! SELECT * FROM getfoo1(1) WITH ORDINALITY AS t1(v,o); v | o ---+--- 1 | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo1(1); SELECT * FROM vw_getfoo; ! getfoo1 ! --------- ! 1 (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo1(1) WITH ORDINALITY as t1(v,o); SELECT * FROM vw_getfoo; v | o ---+--- 1 | 1 (1 row) DROP VIEW vw_getfoo; ! -- sql, proretset = t, prorettype = b ! CREATE FUNCTION getfoo2(int) RETURNS setof int AS 'SELECT fooid FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo2(1) AS t1; t1 ---- 1 1 (2 rows) ! SELECT * FROM getfoo2(1) WITH ORDINALITY AS t1(v,o); v | o ---+--- 1 | 1 1 | 2 (2 rows) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo2(1); SELECT * FROM vw_getfoo; ! getfoo2 ! --------- ! 1 ! 1 (2 rows) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo2(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; v | o ---+--- *************** SELECT * FROM vw_getfoo; *** 262,295 **** 1 | 2 (2 rows) - -- sql, proretset = t, prorettype = b DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof text AS 'SELECT fooname FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; t1 ----- Joe Ed (2 rows) ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); v | o -----+--- Joe | 1 Ed | 2 (2 rows) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; ! getfoo ! -------- Joe Ed (2 rows) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; v | o -----+--- --- 409,441 ---- 1 | 2 (2 rows) DROP VIEW vw_getfoo; ! -- sql, proretset = t, prorettype = b ! CREATE FUNCTION getfoo3(int) RETURNS setof text AS 'SELECT fooname FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo3(1) AS t1; t1 ----- Joe Ed (2 rows) ! SELECT * FROM getfoo3(1) WITH ORDINALITY AS t1(v,o); v | o -----+--- Joe | 1 Ed | 2 (2 rows) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo3(1); SELECT * FROM vw_getfoo; ! getfoo3 ! --------- Joe Ed (2 rows) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo3(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; v | o -----+--- *************** SELECT * FROM vw_getfoo; *** 297,319 **** Ed | 2 (2 rows) - -- sql, proretset = f, prorettype = c DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe (1 row) ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; fooid | foosubid | fooname -------+----------+--------- --- 443,464 ---- Ed | 2 (2 rows) DROP VIEW vw_getfoo; ! -- sql, proretset = f, prorettype = c ! CREATE FUNCTION getfoo4(int) RETURNS foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo4(1) AS t1; fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe (1 row) ! SELECT * FROM getfoo4(1) WITH ORDINALITY AS t1(a,b,c,o); a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo4(1); SELECT * FROM vw_getfoo; fooid | foosubid | fooname -------+----------+--------- *************** SELECT * FROM vw_getfoo; *** 321,352 **** (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 (1 row) - -- sql, proretset = t, prorettype = c DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe 1 | 2 | Ed (2 rows) ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 1 | 2 | Ed | 2 (2 rows) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; fooid | foosubid | fooname -------+----------+--------- --- 466,496 ---- (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo4(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 (1 row) DROP VIEW vw_getfoo; ! -- sql, proretset = t, prorettype = c ! CREATE FUNCTION getfoo5(int) RETURNS setof foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo5(1) AS t1; fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe 1 | 2 | Ed (2 rows) ! SELECT * FROM getfoo5(1) WITH ORDINALITY AS t1(a,b,c,o); a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 1 | 2 | Ed | 2 (2 rows) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo5(1); SELECT * FROM vw_getfoo; fooid | foosubid | fooname -------+----------+--------- *************** SELECT * FROM vw_getfoo; *** 355,361 **** (2 rows) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; a | b | c | o ---+---+-----+--- --- 499,505 ---- (2 rows) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo5(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; a | b | c | o ---+---+-----+--- *************** SELECT * FROM vw_getfoo; *** 363,380 **** 1 | 2 | Ed | 2 (2 rows) - -- ordinality not supported for returns record yet - -- sql, proretset = f, prorettype = record DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS RECORD AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1(fooid int, foosubid int, fooname text); fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; fooid | foosubid | fooname --- 507,528 ---- 1 | 2 | Ed | 2 (2 rows) DROP VIEW vw_getfoo; ! -- sql, proretset = f, prorettype = record ! CREATE FUNCTION getfoo6(int) RETURNS RECORD AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo6(1) AS t1(fooid int, foosubid int, fooname text); fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe (1 row) ! SELECT * FROM TABLE( getfoo6(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY; ! fooid | foosubid | fooname | ordinality ! -------+----------+---------+------------ ! 1 | 1 | Joe | 1 ! (1 row) ! ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo6(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; fooid | foosubid | fooname *************** SELECT * FROM vw_getfoo; *** 382,399 **** 1 | 1 | Joe (1 row) - -- sql, proretset = t, prorettype = record DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof record AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1(fooid int, foosubid int, fooname text); fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe 1 | 2 | Ed (2 rows) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; fooid | foosubid | fooname --- 530,563 ---- 1 | 1 | Joe (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS ! SELECT * FROM TABLE( getfoo6(1) AS (fooid int, foosubid int, fooname text) ) ! WITH ORDINALITY; ! SELECT * FROM vw_getfoo; ! fooid | foosubid | fooname | ordinality ! -------+----------+---------+------------ ! 1 | 1 | Joe | 1 ! (1 row) ! ! DROP VIEW vw_getfoo; ! -- sql, proretset = t, prorettype = record ! CREATE FUNCTION getfoo7(int) RETURNS setof record AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo7(1) AS t1(fooid int, foosubid int, fooname text); fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe 1 | 2 | Ed (2 rows) ! SELECT * FROM TABLE( getfoo7(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY; ! fooid | foosubid | fooname | ordinality ! -------+----------+---------+------------ ! 1 | 1 | Joe | 1 ! 1 | 2 | Ed | 2 ! (2 rows) ! ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo7(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; fooid | foosubid | fooname *************** SELECT * FROM vw_getfoo; *** 402,455 **** 1 | 2 | Ed (2 rows) - -- plpgsql, proretset = f, prorettype = b DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS int AS 'DECLARE fooint int; BEGIN SELECT fooid into fooint FROM foo WHERE fooid = $1;RETURN fooint; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo(1) AS t1; t1 ---- 1 (1 row) ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); v | o ---+--- 1 | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; ! getfoo ! -------- ! 1 (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; v | o ---+--- 1 | 1 (1 row) - -- plpgsql, proretset = f, prorettype = c DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS foo AS 'DECLARE footup foo%ROWTYPE; BEGIN SELECT * into footup FROM foo WHERE fooid= $1; RETURN footup; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo(1) AS t1; fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe (1 row) ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; fooid | foosubid | fooname -------+----------+--------- --- 566,628 ---- 1 | 2 | Ed (2 rows) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS ! SELECT * FROM TABLE( getfoo7(1) AS (fooid int, foosubid int, fooname text) ) ! WITH ORDINALITY; ! SELECT * FROM vw_getfoo; ! fooid | foosubid | fooname | ordinality ! -------+----------+---------+------------ ! 1 | 1 | Joe | 1 ! 1 | 2 | Ed | 2 ! (2 rows) ! ! DROP VIEW vw_getfoo; ! -- plpgsql, proretset = f, prorettype = b ! CREATE FUNCTION getfoo8(int) RETURNS int AS 'DECLARE fooint int; BEGIN SELECT fooid into fooint FROM foo WHERE fooid =$1; RETURN fooint; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo8(1) AS t1; t1 ---- 1 (1 row) ! SELECT * FROM getfoo8(1) WITH ORDINALITY AS t1(v,o); v | o ---+--- 1 | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo8(1); SELECT * FROM vw_getfoo; ! getfoo8 ! --------- ! 1 (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo8(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; v | o ---+--- 1 | 1 (1 row) DROP VIEW vw_getfoo; ! -- plpgsql, proretset = f, prorettype = c ! CREATE FUNCTION getfoo9(int) RETURNS foo AS 'DECLARE footup foo%ROWTYPE; BEGIN SELECT * into footup FROM foo WHERE fooid= $1; RETURN footup; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo9(1) AS t1; fooid | foosubid | fooname -------+----------+--------- 1 | 1 | Joe (1 row) ! SELECT * FROM getfoo9(1) WITH ORDINALITY AS t1(a,b,c,o); a | b | c | o ---+---+-----+--- 1 | 1 | Joe | 1 (1 row) ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo9(1); SELECT * FROM vw_getfoo; fooid | foosubid | fooname -------+----------+--------- *************** SELECT * FROM vw_getfoo; *** 457,463 **** (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; a | b | c | o ---+---+-----+--- --- 630,636 ---- (1 row) DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo9(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; a | b | c | o ---+---+-----+--- *************** SELECT * FROM vw_getfoo; *** 465,487 **** (1 row) DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); DROP FUNCTION foot(int); DROP TABLE foo2; DROP TABLE foo; -- Rescan tests -- ! CREATE TEMPORARY SEQUENCE foo_rescan_seq; CREATE TYPE foo_rescan_t AS (i integer, s bigint); ! CREATE FUNCTION foo_sql(int,int) RETURNS setof foo_rescan_t AS 'SELECT i, nextval(''foo_rescan_seq'') FROM generate_series($1,$2)i;' LANGUAGE SQL; -- plpgsql functions use materialize mode ! CREATE FUNCTION foo_mat(int,int) RETURNS setof foo_rescan_t AS 'begin for i in $1..$2 loop return next (i, nextval(''foo_rescan_seq''));end loop; end;' LANGUAGE plpgsql; --invokes ExecReScanFunctionScan - all these cases should materialize the function only once -- LEFT JOIN on a condition that the planner can't prove to be true is used to ensure the function -- is on the inner path of a nestloop join ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) ON (r+i)<100; --- 638,719 ---- (1 row) DROP VIEW vw_getfoo; ! -- mix 'n match kinds, to exercise expandRTE and related logic ! select * from table(getfoo1(1),getfoo2(1),getfoo3(1),getfoo4(1),getfoo5(1), ! getfoo6(1) AS (fooid int, foosubid int, fooname text), ! getfoo7(1) AS (fooid int, foosubid int, fooname text), ! getfoo8(1),getfoo9(1)) ! with ordinality as t1(a,b,c,d,e,f,g,h,i,j,k,l,m,o,p,q,r,s,t,u); ! a | b | c | d | e | f | g | h | i | j | k | l | m | o | p | q | r | s | t | u ! ---+---+-----+---+---+-----+---+---+-----+---+---+-----+---+---+-----+---+---+---+-----+--- ! 1 | 1 | Joe | 1 | 1 | Joe | 1 | 1 | Joe | 1 | 1 | Joe | 1 | 1 | Joe | 1 | 1 | 1 | Joe | 1 ! | 1 | Ed | | | | 1 | 2 | Ed | | | | 1 | 2 | Ed | | | | | 2 ! (2 rows) ! ! select * from table(getfoo9(1),getfoo8(1), ! getfoo7(1) AS (fooid int, foosubid int, fooname text), ! getfoo6(1) AS (fooid int, foosubid int, fooname text), ! getfoo5(1),getfoo4(1),getfoo3(1),getfoo2(1),getfoo1(1)) ! with ordinality as t1(a,b,c,d,e,f,g,h,i,j,k,l,m,o,p,q,r,s,t,u); ! a | b | c | d | e | f | g | h | i | j | k | l | m | o | p | q | r | s | t | u ! ---+---+-----+---+---+---+-----+---+---+-----+---+---+-----+---+---+-----+-----+---+---+--- ! 1 | 1 | Joe | 1 | 1 | 1 | Joe | 1 | 1 | Joe | 1 | 1 | Joe | 1 | 1 | Joe | Joe | 1 | 1 | 1 ! | | | | 1 | 2 | Ed | | | | 1 | 2 | Ed | | | | Ed | 1 | | 2 ! (2 rows) ! ! create temporary view vw_foo as ! select * from table(getfoo9(1), ! getfoo7(1) AS (fooid int, foosubid int, fooname text), ! getfoo1(1)) ! with ordinality as t1(a,b,c,d,e,f,g,n); ! select * from vw_foo; ! a | b | c | d | e | f | g | n ! ---+---+-----+---+---+-----+---+--- ! 1 | 1 | Joe | 1 | 1 | Joe | 1 | 1 ! | | | 1 | 2 | Ed | | 2 ! (2 rows) ! ! select pg_get_viewdef('vw_foo'); ! pg_get_viewdef ! -------------------------------------------------------------------------------------------------------------------------------------------------- ! SELECT t1.a, + ! t1.b, + ! t1.c, + ! t1.d, + ! t1.e, + ! t1.f, + ! t1.g, + ! t1.n + ! FROM TABLE(getfoo9(1), getfoo7(1) AS (fooid integer, foosubid integer, fooname text), getfoo1(1)) WITH ORDINALITY t1(a,b, c, d, e, f, g, n); ! (1 row) ! ! drop view vw_foo; ! DROP FUNCTION getfoo1(int); ! DROP FUNCTION getfoo2(int); ! DROP FUNCTION getfoo3(int); ! DROP FUNCTION getfoo4(int); ! DROP FUNCTION getfoo5(int); ! DROP FUNCTION getfoo6(int); ! DROP FUNCTION getfoo7(int); ! DROP FUNCTION getfoo8(int); ! DROP FUNCTION getfoo9(int); DROP FUNCTION foot(int); DROP TABLE foo2; DROP TABLE foo; -- Rescan tests -- ! CREATE TEMPORARY SEQUENCE foo_rescan_seq1; ! CREATE TEMPORARY SEQUENCE foo_rescan_seq2; CREATE TYPE foo_rescan_t AS (i integer, s bigint); ! CREATE FUNCTION foo_sql(int,int) RETURNS setof foo_rescan_t AS 'SELECT i, nextval(''foo_rescan_seq1'') FROM generate_series($1,$2)i;' LANGUAGE SQL; -- plpgsql functions use materialize mode ! CREATE FUNCTION foo_mat(int,int) RETURNS setof foo_rescan_t AS 'begin for i in $1..$2 loop return next (i, nextval(''foo_rescan_seq2''));end loop; end;' LANGUAGE plpgsql; --invokes ExecReScanFunctionScan - all these cases should materialize the function only once -- LEFT JOIN on a condition that the planner can't prove to be true is used to ensure the function -- is on the inner path of a nestloop join ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) ON (r+i)<100; *************** SELECT * FROM (VALUES (1),(2),(3)) v(r) *** 498,507 **** 3 | 13 | 3 (9 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; --- 730,739 ---- 3 | 13 | 3 (9 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; *************** SELECT * FROM (VALUES (1),(2),(3)) v(r) *** 518,527 **** 3 | 13 | 3 | 3 (9 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) ON (r+i)<100; --- 750,759 ---- 3 | 13 | 3 | 3 (9 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) ON (r+i)<100; *************** SELECT * FROM (VALUES (1),(2),(3)) v(r) *** 538,547 **** 3 | 13 | 3 (9 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; --- 770,779 ---- 3 | 13 | 3 (9 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; *************** SELECT * FROM (VALUES (1),(2),(3)) v(r) *** 558,563 **** --- 790,815 ---- 3 | 13 | 3 | 3 (9 rows) + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + setval | setval + --------+-------- + 1 | 1 + (1 row) + + SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN TABLE( foo_sql(11,13), foo_mat(11,13) ) WITH ORDINALITY AS f(i1,s1,i2,s2,o)ON (r+i1+i2)<100; + r | i1 | s1 | i2 | s2 | o + ---+----+----+----+----+--- + 1 | 11 | 1 | 11 | 1 | 1 + 1 | 12 | 2 | 12 | 2 | 2 + 1 | 13 | 3 | 13 | 3 | 3 + 2 | 11 | 1 | 11 | 1 | 1 + 2 | 12 | 2 | 12 | 2 | 2 + 2 | 13 | 3 | 13 | 3 | 3 + 3 | 11 | 1 | 11 | 1 | 1 + 3 | 12 | 2 | 12 | 2 | 2 + 3 | 13 | 3 | 13 | 3 | 3 + (9 rows) + SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN generate_series(11,13) f(i) ON (r+i)<100; r | i ---+---- *************** SELECT * FROM (VALUES (1),(2),(3)) v(r) *** 615,624 **** (9 rows) --invokes ExecReScanFunctionScan with chgParam != NULL (using implied LATERAL) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13); --- 867,876 ---- (9 rows) --invokes ExecReScanFunctionScan with chgParam != NULL (using implied LATERAL) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 632,641 **** 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13) WITH ORDINALITY AS f(i,s,o); --- 884,893 ---- 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13) WITH ORDINALITY AS f(i,s,o); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 649,658 **** 3 | 13 | 6 | 1 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r); --- 901,910 ---- 3 | 13 | 6 | 1 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 666,675 **** 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r) WITH ORDINALITY AS f(i,s,o); --- 918,927 ---- 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r) WITH ORDINALITY AS f(i,s,o); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 683,692 **** 3 | 13 | 6 | 3 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2); --- 935,944 ---- 3 | 13 | 6 | 3 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2); *************** SELECT * FROM (VALUES (11,12),(13,15),(1 *** 704,713 **** 16 | 20 | 20 | 10 (10 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2) WITH ORDINALITY AS f(i,s,o); --- 956,965 ---- 16 | 20 | 20 | 10 (10 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2) WITH ORDINALITY AS f(i,s,o); *************** SELECT * FROM (VALUES (11,12),(13,15),(1 *** 725,734 **** 16 | 20 | 20 | 10 | 5 (10 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13); --- 977,986 ---- 16 | 20 | 20 | 10 | 5 (10 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 742,751 **** 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13) WITH ORDINALITY AS f(i,s,o); --- 994,1003 ---- 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13) WITH ORDINALITY AS f(i,s,o); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 759,768 **** 3 | 13 | 6 | 1 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r); --- 1011,1020 ---- 3 | 13 | 6 | 1 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 776,785 **** 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r) WITH ORDINALITY AS f(i,s,o); --- 1028,1037 ---- 3 | 13 | 6 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r) WITH ORDINALITY AS f(i,s,o); *************** SELECT * FROM (VALUES (1),(2),(3)) v(r), *** 793,802 **** 3 | 13 | 6 | 3 (6 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2); --- 1045,1054 ---- 3 | 13 | 6 | 3 (6 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2); *************** SELECT * FROM (VALUES (11,12),(13,15),(1 *** 814,823 **** 16 | 20 | 20 | 10 (10 rows) ! SELECT setval('foo_rescan_seq',1,false); ! setval ! -------- ! 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2) WITH ORDINALITY AS f(i,s,o); --- 1066,1075 ---- 16 | 20 | 20 | 10 (10 rows) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); ! setval | setval ! --------+-------- ! 1 | 1 (1 row) SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2) WITH ORDINALITY AS f(i,s,o); *************** SELECT * FROM (VALUES (11,12),(13,15),(1 *** 835,840 **** --- 1087,1168 ---- 16 | 20 | 20 | 10 | 5 (10 rows) + -- selective rescan of multiple functions: + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + setval | setval + --------+-------- + 1 | 1 + (1 row) + + SELECT * FROM (VALUES (1),(2),(3)) v(r), TABLE( foo_sql(11,11), foo_mat(10+r,13) ); + r | i | s | i | s + ---+----+---+----+--- + 1 | 11 | 1 | 11 | 1 + 1 | | | 12 | 2 + 1 | | | 13 | 3 + 2 | 11 | 1 | 12 | 4 + 2 | | | 13 | 5 + 3 | 11 | 1 | 13 | 6 + (6 rows) + + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + setval | setval + --------+-------- + 1 | 1 + (1 row) + + SELECT * FROM (VALUES (1),(2),(3)) v(r), TABLE( foo_sql(10+r,13), foo_mat(11,11) ); + r | i | s | i | s + ---+----+---+----+--- + 1 | 11 | 1 | 11 | 1 + 1 | 12 | 2 | | + 1 | 13 | 3 | | + 2 | 12 | 4 | 11 | 1 + 2 | 13 | 5 | | + 3 | 13 | 6 | 11 | 1 + (6 rows) + + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + setval | setval + --------+-------- + 1 | 1 + (1 row) + + SELECT * FROM (VALUES (1),(2),(3)) v(r), TABLE( foo_sql(10+r,13), foo_mat(10+r,13) ); + r | i | s | i | s + ---+----+---+----+--- + 1 | 11 | 1 | 11 | 1 + 1 | 12 | 2 | 12 | 2 + 1 | 13 | 3 | 13 | 3 + 2 | 12 | 4 | 12 | 4 + 2 | 13 | 5 | 13 | 5 + 3 | 13 | 6 | 13 | 6 + (6 rows) + + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + setval | setval + --------+-------- + 1 | 1 + (1 row) + + SELECT * FROM generate_series(1,2) r1, generate_series(r1,3) r2, TABLE( foo_sql(10+r1,13), foo_mat(10+r2,13) ); + r1 | r2 | i | s | i | s + ----+----+----+----+----+--- + 1 | 1 | 11 | 1 | 11 | 1 + 1 | 1 | 12 | 2 | 12 | 2 + 1 | 1 | 13 | 3 | 13 | 3 + 1 | 2 | 11 | 4 | 12 | 4 + 1 | 2 | 12 | 5 | 13 | 5 + 1 | 2 | 13 | 6 | | + 1 | 3 | 11 | 7 | 13 | 6 + 1 | 3 | 12 | 8 | | + 1 | 3 | 13 | 9 | | + 2 | 2 | 12 | 10 | 12 | 7 + 2 | 2 | 13 | 11 | 13 | 8 + 2 | 3 | 12 | 12 | 13 | 9 + 2 | 3 | 13 | 13 | | + (13 rows) + SELECT * FROM (VALUES (1),(2),(3)) v(r), generate_series(10+r,20-r) f(i); r | i ---+---- *************** SELECT * FROM (VALUES (1),(2),(3)) v1(r1 *** 1072,1078 **** DROP FUNCTION foo_sql(int,int); DROP FUNCTION foo_mat(int,int); ! DROP SEQUENCE foo_rescan_seq; -- -- Test cases involving OUT parameters -- --- 1400,1407 ---- DROP FUNCTION foo_sql(int,int); DROP FUNCTION foo_mat(int,int); ! DROP SEQUENCE foo_rescan_seq1; ! DROP SEQUENCE foo_rescan_seq2; -- -- Test cases involving OUT parameters -- *************** SELECT * FROM get_users() WITH ORDINALIT *** 1566,1571 **** --- 1895,1915 ---- id2 | email2 | t | 2 (2 rows) + -- multiple functions vs. dropped columns + SELECT * FROM TABLE(generate_series(10,11), get_users()) WITH ORDINALITY; + generate_series | userid | email | enabled | ordinality + -----------------+--------+--------+---------+------------ + 10 | id | email | t | 1 + 11 | id2 | email2 | t | 2 + (2 rows) + + SELECT * FROM TABLE(get_users(), generate_series(10,11)) WITH ORDINALITY; + userid | email | enabled | generate_series | ordinality + --------+--------+---------+-----------------+------------ + id | email | t | 10 | 1 + id2 | email2 | t | 11 | 2 + (2 rows) + drop function get_first_user(); drop function get_users(); drop table users; diff --git a/src/test/regress/sql/rangefuncs.sql b/src/test/regress/sql/rangefuncs.sql index e82a1d5..ce0c9af 100644 *** a/src/test/regress/sql/rangefuncs.sql --- b/src/test/regress/sql/rangefuncs.sql *************** create temporary view vw_ord as select * *** 21,29 **** select * from vw_ord; select definition from pg_views where viewname='vw_ord'; drop view vw_ord; ! -- ordinality vs. rewind and reverse scan begin; ! declare foo scroll cursor for select * from generate_series(1,5) with ordinality as g(i,o); fetch all from foo; fetch backward all from foo; fetch all from foo; --- 21,55 ---- select * from vw_ord; select definition from pg_views where viewname='vw_ord'; drop view vw_ord; ! ! -- multiple functions ! select * from table(foot(1),foot(2)) with ordinality as z(a,b,c,d,ord); ! create temporary view vw_ord as select * from (values (1)) v(n) join table(foot(1),foot(2)) with ordinality as z(a,b,c,d,ord)on (n=ord); ! select * from vw_ord; ! select definition from pg_views where viewname='vw_ord'; ! drop view vw_ord; ! ! -- expansions of unnest() ! select * from unnest(array[10,20],array['foo','bar'],array[1.0]); ! select * from unnest(array[10,20],array['foo','bar'],array[1.0]) with ordinality as z(a,b,c,ord); ! select * from table(unnest(array[10,20],array['foo','bar'],array[1.0])) with ordinality as z(a,b,c,ord); ! select * from table(unnest(array[10,20],array['foo','bar']), generate_series(101,102)) with ordinality as z(a,b,c,ord); ! create temporary view vw_ord as select * from unnest(array[10,20],array['foo','bar'],array[1.0]) as z(a,b,c); ! select * from vw_ord; ! select definition from pg_views where viewname='vw_ord'; ! drop view vw_ord; ! create temporary view vw_ord as select * from table(unnest(array[10,20],array['foo','bar'],array[1.0])) as z(a,b,c); ! select * from vw_ord; ! select definition from pg_views where viewname='vw_ord'; ! drop view vw_ord; ! create temporary view vw_ord as select * from table(unnest(array[10,20],array['foo','bar']), generate_series(1,2)) as z(a,b,c); ! select * from vw_ord; ! select definition from pg_views where viewname='vw_ord'; ! drop view vw_ord; ! ! -- ordinality and multiple functions vs. rewind and reverse scan begin; ! declare foo scroll cursor for select * from table(generate_series(1,5),generate_series(1,2)) with ordinality as g(i,j,o); fetch all from foo; fetch backward all from foo; fetch all from foo; *************** fetch next from foo; *** 31,36 **** --- 57,68 ---- fetch next from foo; fetch prior from foo; fetch absolute 1 from foo; + fetch next from foo; + fetch next from foo; + fetch next from foo; + fetch prior from foo; + fetch prior from foo; + fetch prior from foo; commit; -- function with implicit LATERAL *************** INSERT INTO foo VALUES(1,2,'Ed'); *** 57,189 **** INSERT INTO foo VALUES(2,1,'Mary'); -- sql, proretset = f, prorettype = b ! CREATE FUNCTION getfoo(int) RETURNS int AS 'SELECT $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY as t1(v,o); SELECT * FROM vw_getfoo; -- sql, proretset = t, prorettype = b ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof int AS 'SELECT fooid FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; -- sql, proretset = t, prorettype = b ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof text AS 'SELECT fooname FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; -- sql, proretset = f, prorettype = c ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; -- sql, proretset = t, prorettype = c ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1; ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; - -- ordinality not supported for returns record yet -- sql, proretset = f, prorettype = record ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS RECORD AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1(fooid int, foosubid int, fooname text); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; -- sql, proretset = t, prorettype = record ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS setof record AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo(1) AS t1(fooid int, foosubid int, fooname text); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; -- plpgsql, proretset = f, prorettype = b ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS int AS 'DECLARE fooint int; BEGIN SELECT fooid into fooint FROM foo WHERE fooid = $1;RETURN fooint; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo(1) AS t1; ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; -- plpgsql, proretset = f, prorettype = c ! DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); ! CREATE FUNCTION getfoo(int) RETURNS foo AS 'DECLARE footup foo%ROWTYPE; BEGIN SELECT * into footup FROM foo WHERE fooid= $1; RETURN footup; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo(1) AS t1; ! SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; - DROP VIEW vw_getfoo; ! DROP FUNCTION getfoo(int); DROP FUNCTION foot(int); DROP TABLE foo2; DROP TABLE foo; -- Rescan tests -- ! CREATE TEMPORARY SEQUENCE foo_rescan_seq; CREATE TYPE foo_rescan_t AS (i integer, s bigint); ! CREATE FUNCTION foo_sql(int,int) RETURNS setof foo_rescan_t AS 'SELECT i, nextval(''foo_rescan_seq'') FROM generate_series($1,$2)i;' LANGUAGE SQL; -- plpgsql functions use materialize mode ! CREATE FUNCTION foo_mat(int,int) RETURNS setof foo_rescan_t AS 'begin for i in $1..$2 loop return next (i, nextval(''foo_rescan_seq''));end loop; end;' LANGUAGE plpgsql; --invokes ExecReScanFunctionScan - all these cases should materialize the function only once -- LEFT JOIN on a condition that the planner can't prove to be true is used to ensure the function -- is on the inner path of a nestloop join ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) ON (r+i)<100; ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) ON (r+i)<100; ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN generate_series(11,13) f(i) ON (r+i)<100; SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN generate_series(11,13) WITH ORDINALITY AS f(i,o) ON (r+i)<100; --- 89,257 ---- INSERT INTO foo VALUES(2,1,'Mary'); -- sql, proretset = f, prorettype = b ! CREATE FUNCTION getfoo1(int) RETURNS int AS 'SELECT $1;' LANGUAGE SQL; ! SELECT * FROM getfoo1(1) AS t1; ! SELECT * FROM getfoo1(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo1(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo1(1) WITH ORDINALITY as t1(v,o); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- sql, proretset = t, prorettype = b ! CREATE FUNCTION getfoo2(int) RETURNS setof int AS 'SELECT fooid FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo2(1) AS t1; ! SELECT * FROM getfoo2(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo2(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo2(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- sql, proretset = t, prorettype = b ! CREATE FUNCTION getfoo3(int) RETURNS setof text AS 'SELECT fooname FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo3(1) AS t1; ! SELECT * FROM getfoo3(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo3(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo3(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- sql, proretset = f, prorettype = c ! CREATE FUNCTION getfoo4(int) RETURNS foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo4(1) AS t1; ! SELECT * FROM getfoo4(1) WITH ORDINALITY AS t1(a,b,c,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo4(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo4(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- sql, proretset = t, prorettype = c ! CREATE FUNCTION getfoo5(int) RETURNS setof foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo5(1) AS t1; ! SELECT * FROM getfoo5(1) WITH ORDINALITY AS t1(a,b,c,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo5(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo5(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- sql, proretset = f, prorettype = record ! CREATE FUNCTION getfoo6(int) RETURNS RECORD AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo6(1) AS t1(fooid int, foosubid int, fooname text); ! SELECT * FROM TABLE( getfoo6(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo6(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; + CREATE VIEW vw_getfoo AS + SELECT * FROM TABLE( getfoo6(1) AS (fooid int, foosubid int, fooname text) ) + WITH ORDINALITY; + SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- sql, proretset = t, prorettype = record ! CREATE FUNCTION getfoo7(int) RETURNS setof record AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL; ! SELECT * FROM getfoo7(1) AS t1(fooid int, foosubid int, fooname text); ! SELECT * FROM TABLE( getfoo7(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo7(1) AS (fooid int, foosubid int, fooname text); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; + CREATE VIEW vw_getfoo AS + SELECT * FROM TABLE( getfoo7(1) AS (fooid int, foosubid int, fooname text) ) + WITH ORDINALITY; + SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- plpgsql, proretset = f, prorettype = b ! CREATE FUNCTION getfoo8(int) RETURNS int AS 'DECLARE fooint int; BEGIN SELECT fooid into fooint FROM foo WHERE fooid =$1; RETURN fooint; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo8(1) AS t1; ! SELECT * FROM getfoo8(1) WITH ORDINALITY AS t1(v,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo8(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo8(1) WITH ORDINALITY AS t1(v,o); SELECT * FROM vw_getfoo; + DROP VIEW vw_getfoo; -- plpgsql, proretset = f, prorettype = c ! CREATE FUNCTION getfoo9(int) RETURNS foo AS 'DECLARE footup foo%ROWTYPE; BEGIN SELECT * into footup FROM foo WHERE fooid= $1; RETURN footup; END;' LANGUAGE plpgsql; ! SELECT * FROM getfoo9(1) AS t1; ! SELECT * FROM getfoo9(1) WITH ORDINALITY AS t1(a,b,c,o); ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo9(1); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! CREATE VIEW vw_getfoo AS SELECT * FROM getfoo9(1) WITH ORDINALITY AS t1(a,b,c,o); SELECT * FROM vw_getfoo; DROP VIEW vw_getfoo; ! ! -- mix 'n match kinds, to exercise expandRTE and related logic ! ! select * from table(getfoo1(1),getfoo2(1),getfoo3(1),getfoo4(1),getfoo5(1), ! getfoo6(1) AS (fooid int, foosubid int, fooname text), ! getfoo7(1) AS (fooid int, foosubid int, fooname text), ! getfoo8(1),getfoo9(1)) ! with ordinality as t1(a,b,c,d,e,f,g,h,i,j,k,l,m,o,p,q,r,s,t,u); ! select * from table(getfoo9(1),getfoo8(1), ! getfoo7(1) AS (fooid int, foosubid int, fooname text), ! getfoo6(1) AS (fooid int, foosubid int, fooname text), ! getfoo5(1),getfoo4(1),getfoo3(1),getfoo2(1),getfoo1(1)) ! with ordinality as t1(a,b,c,d,e,f,g,h,i,j,k,l,m,o,p,q,r,s,t,u); ! ! create temporary view vw_foo as ! select * from table(getfoo9(1), ! getfoo7(1) AS (fooid int, foosubid int, fooname text), ! getfoo1(1)) ! with ordinality as t1(a,b,c,d,e,f,g,n); ! select * from vw_foo; ! select pg_get_viewdef('vw_foo'); ! drop view vw_foo; ! ! DROP FUNCTION getfoo1(int); ! DROP FUNCTION getfoo2(int); ! DROP FUNCTION getfoo3(int); ! DROP FUNCTION getfoo4(int); ! DROP FUNCTION getfoo5(int); ! DROP FUNCTION getfoo6(int); ! DROP FUNCTION getfoo7(int); ! DROP FUNCTION getfoo8(int); ! DROP FUNCTION getfoo9(int); DROP FUNCTION foot(int); DROP TABLE foo2; DROP TABLE foo; -- Rescan tests -- ! CREATE TEMPORARY SEQUENCE foo_rescan_seq1; ! CREATE TEMPORARY SEQUENCE foo_rescan_seq2; CREATE TYPE foo_rescan_t AS (i integer, s bigint); ! CREATE FUNCTION foo_sql(int,int) RETURNS setof foo_rescan_t AS 'SELECT i, nextval(''foo_rescan_seq1'') FROM generate_series($1,$2)i;' LANGUAGE SQL; -- plpgsql functions use materialize mode ! CREATE FUNCTION foo_mat(int,int) RETURNS setof foo_rescan_t AS 'begin for i in $1..$2 loop return next (i, nextval(''foo_rescan_seq2''));end loop; end;' LANGUAGE plpgsql; --invokes ExecReScanFunctionScan - all these cases should materialize the function only once -- LEFT JOIN on a condition that the planner can't prove to be true is used to ensure the function -- is on the inner path of a nestloop join ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) ON (r+i)<100; ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_sql(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) ON (r+i)<100; ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN foo_mat(11,13) WITH ORDINALITY AS f(i,s,o) ON (r+i)<100; + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN TABLE( foo_sql(11,13), foo_mat(11,13) ) WITH ORDINALITY AS f(i1,s1,i2,s2,o)ON (r+i1+i2)<100; SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN generate_series(11,13) f(i) ON (r+i)<100; SELECT * FROM (VALUES (1),(2),(3)) v(r) LEFT JOIN generate_series(11,13) WITH ORDINALITY AS f(i,o) ON (r+i)<100; *************** SELECT * FROM (VALUES (1),(2),(3)) v(r) *** 193,224 **** --invokes ExecReScanFunctionScan with chgParam != NULL (using implied LATERAL) ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2); ! SELECT setval('foo_rescan_seq',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2) WITH ORDINALITY AS f(i,s,o); SELECT * FROM (VALUES (1),(2),(3)) v(r), generate_series(10+r,20-r) f(i); SELECT * FROM (VALUES (1),(2),(3)) v(r), generate_series(10+r,20-r) WITH ORDINALITY AS f(i,o); --- 261,304 ---- --invokes ExecReScanFunctionScan with chgParam != NULL (using implied LATERAL) ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(10+r,13) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_sql(11,10+r) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_sql(r1,r2) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(10+r,13) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (1),(2),(3)) v(r), foo_mat(11,10+r) WITH ORDINALITY AS f(i,s,o); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2); ! SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); SELECT * FROM (VALUES (11,12),(13,15),(16,20)) v(r1,r2), foo_mat(r1,r2) WITH ORDINALITY AS f(i,s,o); + -- selective rescan of multiple functions: + + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + SELECT * FROM (VALUES (1),(2),(3)) v(r), TABLE( foo_sql(11,11), foo_mat(10+r,13) ); + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + SELECT * FROM (VALUES (1),(2),(3)) v(r), TABLE( foo_sql(10+r,13), foo_mat(11,11) ); + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + SELECT * FROM (VALUES (1),(2),(3)) v(r), TABLE( foo_sql(10+r,13), foo_mat(10+r,13) ); + + SELECT setval('foo_rescan_seq1',1,false),setval('foo_rescan_seq2',1,false); + SELECT * FROM generate_series(1,2) r1, generate_series(r1,3) r2, TABLE( foo_sql(10+r1,13), foo_mat(10+r2,13) ); + SELECT * FROM (VALUES (1),(2),(3)) v(r), generate_series(10+r,20-r) f(i); SELECT * FROM (VALUES (1),(2),(3)) v(r), generate_series(10+r,20-r) WITH ORDINALITY AS f(i,o); *************** SELECT * FROM (VALUES (1),(2),(3)) v1(r1 *** 242,248 **** DROP FUNCTION foo_sql(int,int); DROP FUNCTION foo_mat(int,int); ! DROP SEQUENCE foo_rescan_seq; -- -- Test cases involving OUT parameters --- 322,329 ---- DROP FUNCTION foo_sql(int,int); DROP FUNCTION foo_mat(int,int); ! DROP SEQUENCE foo_rescan_seq1; ! DROP SEQUENCE foo_rescan_seq2; -- -- Test cases involving OUT parameters *************** language sql stable; *** 466,471 **** --- 547,555 ---- SELECT get_users(); SELECT * FROM get_users(); SELECT * FROM get_users() WITH ORDINALITY; -- make sure ordinality copes + -- multiple functions vs. dropped columns + SELECT * FROM TABLE(generate_series(10,11), get_users()) WITH ORDINALITY; + SELECT * FROM TABLE(get_users(), generate_series(10,11)) WITH ORDINALITY; drop function get_first_user(); drop function get_users();
I wrote: > Andrew Gierth <andrew@tao11.riddles.org.uk> writes: >> Here is a new patch with the following changes on top of Heikki's >> version (all the changes in which I've otherwise kept): > Here is an updated version: I've been hacking on this patch all day yesterday. What I'm on about at the moment is reversing the decision to move range functions' funccoltypes etc into FuncExpr. That's a bad idea on the grounds of bloating FuncExpr, but the real problem with it is this: what happens if the planner decides to inline or const-simplify the function expression? You just lost a critical part of the RTE's infrastructure, that's what. So that's got to go, and I've been fooling with different ways to represent the info for multiple functions within RangeTblEntry. What I have at the moment is /* * Fields valid for a function RTE (else NIL/zero): * * There can be multiple function expressions in a functionRTE. * funccoldeflist is an integer list (of the same length as funcexprs) * containing true if function hada column definition list, else false. * funccolcounts is an integer list (of the same length as funcexprs) * showingthe number of RTE output columns produced by each function. * The length of eref->colnames must be equal to eitherthe sum of the * funccolcounts entries, or one more than the sum if funcordinality is * true. funccoltypes,funccoltypmods, and funccolcollations give type * information about each output column (these lists musthave the same * length as eref->colnames). Remember that when a function returns a * named composite type, anydropped columns in that type will have dummy * corresponding entries in these lists. * * Note: funccoltypes etcare derived from either the functions' declared * result types, or their column definition lists in case of functions * returning RECORD. Storing this data in the RTE is redundant in the * former case, but for simplicity westore it always. */ List *funcexprs; /* expression trees for func calls */ List *funccoldeflist; /* integer list of has-coldeflist booleans */ List *funccolcounts; /* number of output columnsfrom each func */ List *funccoltypes; /* OID list of column type OIDs */ List *funccoltypmods; /* integer list of column typmods */ List *funccolcollations; /* OID list of column collationOIDs */ bool funcordinality; /* is this called WITH ORDINALITY? */ which has the advantage that the ordinality column is no longer such a special case, it's right there in the lists. However, it turns out that in most places where I thought we could just consult the entry-per-column lists, we can't. We still have to do the get_expr_result_type() dance, because we need up-to-date information about which columns of a composite-returning function's output have been dropped since the RTE was made. That means we'd have to chase the entry-per-function lists in parallel with the entry-per-column lists, which is a PITA. I'm thinking possibly it's worth inventing a new Node type that would just be infrastructure for RTE_FUNCTION RTEs, so that we'd have something like this in RangeTblEntry: List *functions; /* List of RangeTblFunction nodes */ bool funcordinality; /* is this called WITHORDINALITY? */ and a node type RangeTblFunction with fields Node *funcexpr; /* executable expression for the function */ int funccolcount; /* number ofcolumns emitted by function */ /* These lists are NIL unless function had a column definition list: */ List *funccoltypes; /* OID list of column type OIDs */ List *funccoltypmods; /* integer list of column typmods */ List *funccolcollations; /* OID list of column collation OIDs */ BTW, the reason we need to store the column count explicitly is that we have to ignore the added columns if a composite type has had an ADD COLUMN done to it since the RTE was made. The submitted patch fails rather nastily in such cases, if the composite type isn't last in the function list. Thoughts, better ideas? regards, tom lane
>>>>> "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: Tom> I've been hacking on this patch all day yesterday. What I'm onTom> about at the moment is reversing the decision tomove rangeTom> functions' funccoltypes etc into FuncExpr. That's a bad idea onTom> the grounds of bloating FuncExpr, butthe real problem with itTom> is this: what happens if the planner decides to inline orTom> const-simplify the functionexpression? You just lost aTom> critical part of the RTE's infrastructure, that's what. Inlining should already check that the type doesn't change as a result; where exactly is the issue here? What matters is whether get_expr_result_type still works; the only place (other than ruleutils) now that looks at funccoltypes etc. is the guts of that. Is it incorrect to assume that if a FuncExpr is transformed in any way, the result should give the same return from get_expr_result_type? -- Andrew (irc:RhodiumToad)
>>>>> "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: Tom> BTW, the reason we need to store the column count explicitly isTom> that we have to ignore the added columns if a compositetype hasTom> had an ADD COLUMN done to it since the RTE was made. TheTom> submitted patch fails rather nastilyin such cases, if theTom> composite type isn't last in the function list. Am I understanding correctly that the only reason this didn't fail before we added ORDINALITY is that the executor in general does not care if there are more columns in a tuple than it expects? And that adding ORDINALITY broke this already? -- Andrew (irc:RhodiumToad)
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: > Tom> BTW, the reason we need to store the column count explicitly is > Tom> that we have to ignore the added columns if a composite type has > Tom> had an ADD COLUMN done to it since the RTE was made. The > Tom> submitted patch fails rather nastily in such cases, if the > Tom> composite type isn't last in the function list. > Am I understanding correctly that the only reason this didn't fail > before we added ORDINALITY is that the executor in general does not > care if there are more columns in a tuple than it expects? And that > adding ORDINALITY broke this already? Probably it's already broken with ORDINALITY, but I've not checked. regards, tom lane
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: > Tom> I've been hacking on this patch all day yesterday. What I'm on > Tom> about at the moment is reversing the decision to move range > Tom> functions' funccoltypes etc into FuncExpr. That's a bad idea on > Tom> the grounds of bloating FuncExpr, but the real problem with it > Tom> is this: what happens if the planner decides to inline or > Tom> const-simplify the function expression? You just lost a > Tom> critical part of the RTE's infrastructure, that's what. > Inlining should already check that the type doesn't change as a > result; where exactly is the issue here? The issue is that if you want to dig column type information out of a function RTE, that won't necessarily work after preprocess_expression has had its way with the contained expressions. That's needed at the very least in create_functionscan_plan. You might try to argue that flattening of an expression-returning-RECORD is guaranteed to preserve whatever we know about the result type, but that argument sounds mighty flimsy to me. There's nothing much guaranteeing that the expression couldn't be folded to a Const, or at least something that didn't have a FuncExpr at the top. In any case, there is absolutely nothing that is desirable enough about this representation that we should take any risks for it. The historical approach is that the coldeflist data is securely attached to the RangeTblEntry itself, and I think we should stick with that. regards, tom lane
>>>>> "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: >> Inlining should already check that the type doesn't change as a>> result; where exactly is the issue here? Tom> The issue is that if you want to dig column type information outTom> of a function RTE, that won't necessarily workafterTom> preprocess_expression has had its way with the containedTom> expressions. That's needed at the very leastinTom> create_functionscan_plan. My intention was that whatever was in the funcexprs list should be self-describing as far as result type information goes - whether or not it was a FuncExpr node. create_functionscan_plan used to copy the funccoltypes etc. to the FunctionScan node, but I removed that in favour of having get_expr_result_type do the work. Tom> You might try to argue that flattening of anTom> expression-returning-RECORD is guaranteed to preserve whateverTom>we know about the result type, but that argument sounds mightyTom> flimsy to me. There's nothing much guaranteeingthat theTom> expression couldn't be folded to a Const, or at least somethingTom> that didn't have a FuncExprat the top. So, at the moment, get_expr_result_type can't return a tupdesc for an expression tree that doesn't have FuncExpr or OpExpr at the top and which doesn't return a named composite type. If there's an issue here, then it goes beyond functions-returning-RECORD and affects flattening of functions with OUT parameters too; if there were some way for those to get replaced by a Const node (currently there is not: see comment in evaluate_function) then that would break, and that clearly has nothing to do with coldef lists. I can see that it would be nice to allow folding and so on in these cases, but it seems to me that having some infrastructure that would allow get_expr_result_type to return the same result for the transformed call as the original call is a prerequisite for any such change. Tom> In any case, there is absolutely nothing that is desirableTom> enough about this representation that we should takeany risksTom> for it. The historical approach is that the coldeflist data isTom> securely attached to the RangeTblEntryitself, and I think weTom> should stick with that. What I was aiming for was to _remove_ any special-case handling of coldef lists (post-parser) and use only get_expr_result_type. -- Andrew. (irc:RhodiumToad)
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: > Tom> The issue is that if you want to dig column type information out > Tom> of a function RTE, that won't necessarily work after > Tom> preprocess_expression has had its way with the contained > Tom> expressions. That's needed at the very least in > Tom> create_functionscan_plan. > What I was aiming for was to _remove_ any special-case handling of > coldef lists (post-parser) and use only get_expr_result_type. [ thinks for awhile... ] I can see that that would have some value if we were looking to expand the usage of coldeflists to allow "record_returning_function(...) AS (coldeflist)" to appear in any expression context not just function RTEs. However, I can't get excited about that as a future feature, for two reasons: 1. IME, functions returning unconstrained RECORD tend to return sets as well; if you don't know what columns you return, it's unlikely you know how many rows you return. So this would only be a sensible feature addition if we were looking to encourage the use of SRFs outside the FROM clause. I'm not sure whether we are going to deprecate that, but I'm pretty sure we don't want to encourage it. 2. There's a syntactic problem, stemming from the perhaps unfortunate choice to shoehorn coldeflists into the SQL alias syntax: if you've got SELECT foo(...) AS ... it'd be impossible to tell after seeing AS whether what follows is a coldeflist (which'd need to be parsed as part of the function call) or a column alias (which'd need to not be). So this would be a shift-or-reduce conflict for bison, and I venture that humans would get confused too. There are also implementation-level reasons to want to keep this behavior tied to RTE_FUNCTION RTEs rather than being loose in the expression tree evaluator: we can much more easily handle RTEs that return a random collection of columns than we can handle arbitrary rowtypes in expressions. In particular, the latter works only with the "blessed rowtype" hack, which doesn't scale nicely to lots of different rowtypes used in a session. And I've always considered that to be strictly a runtime thing, too --- I don't want the interpretation of parsetrees to require consulting the anonymous-rowtype cache. So on the whole, I can't get excited about decoupling coldeflists from function RTEs. Even if I were excited about it, I'd see it as a separate feature unrelated to the stated goals of this patch. regards, tom lane
Andrew Gierth <andrew@tao11.riddles.org.uk> wrote: > The spec syntax for table function calls, <table function derived table> > in <table reference>, looks like TABLE(func(args...)) AS ... > This patch implements that, plus an extension: it allows multiple > functions, TABLE(func1(...), func2(...), func3(...)) [WITH ORDINALITY] > and defines this as meaning that the functions are to be evaluated in > parallel. I went back and looked at the spec, and so far as I can tell, the claim that this is spec syntax plus an extension is a falsehood. What I read in SQL:2008 7.6 <table reference> is <table function derived table> ::= TABLE <left paren> <collection value expression> <right paren> where <collection value expression> is elsewhere defined to be an expression returning an array or multiset value, and then syntax rule 2 says: * the <collection value expression> shall be a <routine invocation> * this construct is equivalent to UNNEST ( <collection value expression> ) So unless I'm misreading it, the spec's idea is that you could write SELECT ... FROM TABLE( function_returning_array(...) ) and this would result in producing the array elements as a table column. There is nothing in there about a function returning set. You could argue that that leaves us with the freedom to define what the construct does for functions returning set --- but as this patch stands, if a function doesn't return set but does return an array, the behavior will not be what the spec plainly demands. I do like the basic concept of this syntax, but I think it's a serious error to appropriate the TABLE() spelling for something that doesn't agree with the spec's semantics for that spelling. We need to spell it some other way. I've not experimented to see what's practical in bison, but a couple of ideas that come to mind are: 1. Use FUNCTION instead of TABLE. 2. Don't use any keyword, just parens. Right now you get a syntax error from that: regression=# select * from (foo(), bar()) s; ERROR: syntax error at or near "," LINE 1: select * from (foo(), bar()) s; ^ which implies that it's syntax space we could commandeer. On the other hand, I'm a bit worried about the future-proof-ness of such a choice. It's uncomfortably close to one of the ways to write a row expression, so it's not too hard to foresee the SQL committee someday defining something like this in FROM clauses. It's also hard to see what you'd call the construct in documentation or error messages --- no keyword means no easy name to apply. Thoughts, other ideas? regards, tom lane
On Wed, Nov 20, 2013 at 3:07 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote: > Andrew Gierth <andrew@tao11.riddles.org.uk> wrote: >> The spec syntax for table function calls, <table function derived table> >> in <table reference>, looks like TABLE(func(args...)) AS ... > >> This patch implements that, plus an extension: it allows multiple >> functions, TABLE(func1(...), func2(...), func3(...)) [WITH ORDINALITY] >> and defines this as meaning that the functions are to be evaluated in >> parallel. > > I went back and looked at the spec, and so far as I can tell, the claim > that this is spec syntax plus an extension is a falsehood. What > I read in SQL:2008 7.6 <table reference> is > > <table function derived table> ::= > TABLE <left paren> <collection value expression> <right paren> > > where <collection value expression> is elsewhere defined to be an > expression returning an array or multiset value, and then syntax rule 2 > says: > > * the <collection value expression> shall be a <routine invocation> > > * this construct is equivalent to UNNEST ( <collection value expression> ) > > So unless I'm misreading it, the spec's idea is that you could write > > SELECT ... FROM TABLE( function_returning_array(...) ) > > and this would result in producing the array elements as a table column. > There is nothing in there about a function returning set. You could argue > that that leaves us with the freedom to define what the construct does > for functions returning set --- but as this patch stands, if a function > doesn't return set but does return an array, the behavior will not be what > the spec plainly demands. The original post on this thread includes this example, which mixes SRFs and arrays by running the array through UNNEST: select * from table(generate_series(10,20,5), unnest(array['fred','jim'])); But if we think the spec calls for things to be implicitly unnested, you could still get the same effect by adjusting the query. You'd just get rid of the UNNEST from the argument that had it and wrap ARRAY(SELECT ...) around the other one: select * from table(array(select generate_series(10,20,5)), array['fred','jim']); It's not clear to me whether that's likely to be inefficient in practical cases, but there's no real loss of functionality. IOW, I'm not sure we really need to invent a new syntax here; maybe we can just implement the spec, assuming your interpretation thereof is correct. -- Robert Haas EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company
>>>>> "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: Tom> and this would result in producing the array elements as a tableTom> column. There is nothing in there about a functionreturningTom> set. In the spec, there is no such thing as a function returning a set of rows in the sense that we use. Functions can return arrays or multisets of simple or composite types, but a multiset is a single value like an array (just with slightly different semantics), not a set of rows. (And in particular it's not ordered.) -- Andrew (irc:RhodiumToad)
Robert Haas wrote > select * from table(array(select generate_series(10,20,5)), > array['fred','jim']); Can we have our arrays and eat our functions too? (and is someone willing to bake such a complicated cake...) select * from table ( ARRAY | FUNCTION/SET [, ARRAY | FUNCTION/SET ]* ) The standard-compliant case is handled as required - and those who want to write compliant code can use the array(select function) trick - while others can avoid straining their eyes and fingers. Since we would have to invent implicit unnesting anyway to conform, and the function version is working currently, the suggested behavior would seem to be the ideal target. David J. -- View this message in context: http://postgresql.1045698.n5.nabble.com/UNNEST-with-multiple-args-and-TABLE-with-multiple-funcs-tp5767280p5779512.html Sent from the PostgreSQL - hackers mailing list archive at Nabble.com.
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: > Tom> and this would result in producing the array elements as a table > Tom> column. There is nothing in there about a function returning > Tom> set. > In the spec, there is no such thing as a function returning a set of > rows in the sense that we use. Right, but they do have a concept of arrays that's similar to ours, and AFAICS the spec demands different behavior for an array-returning function than what we've got here. We could conceivably say that we'll implicitly UNNEST() if the function returns array, and not otherwise --- but that seems pretty inconsistent and surprise-making to me. I'm not too sure what to do if a function returns setof array, either. regards, tom lane
Robert Haas <robertmhaas@gmail.com> writes: > The original post on this thread includes this example, which mixes > SRFs and arrays by running the array through UNNEST: > select * from table(generate_series(10,20,5), unnest(array['fred','jim'])); > But if we think the spec calls for things to be implicitly unnested, > you could still get the same effect by adjusting the query. You'd > just get rid of the UNNEST from the argument that had it and wrap > ARRAY(SELECT ...) around the other one: > select * from table(array(select generate_series(10,20,5)), > array['fred','jim']); > It's not clear to me whether that's likely to be inefficient in > practical cases, Yeah, it would be :-(. Maybe we could hack something to translate unnest(array(...)) into a no-op, but ugh. You really want to make people use a syntax like that, even if it weren't inefficient? It's verbose, ugly, unintuitive, and redundant given that you could just write UNNEST() instead of TABLE(). But more: we *know* what the common case is going to be, based on existing usage of SRFs, and forced-unnest ain't it. So I'm thinking benign neglect of the spec's syntax is the way to go. If anyone does come along and say they want the spec's semantics, let them implement it, and the syntax to go with it. regards, tom lane
Tom Lane-2 wrote > Andrew Gierth < > andrew@.org > > writes: >> "Tom" == Tom Lane < > tgl@.pa > > writes: >> Tom> and this would result in producing the array elements as a table >> Tom> column. There is nothing in there about a function returning >> Tom> set. > >> In the spec, there is no such thing as a function returning a set of >> rows in the sense that we use. > > Right, but they do have a concept of arrays that's similar to ours, > and AFAICS the spec demands different behavior for an array-returning > function than what we've got here. > > We could conceivably say that we'll implicitly UNNEST() if the function > returns array, and not otherwise --- but that seems pretty inconsistent > and surprise-making to me. I'm not too sure what to do if a function > returns setof array, either. If a function returns a scalar array (RETURNS text[]) we would unnest the array per-spec. If it returns a set (RETURN setof anything {including a single array}) we would not unnest it since set returning functions are non-spec - instead we'd use our SRF processing routine. If the function returns a scalar non-array the implicit single-row returned by the function would be output. How would the spec interpret: CREATE FUNCTION f(IN text, OUT text[]) RETURNS record AS $$ ... TABLE( f('id_123') ) If that is illegal because the result is not just a single array value then we would not unnest the component array and would also output the implicit single-row. My $0.02, quickly gathered David J. -- View this message in context: http://postgresql.1045698.n5.nabble.com/UNNEST-with-multiple-args-and-TABLE-with-multiple-funcs-tp5767280p5779515.html Sent from the PostgreSQL - hackers mailing list archive at Nabble.com.
David Johnston <polobo@yahoo.com> writes: > Tom Lane-2 wrote >> We could conceivably say that we'll implicitly UNNEST() if the function >> returns array, and not otherwise --- but that seems pretty inconsistent >> and surprise-making to me. I'm not too sure what to do if a function >> returns setof array, either. > If a function returns a scalar array (RETURNS text[]) we would unnest the > array per-spec. If it returns a set (RETURN setof anything {including a > single array}) we would not unnest it since set returning functions are > non-spec - instead we'd use our SRF processing routine. If the function > returns a scalar non-array the implicit single-row returned by the function > would be output. I find that way too inconsistent to be a sane specification. regards, tom lane
Tom Lane-2 wrote > We could conceivably say that we'll implicitly UNNEST() if the function > returns array, and not otherwise --- but that seems pretty inconsistent > and surprise-making to me. The use-cases for putting a scalar array returning function call into a TABLE construct, and NOT wanting the array to be un-nested, are likely few and far between. Neither the inconsistency nor surprise-making are serious deal-breakers for me. And if we do go with the "screw the standard" approach then we should just state right now that we will never adhere to standard on "inconsistency grounds" and not even encourage others to make it work. If "TABLE( array_scalar_func() )" ends up only returning a single row then nothing can be done to make it unnest the array and conform with the syntax without breaking backward compatibility. I'd rather change "TABLE" to "FUNCTION" and leave the implementation of TABLE open for future standards-compliance - which maybe you do as well and just haven't carried that sentiment to your more recent responses David J. -- View this message in context: http://postgresql.1045698.n5.nabble.com/UNNEST-with-multiple-args-and-TABLE-with-multiple-funcs-tp5767280p5779518.html Sent from the PostgreSQL - hackers mailing list archive at Nabble.com.
David Johnston <polobo@yahoo.com> writes: > Tom Lane-2 wrote >> We could conceivably say that we'll implicitly UNNEST() if the function >> returns array, and not otherwise --- but that seems pretty inconsistent >> and surprise-making to me. > The use-cases for putting a scalar array returning function call into a > TABLE construct, and NOT wanting the array to be un-nested, are likely few > and far between. > Neither the inconsistency nor surprise-making are serious deal-breakers for > me. Well, they are for me ;-). I'm concerned for example about how we get ruleutils.c to reverse-list into a form that's certain to be interpreted the same by the parser. The whole business with the spec's reading of TABLE() seems bizarre. AFAICS there is nothing about TABLE(foo()) that you can't get with greater clarity by writing UNNEST(foo()) instead. And it's not like it's a legacy feature --- SQL99 has single-argument UNNEST() but not TABLE(), so why'd they add TABLE() later, and why'd they make it a strict subset of what UNNEST() can do? I can't escape the suspicion that I'm misreading the spec somehow ... but the text seems perfectly clear. Anyway, after further thought I've come up with an approach that's purely a syntactic transformation and so less likely to cause surprise: let's say that if we have TABLE() with a single argument, and no coldeflist either inside or outside, then we implicitly insert UNNEST(). Otherwise not. This is sufficient to satisfy the case spelled out in the standard, but it doesn't get in the way of any more-typical use of TABLE(). In particular, if you don't want the implicit UNNEST(), you can just leave off TABLE(), because the case where we'd insert it has no features you can't get in the old syntax. Similarly, because ruleutils.c is already coded not to bother with printing TABLE() if there's a single function and no coldeflist, we needn't worry about falling foul of the implicit action when a printed view is re-parsed. BTW, I looked into the option of choosing a different syntax altogether, but the results weren't too promising. FUNCTION() doesn't work unless we're willing to make that keyword partially reserved, which seems like a bad thing. (TABLE() works because TABLE is already a fully reserved word.) The idea with no keyword at all might work, but it seems way too likely to cause confusion, especially if you think about parenthesized JOIN syntax as an alternative possibility for some slightly-typoed query. Thoughts? regards, tom lane
>>>>> "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: Tom> Anyway, after further thought I've come up with an approachTom> that's purely a syntactic transformation and so lesslikely toTom> cause surprise: let's say that if we have TABLE() with a singleTom> argument, and no coldeflist eitherinside or outside, then weTom> implicitly insert UNNEST(). Otherwise not. This seems ugly beyond belief. Specifically, having TABLE(foo()) and TABLE(foo(),bar()) be radically different constructs, and likewise TABLE(foo()) and TABLE(foo() AS (...)), strikes me as highly objectionable. If there isn't a reasonable syntax alternative to TABLE(...) for the multiple functions case, then frankly I think we should go ahead and burn compatibility with a spec feature which appears to be of negative value. -- Andrew (irc:RhodiumToad)
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: > Tom> Anyway, after further thought I've come up with an approach > Tom> that's purely a syntactic transformation and so less likely to > Tom> cause surprise: let's say that if we have TABLE() with a single > Tom> argument, and no coldeflist either inside or outside, then we > Tom> implicitly insert UNNEST(). Otherwise not. > This seems ugly beyond belief. True :-( > If there isn't a reasonable syntax alternative to TABLE(...) for the > multiple functions case, then frankly I think we should go ahead and > burn compatibility with a spec feature which appears to be of negative > value. TBH, I'm getting close to that conclusion too. The more I look at the spec, the more I think it must be a mistake, or else I'm somehow reading it wrong, because it sure makes no sense for them to have invented something that's just an alternative and less-clear syntax for a feature they already had. Can anyone who's following this thread check the behavior of Oracle or DB2 to see if they interpret TABLE() the way I think the spec says? regards, tom lane
I've committed this patch after some significant editorialization, but leaving the use of TABLE( ... ) syntax in-place. If we decide that we don't want to risk doing that, we can change to some other syntax later. regards, tom lane
>>>>> "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: Tom> I've committed this patch after some significant editorialization, butTom> leaving the use of TABLE( ... ) syntax in-place. If we decide that weTom> don't want to risk doing that, we can change to some other syntax later. Is this intended: create function foo() returns setof footype language plpgsql as $f$ begin return next row(1,true); end; $f$; select pg_typeof(f), row_to_json(f) from foo() with ordinality f(p,q);pg_typeof | row_to_json -----------+---------------------------------record | {"p":1,"q":true,"ordinality":1} (1 row) select pg_typeof(f), row_to_json(f) from foo() f(p,q);pg_typeof | row_to_json -----------+------------------footype | {"a":1,"b":true} (1 row) -- Andrew (irc:RhodiumToad)
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > Is this intended: [ I assume you forgot a create type footype here ] > create function foo() returns setof footype language plpgsql > as $f$ begin return next row(1,true); end; $f$; > select pg_typeof(f), row_to_json(f) from foo() with ordinality f(p,q); > pg_typeof | row_to_json > -----------+--------------------------------- > record | {"p":1,"q":true,"ordinality":1} > (1 row) > select pg_typeof(f), row_to_json(f) from foo() f(p,q); > pg_typeof | row_to_json > -----------+------------------ > footype | {"a":1,"b":true} > (1 row) Well, it's not insane on its face. The rowtype of f in the first example is necessarily a built-on-the-fly record, but in the second case using the properties of the underlying named composite type is possible, and consistent with what happens in 9.3 and earlier. (Not that I'm claiming we were or are totally consistent ...) regards, tom lane
>>>>> "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: Tom> [ I assume you forgot a create type footype here ] yeah, sorry Tom> Well, it's not insane on its face. The rowtype of f in theTom> first example is necessarily a built-on-the-fly record,but inTom> the second case using the properties of the underlying namedTom> composite type is possible, and consistentwith what happens inTom> 9.3 and earlier. (Not that I'm claiming we were or are totallyTom> consistent ...) Right, but your changes to the code make it look like there was an intended change there - with the scan type tupdesc being forced to RECORD type and its column names changed. -- Andrew (irc:RhodiumToad)
Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > "Tom" == Tom Lane <tgl@sss.pgh.pa.us> writes: > Tom> Well, it's not insane on its face. The rowtype of f in the > Tom> first example is necessarily a built-on-the-fly record, but in > Tom> the second case using the properties of the underlying named > Tom> composite type is possible, and consistent with what happens in > Tom> 9.3 and earlier. (Not that I'm claiming we were or are totally > Tom> consistent ...) > Right, but your changes to the code make it look like there was an > intended change there - with the scan type tupdesc being forced to > RECORD type and its column names changed. I did set things up so that if you have a RECORD result, the column names will be those of the query's alias list; this was in response to the comment in the patch that complained that we were inconsistent about where we were getting the names from if you had a mix of named-composite functions and other functions. I believe what is happening in the case you show is that the function is returning a composite Datum that's marked with the composite type's OID, and the upstream consumers are looking at that, not at the scan tupdesc. I'm not really excited about tracing down exactly what the data flow is ... regards, tom lane
On Thu, Nov 21, 2013 at 12:22:57PM -0500, Tom Lane wrote: > Andrew Gierth <andrew@tao11.riddles.org.uk> writes: > > If there isn't a reasonable syntax alternative to TABLE(...) for the > > multiple functions case, then frankly I think we should go ahead and > > burn compatibility with a spec feature which appears to be of negative > > value. > > TBH, I'm getting close to that conclusion too. The more I look at the > spec, the more I think it must be a mistake, or else I'm somehow reading > it wrong, because it sure makes no sense for them to have invented > something that's just an alternative and less-clear syntax for a feature > they already had. > > Can anyone who's following this thread check the behavior of Oracle or > DB2 to see if they interpret TABLE() the way I think the spec says? Oracle's closest analog to SQL-standard arrays is its "varray" feature, and TABLE() behaves like our UNNEST() for those. Note that Oracle has no UNNEST. *SQL> CREATE OR REPLACE TYPE intarray AS VARRAY(100) OF int; * 2 / Type created. *SQL> select * from table(intarray(1,2,3)); COLUMN_VALUE ------------ 1 2 3 I don't have a DB2 installation within reach, but its documentation implies that UNNEST and TABLE are interchangeable: http://pic.dhe.ibm.com/infocenter/db2luw/v10r5/index.jsp?topic=%2Fcom.ibm.db2.luw.sql.ref.doc%2Fdoc%2Fr0055064.html If anyone can test "select x from table(trim_array(array[1,2,3], 1)) as t(x);" in DB2 and provide the output, that would be helpful. On Thu, Nov 21, 2013 at 10:07:53AM -0500, Tom Lane wrote: > The whole business with the spec's reading of TABLE() seems bizarre. > AFAICS there is nothing about TABLE(foo()) that you can't get with > greater clarity by writing UNNEST(foo()) instead. And it's not like > it's a legacy feature --- SQL99 has single-argument UNNEST() but not > TABLE(), so why'd they add TABLE() later, and why'd they make it a > strict subset of what UNNEST() can do? I can't escape the suspicion > that I'm misreading the spec somehow ... but the text seems perfectly > clear. That's how I read it, too. My hypothesis is that the standard adopted TABLE() to rubber-stamp Oracle's traditional name for UNNEST(). On Wed, Nov 20, 2013 at 03:07:17PM -0500, Tom Lane wrote: > I do like the basic concept of this syntax, but I think it's a serious > error to appropriate the TABLE() spelling for something that doesn't > agree with the spec's semantics for that spelling. We need to spell it > some other way. I realize you may have changed your mind later in the thread, but I share this original sentiment. I think of this feature as optimization of and syntactic sugar for full outer joins on ordinality columns. Compare these queries: select * from table(generate_series(1,3), generate_series(2,5)) with ordinality as t(g1,g2); select g1, g2, ordinality from generate_series(1,3) with ordinality as g1 full join generate_series(2,5) with ordinalityas g2 using (ordinality); The new syntax is limited to function calls, but I could imagine extending it to take arbitrary subqueries (or, at the cost of inviting folks to depend on subject-to-change row order, arbitrary from_item's). If this project were just starting, I'd probably favor optimizing ordinality joins in the planner rather than introducing special syntax to request the optimization. I don't claim that's sufficiently better to justify the extensive rework it would now entail, though. Therefore, I propose merely changing the syntax to "TABLE FOR ROWS (...)". As a comparison, think of the standard syntax as "TABLE [FOR ELEMENTS] (...)". Here is a longer list of conflict-free syntax choices that I considered before settling on that one: FUNCTIONS TABLE FUNCTIONS TO TABLE ROWS FOR ROWS FOR EACH ROWS FROM ROWS FROM EACH ROWS FROM FUNCTIONS ROWS TO TABLE TABLE (ROWS OF f0(), ROWS OF f1()) TABLE BY FUNCTIONS TABLE BY ROW TABLE FOR TABLE FOR FUNCTION ROWS TABLE FOR FUNCTIONS TABLE FOR ROWS TABLE FOR ROWS OF TABLE FROM TABLE FROM FUNCTION ROWS TABLE FROM FUNCTIONS TABLE OF TABLE OF EACH TABLE OF FUNCTION ROWS TABLE OF FUNCTIONS TABLE OF ROWS TABLE OF ROWS OF EACH Thanks, nm -- Noah Misch EnterpriseDB http://www.enterprisedb.com
Noah Misch <noah@leadboat.com> writes: > That's how I read it, too. My hypothesis is that the standard adopted TABLE() > to rubber-stamp Oracle's traditional name for UNNEST(). Hmm ... plausible. > ... I propose merely changing the syntax to "TABLE FOR ROWS (...)". Ugh :-(. Verbose and not exactly intuitive, I think. I don't like any of the other options you listed much better. Still, the idea of using more than one word might get us out of the bind that a single word would have to be a fully reserved one. > ROWS FROM This one's a little less awful than the rest. What about "ROWS OF"? regards, tom lane
On Mon, Dec 02, 2013 at 08:56:03PM -0500, Tom Lane wrote: > Noah Misch <noah@leadboat.com> writes: > > ... I propose merely changing the syntax to "TABLE FOR ROWS (...)". > > Ugh :-(. Verbose and not exactly intuitive, I think. I don't like > any of the other options you listed much better. Still, the idea of > using more than one word might get us out of the bind that a single > word would have to be a fully reserved one. > > > ROWS FROM > > This one's a little less awful than the rest. What about "ROWS OF"? I had considered ROWS OF and liked it, but I omitted it from the list on account of the shift/reduce conflict from a naturally-written Bison rule. Distinguishing it from a list of column aliases takes extra look-ahead. We could force that to work. However, if we ever wish to allow an arbitrary from_item in the list, it would become ambiguous: is this drawing rows from "a" or just using an alias with a column list? WITH a AS (SELECT oid FROM pg_am ORDER BY 1) SELECT * FROM rows of(a, a); ROWS FOR is terse and conflict-free. "FOR" evokes the resemblance to looping over the parenthesized section with the functions acting as generators. -- Noah Misch EnterpriseDB http://www.enterprisedb.com
On Mon, Dec 2, 2013 at 11:26 PM, Noah Misch <noah@leadboat.com> wrote: > On Mon, Dec 02, 2013 at 08:56:03PM -0500, Tom Lane wrote: >> Noah Misch <noah@leadboat.com> writes: >> > ... I propose merely changing the syntax to "TABLE FOR ROWS (...)". >> >> Ugh :-(. Verbose and not exactly intuitive, I think. I don't like >> any of the other options you listed much better. Still, the idea of >> using more than one word might get us out of the bind that a single >> word would have to be a fully reserved one. >> >> > ROWS FROM >> >> This one's a little less awful than the rest. What about "ROWS OF"? > > I had considered ROWS OF and liked it, but I omitted it from the list on > account of the shift/reduce conflict from a naturally-written Bison rule. > Distinguishing it from a list of column aliases takes extra look-ahead. We > could force that to work. However, if we ever wish to allow an arbitrary > from_item in the list, it would become ambiguous: is this drawing rows from > "a" or just using an alias with a column list? > > WITH a AS (SELECT oid FROM pg_am ORDER BY 1) SELECT * FROM rows of(a, a); > > ROWS FOR is terse and conflict-free. "FOR" evokes the resemblance to looping > over the parenthesized section with the functions acting as generators. I like the idea of using ROWS + some additional word. I think I mildly prefer Tom's suggestion of ROWS FROM to your suggestion of ROWS FOR, but I can live with either one. -- Robert Haas EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company
Noah Misch <noah@leadboat.com> writes: > On Mon, Dec 02, 2013 at 08:56:03PM -0500, Tom Lane wrote: >> Ugh :-(. Verbose and not exactly intuitive, I think. I don't like >> any of the other options you listed much better. Still, the idea of >> using more than one word might get us out of the bind that a single >> word would have to be a fully reserved one. > I had considered ROWS OF and liked it, but I omitted it from the list on > account of the shift/reduce conflict from a naturally-written Bison rule. > Distinguishing it from a list of column aliases takes extra look-ahead. Hmm, yeah, you're right --- at least one of the first two words needs to be reserved (not something that can be a ColId, at least), or else we can't tell them from a table name and alias. So this approach doesn't give us all that much extra wiggle room. We do have a number of already-fully-reserved prepositions (FOR, FROM, IN, ON, TO) but none of them seem like great choices. > ROWS FOR is terse and conflict-free. "FOR" evokes the resemblance to looping > over the parenthesized section with the functions acting as generators. Meh. I don't find that analogy compelling. After sleeping on it, your other suggestion of TABLE OF, or possibly TABLE FROM, is starting to grow on me. Who else has an opinion? regards, tom lane
* Tom Lane (tgl@sss.pgh.pa.us) wrote: > After sleeping on it, your other suggestion of TABLE OF, or possibly > TABLE FROM, is starting to grow on me. > > Who else has an opinion? Alright, for my 2c, I like having this syntax include 'TABLE' simply because it's what folks coming from Oracle might be looking for. Following from that, to keep it distinct from the spec's notion of 'TABLE', my preference is 'TABLE FROM'. I don't particularly like 'TABLE OF', nor do I like the various 'ROWS' suggestions. Thanks, Stephen
2013/12/3 Stephen Frost <sfrost@snowman.net>
+1
* Tom Lane (tgl@sss.pgh.pa.us) wrote:Alright, for my 2c, I like having this syntax include 'TABLE' simply
> After sleeping on it, your other suggestion of TABLE OF, or possibly
> TABLE FROM, is starting to grow on me.
>
> Who else has an opinion?
because it's what folks coming from Oracle might be looking for.
Following from that, to keep it distinct from the spec's notion of
'TABLE', my preference is 'TABLE FROM'. I don't particularly like
'TABLE OF', nor do I like the various 'ROWS' suggestions.
+1
Pavel
Thanks,
Stephen
On Tue, Dec 03, 2013 at 10:03:32AM -0500, Stephen Frost wrote: > * Tom Lane (tgl@sss.pgh.pa.us) wrote: > > After sleeping on it, your other suggestion of TABLE OF, or possibly > > TABLE FROM, is starting to grow on me. > > > > Who else has an opinion? > > Alright, for my 2c, I like having this syntax include 'TABLE' simply > because it's what folks coming from Oracle might be looking for. > Following from that, to keep it distinct from the spec's notion of > 'TABLE', my preference is 'TABLE FROM'. I don't particularly like > 'TABLE OF', nor do I like the various 'ROWS' suggestions. I like having "ROWS" in there somehow, because it denotes the distinction from SQL-standard TABLE(). Suppose we were to implement the SQL-standard TABLE(), essentially just mapping it to UNNEST(). Then we'd have "TABLE (f())" that unpacks the single array returned by f(), and we'd have "TABLE FROM (f())" that unpacks the set of rows returned by f(). The word "FROM" alone does not indicate that difference the way including "ROWS" does. (I don't object to having "FROM" in addition to "ROWS".) -- Noah Misch EnterpriseDB http://www.enterprisedb.com
Noah Misch <noah@leadboat.com> writes: > On Tue, Dec 03, 2013 at 10:03:32AM -0500, Stephen Frost wrote: >> Alright, for my 2c, I like having this syntax include 'TABLE' simply >> because it's what folks coming from Oracle might be looking for. >> Following from that, to keep it distinct from the spec's notion of >> 'TABLE', my preference is 'TABLE FROM'. I don't particularly like >> 'TABLE OF', nor do I like the various 'ROWS' suggestions. > I like having "ROWS" in there somehow, because it denotes the distinction from > SQL-standard TABLE(). Suppose we were to implement the SQL-standard TABLE(), > essentially just mapping it to UNNEST(). Then we'd have "TABLE (f())" that > unpacks the single array returned by f(), and we'd have "TABLE FROM (f())" > that unpacks the set of rows returned by f(). The word "FROM" alone does not > indicate that difference the way including "ROWS" does. Hm ... fair point, except that "ROWS" doesn't seem to suggest the right thing either, at least not to me. After further thought I've figured out what's been grating on me about Noah's suggestions: he suggests that we're distinguishing "TABLE [FROM ELEMENTS]" from "TABLE FROM ROWS", but this is backwards. What UNNEST() really does is take an array, extract the elements, and make a table of those. Similarly, what our feature does is take a set (the result of a set-returning function), extract the rows, and make a table of those. So what would seem appropriate to me is "TABLE [FROM ARRAY]" versus "TABLE FROM SET". Now I find either of those phrases to be one word too many, but the key point is that I'd probably prefer something involving SET over something involving ROWS. (Both of those are unreserved_keyword, so this doesn't move the ball at all in terms of finding an unambiguous syntax.) Another issue is that if you are used to the Oracle syntax, in which an UNNEST() is presumed, it's not exactly clear that TABLE ROWS, or any other phrase including TABLE, *doesn't* also imply an UNNEST. So to me that's kind of a strike against Stephen's preference --- I'm thinking we might be better off not using the word TABLE. regards, tom lane
* Tom Lane (tgl@sss.pgh.pa.us) wrote: > Another issue is that if you are used to the Oracle syntax, in which an > UNNEST() is presumed, it's not exactly clear that TABLE ROWS, or any other > phrase including TABLE, *doesn't* also imply an UNNEST. So to me that's > kind of a strike against Stephen's preference --- I'm thinking we might be > better off not using the word TABLE. I see the concern there, but I would think a bit of documentation around that would help them find UNNEST quickly, if that's what they're really looking for. On the flip side, I imagine it could be jarring seeing 'TABLE FROM' when you're used to Oracle's 'TABLE'. I haven't got any great suggestions about how to incorporate 'SET' and I I do still like 'TABLE' as that's what we're building, but I'll be happy to have this capability even if it's 'TABLE FROM SET ROWS THING'. Thanks, Stephen
On Tue, Dec 03, 2013 at 02:27:06PM -0500, Tom Lane wrote: > Noah Misch <noah@leadboat.com> writes: > > On Tue, Dec 03, 2013 at 10:03:32AM -0500, Stephen Frost wrote: > >> Alright, for my 2c, I like having this syntax include 'TABLE' simply > >> because it's what folks coming from Oracle might be looking for. > >> Following from that, to keep it distinct from the spec's notion of > >> 'TABLE', my preference is 'TABLE FROM'. I don't particularly like > >> 'TABLE OF', nor do I like the various 'ROWS' suggestions. > > > I like having "ROWS" in there somehow, because it denotes the distinction from > > SQL-standard TABLE(). Suppose we were to implement the SQL-standard TABLE(), > > essentially just mapping it to UNNEST(). Then we'd have "TABLE (f())" that > > unpacks the single array returned by f(), and we'd have "TABLE FROM (f())" > > that unpacks the set of rows returned by f(). The word "FROM" alone does not > > indicate that difference the way including "ROWS" does. > > Hm ... fair point, except that "ROWS" doesn't seem to suggest the right > thing either, at least not to me. After further thought I've figured > out what's been grating on me about Noah's suggestions: he suggests that > we're distinguishing "TABLE [FROM ELEMENTS]" from "TABLE FROM ROWS", > but this is backwards. What UNNEST() really does is take an array, > extract the elements, and make a table of those. Similarly, what our > feature does is take a set (the result of a set-returning function), > extract the rows, and make a table of those. So what would seem > appropriate to me is "TABLE [FROM ARRAY]" versus "TABLE FROM SET". Valid. On the other hand, tables *are* sets, so one could be forgiven for wondering how an operation called TABLE FROM SET modifies anything. Since order matters for this operation, I also get some mathematical angst from use of the word "SET". When we added WITH ORDINALITY, set-returning functions effectively became sequence-returning functions. (Not that actually using the word SEQUENCE would be a net clarification.) I model "ROWS FROM (f0(), f1())" as "cut from the following template, row-wise, to make a table/set: (f0(), f1())". > Another issue is that if you are used to the Oracle syntax, in which an > UNNEST() is presumed, it's not exactly clear that TABLE ROWS, or any other > phrase including TABLE, *doesn't* also imply an UNNEST. So to me that's > kind of a strike against Stephen's preference --- I'm thinking we might be > better off not using the word TABLE. I could go either way on that. Two naming proposals, "ROWS FROM" and "TABLE FROM", got an ACK from more than one person apiece. I move that we settle on "ROWS FROM". -- Noah Misch EnterpriseDB http://www.enterprisedb.com
Noah Misch <noah@leadboat.com> writes: > Two naming proposals, "ROWS FROM" and "TABLE FROM", got an ACK from more than > one person apiece. I move that we settle on "ROWS FROM". I'm not sufficiently annoyed by "ROWS FROM" to object. Other opinions? regards, tom lane
* Tom Lane (tgl@sss.pgh.pa.us) wrote: > Noah Misch <noah@leadboat.com> writes: > > Two naming proposals, "ROWS FROM" and "TABLE FROM", got an ACK from more than > > one person apiece. I move that we settle on "ROWS FROM". > > I'm not sufficiently annoyed by "ROWS FROM" to object. Other opinions? Works well enough for me. Thanks, Stephen
On Thu, Dec 05, 2013 at 10:34:08PM -0500, Stephen Frost wrote: > * Tom Lane (tgl@sss.pgh.pa.us) wrote: > > Noah Misch <noah@leadboat.com> writes: > > > Two naming proposals, "ROWS FROM" and "TABLE FROM", got an ACK from more than > > > one person apiece. I move that we settle on "ROWS FROM". > > > > I'm not sufficiently annoyed by "ROWS FROM" to object. Other opinions? > > Works well enough for me. Great. Here's the patch I'll be using. -- Noah Misch EnterpriseDB http://www.enterprisedb.com