I wrote: > ... There's still a question > of whether reporting the whole script as the query is OK when > we have a syntax error, but I have no good ideas as to how to > make that terser.
I had an idea about this: we can use a pretty simple heuristic such as "break at semicolon-newline sequences". That could fail and show you just a fragment of a statement, but that still seems better than showing a whole extension script. We can ameliorate the problem that we might not show enough to clearly identify what failed by including a separate line number counter. In the attached v4 I included that in the context line that reports the script file, eg +CONTEXT: SQL statement "CREATE OR REPLACE FUNCTION ext_cor_func() RETURNS text + AS $$ SELECT 'ext_cor_func: from extension'::text $$ LANGUAGE sql" +extension script file "test_ext_cor--1.0.sql", near line 8 This way seems a whole lot more usable when dealing with a large extension script. regards, tom lane
From 6a8e6f66bd1abdb3c52b543651eec50ed4ceb071 Mon Sep 17 00:00:00 2001 From: Tom Lane <t...@sss.pgh.pa.us> Date: Tue, 8 Oct 2024 14:36:15 -0400 Subject: [PATCH v4 1/2] Improve parser's reporting of statement start locations. Up to now, the parser's reporting of a statement's stmt_location included any preceding whitespace or comments. This isn't really desirable but was done to avoid accounting honestly for nonterminals that reduce to empty. It causes problems for pg_stat_statements, which partially compensates by manually stripping whitespace, but is not bright enough to strip /*-style comments. There will be more problems with an upcoming patch to improve reporting of errors in extension scripts, so it's time to do something about this. The thing we have to do to make it work right is to adjust YYLLOC_DEFAULT to scan the inputs of each production to find the first one that has a valid location (i.e., did not reduce to empty). In theory this adds a little bit of per-reduction overhead, but in practice it's negligible. I checked by measuring the time to run raw_parser() on the contents of information_schema.sql, and there was basically no change. Having done that, we can rely on any nonterminal that didn't reduce to completely empty to have a correct starting location, and we don't need the kluges the stmtmulti production formerly used. This should have a side benefit of allowing parse error reports to include an error position in some cases where they formerly failed to do so, due to trying to report the position of an empty nonterminal. I did not go looking for an example though. The one previously known case where that could happen (OptSchemaEltList) no longer needs the kluge it had; but I rather doubt that that was the only case. Discussion: https://postgr.es/m/zvv1clhnbjlcz...@msg.df7cb.de --- .../pg_stat_statements/expected/select.out | 5 +- contrib/pg_stat_statements/sql/select.sql | 3 +- src/backend/nodes/queryjumblefuncs.c | 6 ++ src/backend/parser/gram.y | 66 +++++++------------ 4 files changed, 34 insertions(+), 46 deletions(-) diff --git a/contrib/pg_stat_statements/expected/select.out b/contrib/pg_stat_statements/expected/select.out index dd6c756f67..e0e2fa265c 100644 --- a/contrib/pg_stat_statements/expected/select.out +++ b/contrib/pg_stat_statements/expected/select.out @@ -19,8 +19,9 @@ SELECT 1 AS "int"; 1 (1 row) +/* this comment should not appear in the output */ SELECT 'hello' - -- multiline + -- but this one will appear AS "text"; text ------- @@ -129,7 +130,7 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; -------+------+------------------------------------------------------------------------------ 1 | 1 | PREPARE pgss_test (int) AS SELECT $1, $2 LIMIT $3 4 | 4 | SELECT $1 + - | | -- multiline + + | | -- but this one will appear + | | AS "text" 2 | 2 | SELECT $1 + $2 3 | 3 | SELECT $1 + $2 + $3 AS "add" diff --git a/contrib/pg_stat_statements/sql/select.sql b/contrib/pg_stat_statements/sql/select.sql index eb45cb81ad..e0be58d5e2 100644 --- a/contrib/pg_stat_statements/sql/select.sql +++ b/contrib/pg_stat_statements/sql/select.sql @@ -12,8 +12,9 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t; -- SELECT 1 AS "int"; +/* this comment should not appear in the output */ SELECT 'hello' - -- multiline + -- but this one will appear AS "text"; SELECT 'world' AS "text"; diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index 5e43fd9229..e8bf95690b 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -90,6 +90,12 @@ CleanQuerytext(const char *query, int *location, int *len) /* * Discard leading and trailing whitespace, too. Use scanner_isspace() * not libc's isspace(), because we want to match the lexer's behavior. + * + * Note: the parser now strips leading comments and whitespace from the + * reported stmt_location, so this first loop will only iterate in the + * unusual case that the location didn't propagate to here. But the + * statement length will extend to the end-of-string or terminating + * semicolon, so the second loop often does something useful. */ while (query_len > 0 && scanner_isspace(query[0])) query++, query_location++, query_len--; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 4aa8646af7..4bab2117d9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -67,39 +67,25 @@ /* - * Location tracking support --- simpler than bison's default, since we only - * want to track the start position not the end position of each nonterminal. + * Location tracking support. Unlike bison's default, we only want + * to track the start position not the end position of each nonterminal. + * Nonterminals that reduce to empty receive position "-1". Since a + * production's leading RHS nonterminal(s) may have reduced to empty, + * we have to scan to find the first one that's not -1. */ #define YYLLOC_DEFAULT(Current, Rhs, N) \ do { \ - if ((N) > 0) \ - (Current) = (Rhs)[1]; \ - else \ - (Current) = (-1); \ + (Current) = (-1); \ + for (int _i = 1; _i <= (N); _i++) \ + { \ + if ((Rhs)[_i] >= 0) \ + { \ + (Current) = (Rhs)[_i]; \ + break; \ + } \ + } \ } while (0) -/* - * The above macro assigns -1 (unknown) as the parse location of any - * nonterminal that was reduced from an empty rule, or whose leftmost - * component was reduced from an empty rule. This is problematic - * for nonterminals defined like - * OptFooList: / * EMPTY * / { ... } | OptFooList Foo { ... } ; - * because we'll set -1 as the location during the first reduction and then - * copy it during each subsequent reduction, leaving us with -1 for the - * location even when the list is not empty. To fix that, do this in the - * action for the nonempty rule(s): - * if (@$ < 0) @$ = @2; - * (Although we have many nonterminals that follow this pattern, we only - * bother with fixing @$ like this when the nonterminal's parse location - * is actually referenced in some rule.) - * - * A cleaner answer would be to make YYLLOC_DEFAULT scan all the Rhs - * locations until it's found one that's not -1. Then we'd get a correct - * location for any nonterminal that isn't entirely empty. But this way - * would add overhead to every rule reduction, and so far there's not been - * a compelling reason to pay that overhead. - */ - /* * Bison doesn't allocate anything that needs to live across parser calls, * so we can easily have it use palloc instead of malloc. This prevents @@ -930,7 +916,7 @@ parse_toplevel: | MODE_PLPGSQL_EXPR PLpgSQL_Expr { pg_yyget_extra(yyscanner)->parsetree = - list_make1(makeRawStmt($2, 0)); + list_make1(makeRawStmt($2, @2)); } | MODE_PLPGSQL_ASSIGN1 PLAssignStmt { @@ -938,7 +924,7 @@ parse_toplevel: n->nnames = 1; pg_yyget_extra(yyscanner)->parsetree = - list_make1(makeRawStmt((Node *) n, 0)); + list_make1(makeRawStmt((Node *) n, @2)); } | MODE_PLPGSQL_ASSIGN2 PLAssignStmt { @@ -946,7 +932,7 @@ parse_toplevel: n->nnames = 2; pg_yyget_extra(yyscanner)->parsetree = - list_make1(makeRawStmt((Node *) n, 0)); + list_make1(makeRawStmt((Node *) n, @2)); } | MODE_PLPGSQL_ASSIGN3 PLAssignStmt { @@ -954,19 +940,15 @@ parse_toplevel: n->nnames = 3; pg_yyget_extra(yyscanner)->parsetree = - list_make1(makeRawStmt((Node *) n, 0)); + list_make1(makeRawStmt((Node *) n, @2)); } ; /* * At top level, we wrap each stmt with a RawStmt node carrying start location - * and length of the stmt's text. Notice that the start loc/len are driven - * entirely from semicolon locations (@2). It would seem natural to use - * @1 or @3 to get the true start location of a stmt, but that doesn't work - * for statements that can start with empty nonterminals (opt_with_clause is - * the main offender here); as noted in the comments for YYLLOC_DEFAULT, - * we'd get -1 for the location in such cases. - * We also take care to discard empty statements entirely. + * and length of the stmt's text. + * We also take care to discard empty statements entirely (which among other + * things dodges the problem of assigning them a location). */ stmtmulti: stmtmulti ';' toplevel_stmt { @@ -976,14 +958,14 @@ stmtmulti: stmtmulti ';' toplevel_stmt updateRawStmtEnd(llast_node(RawStmt, $1), @2); } if ($3 != NULL) - $$ = lappend($1, makeRawStmt($3, @2 + 1)); + $$ = lappend($1, makeRawStmt($3, @3)); else $$ = $1; } | toplevel_stmt { if ($1 != NULL) - $$ = list_make1(makeRawStmt($1, 0)); + $$ = list_make1(makeRawStmt($1, @1)); else $$ = NIL; } @@ -1584,8 +1566,6 @@ CreateSchemaStmt: OptSchemaEltList: OptSchemaEltList schema_stmt { - if (@$ < 0) /* see comments for YYLLOC_DEFAULT */ - @$ = @2; $$ = lappend($1, $2); } | /* EMPTY */ -- 2.43.5
From ab3e000e063f5291aee1b1b8914d029bdf79b90e Mon Sep 17 00:00:00 2001 From: Tom Lane <t...@sss.pgh.pa.us> Date: Tue, 8 Oct 2024 16:02:57 -0400 Subject: [PATCH v4 2/2] Improve reporting of errors in extension script files. Previously, CREATE/ALTER EXTENSION gave basically no useful context about errors reported while executing script files. I think the idea was that you could run the same commands manually to see the error, but that's often quite inconvenient. Let's improve that. If we get an error during raw parsing, we won't have a current statement identified by a RawStmt node, but we should always get a syntax error position. Show the portion of the script from the last semicolon-newline before the error position to the first one after it. There are cases where this might show only a fragment of a statement, but that should be uncommon, and it seems better than showing the whole script file. Without an error cursor, if we have gotten past raw parsing (which we probably have), we can report just the current SQL statement as an item of error context. In any case also report the script file name as error context, since it might not be entirely obvious which of a series of update scripts failed. We can also show an approximate script line number in case whatever we printed of the query isn't sufficiently identifiable. The error-context code path is already exercised by some test_extensions test cases, but add tests for the syntax-error path. Discussion: https://postgr.es/m/zvv1clhnbjlcz...@msg.df7cb.de --- src/backend/commands/extension.c | 167 +++++++++++++++++- src/test/modules/test_extensions/Makefile | 4 +- .../expected/test_extensions.out | 42 +++++ src/test/modules/test_extensions/meson.build | 2 + .../test_extensions/sql/test_extensions.sql | 4 + .../test_ext7--2.0--2.1bad.sql | 14 ++ .../test_ext7--2.0--2.2bad.sql | 13 ++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext7--2.0--2.1bad.sql create mode 100644 src/test/modules/test_extensions/test_ext7--2.0--2.2bad.sql diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index fab59ad5f6..86ea9cd9da 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -54,6 +54,7 @@ #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/queryjumble.h" #include "storage/fd.h" #include "tcop/utility.h" #include "utils/acl.h" @@ -107,6 +108,17 @@ typedef struct ExtensionVersionInfo struct ExtensionVersionInfo *previous; /* current best predecessor */ } ExtensionVersionInfo; +/* + * Information for script_error_callback() + */ +typedef struct +{ + const char *sql; /* entire script file contents */ + const char *filename; /* script file pathname */ + ParseLoc stmt_location; /* current stmt start loc, or -1 if unknown */ + ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ +} script_error_callback_arg; + /* Local functions */ static List *find_update_path(List *evi_list, ExtensionVersionInfo *evi_start, @@ -670,9 +682,139 @@ read_extension_script_file(const ExtensionControlFile *control, return dest_str; } +/* + * error context callback for failures in script-file execution + */ +static void +script_error_callback(void *arg) +{ + script_error_callback_arg *callback_arg = (script_error_callback_arg *) arg; + const char *query = callback_arg->sql; + int location = callback_arg->stmt_location; + int len = callback_arg->stmt_len; + int syntaxerrposition; + const char *lastslash; + + /* + * If there is a syntax error position, convert to internal syntax error; + * otherwise report the current query as an item of context stack. + * + * Note: we'll provide no context except the filename if there's neither + * an error position nor any known current query. That shouldn't happen + * though: all errors reported during raw parsing should come with an + * error position. + */ + syntaxerrposition = geterrposition(); + if (syntaxerrposition > 0) + { + /* + * If we do not know the bounds of the current statement (as would + * happen for an error occurring during initial raw parsing), we have + * to use a heuristic to decide how much of the script to show. We'll + * also use the heuristic in the unlikely case that syntaxerrposition + * is outside what we think the statement bounds are. + */ + if (location < 0 || syntaxerrposition < location || + (len > 0 && syntaxerrposition > location + len)) + { + /* + * Our heuristic is pretty simple: look for semicolon-newline + * sequences, and break at the last one strictly before + * syntaxerrposition and the first one strictly after. It's + * certainly possible to fool this with semicolon-newline embedded + * in a string literal, but it seems better to do this than to + * show the entire extension script. + */ + int slen = strlen(query); + + location = len = 0; + for (int loc = 0; loc < slen; loc++) + { + if (query[loc] != ';') + continue; + if (query[loc + 1] == '\r') + loc++; + if (query[loc + 1] == '\n') + { + int bkpt = loc + 2; + + if (bkpt < syntaxerrposition) + location = bkpt; + else if (bkpt > syntaxerrposition) + { + len = bkpt - location; + break; /* no need to keep searching */ + } + } + } + } + + /* Trim leading/trailing whitespace, for consistency */ + query = CleanQuerytext(query, &location, &len); + + /* + * Adjust syntaxerrposition. It shouldn't be pointing into the + * whitespace we just trimmed, but cope if it is. + */ + syntaxerrposition -= location; + if (syntaxerrposition < 0) + syntaxerrposition = 0; + else if (syntaxerrposition > len) + syntaxerrposition = len; + + /* And report. */ + errposition(0); + internalerrposition(syntaxerrposition); + internalerrquery(pnstrdup(query, len)); + } + else if (location >= 0) + { + /* + * Since no syntax cursor will be shown, it's okay and helpful to trim + * the reported query string to just the current statement. + */ + query = CleanQuerytext(query, &location, &len); + errcontext("SQL statement \"%.*s\"", len, query); + } + + /* + * Trim the reported file name to remove the path. We know that + * get_extension_script_filename() inserted a '/', regardless of whether + * we're on Windows. + */ + lastslash = strrchr(callback_arg->filename, '/'); + if (lastslash) + lastslash++; + else + lastslash = callback_arg->filename; /* shouldn't happen, but cope */ + + /* + * If we have a location (which, as said above, we really always should) + * then report a line number to aid in localizing problems in big scripts. + */ + if (location >= 0) + { + int linenumber = 1; + + for (query = callback_arg->sql; *query; query++) + { + if (--location < 0) + break; + if (*query == '\n') + linenumber++; + } + errcontext("extension script file \"%s\", near line %d", + lastslash, linenumber); + } + else + errcontext("extension script file \"%s\"", lastslash); +} + /* * Execute given SQL string. * + * The filename the string came from is also provided, for error reporting. + * * Note: it's tempting to just use SPI to execute the string, but that does * not work very well. The really serious problem is that SPI will parse, * analyze, and plan the whole string before executing any of it; of course @@ -682,12 +824,27 @@ read_extension_script_file(const ExtensionControlFile *control, * could be very long. */ static void -execute_sql_string(const char *sql) +execute_sql_string(const char *sql, const char *filename) { + script_error_callback_arg callback_arg; + ErrorContextCallback scripterrcontext; List *raw_parsetree_list; DestReceiver *dest; ListCell *lc1; + /* + * Setup error traceback support for ereport(). + */ + callback_arg.sql = sql; + callback_arg.filename = filename; + callback_arg.stmt_location = -1; + callback_arg.stmt_len = -1; + + scripterrcontext.callback = script_error_callback; + scripterrcontext.arg = (void *) &callback_arg; + scripterrcontext.previous = error_context_stack; + error_context_stack = &scripterrcontext; + /* * Parse the SQL string into a list of raw parse trees. */ @@ -709,6 +866,10 @@ execute_sql_string(const char *sql) List *stmt_list; ListCell *lc2; + /* Report location of this query for error context callback */ + callback_arg.stmt_location = parsetree->stmt_location; + callback_arg.stmt_len = parsetree->stmt_len; + /* * We do the work for each parsetree in a short-lived context, to * limit the memory used when there are many commands in the string. @@ -778,6 +939,8 @@ execute_sql_string(const char *sql) MemoryContextDelete(per_parsetree_context); } + error_context_stack = scripterrcontext.previous; + /* Be sure to advance the command counter after the last script command */ CommandCounterIncrement(); } @@ -1054,7 +1217,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, /* And now back to C string */ c_sql = text_to_cstring(DatumGetTextPP(t_sql)); - execute_sql_string(c_sql); + execute_sql_string(c_sql, filename); } PG_FINALLY(); { diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 05272e6a40..1dbec14cba 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -13,7 +13,9 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \ - test_ext7--1.0.sql test_ext7--1.0--2.0.sql test_ext8--1.0.sql \ + test_ext7--1.0.sql test_ext7--1.0--2.0.sql \ + test_ext7--2.0--2.1bad.sql test_ext7--2.0--2.2bad.sql \ + test_ext8--1.0.sql \ test_ext9--1.0.sql \ test_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index f357cc21aa..d5388a1fec 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -72,6 +72,22 @@ Objects in extension "test_ext7" type ext7_table2[] (4 rows) +-- test reporting of errors in extension scripts +alter extension test_ext7 update to '2.1bad'; +ERROR: syntax error at or near "FUNCTIN" +LINE 1: CREATE FUNCTIN my_erroneous_func(int) RETURNS int LANGUAGE S... + ^ +QUERY: CREATE FUNCTIN my_erroneous_func(int) RETURNS int LANGUAGE SQL +AS $$ SELECT $1 + 1 $$; +CONTEXT: extension script file "test_ext7--2.0--2.1bad.sql", near line 10 +alter extension test_ext7 update to '2.2bad'; +ERROR: syntax error at or near "," +LINE 1: SELECT $1 + , 1 + ^ +QUERY: SELECT $1 + , 1 +CONTEXT: SQL statement "CREATE FUNCTION my_erroneous_func(int) RETURNS int LANGUAGE SQL +AS $$ SELECT $1 + , 1 $$" +extension script file "test_ext7--2.0--2.2bad.sql", near line 9 -- test handling of temp objects created by extensions create extension test_ext8; -- \dx+ would expose a variable pg_temp_nn schema name, so we can't use it here @@ -295,6 +311,9 @@ CREATE FUNCTION ext_cor_func() RETURNS text CREATE EXTENSION test_ext_cor; -- fail ERROR: function ext_cor_func() is not a member of extension "test_ext_cor" DETAIL: An extension is not allowed to replace an object that it does not own. +CONTEXT: SQL statement "CREATE OR REPLACE FUNCTION ext_cor_func() RETURNS text + AS $$ SELECT 'ext_cor_func: from extension'::text $$ LANGUAGE sql" +extension script file "test_ext_cor--1.0.sql", near line 8 SELECT ext_cor_func(); ext_cor_func ------------------------ @@ -307,6 +326,9 @@ CREATE VIEW ext_cor_view AS CREATE EXTENSION test_ext_cor; -- fail ERROR: view ext_cor_view is not a member of extension "test_ext_cor" DETAIL: An extension is not allowed to replace an object that it does not own. +CONTEXT: SQL statement "CREATE OR REPLACE VIEW ext_cor_view AS + SELECT 'ext_cor_view: from extension'::text AS col" +extension script file "test_ext_cor--1.0.sql", near line 11 SELECT ext_cor_func(); ERROR: function ext_cor_func() does not exist LINE 1: SELECT ext_cor_func(); @@ -323,6 +345,8 @@ CREATE TYPE test_ext_type; CREATE EXTENSION test_ext_cor; -- fail ERROR: type test_ext_type is not a member of extension "test_ext_cor" DETAIL: An extension is not allowed to replace an object that it does not own. +CONTEXT: SQL statement "CREATE TYPE test_ext_type AS ENUM('x', 'y')" +extension script file "test_ext_cor--1.0.sql", near line 17 DROP TYPE test_ext_type; -- this makes a shell "point <<@@ polygon" operator too CREATE OPERATOR @@>> ( PROCEDURE = poly_contain_pt, @@ -331,6 +355,9 @@ CREATE OPERATOR @@>> ( PROCEDURE = poly_contain_pt, CREATE EXTENSION test_ext_cor; -- fail ERROR: operator <<@@(point,polygon) is not a member of extension "test_ext_cor" DETAIL: An extension is not allowed to replace an object that it does not own. +CONTEXT: SQL statement "CREATE OPERATOR <<@@ ( PROCEDURE = pt_contained_poly, + LEFTARG = point, RIGHTARG = polygon )" +extension script file "test_ext_cor--1.0.sql", near line 19 DROP OPERATOR <<@@ (point, polygon); CREATE EXTENSION test_ext_cor; -- now it should work SELECT ext_cor_func(); @@ -379,37 +406,52 @@ CREATE COLLATION ext_cine_coll CREATE EXTENSION test_ext_cine; -- fail ERROR: collation ext_cine_coll is not a member of extension "test_ext_cine" DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +CONTEXT: SQL statement "CREATE COLLATION IF NOT EXISTS ext_cine_coll + ( LC_COLLATE = "POSIX", LC_CTYPE = "POSIX" )" +extension script file "test_ext_cine--1.0.sql", near line 10 DROP COLLATION ext_cine_coll; CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1; CREATE EXTENSION test_ext_cine; -- fail ERROR: materialized view ext_cine_mv is not a member of extension "test_ext_cine" DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +CONTEXT: SQL statement "CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1" +extension script file "test_ext_cine--1.0.sql", near line 13 DROP MATERIALIZED VIEW ext_cine_mv; CREATE FOREIGN DATA WRAPPER dummy; CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy; CREATE EXTENSION test_ext_cine; -- fail ERROR: server ext_cine_srv is not a member of extension "test_ext_cine" DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +CONTEXT: SQL statement "CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw" +extension script file "test_ext_cine--1.0.sql", near line 17 DROP SERVER ext_cine_srv; CREATE SCHEMA ext_cine_schema; CREATE EXTENSION test_ext_cine; -- fail ERROR: schema ext_cine_schema is not a member of extension "test_ext_cine" DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +CONTEXT: SQL statement "CREATE SCHEMA IF NOT EXISTS ext_cine_schema" +extension script file "test_ext_cine--1.0.sql", near line 19 DROP SCHEMA ext_cine_schema; CREATE SEQUENCE ext_cine_seq; CREATE EXTENSION test_ext_cine; -- fail ERROR: sequence ext_cine_seq is not a member of extension "test_ext_cine" DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +CONTEXT: SQL statement "CREATE SEQUENCE IF NOT EXISTS ext_cine_seq" +extension script file "test_ext_cine--1.0.sql", near line 21 DROP SEQUENCE ext_cine_seq; CREATE TABLE ext_cine_tab1 (x int); CREATE EXTENSION test_ext_cine; -- fail ERROR: table ext_cine_tab1 is not a member of extension "test_ext_cine" DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +CONTEXT: SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int)" +extension script file "test_ext_cine--1.0.sql", near line 23 DROP TABLE ext_cine_tab1; CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y; CREATE EXTENSION test_ext_cine; -- fail ERROR: table ext_cine_tab2 is not a member of extension "test_ext_cine" DETAIL: An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns. +CONTEXT: SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y" +extension script file "test_ext_cine--1.0.sql", near line 25 DROP TABLE ext_cine_tab2; CREATE EXTENSION test_ext_cine; \dx+ test_ext_cine diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index c5f3424da5..2b31cbf425 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -15,6 +15,8 @@ test_install_data += files( 'test_ext6.control', 'test_ext7--1.0--2.0.sql', 'test_ext7--1.0.sql', + 'test_ext7--2.0--2.1bad.sql', + 'test_ext7--2.0--2.2bad.sql', 'test_ext7.control', 'test_ext8--1.0.sql', 'test_ext8.control', diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 642c82ff5d..b5878f6f80 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -28,6 +28,10 @@ create extension test_ext7; alter extension test_ext7 update to '2.0'; \dx+ test_ext7 +-- test reporting of errors in extension scripts +alter extension test_ext7 update to '2.1bad'; +alter extension test_ext7 update to '2.2bad'; + -- test handling of temp objects created by extensions create extension test_ext8; diff --git a/src/test/modules/test_extensions/test_ext7--2.0--2.1bad.sql b/src/test/modules/test_extensions/test_ext7--2.0--2.1bad.sql new file mode 100644 index 0000000000..a055a2dbb2 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext7--2.0--2.1bad.sql @@ -0,0 +1,14 @@ +/* src/test/modules/test_extensions/test_ext7--2.0--2.1bad.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext7 UPDATE TO '2.1bad'" to load this file. \quit + +-- test reporting of a simple syntax error in an extension script +CREATE FUNCTION my_okay_func(int) RETURNS int LANGUAGE SQL +AS $$ SELECT $1 + 1 $$; + +CREATE FUNCTIN my_erroneous_func(int) RETURNS int LANGUAGE SQL +AS $$ SELECT $1 + 1 $$; + +CREATE FUNCTION my_other_func(int) RETURNS int LANGUAGE SQL +AS $$ SELECT $1 + 1 $$; diff --git a/src/test/modules/test_extensions/test_ext7--2.0--2.2bad.sql b/src/test/modules/test_extensions/test_ext7--2.0--2.2bad.sql new file mode 100644 index 0000000000..19aa9bf264 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext7--2.0--2.2bad.sql @@ -0,0 +1,13 @@ +/* src/test/modules/test_extensions/test_ext7--2.0--2.2bad.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext7 UPDATE TO '2.2bad'" to load this file. \quit + +-- test reporting of a nested syntax error in an extension script +SET LOCAL check_function_bodies = on; + +CREATE FUNCTION my_erroneous_func(int) RETURNS int LANGUAGE SQL +AS $$ SELECT $1 + , 1 $$; + +CREATE FUNCTION my_other_func(int) RETURNS int LANGUAGE SQL +AS $$ SELECT $1 + 1 $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a65e1c07c5..b9dde65150 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3884,6 +3884,7 @@ saophash_hash save_buffer scram_state scram_state_enum +script_error_callback_arg security_class_t sem_t sepgsql_context_info_t -- 2.43.5