From a05a926ccb3be7f61bda0b075cfa92fe6f7305bf Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Fri, 24 Jan 2020 09:06:38 +0100 Subject: [PATCH v2] Polymorphic table functions --- contrib/tablefunc/expected/tablefunc.out | 46 +++++++++++++++ contrib/tablefunc/sql/tablefunc.sql | 8 +++ contrib/tablefunc/tablefunc--1.0.sql | 7 +++ contrib/tablefunc/tablefunc.c | 69 ++++++++++++++++++++++ contrib/xml2/expected/xml2.out | 10 ++++ contrib/xml2/sql/xml2.sql | 6 ++ contrib/xml2/xml2--1.1.sql | 6 ++ contrib/xml2/xpath.c | 72 +++++++++++++++++++++++ doc/src/sgml/catalogs.sgml | 12 ++++ doc/src/sgml/queries.sgml | 8 +++ doc/src/sgml/ref/create_function.sgml | 14 +++++ doc/src/sgml/xfunc.sgml | 66 +++++++++++++++++++++ src/backend/catalog/pg_aggregate.c | 1 + src/backend/catalog/pg_proc.c | 11 ++++ src/backend/commands/functioncmds.c | 30 +++++++++- src/backend/commands/proclang.c | 3 + src/backend/commands/typecmds.c | 1 + src/backend/executor/execSRF.c | 1 + src/backend/executor/nodeFunctionscan.c | 1 + src/backend/optimizer/prep/prepjointree.c | 1 + src/backend/optimizer/util/clauses.c | 3 +- src/backend/parser/gram.y | 7 ++- src/backend/parser/parse_relation.c | 2 + src/backend/utils/adt/jsonfuncs.c | 48 +++++++++++++++ src/backend/utils/fmgr/funcapi.c | 49 ++++++++++++++- src/include/catalog/pg_class.dat | 2 +- src/include/catalog/pg_proc.dat | 6 +- src/include/catalog/pg_proc.h | 4 ++ src/include/funcapi.h | 1 + src/include/parser/kwlist.h | 1 + src/interfaces/ecpg/preproc/ecpg.tokens | 2 +- src/interfaces/ecpg/preproc/ecpg.trailer | 11 ++-- src/interfaces/ecpg/preproc/ecpg_kwlist.h | 1 - src/test/regress/expected/json.out | 6 ++ src/test/regress/sql/json.sql | 2 + 35 files changed, 504 insertions(+), 14 deletions(-) diff --git a/contrib/tablefunc/expected/tablefunc.out b/contrib/tablefunc/expected/tablefunc.out index fffadc6e1b..485ddfba87 100644 --- a/contrib/tablefunc/expected/tablefunc.out +++ b/contrib/tablefunc/expected/tablefunc.out @@ -328,6 +328,29 @@ SELECT * FROM connectby('connectby_text', 'keyid', 'parent_keyid', 'pos', 'row2' row8 | row6 | 3 | 6 (6 rows) +-- PTF +SELECT * FROM connectby('connectby_text', 'keyid', 'parent_keyid', 'row2', 0, '~'); + keyid | parent_keyid | level | branch +-------+--------------+-------+--------------------- + row2 | | 0 | row2 + row4 | row2 | 1 | row2~row4 + row6 | row4 | 2 | row2~row4~row6 + row8 | row6 | 3 | row2~row4~row6~row8 + row5 | row2 | 1 | row2~row5 + row9 | row5 | 2 | row2~row5~row9 +(6 rows) + +SELECT * FROM connectby('connectby_text', 'keyid', 'parent_keyid', 'row2', 0); + keyid | parent_keyid | level +-------+--------------+------- + row2 | | 0 + row4 | row2 | 1 + row6 | row4 | 2 + row8 | row6 | 3 + row5 | row2 | 1 + row9 | row5 | 2 +(6 rows) + -- test connectby with int based hierarchy CREATE TABLE connectby_int(keyid int, parent_keyid int); \copy connectby_int from 'data/connectby_int.data' @@ -355,6 +378,29 @@ SELECT * FROM connectby('connectby_int', 'keyid', 'parent_keyid', '2', 0) AS t(k 9 | 5 | 2 (6 rows) +-- PTF +SELECT * FROM connectby('connectby_int', 'keyid', 'parent_keyid', '2', 0, '~'); + keyid | parent_keyid | level | branch +-------+--------------+-------+--------- + 2 | | 0 | 2 + 4 | 2 | 1 | 2~4 + 6 | 4 | 2 | 2~4~6 + 8 | 6 | 3 | 2~4~6~8 + 5 | 2 | 1 | 2~5 + 9 | 5 | 2 | 2~5~9 +(6 rows) + +SELECT * FROM connectby('connectby_int', 'keyid', 'parent_keyid', '2', 0); + keyid | parent_keyid | level +-------+--------------+------- + 2 | | 0 + 4 | 2 | 1 + 6 | 4 | 2 + 8 | 6 | 3 + 5 | 2 | 1 + 9 | 5 | 2 +(6 rows) + -- recursion detection INSERT INTO connectby_int VALUES(10,9); INSERT INTO connectby_int VALUES(11,10); diff --git a/contrib/tablefunc/sql/tablefunc.sql b/contrib/tablefunc/sql/tablefunc.sql index ec375b05c6..375f59bc7a 100644 --- a/contrib/tablefunc/sql/tablefunc.sql +++ b/contrib/tablefunc/sql/tablefunc.sql @@ -158,6 +158,10 @@ CREATE TABLE connectby_text(keyid text, parent_keyid text, pos int); -- without branch, with orderby SELECT * FROM connectby('connectby_text', 'keyid', 'parent_keyid', 'pos', 'row2', 0) AS t(keyid text, parent_keyid text, level int, pos int) ORDER BY t.pos; +-- PTF +SELECT * FROM connectby('connectby_text', 'keyid', 'parent_keyid', 'row2', 0, '~'); +SELECT * FROM connectby('connectby_text', 'keyid', 'parent_keyid', 'row2', 0); + -- test connectby with int based hierarchy CREATE TABLE connectby_int(keyid int, parent_keyid int); \copy connectby_int from 'data/connectby_int.data' @@ -168,6 +172,10 @@ CREATE TABLE connectby_int(keyid int, parent_keyid int); -- without branch SELECT * FROM connectby('connectby_int', 'keyid', 'parent_keyid', '2', 0) AS t(keyid int, parent_keyid int, level int); +-- PTF +SELECT * FROM connectby('connectby_int', 'keyid', 'parent_keyid', '2', 0, '~'); +SELECT * FROM connectby('connectby_int', 'keyid', 'parent_keyid', '2', 0); + -- recursion detection INSERT INTO connectby_int VALUES(10,9); INSERT INTO connectby_int VALUES(11,10); diff --git a/contrib/tablefunc/tablefunc--1.0.sql b/contrib/tablefunc/tablefunc--1.0.sql index 8681ff4706..e75bdc0510 100644 --- a/contrib/tablefunc/tablefunc--1.0.sql +++ b/contrib/tablefunc/tablefunc--1.0.sql @@ -65,13 +65,20 @@ CREATE FUNCTION crosstab(text,text) AS 'MODULE_PATHNAME','crosstab_hash' LANGUAGE C STABLE STRICT; +CREATE FUNCTION connectby_describe(internal) +RETURNS internal +AS 'MODULE_PATHNAME', 'connectby_describe' +LANGUAGE C; + CREATE FUNCTION connectby(text,text,text,text,int,text) RETURNS setof record +DESCRIBE WITH connectby_describe AS 'MODULE_PATHNAME','connectby_text' LANGUAGE C STABLE STRICT; CREATE FUNCTION connectby(text,text,text,text,int) RETURNS setof record +DESCRIBE WITH connectby_describe AS 'MODULE_PATHNAME','connectby_text' LANGUAGE C STABLE STRICT; diff --git a/contrib/tablefunc/tablefunc.c b/contrib/tablefunc/tablefunc.c index 3802ae905e..a823347019 100644 --- a/contrib/tablefunc/tablefunc.c +++ b/contrib/tablefunc/tablefunc.c @@ -35,6 +35,7 @@ #include #include "access/htup_details.h" +#include "catalog/namespace.h" #include "catalog/pg_type.h" #include "executor/spi.h" #include "funcapi.h" @@ -42,6 +43,8 @@ #include "miscadmin.h" #include "tablefunc.h" #include "utils/builtins.h" +#include "utils/regproc.h" +#include "utils/lsyscache.h" PG_MODULE_MAGIC; @@ -1587,3 +1590,69 @@ compatCrosstabTupleDescs(TupleDesc ret_tupdesc, TupleDesc sql_tupdesc) /* OK, the two tupdescs are compatible for our purposes */ return true; } + +PG_FUNCTION_INFO_V1(connectby_describe); + +Datum +connectby_describe(PG_FUNCTION_ARGS) +{ + char *relname; + char *key_fld; + char *parent_key_fld; + bool show_branch; + TupleDesc tupdesc; + List *names; + Oid relid; + AttrNumber keyattnum; + Oid keytype; + int32 keytypmod; + Oid keycoll; + + if (PG_ARGISNULL(0) || PG_ARGISNULL(1) || PG_ARGISNULL(2)) + PG_RETURN_NULL(); + + show_branch = (PG_NARGS() == 6); + + if (show_branch && PG_ARGISNULL(5)) + PG_RETURN_NULL(); + + relname = text_to_cstring(PG_GETARG_TEXT_PP(0)); + key_fld = text_to_cstring(PG_GETARG_TEXT_PP(1)); + parent_key_fld = text_to_cstring(PG_GETARG_TEXT_PP(2)); + + tupdesc = CreateTemplateTupleDesc(3 + (show_branch ? 1 : 0)); + + names = stringToQualifiedNameList(relname); + relid = RangeVarGetRelid(makeRangeVarFromNameList(names), AccessShareLock, false); + + keyattnum = get_attnum(relid, key_fld); + if (keyattnum == InvalidAttrNumber) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + key_fld, relname))); + get_atttypetypmodcoll(relid, keyattnum, + &keytype, &keytypmod, &keycoll); + TupleDescInitEntry(tupdesc, 1, key_fld, keytype, keytypmod, 0); + TupleDescInitEntryCollation(tupdesc, 1, keycoll); + + keyattnum = get_attnum(relid, parent_key_fld); + if (keyattnum == InvalidAttrNumber) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + parent_key_fld, relname))); + get_atttypetypmodcoll(relid, keyattnum, + &keytype, &keytypmod, &keycoll); + TupleDescInitEntry(tupdesc, 2, parent_key_fld, keytype, keytypmod, 0); + TupleDescInitEntryCollation(tupdesc, 2, keycoll); + + TupleDescInitEntry(tupdesc, 3, "level", INT4OID, -1, 0); + + if (show_branch) + TupleDescInitEntry(tupdesc, 4, "branch", TEXTOID, -1, 0); + + BlessTupleDesc(tupdesc); + + PG_RETURN_POINTER(tupdesc); +} diff --git a/contrib/xml2/expected/xml2.out b/contrib/xml2/expected/xml2.out index eba6ae6036..1df04be6c4 100644 --- a/contrib/xml2/expected/xml2.out +++ b/contrib/xml2/expected/xml2.out @@ -88,6 +88,16 @@ as t(id int4, doc int4); 1 | 1 (1 row) +-- PTF +DROP TABLE xpath_test; +CREATE TABLE xpath_test (id integer NOT NULL, t text); +INSERT INTO xpath_test VALUES (1, 'foobar'); +SELECT * FROM xpath_table('id', 't', 'xpath_test', '/doc/data|/doc/stuff', 'true'); + id | t1 | t2 +----+-----+----- + 1 | foo | bar +(1 row) + create table articles (article_id integer, article_xml xml, date_entered date); insert into articles (article_id, article_xml, date_entered) values (2, '
test37
', now()); diff --git a/contrib/xml2/sql/xml2.sql b/contrib/xml2/sql/xml2.sql index ac49cfa7c5..d1a9cb3494 100644 --- a/contrib/xml2/sql/xml2.sql +++ b/contrib/xml2/sql/xml2.sql @@ -34,6 +34,12 @@ CREATE TABLE xpath_test (id integer NOT NULL, t text); SELECT * FROM xpath_table('id', 't', 'xpath_test', '/doc/int', 'true') as t(id int4, doc int4); +-- PTF +DROP TABLE xpath_test; +CREATE TABLE xpath_test (id integer NOT NULL, t text); +INSERT INTO xpath_test VALUES (1, 'foobar'); +SELECT * FROM xpath_table('id', 't', 'xpath_test', '/doc/data|/doc/stuff', 'true'); + create table articles (article_id integer, article_xml xml, date_entered date); insert into articles (article_id, article_xml, date_entered) values (2, '
test37
', now()); diff --git a/contrib/xml2/xml2--1.1.sql b/contrib/xml2/xml2--1.1.sql index 671372cb27..02588266d0 100644 --- a/contrib/xml2/xml2--1.1.sql +++ b/contrib/xml2/xml2--1.1.sql @@ -54,8 +54,14 @@ CREATE FUNCTION xpath_nodeset(text,text,text) -- Table function +CREATE FUNCTION xpath_table_describe(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C; + CREATE FUNCTION xpath_table(text,text,text,text,text) RETURNS setof record +DESCRIBE WITH xpath_table_describe AS 'MODULE_PATHNAME' LANGUAGE C STRICT STABLE PARALLEL SAFE; diff --git a/contrib/xml2/xpath.c b/contrib/xml2/xpath.c index 1e5b71d9a0..67731509f9 100644 --- a/contrib/xml2/xpath.c +++ b/contrib/xml2/xpath.c @@ -7,12 +7,16 @@ #include "postgres.h" #include "access/htup_details.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" #include "executor/spi.h" #include "fmgr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/regproc.h" #include "utils/xml.h" /* libxml includes */ @@ -519,6 +523,74 @@ pgxml_result_to_text(xmlXPathObjectPtr res, * xpath_table is a table function. It needs some tidying (as do the * other functions here! */ +PG_FUNCTION_INFO_V1(xpath_table_describe); + +Datum +xpath_table_describe(PG_FUNCTION_ARGS) +{ + char *pkeyfield; + char *xmlfield; + char *relname; + List *names; + Oid relid; + AttrNumber pkeyattnum; + Oid pkeytype; + int32 pkeytypmod; + Oid pkeycoll; + char *xpathset; + const char *pathsep = "|"; + int numpaths; + TupleDesc tupdesc; + + if (PG_ARGISNULL(0) || PG_ARGISNULL(1) || PG_ARGISNULL(3)) + PG_RETURN_NULL(); + + pkeyfield = text_to_cstring(PG_GETARG_TEXT_PP(0)); + xmlfield = text_to_cstring(PG_GETARG_TEXT_PP(1)); + relname = text_to_cstring(PG_GETARG_TEXT_PP(2)); + xpathset = text_to_cstring(PG_GETARG_TEXT_PP(3)); + + names = stringToQualifiedNameList(relname); + relid = RangeVarGetRelid(makeRangeVarFromNameList(names), AccessShareLock, false); + + pkeyattnum = get_attnum(relid, pkeyfield); + if (pkeyattnum == InvalidAttrNumber) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + pkeyfield, relname))); + get_atttypetypmodcoll(relid, pkeyattnum, + &pkeytype, &pkeytypmod, &pkeycoll); + + /* count XPaths */ + numpaths = 1; + for (char *pos = xpathset;;) + { + pos = strstr(pos, pathsep); + if (!pos) + break; + numpaths++; + pos++; + } + + tupdesc = CreateTemplateTupleDesc(numpaths + 1); + + TupleDescInitEntry(tupdesc, 1, pkeyfield, pkeytype, pkeytypmod, 0); + TupleDescInitEntryCollation(tupdesc, 1, pkeycoll); + + for (int i = 0; i < numpaths; i++) + { + AttrNumber attnum = 2 + i; + char *attname = psprintf("%s%d", xmlfield, i + 1); + + TupleDescInitEntry(tupdesc, attnum, attname, TEXTOID, -1, 0); + } + + BlessTupleDesc(tupdesc); + + PG_RETURN_POINTER(tupdesc); +} + PG_FUNCTION_INFO_V1(xpath_table); Datum diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 85ac79f07e..cb5ed0cb2d 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -5328,6 +5328,18 @@ <structname>pg_proc</structname> Columns Data type of the return value + + prodescribe + regproc + pg_proc.oid + + For functions returning type record, this can point to + another function that returns a row description of the result row of + this function invocation. See for + details. + + + proargtypes oidvector diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml index 22252556be..5a9d2f9489 100644 --- a/doc/src/sgml/queries.sgml +++ b/doc/src/sgml/queries.sgml @@ -803,6 +803,14 @@ Table Functions that the parser knows, for example, what * should expand to. + + + Some functions returning type record allow the column list + to be omitted if they can compute the result row type from the constant + input arguments. See for details. In + that case, such functions can be called like functions with a specific + composite type return type. + diff --git a/doc/src/sgml/ref/create_function.sgml b/doc/src/sgml/ref/create_function.sgml index dd6a2f7304..f53c8bc85f 100644 --- a/doc/src/sgml/ref/create_function.sgml +++ b/doc/src/sgml/ref/create_function.sgml @@ -25,6 +25,7 @@ [ RETURNS rettype | RETURNS TABLE ( column_name column_type [, ...] ) ] { LANGUAGE lang_name + | DESCRIBE WITH describe_func | TRANSFORM { FOR TYPE type_name } [, ... ] | WINDOW | IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF @@ -262,6 +263,19 @@ Parameters + + DESCRIBE WITH describe_func + + + + For functions returning type record, this optional clause + points to another function that is called to compute the return row + structure of a particular call. See + for more information. + + + + TRANSFORM { FOR TYPE type_name } [, ... ] } diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index ca5e6efd7e..29bc399c63 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -3239,6 +3239,72 @@ Polymorphic Arguments and Return Types + + Polymorphic Composite Return Type Describe Function + + + A function that returns type record normally needs to be + called with an explicit column list to specify the names and types of the + result columns (see ). In some + cases, this can be avoided if the function can inform the database system + about the result row structure based on the input parameters of a + specific call. To do that, a helper function called the + describe function is attached to the + record-returning function to communicate the result row structure. + + + + Consider, as an example, a function that unpacks a CSV document, passed + as a single text argument, into a set of rows. It might be defined like + this: + +CREATE FUNCTION csvtable(doc text) RETURNS SETOF record + LANGUAGE C + ... + + Because the structure of the CSV document varies, the return type is + declared as record. However, if the passed document + argument is a constant, then the return row structure can be computed at + parse time. To do that, first define a separate describe function: + +CREATE FUNCTION cvstable_describe(internal) RETURNS internal + LANGUAGE C + ... + + and specify that in the definition of the original function: + +CREATE FUNCTION csvtable(doc text) RETURNS SETOF record + DESCRIBE WITH csvtable_describe + LANGUAGE C + ... + + (The name of the describe function does not matter.) + + + + The describe function will be called at parse time with the same argument + types as the parent function. Only arguments that are constant at parse + time are passed; all other arguments are passed as null values. The + describe function must either return a TupleDesc or may + return SQL null (not C NULL) if it cannot compute the + row structure with the provided information. + + + + In the example of the CSV processing function, the describe function + could parse the first line of the document to discover the number of + fields and their names. + + + + If the describe function returns a null value, the function call will + error. But the same function call could still succeed if the caller + provides an explicit column list, as with record-returning functions + without a describe helper. If an explicit column list is provided, the + describe function is not called at all. + + + Shared Memory and LWLocks diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c index 0b7face4cc..cfff80c493 100644 --- a/src/backend/catalog/pg_aggregate.c +++ b/src/backend/catalog/pg_aggregate.c @@ -615,6 +615,7 @@ AggregateCreate(const char *aggName, replace, /* maybe replacement */ false, /* doesn't return a set */ finaltype, /* returnType */ + InvalidOid, /* describe */ GetUserId(), /* proowner */ INTERNALlanguageId, /* languageObjectId */ InvalidOid, /* no validator */ diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c index 5194dcaac0..a165aea63e 100644 --- a/src/backend/catalog/pg_proc.c +++ b/src/backend/catalog/pg_proc.c @@ -70,6 +70,7 @@ ProcedureCreate(const char *procedureName, bool replace, bool returnsSet, Oid returnType, + Oid describeFuncId, Oid proowner, Oid languageObjectId, Oid languageValidator, @@ -331,6 +332,7 @@ ProcedureCreate(const char *procedureName, values[Anum_pg_proc_pronargs - 1] = UInt16GetDatum(parameterCount); values[Anum_pg_proc_pronargdefaults - 1] = UInt16GetDatum(list_length(parameterDefaults)); values[Anum_pg_proc_prorettype - 1] = ObjectIdGetDatum(returnType); + values[Anum_pg_proc_prodescribe - 1] = ObjectIdGetDatum(describeFuncId); values[Anum_pg_proc_proargtypes - 1] = PointerGetDatum(parameterTypes); if (allParameterTypes != PointerGetDatum(NULL)) values[Anum_pg_proc_proallargtypes - 1] = allParameterTypes; @@ -629,6 +631,15 @@ ProcedureCreate(const char *procedureName, referenced.objectSubId = 0; recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + /* dependency on describe function */ + if (describeFuncId) + { + referenced.classId = ProcedureRelationId; + referenced.objectId = describeFuncId; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + /* dependency on transform used by return type, if any */ if ((trfid = get_transform_oid(returnType, languageObjectId, true))) { diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c index c31c57e5e9..342b7fbd38 100644 --- a/src/backend/commands/functioncmds.c +++ b/src/backend/commands/functioncmds.c @@ -695,6 +695,7 @@ compute_function_attributes(ParseState *pstate, List *options, List **as, char **language, + Node **describe, Node **transform, bool *windowfunc_p, char *volatility_p, @@ -709,6 +710,7 @@ compute_function_attributes(ParseState *pstate, { ListCell *option; DefElem *as_item = NULL; + DefElem *describe_item = NULL; DefElem *language_item = NULL; DefElem *transform_item = NULL; DefElem *windowfunc_item = NULL; @@ -735,6 +737,15 @@ compute_function_attributes(ParseState *pstate, parser_errposition(pstate, defel->location))); as_item = defel; } + else if (strcmp(defel->defname, "describe") == 0) + { + if (describe_item) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"), + parser_errposition(pstate, defel->location))); + describe_item = defel; + } else if (strcmp(defel->defname, "language") == 0) { if (language_item) @@ -810,6 +821,8 @@ compute_function_attributes(ParseState *pstate, } /* process optional items */ + if (describe_item) + *describe = describe_item->arg; if (transform_item) *transform = transform_item->arg; if (windowfunc_item) @@ -926,6 +939,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) char *language; Oid languageOid; Oid languageValidator; + Node *describeDefElem = NULL; Node *transformDefElem = NULL; char *funcname; Oid namespaceId; @@ -936,6 +950,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) ArrayType *parameterNames; List *parameterDefaults; Oid variadicArgType; + Oid describeFuncOid = InvalidOid; List *trftypes_list = NIL; ArrayType *trftypes; Oid requiredResultType; @@ -979,7 +994,8 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) compute_function_attributes(pstate, stmt->is_procedure, stmt->options, - &as_clause, &language, &transformDefElem, + &as_clause, &language, + &describeDefElem, &transformDefElem, &isWindowFunc, &volatility, &isStrict, &security, &isLeakProof, &proconfig, &procost, &prorows, @@ -1029,6 +1045,17 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("only superuser can define a leakproof function"))); + if (describeDefElem) + { + describeFuncOid = LookupFuncWithArgs(OBJECT_FUNCTION, castNode(ObjectWithArgs, describeDefElem), false); + + if (get_func_rettype(describeFuncOid) != INTERNALOID) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("describe function must return type %s", + "internal"))); + } + if (transformDefElem) { ListCell *lc; @@ -1152,6 +1179,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) stmt->replace, returnsSet, prorettype, + describeFuncOid, GetUserId(), languageOid, languageValidator, diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c index cdff43d3ce..cf0c745997 100644 --- a/src/backend/commands/proclang.c +++ b/src/backend/commands/proclang.c @@ -122,6 +122,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt) false, /* replace */ false, /* returnsSet */ LANGUAGE_HANDLEROID, + InvalidOid, /* describe */ BOOTSTRAP_SUPERUSERID, ClanguageId, F_FMGR_C_VALIDATOR, @@ -162,6 +163,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt) false, /* replace */ false, /* returnsSet */ VOIDOID, + InvalidOid, /* describe */ BOOTSTRAP_SUPERUSERID, ClanguageId, F_FMGR_C_VALIDATOR, @@ -205,6 +207,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt) false, /* replace */ false, /* returnsSet */ VOIDOID, + InvalidOid, /* describe */ BOOTSTRAP_SUPERUSERID, ClanguageId, F_FMGR_C_VALIDATOR, diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 52097363fd..9829730f93 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -1648,6 +1648,7 @@ makeRangeConstructors(const char *name, Oid namespace, false, /* replace */ false, /* returns set */ rangeOid, /* return type */ + InvalidOid, /* describe */ BOOTSTRAP_SUPERUSERID, /* proowner */ INTERNALlanguageId, /* language */ F_FMGR_INTERNAL_VALIDATOR, /* language validator */ diff --git a/src/backend/executor/execSRF.c b/src/backend/executor/execSRF.c index 2312cc7142..555f7a7838 100644 --- a/src/backend/executor/execSRF.c +++ b/src/backend/executor/execSRF.c @@ -733,6 +733,7 @@ init_sexpr(Oid foid, Oid input_collation, Expr *node, MemoryContext oldcontext; functypclass = get_expr_result_type(sexpr->func.fn_expr, + false, &funcrettype, &tupdesc); diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c index ccb66ce1aa..bafdac1357 100644 --- a/src/backend/executor/nodeFunctionscan.c +++ b/src/backend/executor/nodeFunctionscan.c @@ -368,6 +368,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) * was made; we have to ignore any columns beyond "colcount". */ functypclass = get_expr_result_type(funcexpr, + rtfunc->funccolnames ? false : true, &funcrettype, &tupdesc); diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 14521728c6..52dc049d05 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1719,6 +1719,7 @@ pull_up_constant_function(PlannerInfo *root, Node *jtnode, return jtnode; /* definitely composite */ functypclass = get_expr_result_type(rtf->funcexpr, + false, &funcrettype, &tupdesc); if (functypclass != TYPEFUNC_SCALAR) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 2d3ec22407..aeeaca3690 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -4492,6 +4492,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, /* fexpr also provides a convenient way to resolve a composite result */ (void) get_expr_result_type((Node *) fexpr, + false, NULL, &rettupdesc); @@ -5028,7 +5029,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) * function is just declared to return RECORD, dig the info out of the AS * clause. */ - functypclass = get_expr_result_type((Node *) fexpr, NULL, &rettupdesc); + functypclass = get_expr_result_type((Node *) fexpr, false, NULL, &rettupdesc); if (functypclass == TYPEFUNC_RECORD) rettupdesc = BuildDescFromLists(rtfunc->funccolnames, rtfunc->funccoltypes, diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ba5916b4d2..8b1184de30 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -634,7 +634,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS - DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DESC + DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DESC DESCRIBE DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP @@ -7962,6 +7962,10 @@ createfunc_opt_item: { $$ = makeDefElem("as", (Node *)$2, @1); } + | DESCRIBE WITH function_with_argtypes + { + $$ = makeDefElem("describe", (Node *)$3, @1); + } | LANGUAGE NonReservedWord_or_Sconst { $$ = makeDefElem("language", (Node *)makeString($2), @1); @@ -15193,6 +15197,7 @@ unreserved_keyword: | DELIMITER | DELIMITERS | DEPENDS + | DESCRIBE | DETACH | DICTIONARY | DISABLE_P diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index b875a50646..176957d5fb 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -1732,6 +1732,7 @@ addRangeTableEntryForFunction(ParseState *pstate, * Now determine if the function returns a simple or composite type. */ functypclass = get_expr_result_type(funcexpr, + coldeflist ? false : true, &funcrettype, &tupdesc); @@ -2588,6 +2589,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, TupleDesc tupdesc; functypclass = get_expr_result_type(rtfunc->funcexpr, + rtfunc->funccolnames ? false : true, &funcrettype, &tupdesc); if (functypclass == TYPEFUNC_COMPOSITE || diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 38758a626b..2f71725735 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -2256,6 +2256,54 @@ json_to_record(PG_FUNCTION_ARGS) true, false); } +Datum +json_to_record_describe(PG_FUNCTION_ARGS) +{ + text *arg; + JsonLexContext *lex; + JsonSemAction *sem; + OkeysState *state; + TupleDesc tupdesc; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + arg = PG_GETARG_TEXT_PP(0); + + lex = makeJsonLexContext(arg, true); + + state = palloc(sizeof(OkeysState)); + sem = palloc0(sizeof(JsonSemAction)); + + state->lex = lex; + state->result_size = 256; + state->result_count = 0; + state->sent_count = 0; + state->result = palloc(256 * sizeof(char *)); + + sem->semstate = (void *) state; + sem->array_start = okeys_array_start; + sem->scalar = okeys_scalar; + sem->object_field_start = okeys_object_field_start; + /* remainder are all NULL, courtesy of palloc0 above */ + + pg_parse_json(lex, sem); + /* keys are now in state->result */ + + pfree(lex->strval->data); + pfree(lex->strval); + pfree(lex); + pfree(sem); + + tupdesc = CreateTemplateTupleDesc(state->result_count); + for (int i = 0; i < state->result_count; i++) + TupleDescInitEntry(tupdesc, i + 1, state->result[i], TEXTOID, -1, 0); + + BlessTupleDesc(tupdesc); + + PG_RETURN_POINTER(tupdesc); +} + /* helper function for diagnostics */ static void populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) diff --git a/src/backend/utils/fmgr/funcapi.c b/src/backend/utils/fmgr/funcapi.c index b7eee3da1d..4cfc95fa30 100644 --- a/src/backend/utils/fmgr/funcapi.c +++ b/src/backend/utils/fmgr/funcapi.c @@ -34,6 +34,7 @@ static void shutdown_MultiFuncCall(Datum arg); static TypeFuncClass internal_get_result_type(Oid funcid, Node *call_expr, + bool try_describe, ReturnSetInfo *rsinfo, Oid *resultTypeId, TupleDesc *resultTupleDesc); @@ -199,6 +200,7 @@ get_call_result_type(FunctionCallInfo fcinfo, { return internal_get_result_type(fcinfo->flinfo->fn_oid, fcinfo->flinfo->fn_expr, + false, (ReturnSetInfo *) fcinfo->resultinfo, resultTypeId, resultTupleDesc); @@ -210,6 +212,7 @@ get_call_result_type(FunctionCallInfo fcinfo, */ TypeFuncClass get_expr_result_type(Node *expr, + bool try_describe, Oid *resultTypeId, TupleDesc *resultTupleDesc) { @@ -218,12 +221,14 @@ get_expr_result_type(Node *expr, if (expr && IsA(expr, FuncExpr)) result = internal_get_result_type(((FuncExpr *) expr)->funcid, expr, + try_describe, NULL, resultTypeId, resultTupleDesc); else if (expr && IsA(expr, OpExpr)) result = internal_get_result_type(get_opcode(((OpExpr *) expr)->opno), expr, + try_describe, NULL, resultTypeId, resultTupleDesc); @@ -292,6 +297,7 @@ get_func_result_type(Oid functionId, { return internal_get_result_type(functionId, NULL, + false, NULL, resultTypeId, resultTupleDesc); @@ -308,6 +314,7 @@ get_func_result_type(Oid functionId, static TypeFuncClass internal_get_result_type(Oid funcid, Node *call_expr, + bool try_describe, ReturnSetInfo *rsinfo, Oid *resultTypeId, TupleDesc *resultTupleDesc) @@ -362,6 +369,46 @@ internal_get_result_type(Oid funcid, return result; } + if (rettype == RECORDOID && procform->prodescribe && try_describe) + { + FmgrInfo flinfo; + LOCAL_FCINFO(fcinfo, FUNC_MAX_ARGS); + Datum funcres; + FuncExpr *fexpr; + + fmgr_info(procform->prodescribe, &flinfo); + InitFunctionCallInfoData(*fcinfo, &flinfo, procform->pronargs, InvalidOid, NULL, NULL); + + Assert(call_expr); + fexpr = castNode(FuncExpr, call_expr); + + for (int i = 0; i < procform->pronargs; i++) + { + Node *arg = list_nth(fexpr->args, i); + + if (IsA(arg, Const)) + { + Const *c = castNode(Const, arg); + + fcinfo->args[i].value = c->constvalue; + fcinfo->args[i].isnull = c->constisnull; + } + else + fcinfo->args[i].isnull = true; + } + + funcres = FunctionCallInvoke(fcinfo); + + if (!fcinfo->isnull) + { + if (resultTupleDesc) + *resultTupleDesc = (TupleDesc) DatumGetPointer(funcres); + + ReleaseSysCache(tp); + return TYPEFUNC_COMPOSITE; + } + } + /* * If scalar polymorphic result, try to resolve it. */ @@ -431,7 +478,7 @@ get_expr_result_tupdesc(Node *expr, bool noError) TupleDesc tupleDesc; TypeFuncClass functypclass; - functypclass = get_expr_result_type(expr, NULL, &tupleDesc); + functypclass = get_expr_result_type(expr, true, NULL, &tupleDesc); if (functypclass == TYPEFUNC_COMPOSITE || functypclass == TYPEFUNC_COMPOSITE_DOMAIN) diff --git a/src/include/catalog/pg_class.dat b/src/include/catalog/pg_class.dat index f70d5bacb9..490bf87082 100644 --- a/src/include/catalog/pg_class.dat +++ b/src/include/catalog/pg_class.dat @@ -44,7 +44,7 @@ relname => 'pg_proc', reltype => 'pg_proc', relam => 'heap', relfilenode => '0', relpages => '0', reltuples => '0', relallvisible => '0', reltoastrelid => '0', relhasindex => 'f', relisshared => 'f', - relpersistence => 'p', relkind => 'r', relnatts => '29', relchecks => '0', + relpersistence => 'p', relkind => 'r', relnatts => '30', relchecks => '0', relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f', relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't', relreplident => 'n', relispartition => 'f', relfrozenxid => '3', diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fcf2a1214c..b7b86709ef 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8348,7 +8348,11 @@ proargtypes => 'anyelement json bool', prosrc => 'json_populate_recordset' }, { oid => '3204', descr => 'get record fields from a json object', proname => 'json_to_record', provolatile => 's', prorettype => 'record', - proargtypes => 'json', prosrc => 'json_to_record' }, + proargtypes => 'json', prosrc => 'json_to_record', + prodescribe => 'json_to_record_describe' }, +{ oid => '3434', descr => 'describe function for json_to_record', + proname => 'json_to_record_describe', provolatile => 's', prorettype => 'internal', + proargtypes => 'internal', prosrc => 'json_to_record_describe' }, { oid => '3205', descr => 'get set of records with fields from a json array of objects', proname => 'json_to_recordset', prorows => '100', proisstrict => 'f', diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index ee3959da09..e95a6310ec 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -86,6 +86,9 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce /* OID of result type */ Oid prorettype BKI_LOOKUP(pg_type); + /* function that describes the result row of this function, if 0 if none */ + regproc prodescribe BKI_DEFAULT(0) BKI_LOOKUP(pg_proc); + /* * variable-length fields start here, but we allow direct access to * proargtypes @@ -182,6 +185,7 @@ extern ObjectAddress ProcedureCreate(const char *procedureName, bool replace, bool returnsSet, Oid returnType, + Oid describeFuncId, Oid proowner, Oid languageObjectId, Oid languageValidator, diff --git a/src/include/funcapi.h b/src/include/funcapi.h index f9b75ae390..2d80a0ff8c 100644 --- a/src/include/funcapi.h +++ b/src/include/funcapi.h @@ -156,6 +156,7 @@ extern TypeFuncClass get_call_result_type(FunctionCallInfo fcinfo, Oid *resultTypeId, TupleDesc *resultTupleDesc); extern TypeFuncClass get_expr_result_type(Node *expr, + bool try_describe, Oid *resultTypeId, TupleDesc *resultTupleDesc); extern TypeFuncClass get_func_result_type(Oid functionId, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index b1184c2d15..10702cc611 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -129,6 +129,7 @@ PG_KEYWORD("delimiter", DELIMITER, UNRESERVED_KEYWORD) PG_KEYWORD("delimiters", DELIMITERS, UNRESERVED_KEYWORD) PG_KEYWORD("depends", DEPENDS, UNRESERVED_KEYWORD) PG_KEYWORD("desc", DESC, RESERVED_KEYWORD) +PG_KEYWORD("describe", DESCRIBE, UNRESERVED_KEYWORD) PG_KEYWORD("detach", DETACH, UNRESERVED_KEYWORD) PG_KEYWORD("dictionary", DICTIONARY, UNRESERVED_KEYWORD) PG_KEYWORD("disable", DISABLE_P, UNRESERVED_KEYWORD) diff --git a/src/interfaces/ecpg/preproc/ecpg.tokens b/src/interfaces/ecpg/preproc/ecpg.tokens index 8e0527fdb7..cb64ef9999 100644 --- a/src/interfaces/ecpg/preproc/ecpg.tokens +++ b/src/interfaces/ecpg/preproc/ecpg.tokens @@ -5,7 +5,7 @@ SQL_CARDINALITY SQL_CONNECT SQL_COUNT SQL_DATETIME_INTERVAL_CODE - SQL_DATETIME_INTERVAL_PRECISION SQL_DESCRIBE + SQL_DATETIME_INTERVAL_PRECISION SQL_DESCRIPTOR SQL_DISCONNECT SQL_FOUND SQL_FREE SQL_GET SQL_GO SQL_GOTO SQL_IDENTIFIED SQL_INDICATOR SQL_KEY_MEMBER SQL_LENGTH diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 0dbdfdc122..f3843146ce 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -1058,14 +1058,14 @@ UsingConst: Iconst { $$ = $1; } /* * We accept DESCRIBE [OUTPUT] but do nothing with DESCRIBE INPUT so far. */ -ECPGDescribe: SQL_DESCRIBE INPUT_P prepared_name using_descriptor +ECPGDescribe: DESCRIBE INPUT_P prepared_name using_descriptor { const char *con = connection ? connection : "NULL"; mmerror(PARSE_ERROR, ET_WARNING, "using unsupported DESCRIBE statement"); $$ = (char *) mm_alloc(sizeof("1, , ") + strlen(con) + strlen($3)); sprintf($$, "1, %s, %s", con, $3); } - | SQL_DESCRIBE opt_output prepared_name using_descriptor + | DESCRIBE opt_output prepared_name using_descriptor { const char *con = connection ? connection : "NULL"; struct variable *var; @@ -1077,20 +1077,20 @@ ECPGDescribe: SQL_DESCRIBE INPUT_P prepared_name using_descriptor $$ = (char *) mm_alloc(sizeof("0, , ") + strlen(con) + strlen($3)); sprintf($$, "0, %s, %s", con, $3); } - | SQL_DESCRIBE opt_output prepared_name into_descriptor + | DESCRIBE opt_output prepared_name into_descriptor { const char *con = connection ? connection : "NULL"; $$ = (char *) mm_alloc(sizeof("0, , ") + strlen(con) + strlen($3)); sprintf($$, "0, %s, %s", con, $3); } - | SQL_DESCRIBE INPUT_P prepared_name into_sqlda + | DESCRIBE INPUT_P prepared_name into_sqlda { const char *con = connection ? connection : "NULL"; mmerror(PARSE_ERROR, ET_WARNING, "using unsupported DESCRIBE statement"); $$ = (char *) mm_alloc(sizeof("1, , ") + strlen(con) + strlen($3)); sprintf($$, "1, %s, %s", con, $3); } - | SQL_DESCRIBE opt_output prepared_name into_sqlda + | DESCRIBE opt_output prepared_name into_sqlda { const char *con = connection ? connection : "NULL"; $$ = (char *) mm_alloc(sizeof("0, , ") + strlen(con) + strlen($3)); @@ -1502,7 +1502,6 @@ ECPGKeywords_vanames: SQL_BREAK { $$ = mm_strdup("break"); } ; ECPGKeywords_rest: SQL_CONNECT { $$ = mm_strdup("connect"); } - | SQL_DESCRIBE { $$ = mm_strdup("describe"); } | SQL_DISCONNECT { $$ = mm_strdup("disconnect"); } | SQL_OPEN { $$ = mm_strdup("open"); } | SQL_VAR { $$ = mm_strdup("var"); } diff --git a/src/interfaces/ecpg/preproc/ecpg_kwlist.h b/src/interfaces/ecpg/preproc/ecpg_kwlist.h index 0170bfefdc..e8e15f3d8e 100644 --- a/src/interfaces/ecpg/preproc/ecpg_kwlist.h +++ b/src/interfaces/ecpg/preproc/ecpg_kwlist.h @@ -33,7 +33,6 @@ PG_KEYWORD("connect", SQL_CONNECT) PG_KEYWORD("count", SQL_COUNT) PG_KEYWORD("datetime_interval_code", SQL_DATETIME_INTERVAL_CODE) PG_KEYWORD("datetime_interval_precision", SQL_DATETIME_INTERVAL_PRECISION) -PG_KEYWORD("describe", SQL_DESCRIBE) PG_KEYWORD("descriptor", SQL_DESCRIPTOR) PG_KEYWORD("disconnect", SQL_DISCONNECT) PG_KEYWORD("found", SQL_FOUND) diff --git a/src/test/regress/expected/json.out b/src/test/regress/expected/json.out index c4156cf2a6..66548702b2 100644 --- a/src/test/regress/expected/json.out +++ b/src/test/regress/expected/json.out @@ -2219,6 +2219,12 @@ select json_object('{a,b,"","d e f"}','{1,2,3,"a b c"}'); (1 row) -- json_to_record and json_to_recordset +select * from json_to_record('{"a":1,"b":"foo","c":"bar"}'); + a | b | c +---+-----+----- + 1 | foo | bar +(1 row) + select * from json_to_record('{"a":1,"b":"foo","c":"bar"}') as x(a int, b text, d text); a | b | d diff --git a/src/test/regress/sql/json.sql b/src/test/regress/sql/json.sql index 20354f04e3..7f18d8d900 100644 --- a/src/test/regress/sql/json.sql +++ b/src/test/regress/sql/json.sql @@ -724,6 +724,8 @@ CREATE TEMP TABLE foo (serial_num int, name text, type text); -- json_to_record and json_to_recordset +select * from json_to_record('{"a":1,"b":"foo","c":"bar"}'); + select * from json_to_record('{"a":1,"b":"foo","c":"bar"}') as x(a int, b text, d text); base-commit: 6de7bcb76f6593dcd107a6bfed645f2142bf3225 -- 2.25.0