From 303b26c1bb14abf20a35a9cddebee65e10f5ebd4 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 2 Dec 2022 14:24:44 -0500 Subject: [PATCH] Fix psql's \sf and \ef for new-style SQL functions. Some options of these commands need to be able to identify the start of the function body within the output of pg_get_functiondef(). It used to be that that always began with "AS", but since the introduction of new-style SQL functions, it might also start with "BEGIN" or "RETURN". Fix that on the psql side, and add some regression tests. Noted by me awhile ago, but I didn't do anything about it. Thanks to David Johnston for a nag. Discussion: https://postgr.es/m/AM9PR01MB8268D5CDABDF044EE9F42173FE8C9@AM9PR01MB8268.eurprd01.prod.exchangelabs.com --- src/backend/utils/adt/ruleutils.c | 4 +- src/bin/psql/command.c | 46 +++++++++++----------- src/test/regress/expected/psql.out | 62 ++++++++++++++++++++++++++++++ src/test/regress/sql/psql.sql | 8 ++++ 4 files changed, 94 insertions(+), 26 deletions(-) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 8e428e93286..b4eb4e18e6a 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2778,8 +2778,8 @@ pg_get_serial_sequence(PG_FUNCTION_ARGS) * * Note: if you change the output format of this function, be careful not * to break psql's rules (in \ef and \sf) for identifying the start of the - * function body. To wit: the function body starts on a line that begins - * with "AS ", and no preceding line will look like that. + * function body. To wit: the function body starts on a line that begins with + * "AS ", "BEGIN ", or "RETURN ", and no preceding line will look like that. */ Datum pg_get_functiondef(PG_FUNCTION_ARGS) diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 062c79ccbd1..997ab2bf0b6 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -164,8 +164,7 @@ static bool get_create_object_cmd(EditableObjectType obj_type, Oid oid, PQExpBuffer buf); static int strip_lineno_from_objdesc(char *obj); static int count_lines_in_buf(PQExpBuffer buf); -static void print_with_linenumbers(FILE *output, char *lines, - const char *header_keyword); +static void print_with_linenumbers(FILE *output, char *lines, bool is_func); static void minimal_error_message(PGresult *res); static void printSSLInfo(void); @@ -1165,17 +1164,19 @@ exec_command_ef_ev(PsqlScanState scan_state, bool active_branch, /* * lineno "1" should correspond to the first line of the * function body. We expect that pg_get_functiondef() will - * emit that on a line beginning with "AS ", and that there - * can be no such line before the real start of the function - * body. Increment lineno by the number of lines before that - * line, so that it becomes relative to the first line of the - * function definition. + * emit that on a line beginning with "AS ", "BEGIN ", or + * "RETURN ", and that there can be no such line before the + * real start of the function body. Increment lineno by the + * number of lines before that line, so that it becomes + * relative to the first line of the function definition. */ const char *lines = query_buf->data; while (*lines != '\0') { - if (strncmp(lines, "AS ", 3) == 0) + if (strncmp(lines, "AS ", 3) == 0 || + strncmp(lines, "BEGIN ", 6) == 0 || + strncmp(lines, "RETURN ", 7) == 0) break; lineno++; /* find start of next line */ @@ -2452,15 +2453,8 @@ exec_command_sf_sv(PsqlScanState scan_state, bool active_branch, if (show_linenumbers) { - /* - * For functions, lineno "1" should correspond to the first - * line of the function body. We expect that - * pg_get_functiondef() will emit that on a line beginning - * with "AS ", and that there can be no such line before the - * real start of the function body. - */ - print_with_linenumbers(output, buf->data, - is_func ? "AS " : NULL); + /* add line numbers */ + print_with_linenumbers(output, buf->data, is_func); } else { @@ -5353,24 +5347,28 @@ count_lines_in_buf(PQExpBuffer buf) /* * Write text at *lines to output with line numbers. * - * If header_keyword isn't NULL, then line 1 should be the first line beginning - * with header_keyword; lines before that are unnumbered. + * For functions, lineno "1" should correspond to the first line of the + * function body; lines before that are unnumbered. We expect that + * pg_get_functiondef() will emit that on a line beginning with "AS ", + * "BEGIN ", or "RETURN ", and that there can be no such line before + * the real start of the function body. * * Caution: this scribbles on *lines. */ static void -print_with_linenumbers(FILE *output, char *lines, - const char *header_keyword) +print_with_linenumbers(FILE *output, char *lines, bool is_func) { - bool in_header = (header_keyword != NULL); - size_t header_sz = in_header ? strlen(header_keyword) : 0; + bool in_header = is_func; int lineno = 0; while (*lines != '\0') { char *eol; - if (in_header && strncmp(lines, header_keyword, header_sz) == 0) + if (in_header && + (strncmp(lines, "AS ", 3) == 0 || + strncmp(lines, "BEGIN ", 6) == 0 || + strncmp(lines, "RETURN ", 7) == 0)) in_header = false; /* increment lineno only for body's lines */ diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index bff68bd27fc..a15db7c78ed 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5165,6 +5165,13 @@ List of access methods pg_catalog | bit_xor | smallint | smallint | agg (3 rows) +\df *._pg_expandarray + List of functions + Schema | Name | Result data type | Argument data types | Type +--------------------+-----------------+------------------+-------------------------------------------+------ + information_schema | _pg_expandarray | SETOF record | anyarray, OUT x anyelement, OUT n integer | func +(1 row) + \do - pg_catalog.int4 List of operators Schema | Name | Left arg type | Right arg type | Result type | Description @@ -5179,6 +5186,61 @@ List of access methods pg_catalog | && | anyarray | anyarray | boolean | overlaps (1 row) +-- check \sf +\sf information_schema._pg_expandarray +CREATE OR REPLACE FUNCTION information_schema._pg_expandarray(anyarray, OUT x anyelement, OUT n integer) + RETURNS SETOF record + LANGUAGE sql + IMMUTABLE PARALLEL SAFE STRICT +AS $function$select $1[s], + s operator(pg_catalog.-) pg_catalog.array_lower($1,1) operator(pg_catalog.+) 1 + from pg_catalog.generate_series(pg_catalog.array_lower($1,1), + pg_catalog.array_upper($1,1), + 1) as g(s)$function$ +\sf+ information_schema._pg_expandarray + CREATE OR REPLACE FUNCTION information_schema._pg_expandarray(anyarray, OUT x anyelement, OUT n integer) + RETURNS SETOF record + LANGUAGE sql + IMMUTABLE PARALLEL SAFE STRICT +1 AS $function$select $1[s], +2 s operator(pg_catalog.-) pg_catalog.array_lower($1,1) operator(pg_catalog.+) 1 +3 from pg_catalog.generate_series(pg_catalog.array_lower($1,1), +4 pg_catalog.array_upper($1,1), +5 1) as g(s)$function$ +\sf+ interval_pl_time + CREATE OR REPLACE FUNCTION pg_catalog.interval_pl_time(interval, time without time zone) + RETURNS time without time zone + LANGUAGE sql + IMMUTABLE PARALLEL SAFE STRICT COST 1 +1 RETURN ($2 + $1) +\sf ts_debug(text) +CREATE OR REPLACE FUNCTION pg_catalog.ts_debug(document text, OUT alias text, OUT description text, OUT token text, OUT dictionaries regdictionary[], OUT dictionary regdictionary, OUT lexemes text[]) + RETURNS SETOF record + LANGUAGE sql + STABLE PARALLEL SAFE STRICT +BEGIN ATOMIC + SELECT ts_debug.alias, + ts_debug.description, + ts_debug.token, + ts_debug.dictionaries, + ts_debug.dictionary, + ts_debug.lexemes + FROM ts_debug(get_current_ts_config(), ts_debug.document) ts_debug(alias, description, token, dictionaries, dictionary, lexemes); +END +\sf+ ts_debug(text) + CREATE OR REPLACE FUNCTION pg_catalog.ts_debug(document text, OUT alias text, OUT description text, OUT token text, OUT dictionaries regdictionary[], OUT dictionary regdictionary, OUT lexemes text[]) + RETURNS SETOF record + LANGUAGE sql + STABLE PARALLEL SAFE STRICT +1 BEGIN ATOMIC +2 SELECT ts_debug.alias, +3 ts_debug.description, +4 ts_debug.token, +5 ts_debug.dictionaries, +6 ts_debug.dictionary, +7 ts_debug.lexemes +8 FROM ts_debug(get_current_ts_config(), ts_debug.document) ts_debug(alias, description, token, dictionaries, dictionary, lexemes); +9 END -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 306c1d57a82..338a904ec77 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1239,9 +1239,17 @@ drop role regress_partitioning_role; \df has_database_privilege oid text \df has_database_privilege oid text - \dfa bit* small* +\df *._pg_expandarray \do - pg_catalog.int4 \do && anyarray * +-- check \sf +\sf information_schema._pg_expandarray +\sf+ information_schema._pg_expandarray +\sf+ interval_pl_time +\sf ts_debug(text) +\sf+ ts_debug(text) + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap