agora inbox for pgsql-hackers@postgresql.orghelp / color / mirror / Atom feed
[PATCH] Allow wildcard (%) in extension upgrade paths 14+ messages / 2 participants [nested] [flat]
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 8 ++++ src/backend/commands/extension.c | 42 ++++++++++++++++--- src/test/modules/test_extensions/Makefile | 6 ++- .../expected/test_extensions.out | 15 +++++++ src/test/modules/test_extensions/meson.build | 3 ++ .../test_extensions/sql/test_extensions.sql | 7 ++++ .../test_ext_wildcard1--%--2.0.sql | 6 +++ .../test_ext_wildcard1--1.0.sql | 6 +++ .../test_ext_wildcard1.control | 3 ++ 9 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 46e873a166..c79140f669 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1081,6 +1081,14 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr <literal>1.1</literal>). </para> + <para> + The literal value <literal>%</literal> can be used as the + <replaceable>old_version</replaceable> component in an extension + update script for it to match any version. Such wildcard update + scripts will only be used when no explicit path is found from + old to target version. + </para> + <para> Given that a suitable update script is available, the command <command>ALTER EXTENSION UPDATE</command> will update an installed extension diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..e3ea9dba30 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -128,6 +128,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -890,7 +891,14 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( ! file_exists(filename) ) + { + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1214,14 +1222,19 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + /* Find wildcard path, if no explicit path was found */ + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - return result; + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3405,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index c3139ab0fc..4fe2d82b6e 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -6,14 +6,16 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ - test_ext_evttrig + test_ext_evttrig test_ext_wildcard1 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_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ - test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ + REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 821fed38d1..1c4dc5be42 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -312,3 +312,18 @@ Objects in extension "test_ext_cine" table ext_cine_tab3 (9 rows) +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index e95a9f2e7e..79d90b34c1 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -29,6 +29,9 @@ install_data( 'test_ext_evttrig--1.0--2.0.sql', 'test_ext_evttrig--1.0.sql', 'test_ext_evttrig.control', + 'test_ext_wildcard1--1.0.sql', + 'test_ext_wildcard1--%--2.0.sql', + 'test_ext_wildcard1.control', kwargs: contrib_data_args, ) diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 41b6cddf0b..071845e8df 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -209,3 +209,10 @@ CREATE EXTENSION test_ext_cine; ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; \dx+ test_ext_cine + + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..0c2fc6fca6 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,3 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true -- 2.34.1 --flyqbkgig2a5xh3s-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 8 ++++ src/backend/commands/extension.c | 42 ++++++++++++++++--- src/test/modules/test_extensions/Makefile | 6 ++- .../expected/test_extensions.out | 15 +++++++ src/test/modules/test_extensions/meson.build | 3 ++ .../test_extensions/sql/test_extensions.sql | 7 ++++ .../test_ext_wildcard1--%--2.0.sql | 6 +++ .../test_ext_wildcard1--1.0.sql | 6 +++ .../test_ext_wildcard1.control | 3 ++ 9 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 46e873a166..c79140f669 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1081,6 +1081,14 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr <literal>1.1</literal>). </para> + <para> + The literal value <literal>%</literal> can be used as the + <replaceable>old_version</replaceable> component in an extension + update script for it to match any version. Such wildcard update + scripts will only be used when no explicit path is found from + old to target version. + </para> + <para> Given that a suitable update script is available, the command <command>ALTER EXTENSION UPDATE</command> will update an installed extension diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..e3ea9dba30 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -128,6 +128,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -890,7 +891,14 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( ! file_exists(filename) ) + { + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1214,14 +1222,19 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + /* Find wildcard path, if no explicit path was found */ + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - return result; + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3405,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index c3139ab0fc..4fe2d82b6e 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -6,14 +6,16 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ - test_ext_evttrig + test_ext_evttrig test_ext_wildcard1 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_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ - test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ + REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 821fed38d1..1c4dc5be42 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -312,3 +312,18 @@ Objects in extension "test_ext_cine" table ext_cine_tab3 (9 rows) +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index e95a9f2e7e..79d90b34c1 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -29,6 +29,9 @@ install_data( 'test_ext_evttrig--1.0--2.0.sql', 'test_ext_evttrig--1.0.sql', 'test_ext_evttrig.control', + 'test_ext_wildcard1--1.0.sql', + 'test_ext_wildcard1--%--2.0.sql', + 'test_ext_wildcard1.control', kwargs: contrib_data_args, ) diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 41b6cddf0b..071845e8df 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -209,3 +209,10 @@ CREATE EXTENSION test_ext_cine; ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; \dx+ test_ext_cine + + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..0c2fc6fca6 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,3 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true -- 2.34.1 --flyqbkgig2a5xh3s-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH v2] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Using wildcards needs to be explicitly requested by extensions via a "wildcard_upgrades" setting in their control file. --- src/backend/commands/extension.c | 58 ++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 6b6720c690..e36a79ae75 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -86,6 +86,7 @@ typedef struct ExtensionControlFile bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ bool superuser; /* must be superuser to install? */ bool trusted; /* allow becoming superuser on the fly? */ + bool wildcard_upgrades; /* allow using wildcards in upgrade scripts */ int encoding; /* encoding of the script file, or -1 */ List *requires; /* names of prerequisite extensions */ } ExtensionControlFile; @@ -128,6 +129,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -579,6 +581,14 @@ parse_extension_control_file(ExtensionControlFile *control, errmsg("parameter \"%s\" requires a Boolean value", item->name))); } + else if (strcmp(item->name, "wildcard_upgrades") == 0) + { + if (!parse_bool(item->value, &control->wildcard_upgrades)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" requires a Boolean value", + item->name))); + } else if (strcmp(item->name, "encoding") == 0) { control->encoding = pg_valid_server_encoding(item->value); @@ -636,6 +646,7 @@ read_extension_control_file(const char *extname) control->relocatable = false; control->superuser = true; control->trusted = false; + control->wildcard_upgrades = false; control->encoding = -1; /* @@ -890,7 +901,15 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( control->wildcard_upgrades && ! file_exists(filename) ) + { + elog(DEBUG1, "extension upgrade script \"%s\" does not exist, will try wildcard", filename); + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1215,13 +1234,23 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + if (result != NIL) + return result; - return result; + /* Find wildcard path, if allowed by control file */ + if ( control->wildcard_upgrades ) + { + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + + if (result != NIL) + return result; + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3421,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + AssertArg(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} -- 2.34.1 --339zka8KxudZ+6RE-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH v4] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Using wildcards needs to be explicitly requested by extensions via a "wildcard_upgrades" setting in their control file. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 14 +++++ src/backend/commands/extension.c | 58 +++++++++++++++++-- src/test/modules/test_extensions/Makefile | 7 ++- .../expected/test_extensions.out | 18 ++++++ .../test_extensions/sql/test_extensions.sql | 9 +++ .../test_ext_wildcard1--%--2.0.sql | 6 ++ .../test_ext_wildcard1--1.0.sql | 6 ++ .../test_ext_wildcard1.control | 4 ++ 8 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 218940ee5c..3d4003eaef 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -822,6 +822,20 @@ RETURNS anycompatible AS ... </para> </listitem> </varlistentry> + + <varlistentry id="extend-extensions-wildcard-upgrade"> + <term><varname>wildcard_upgrades</varname> (<type>boolean</type>)</term> + <listitem> + <para> + This parameter, if set to <literal>true</literal> (which is not the + default), allows <command>ALTER EXTENSION</command> to consider + a wildcard character <literal>%</literal> as matching any version of + the extension. Such wildcard match will only be used when no + perfect match is found for a version. + </para> + </listitem> + </varlistentry> + </variablelist> <para> diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 535072d181..c05055f5a0 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -88,6 +88,7 @@ typedef struct ExtensionControlFile bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ bool superuser; /* must be superuser to install? */ bool trusted; /* allow becoming superuser on the fly? */ + bool wildcard_upgrades; /* allow using wildcards in upgrade scripts */ int encoding; /* encoding of the script file, or -1 */ List *requires; /* names of prerequisite extensions */ List *no_relocate; /* names of prerequisite extensions that @@ -132,6 +133,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -584,6 +586,14 @@ parse_extension_control_file(ExtensionControlFile *control, errmsg("parameter \"%s\" requires a Boolean value", item->name))); } + else if (strcmp(item->name, "wildcard_upgrades") == 0) + { + if (!parse_bool(item->value, &control->wildcard_upgrades)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" requires a Boolean value", + item->name))); + } else if (strcmp(item->name, "encoding") == 0) { control->encoding = pg_valid_server_encoding(item->value); @@ -656,6 +666,7 @@ read_extension_control_file(const char *extname) control->relocatable = false; control->superuser = true; control->trusted = false; + control->wildcard_upgrades = false; control->encoding = -1; /* @@ -913,7 +924,15 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( control->wildcard_upgrades && ! file_exists(filename) ) + { + elog(DEBUG1, "extension upgrade script \"%s\" does not exist, will try wildcard", filename); + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1281,13 +1300,23 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + if (result != NIL) + return result; - return result; + /* Find wildcard path, if allowed by control file */ + if ( control->wildcard_upgrades ) + { + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + + if (result != NIL) + return result; + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3491,3 +3520,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1388c0fb0b..105138d08a 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -8,8 +8,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext_cyclic1 test_ext_cyclic2 \ test_ext_extschema \ test_ext_evttrig \ - test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 - + test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \ + test_ext_wildcard1 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 \ @@ -20,7 +20,8 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ test_ext_req_schema1--1.0.sql \ test_ext_req_schema2--1.0.sql \ - test_ext_req_schema3--1.0.sql + test_ext_req_schema3--1.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 472627a232..270840183d 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -445,3 +445,21 @@ SELECT test_s_dep.dep_req2(); DROP EXTENSION test_ext_req_schema1 CASCADE; NOTICE: drop cascades to extension test_ext_req_schema2 +-- +-- Test wildcard based upgrade paths +-- +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 51327cc321..bb567e0f19 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -276,3 +276,12 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok SELECT test_s_dep2.dep_req1(); SELECT test_s_dep.dep_req2(); DROP EXTENSION test_ext_req_schema1 CASCADE; + +-- +-- Test wildcard based upgrade paths +-- +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..865e37fa88 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,4 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true +wildcard_upgrades = true -- 2.34.1 --6ay2r5v7k3ozwyik-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH v3] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Using wildcards needs to be explicitly requested by extensions via a "wildcard_upgrades" setting in their control file. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 14 +++++ src/backend/commands/extension.c | 58 +++++++++++++++++-- src/test/modules/test_extensions/Makefile | 7 ++- .../expected/test_extensions.out | 15 +++++ .../test_extensions/sql/test_extensions.sql | 9 +++ .../test_ext_wildcard1--%--2.0.sql | 6 ++ .../test_ext_wildcard1--1.0.sql | 6 ++ .../test_ext_wildcard1.control | 4 ++ 8 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 218940ee5c..3d4003eaef 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -822,6 +822,20 @@ RETURNS anycompatible AS ... </para> </listitem> </varlistentry> + + <varlistentry id="extend-extensions-wildcard-upgrade"> + <term><varname>wildcard_upgrades</varname> (<type>boolean</type>)</term> + <listitem> + <para> + This parameter, if set to <literal>true</literal> (which is not the + default), allows <command>ALTER EXTENSION</command> to consider + a wildcard character <literal>%</literal> as matching any version of + the extension. Such wildcard match will only be used when no + perfect match is found for a version. + </para> + </listitem> + </varlistentry> + </variablelist> <para> diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 0eabe18335..207b4649f2 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -88,6 +88,7 @@ typedef struct ExtensionControlFile bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ bool superuser; /* must be superuser to install? */ bool trusted; /* allow becoming superuser on the fly? */ + bool wildcard_upgrades; /* allow using wildcards in upgrade scripts */ int encoding; /* encoding of the script file, or -1 */ List *requires; /* names of prerequisite extensions */ List *no_relocate; /* names of prerequisite extensions that @@ -132,6 +133,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -584,6 +586,14 @@ parse_extension_control_file(ExtensionControlFile *control, errmsg("parameter \"%s\" requires a Boolean value", item->name))); } + else if (strcmp(item->name, "wildcard_upgrades") == 0) + { + if (!parse_bool(item->value, &control->wildcard_upgrades)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" requires a Boolean value", + item->name))); + } else if (strcmp(item->name, "encoding") == 0) { control->encoding = pg_valid_server_encoding(item->value); @@ -656,6 +666,7 @@ read_extension_control_file(const char *extname) control->relocatable = false; control->superuser = true; control->trusted = false; + control->wildcard_upgrades = false; control->encoding = -1; /* @@ -913,7 +924,15 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( control->wildcard_upgrades && ! file_exists(filename) ) + { + elog(DEBUG1, "extension upgrade script \"%s\" does not exist, will try wildcard", filename); + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1259,13 +1278,23 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + if (result != NIL) + return result; - return result; + /* Find wildcard path, if allowed by control file */ + if ( control->wildcard_upgrades ) + { + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + + if (result != NIL) + return result; + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3470,3 +3499,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 70fc0c8e66..5a8205fd5d 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -7,8 +7,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ test_ext_evttrig \ - test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 - + test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \ + test_ext_wildcard1 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 \ @@ -18,7 +18,8 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ test_ext_req_schema1--1.0.sql \ test_ext_req_schema2--1.0.sql \ - test_ext_req_schema3--1.0.sql + test_ext_req_schema3--1.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index a31775a260..0d4d8b4b70 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -389,3 +389,18 @@ SELECT test_s_dep.dep_req2(); DROP EXTENSION test_ext_req_schema1 CASCADE; NOTICE: drop cascades to extension test_ext_req_schema2 +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index f4947e7da6..3c40710fc1 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -232,3 +232,12 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok SELECT test_s_dep2.dep_req1(); SELECT test_s_dep.dep_req2(); DROP EXTENSION test_ext_req_schema1 CASCADE; + +-- +-- Test wildcard based upgrade paths +-- +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..865e37fa88 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,4 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true +wildcard_upgrades = true -- 2.34.1 --twb4kgisshvw5z4p-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH v1] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 8 ++++ src/backend/commands/extension.c | 42 ++++++++++++++++--- src/test/modules/test_extensions/Makefile | 6 ++- .../expected/test_extensions.out | 15 +++++++ src/test/modules/test_extensions/meson.build | 3 ++ .../test_extensions/sql/test_extensions.sql | 7 ++++ .../test_ext_wildcard1--%--2.0.sql | 6 +++ .../test_ext_wildcard1--1.0.sql | 6 +++ .../test_ext_wildcard1.control | 3 ++ 9 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index b70cbe83ae..f1f0ae1244 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1081,6 +1081,14 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr <literal>1.1</literal>). </para> + <para> + The literal value <literal>%</literal> can be used as the + <replaceable>old_version</replaceable> component in an extension + update script for it to match any version. Such wildcard update + scripts will only be used when no explicit path is found from + old to target version. + </para> + <para> Given that a suitable update script is available, the command <command>ALTER EXTENSION UPDATE</command> will update an installed extension diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 02ff4a9a7f..6df0fd403a 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -130,6 +130,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -893,7 +894,14 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( ! file_exists(filename) ) + { + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1217,14 +1225,19 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + /* Find wildcard path, if no explicit path was found */ + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - return result; + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3395,3 +3408,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index c3139ab0fc..4fe2d82b6e 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -6,14 +6,16 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ - test_ext_evttrig + test_ext_evttrig test_ext_wildcard1 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_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ - test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ + REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 821fed38d1..1c4dc5be42 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -312,3 +312,18 @@ Objects in extension "test_ext_cine" table ext_cine_tab3 (9 rows) +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index c3af3e1721..026be6a879 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -30,6 +30,9 @@ test_install_data += files( 'test_ext_evttrig--1.0--2.0.sql', 'test_ext_evttrig--1.0.sql', 'test_ext_evttrig.control', + 'test_ext_wildcard1--1.0.sql', + 'test_ext_wildcard1--%--2.0.sql', + 'test_ext_wildcard1.control', ) tests += { diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 41b6cddf0b..071845e8df 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -209,3 +209,10 @@ CREATE EXTENSION test_ext_cine; ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; \dx+ test_ext_cine + + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..0c2fc6fca6 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,3 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true -- 2.34.1 --bhtumk242qxsvzi5-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH v2] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 8 ++++ src/backend/commands/extension.c | 42 ++++++++++++++++--- src/test/modules/test_extensions/Makefile | 7 ++-- .../expected/test_extensions.out | 16 +++++++ src/test/modules/test_extensions/meson.build | 3 ++ .../test_extensions/sql/test_extensions.sql | 9 ++++ .../test_ext_wildcard1--%--2.0.sql | 6 +++ .../test_ext_wildcard1--1.0.sql | 6 +++ .../test_ext_wildcard1.control | 3 ++ 9 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 218940ee5c..bdd463b81f 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1120,6 +1120,14 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr <literal>1.1</literal>). </para> + <para> + The literal value <literal>%</literal> can be used as the + <replaceable>old_version</replaceable> component in an extension + update script for it to match any version. Such wildcard update + scripts will only be used when no explicit path is found from + old to target version. + </para> + <para> Given that a suitable update script is available, the command <command>ALTER EXTENSION UPDATE</command> will update an installed extension diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 0eabe18335..36b6d7e01a 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -132,6 +132,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -913,7 +914,14 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( ! file_exists(filename) ) + { + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1258,14 +1266,19 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + /* Find wildcard path, if no explicit path was found */ + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - return result; + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3470,3 +3483,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 70fc0c8e66..beec04eea3 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -7,8 +7,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ test_ext_evttrig \ - test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 - + test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \ + test_ext_evttrig test_ext_wildcard1 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 \ @@ -18,7 +18,8 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ test_ext_req_schema1--1.0.sql \ test_ext_req_schema2--1.0.sql \ - test_ext_req_schema3--1.0.sql + test_ext_req_schema3--1.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index a31775a260..790b9b9368 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -389,3 +389,19 @@ SELECT test_s_dep.dep_req2(); DROP EXTENSION test_ext_req_schema1 CASCADE; NOTICE: drop cascades to extension test_ext_req_schema2 + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index 29e5bb2fb5..7b14d65545 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -36,6 +36,9 @@ test_install_data += files( 'test_ext_req_schema2.control', 'test_ext_req_schema3--1.0.sql', 'test_ext_req_schema3.control', + 'test_ext_wildcard1--1.0.sql', + 'test_ext_wildcard1--%--2.0.sql', + 'test_ext_wildcard1.control', ) tests += { diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index f4947e7da6..676face363 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -232,3 +232,12 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok SELECT test_s_dep2.dep_req1(); SELECT test_s_dep.dep_req2(); DROP EXTENSION test_ext_req_schema1 CASCADE; + +-- +-- Test wildcard upgrade +-- +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..0c2fc6fca6 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,3 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true -- 2.34.1 --nzgxg63vlhpg7nhu-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Using wildcards needs to be explicitly requested by extensions via a "wildcard_upgrades" setting in their control file. --- src/backend/commands/extension.c | 58 ++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..ea8825fcff 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -86,6 +86,7 @@ typedef struct ExtensionControlFile bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ bool superuser; /* must be superuser to install? */ bool trusted; /* allow becoming superuser on the fly? */ + bool wildcard_upgrades; /* allow using wildcards in upgrade scripts */ int encoding; /* encoding of the script file, or -1 */ List *requires; /* names of prerequisite extensions */ } ExtensionControlFile; @@ -128,6 +129,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -579,6 +581,14 @@ parse_extension_control_file(ExtensionControlFile *control, errmsg("parameter \"%s\" requires a Boolean value", item->name))); } + else if (strcmp(item->name, "wildcard_upgrades") == 0) + { + if (!parse_bool(item->value, &control->wildcard_upgrades)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" requires a Boolean value", + item->name))); + } else if (strcmp(item->name, "encoding") == 0) { control->encoding = pg_valid_server_encoding(item->value); @@ -636,6 +646,7 @@ read_extension_control_file(const char *extname) control->relocatable = false; control->superuser = true; control->trusted = false; + control->wildcard_upgrades = false; control->encoding = -1; /* @@ -890,7 +901,15 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( control->wildcard_upgrades && ! file_exists(filename) ) + { + elog(DEBUG1, "extension upgrade script \"%s\" does not exist, will try wildcard", filename); + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1215,13 +1234,23 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + if (result != NIL) + return result; - return result; + /* Find wildcard path, if allowed by control file */ + if ( control->wildcard_upgrades ) + { + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + + if (result != NIL) + return result; + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3421,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} -- 2.34.1 --jbsevty42g3gdjf3-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Using wildcards needs to be explicitly requested by extensions via a "wildcard_upgrades" setting in their control file. --- src/backend/commands/extension.c | 58 ++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..ea8825fcff 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -86,6 +86,7 @@ typedef struct ExtensionControlFile bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ bool superuser; /* must be superuser to install? */ bool trusted; /* allow becoming superuser on the fly? */ + bool wildcard_upgrades; /* allow using wildcards in upgrade scripts */ int encoding; /* encoding of the script file, or -1 */ List *requires; /* names of prerequisite extensions */ } ExtensionControlFile; @@ -128,6 +129,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -579,6 +581,14 @@ parse_extension_control_file(ExtensionControlFile *control, errmsg("parameter \"%s\" requires a Boolean value", item->name))); } + else if (strcmp(item->name, "wildcard_upgrades") == 0) + { + if (!parse_bool(item->value, &control->wildcard_upgrades)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" requires a Boolean value", + item->name))); + } else if (strcmp(item->name, "encoding") == 0) { control->encoding = pg_valid_server_encoding(item->value); @@ -636,6 +646,7 @@ read_extension_control_file(const char *extname) control->relocatable = false; control->superuser = true; control->trusted = false; + control->wildcard_upgrades = false; control->encoding = -1; /* @@ -890,7 +901,15 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( control->wildcard_upgrades && ! file_exists(filename) ) + { + elog(DEBUG1, "extension upgrade script \"%s\" does not exist, will try wildcard", filename); + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1215,13 +1234,23 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + if (result != NIL) + return result; - return result; + /* Find wildcard path, if allowed by control file */ + if ( control->wildcard_upgrades ) + { + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + + if (result != NIL) + return result; + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3421,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} -- 2.34.1 --jbsevty42g3gdjf3-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Using wildcards needs to be explicitly requested by extensions via a "wildcard_upgrades" setting in their control file. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 14 +++++ src/backend/commands/extension.c | 58 +++++++++++++++++-- src/test/modules/test_extensions/Makefile | 6 +- .../expected/test_extensions.out | 15 +++++ .../test_extensions/sql/test_extensions.sql | 7 +++ .../test_ext_wildcard1--%--2.0.sql | 6 ++ .../test_ext_wildcard1--1.0.sql | 6 ++ .../test_ext_wildcard1.control | 4 ++ 8 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 46e873a166..4012652574 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -807,6 +807,20 @@ RETURNS anycompatible AS ... </para> </listitem> </varlistentry> + + <varlistentry> + <term><varname>wildcard_upgrades</varname> (<type>boolean</type>)</term> + <listitem> + <para> + This parameter, if set to <literal>true</literal> (which is not the + default), allows <command>ALTER EXTENSION</command> to consider + a wildcard character <literal>%</literal> as matching any version of + the extension. Such wildcard match will only be used when no + perfect match is found for a version. + </para> + </listitem> + </varlistentry> + </variablelist> <para> diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..ea8825fcff 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -86,6 +86,7 @@ typedef struct ExtensionControlFile bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ bool superuser; /* must be superuser to install? */ bool trusted; /* allow becoming superuser on the fly? */ + bool wildcard_upgrades; /* allow using wildcards in upgrade scripts */ int encoding; /* encoding of the script file, or -1 */ List *requires; /* names of prerequisite extensions */ } ExtensionControlFile; @@ -128,6 +129,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -579,6 +581,14 @@ parse_extension_control_file(ExtensionControlFile *control, errmsg("parameter \"%s\" requires a Boolean value", item->name))); } + else if (strcmp(item->name, "wildcard_upgrades") == 0) + { + if (!parse_bool(item->value, &control->wildcard_upgrades)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" requires a Boolean value", + item->name))); + } else if (strcmp(item->name, "encoding") == 0) { control->encoding = pg_valid_server_encoding(item->value); @@ -636,6 +646,7 @@ read_extension_control_file(const char *extname) control->relocatable = false; control->superuser = true; control->trusted = false; + control->wildcard_upgrades = false; control->encoding = -1; /* @@ -890,7 +901,15 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( control->wildcard_upgrades && ! file_exists(filename) ) + { + elog(DEBUG1, "extension upgrade script \"%s\" does not exist, will try wildcard", filename); + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1215,13 +1234,23 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + if (result != NIL) + return result; - return result; + /* Find wildcard path, if allowed by control file */ + if ( control->wildcard_upgrades ) + { + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + + if (result != NIL) + return result; + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3421,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index c3139ab0fc..4fe2d82b6e 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -6,14 +6,16 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ - test_ext_evttrig + test_ext_evttrig test_ext_wildcard1 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_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ - test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ + REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 821fed38d1..1c4dc5be42 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -312,3 +312,18 @@ Objects in extension "test_ext_cine" table ext_cine_tab3 (9 rows) +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 41b6cddf0b..071845e8df 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -209,3 +209,10 @@ CREATE EXTENSION test_ext_cine; ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; \dx+ test_ext_cine + + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..865e37fa88 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,4 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true +wildcard_upgrades = true -- 2.34.1 --sbf3wqvs4ajivmh2-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 8 ++++ src/backend/commands/extension.c | 42 ++++++++++++++++--- src/test/modules/test_extensions/Makefile | 6 ++- .../expected/test_extensions.out | 15 +++++++ .../test_extensions/sql/test_extensions.sql | 7 ++++ .../test_ext_wildcard1--%--2.0.sql | 6 +++ .../test_ext_wildcard1--1.0.sql | 6 +++ .../test_ext_wildcard1.control | 3 ++ 8 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 46e873a166..c79140f669 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1081,6 +1081,14 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr <literal>1.1</literal>). </para> + <para> + The literal value <literal>%</literal> can be used as the + <replaceable>old_version</replaceable> component in an extension + update script for it to match any version. Such wildcard update + scripts will only be used when no explicit path is found from + old to target version. + </para> + <para> Given that a suitable update script is available, the command <command>ALTER EXTENSION UPDATE</command> will update an installed extension diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..e3ea9dba30 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -128,6 +128,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -890,7 +891,14 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( ! file_exists(filename) ) + { + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1214,14 +1222,19 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + /* Find wildcard path, if no explicit path was found */ + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - return result; + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3405,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index c3139ab0fc..4fe2d82b6e 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -6,14 +6,16 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ - test_ext_evttrig + test_ext_evttrig test_ext_wildcard1 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_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ - test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ + REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 821fed38d1..1c4dc5be42 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -312,3 +312,18 @@ Objects in extension "test_ext_cine" table ext_cine_tab3 (9 rows) +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 41b6cddf0b..071845e8df 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -209,3 +209,10 @@ CREATE EXTENSION test_ext_cine; ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; \dx+ test_ext_cine + + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..0c2fc6fca6 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,3 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true -- 2.34.1 --pikujl27r76rlaub-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Using wildcards needs to be explicitly requested by extensions via a "wildcard_upgrades" setting in their control file. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 14 +++++ src/backend/commands/extension.c | 58 +++++++++++++++++-- src/test/modules/test_extensions/Makefile | 6 +- .../expected/test_extensions.out | 15 +++++ .../test_extensions/sql/test_extensions.sql | 7 +++ .../test_ext_wildcard1--%--2.0.sql | 6 ++ .../test_ext_wildcard1--1.0.sql | 6 ++ .../test_ext_wildcard1.control | 4 ++ 8 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 46e873a166..4012652574 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -807,6 +807,20 @@ RETURNS anycompatible AS ... </para> </listitem> </varlistentry> + + <varlistentry> + <term><varname>wildcard_upgrades</varname> (<type>boolean</type>)</term> + <listitem> + <para> + This parameter, if set to <literal>true</literal> (which is not the + default), allows <command>ALTER EXTENSION</command> to consider + a wildcard character <literal>%</literal> as matching any version of + the extension. Such wildcard match will only be used when no + perfect match is found for a version. + </para> + </listitem> + </varlistentry> + </variablelist> <para> diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..ea8825fcff 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -86,6 +86,7 @@ typedef struct ExtensionControlFile bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ bool superuser; /* must be superuser to install? */ bool trusted; /* allow becoming superuser on the fly? */ + bool wildcard_upgrades; /* allow using wildcards in upgrade scripts */ int encoding; /* encoding of the script file, or -1 */ List *requires; /* names of prerequisite extensions */ } ExtensionControlFile; @@ -128,6 +129,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -579,6 +581,14 @@ parse_extension_control_file(ExtensionControlFile *control, errmsg("parameter \"%s\" requires a Boolean value", item->name))); } + else if (strcmp(item->name, "wildcard_upgrades") == 0) + { + if (!parse_bool(item->value, &control->wildcard_upgrades)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" requires a Boolean value", + item->name))); + } else if (strcmp(item->name, "encoding") == 0) { control->encoding = pg_valid_server_encoding(item->value); @@ -636,6 +646,7 @@ read_extension_control_file(const char *extname) control->relocatable = false; control->superuser = true; control->trusted = false; + control->wildcard_upgrades = false; control->encoding = -1; /* @@ -890,7 +901,15 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( control->wildcard_upgrades && ! file_exists(filename) ) + { + elog(DEBUG1, "extension upgrade script \"%s\" does not exist, will try wildcard", filename); + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1215,13 +1234,23 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + if (result != NIL) + return result; - return result; + /* Find wildcard path, if allowed by control file */ + if ( control->wildcard_upgrades ) + { + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + + if (result != NIL) + return result; + } + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3421,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index c3139ab0fc..4fe2d82b6e 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -6,14 +6,16 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ - test_ext_evttrig + test_ext_evttrig test_ext_wildcard1 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_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ - test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ + REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 821fed38d1..1c4dc5be42 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -312,3 +312,18 @@ Objects in extension "test_ext_cine" table ext_cine_tab3 (9 rows) +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 41b6cddf0b..071845e8df 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -209,3 +209,10 @@ CREATE EXTENSION test_ext_cine; ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; \dx+ test_ext_cine + + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..865e37fa88 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,4 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true +wildcard_upgrades = true -- 2.34.1 --sbf3wqvs4ajivmh2-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH] Allow wildcard (%) in extension upgrade paths @ 2022-09-14 09:10 Sandro Santilli <strk@kbt.io> 0 siblings, 0 replies; 14+ messages in thread From: Sandro Santilli @ 2022-09-14 09:10 UTC (permalink / raw) A wildcard character "%" will be accepted in the "source" side of the upgrade script and be considered usable to upgrade any version to the "target" side. Includes regression test and documentation. --- doc/src/sgml/extend.sgml | 8 ++++ src/backend/commands/extension.c | 42 ++++++++++++++++--- src/test/modules/test_extensions/Makefile | 6 ++- .../expected/test_extensions.out | 15 +++++++ .../test_extensions/sql/test_extensions.sql | 7 ++++ .../test_ext_wildcard1--%--2.0.sql | 6 +++ .../test_ext_wildcard1--1.0.sql | 6 +++ .../test_ext_wildcard1.control | 3 ++ 8 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql create mode 100644 src/test/modules/test_extensions/test_ext_wildcard1.control diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 46e873a166..c79140f669 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1081,6 +1081,14 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr <literal>1.1</literal>). </para> + <para> + The literal value <literal>%</literal> can be used as the + <replaceable>old_version</replaceable> component in an extension + update script for it to match any version. Such wildcard update + scripts will only be used when no explicit path is found from + old to target version. + </para> + <para> Given that a suitable update script is available, the command <command>ALTER EXTENSION UPDATE</command> will update an installed extension diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1a62e5dac5..e3ea9dba30 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -128,6 +128,7 @@ static void ApplyExtensionUpdates(Oid extensionOid, bool cascade, bool is_create); static char *read_whole_file(const char *filename, int *length); +static bool file_exists(const char *name); /* @@ -890,7 +891,14 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, if (from_version == NULL) elog(DEBUG1, "executing extension script for \"%s\" version '%s'", control->name, version); else + { + if ( ! file_exists(filename) ) + { + /* if filename does not exist, try wildcard */ + filename = get_extension_script_filename(control, "%", version); + } elog(DEBUG1, "executing extension script for \"%s\" update from version '%s' to '%s'", control->name, from_version, version); + } /* * If installing a trusted extension on behalf of a non-superuser, become @@ -1214,14 +1222,19 @@ identify_update_path(ExtensionControlFile *control, /* Find shortest path */ result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - if (result == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", - control->name, oldVersion, newVersion))); + /* Find wildcard path, if no explicit path was found */ + evi_start = get_ext_ver_info("%", &evi_list); + result = find_update_path(evi_list, evi_start, evi_target, false, false); + if (result != NIL) + return result; - return result; + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); } /* @@ -3392,3 +3405,20 @@ read_whole_file(const char *filename, int *length) buf[*length] = '\0'; return buf; } + +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name != NULL); + + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == ENOTDIR || errno == EACCES)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + + return false; +} diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index c3139ab0fc..4fe2d82b6e 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -6,14 +6,16 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext7 test_ext8 test_ext_cine test_ext_cor \ test_ext_cyclic1 test_ext_cyclic2 \ - test_ext_evttrig + test_ext_evttrig test_ext_wildcard1 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_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \ test_ext_cor--1.0.sql \ test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \ - test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql + test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \ + test_ext_wildcard1--1.0.sql test_ext_wildcard1--%--2.0.sql \ + REGRESS = test_extensions test_extdepend diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 821fed38d1..1c4dc5be42 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -312,3 +312,18 @@ Objects in extension "test_ext_cine" table ext_cine_tab3 (9 rows) +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 1.0 +(1 row) + +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); + ext_wildcard1_version +----------------------- + 2.0 +(1 row) + +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 41b6cddf0b..071845e8df 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -209,3 +209,10 @@ CREATE EXTENSION test_ext_cine; ALTER EXTENSION test_ext_cine UPDATE TO '1.1'; \dx+ test_ext_cine + + +CREATE EXTENSION test_ext_wildcard1; +SELECT ext_wildcard1_version(); +ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'; +SELECT ext_wildcard1_version(); +DROP EXTENSION test_ext_wildcard1; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql new file mode 100644 index 0000000000..75154e5c55 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--%--2.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION test_ext_wildcard1 UPDATE TO '2.0'" to load this file. \quit + +CREATE OR REPLACE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 2.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql new file mode 100644 index 0000000000..a69e791fda --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql @@ -0,0 +1,6 @@ +/* src/test/modules/test_extensions/test_ext_wildcard1--1.0.sql */ +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "CREATE EXTENSION test_ext_wildcard1" to load this file. \quit + +CREATE FUNCTION ext_wildcard1_version() returns TEXT +AS 'SELECT 1.0' LANGUAGE 'sql'; diff --git a/src/test/modules/test_extensions/test_ext_wildcard1.control b/src/test/modules/test_extensions/test_ext_wildcard1.control new file mode 100644 index 0000000000..0c2fc6fca6 --- /dev/null +++ b/src/test/modules/test_extensions/test_ext_wildcard1.control @@ -0,0 +1,3 @@ +comment = 'Test extension wildcard 1' +default_version = '1.0' +relocatable = true -- 2.34.1 --pikujl27r76rlaub-- ^ permalink raw reply [nested|flat] 14+ messages in thread
* [PATCH v1 1/2] Support changing a column into a stored generated column @ 2026-03-16 23:25 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 14+ messages in thread From: Alberto Piai @ 2026-03-16 23:25 UTC (permalink / raw) This adds basic support for an ALTER TABLE ... ALTER COLUMN command to turn a regular column into a stored generated column. The syntax is chosen to be similar to ... ALTER COLUMN ... ADD GENERATED ... AS IDENTITY with the difference that in this case, since we're dealing with a generated column, only ALWAYS is supported. Additionally, STORED must always be specified. Since this is a first prototype, no thought has been given to partitioned nor foreign tables, so these are not supported either. This operation always rewrites the contents of the column using the new generated expression. --- src/backend/commands/tablecmds.c | 137 +++++++++++++++++- src/backend/parser/gram.y | 31 ++++ src/include/nodes/parsenodes.h | 1 + .../test_ddl_deparse/test_ddl_deparse.c | 2 + src/test/regress/expected/alter_table.out | 122 ++++++++++++++++ src/test/regress/sql/alter_table.sql | 69 +++++++++ 6 files changed, 361 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 67e42e5df29..e7386e81b07 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -760,6 +760,10 @@ static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, PartitionCmd *cmd, AlterTableUtilityContext *context); +static ObjectAddress ATExecAddGeneratedAsExprStored(AlteredTableInfo *tab, + Relation rel, + const char *colName, + Constraint *def); /* ---------------------------------------------------------------- * DefineRelation @@ -4746,6 +4750,7 @@ AlterTableGetLockLevel(List *cmds) case AT_SetExpression: case AT_DropExpression: case AT_SetCompression: + case AT_AddGeneratedAsExprStored: cmd_lockmode = AccessExclusiveLock; break; @@ -5321,6 +5326,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* No command-specific prep needed */ pass = AT_PASS_MISC; break; + case AT_AddGeneratedAsExprStored: + /* No support yet for: partitioned tables, foreign tables */ + ATSimplePermissions(cmd->subtype, rel, ATT_TABLE); + + /* + * This has similar mechanics to AT_SetExpression, let's use the + * same pass. + */ + pass = AT_PASS_SET_EXPRESSION; + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -5733,6 +5748,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, ATExecSplitPartition(wqueue, tab, rel, (PartitionCmd *) cmd->def, context); break; + case AT_AddGeneratedAsExprStored: + cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode, + cur_pass, context); + Assert(cmd != NULL); + address = ATExecAddGeneratedAsExprStored(tab, rel, cmd->name, (Constraint *) cmd->def); + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -6785,6 +6806,8 @@ alter_table_type_to_string(AlterTableType cmdtype) return "ALTER COLUMN ... DROP IDENTITY"; case AT_ReAddStatistics: return NULL; /* not real grammar */ + case AT_AddGeneratedAsExprStored: + return "ALTER COLUMN ... ADD GENERATED ALWAYS AS (...) STORED"; } return NULL; @@ -8823,6 +8846,118 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, return address; } +/* + * ALTER TABLE ALTER COLUMN ADD GENERATED ALWAYS AS expr STORED + */ +static ObjectAddress +ATExecAddGeneratedAsExprStored(AlteredTableInfo *tab, + Relation rel, + const char *colName, + Constraint *def) +{ + HeapTuple tuple; + Form_pg_attribute attTup; + AttrNumber attnum; + ObjectAddress address; + Expr *defval; + NewColumnValue *newval; + RawColumnDefault *rawEnt; + Relation pg_attribute; + + Assert(def->raw_expr != NULL); + Assert(def->cooked_expr == NULL); + Assert(def->generated_when == ATTRIBUTE_IDENTITY_ALWAYS); + Assert(def->generated_kind == ATTRIBUTE_GENERATED_STORED); + + tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)))); + + attTup = (Form_pg_attribute) GETSTRUCT(tuple); + + attnum = attTup->attnum; + if (attnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter system column \"%s\"", + colName))); + + if (attTup->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("column \"%s\" of relation \"%s\" is already a generated column", + colName, RelationGetRelationName(rel)))); + + /* Mark as generated stored in pg_attribute */ + pg_attribute = table_open(AttributeRelationId, RowExclusiveLock); + attTup->attgenerated = ATTRIBUTE_GENERATED_STORED; + CatalogTupleUpdate(pg_attribute, &tuple->t_self, tuple); + table_close(pg_attribute, RowExclusiveLock); + + /* Make above changes visible */ + CommandCounterIncrement(); + + ReleaseSysCache(tuple); + + /* + * Find everything that depends on the column (constraints, indexes, etc), + * and record enough information to let us recreate the objects. + */ + RememberAllDependentForRebuilding(tab, AT_AddGeneratedAsExprStored, + rel, attnum, colName); + + /* + * Remove previous default value, if any, and store the new generator + * expression. + */ + RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, + false, false); + + rawEnt = palloc_object(RawColumnDefault); + rawEnt->attnum = attnum; + rawEnt->raw_default = def->raw_expr; + rawEnt->generated = def->generated_kind; + AddRelationNewConstraints(rel, list_make1(rawEnt), NIL, + false, true, false, NULL); + + /* Make above changes visible */ + CommandCounterIncrement(); + + /* + * Clear all the missing values if we're rewriting the table, since this + * renders them pointless. + */ + RelationClearMissing(rel); + + /* Make above changes visible */ + CommandCounterIncrement(); + + /* Drop any pg_statistic entry for the column */ + RemoveStatistics(RelationGetRelid(rel), attnum); + + /* Build a concrete expression for the new default (generated) value */ + defval = (Expr *) build_column_default(rel, attnum); + defval = expression_planner(defval); + + /* Schedule a rewrite */ + newval = palloc0_object(NewColumnValue); + newval->attnum = attnum; + newval->expr = defval; + newval->is_generated = true; + tab->newvals = lappend(tab->newvals, newval); + tab->rewrite |= AT_REWRITE_DEFAULT_VAL; + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), attnum); + + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + return address; +} + /* * ALTER TABLE ALTER COLUMN DROP EXPRESSION */ @@ -15290,7 +15425,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, SysScanDesc scan; HeapTuple depTup; - Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression); + Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression || subtype == AT_AddGeneratedAsExprStored); depRel = table_open(DependRelationId, RowExclusiveLock); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c2584249603..74440e801d4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2717,6 +2717,37 @@ alter_table_cmd: n->name = $3; n->def = (Node *) c; + $$ = (Node *) n; + } + /* ALTER TABLE <name> ALTER [COLUMN] <colname> ADD GENERATED ALWAYS AS ( <expression> ) STORED */ + | ALTER opt_column ColId ADD_P GENERATED generated_when AS '(' a_expr ')' STORED + { + AlterTableCmd *n = makeNode(AlterTableCmd); + Constraint *c = makeNode(Constraint); + + c->contype = CONSTR_GENERATED; + c->generated_when = $6; + c->raw_expr = $9; + c->cooked_expr = NULL; + c->generated_kind = ATTRIBUTE_GENERATED_STORED; + c->location = @5; + + /* + * Like in the case of ColConstraintElem, we cannot handle + * this in the grammar because IDENTITY allows both ALWAYS + * and BY DEFAULT, while generated columns only allow + * ALWAYS. This would lead to shift/reduce conflicts. + */ + if (c->generated_when != ATTRIBUTE_IDENTITY_ALWAYS) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("for a generated column, GENERATED ALWAYS must be specified"), + parser_errposition(@6))); + + n->subtype = AT_AddGeneratedAsExprStored; + n->name = $3; + n->def = (Node *) c; + $$ = (Node *) n; } /* ALTER TABLE <name> ALTER [COLUMN] <colname> SET <sequence options>/RESET */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ffadd667167..6b61513e6d0 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2568,6 +2568,7 @@ typedef enum AlterTableType AT_SetIdentity, /* SET identity column options */ AT_DropIdentity, /* DROP IDENTITY */ AT_ReAddStatistics, /* internal to commands/tablecmds.c */ + AT_AddGeneratedAsExprStored, /* ADD GENERATED ALWAYS AS (...) STORED */ } AlterTableType; typedef struct AlterTableCmd /* one subcommand of an ALTER TABLE */ diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c index 64a1dfa9f79..7c1699d538c 100644 --- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c @@ -315,6 +315,8 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS) case AT_ReAddStatistics: strtype = "(re) ADD STATS"; break; + case AT_AddGeneratedAsExprStored: + strtype = "ADD GENERATED ALWAYS AS (...) STORED"; } if (subcmd->recurse) diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index ccd79dfecc0..75f64628aef 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4863,3 +4863,125 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Tests for ALTER COLUMN ... ADD GENERATED ALWAYS as ( expr ) STORED +-- turning a regular column into a stored generated column +create schema testgen; +create table testgen.t1 (a int, b int not null); +insert into testgen.t1 (a, b) + select x, x from generate_series(1, 10) x; +alter table testgen.t1 alter column b + add generated always as (a * 2) stored; +\d+ testgen.t1 + Table "testgen.t1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+------------------------------------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | generated always as (a * 2) stored | plain | | +Not-null constraints: + "t1_b_not_null" NOT NULL "b" + +select a, b, a * 2 as expected, b = (a * 2) as correct + from testgen.t1 order by a; + a | b | expected | correct +----+----+----------+--------- + 1 | 2 | 2 | t + 2 | 4 | 4 | t + 3 | 6 | 6 | t + 4 | 8 | 8 | t + 5 | 10 | 10 | t + 6 | 12 | 12 | t + 7 | 14 | 14 | t + 8 | 16 | 16 | t + 9 | 18 | 18 | t + 10 | 20 | 20 | t +(10 rows) + +insert into testgen.t1 (a, b) values (10, 20); +ERROR: cannot insert a non-DEFAULT value into column "b" +DETAIL: Column "b" is a generated column. +insert into testgen.t1 (a, b) values (10, 21); +ERROR: cannot insert a non-DEFAULT value into column "b" +DETAIL: Column "b" is a generated column. +drop table testgen.t1; +-- turning a regular column into a stored generated column +-- fails when another constraint conflicts with the new expression +create table testgen.t2 (a int, b int not null); +insert into testgen.t2 (a, b) select x, x * 2 from generate_series(0, 5) x; +alter table testgen.t2 add constraint chk_gen_clause check (b = a * 2); +select pg_relation_filenode('testgen.t2') as t2_filenode_before \gset +alter table testgen.t2 alter column b add generated always as (a * 3) stored; +ERROR: check constraint "chk_gen_clause" of relation "t2" is violated by some row +select pg_relation_filenode('testgen.t2') as t2_filenode_after \gset +select :t2_filenode_before = :t2_filenode_after as did_not_rewrite; + did_not_rewrite +----------------- + t +(1 row) + +\d+ testgen.t2 + Table "testgen.t2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Check constraints: + "chk_gen_clause" CHECK (b = (a * 2)) +Not-null constraints: + "t2_b_not_null" NOT NULL "b" + +drop table testgen.t2; +-- rewrite an indexed column +create table testgen.t3 (a int, b int); +create index idx_b on testgen.t3 (b); +insert into testgen.t3 (a, b) select x, x from generate_series(1, 10) x; +select pg_relation_filenode('testgen.idx_b') as idx_filenode_before \gset +alter table testgen.t3 alter column b add generated always as (a * 2) stored; +select pg_relation_filenode('testgen.idx_b') as idx_filenode_after \gset +select :idx_filenode_before != :idx_filenode_after as did_rewrite_idx; + did_rewrite_idx +----------------- + t +(1 row) + +-- tests for invalid invocations +alter table doesnotexist alter column foo + add generated always as (bar * 2) stored; +ERROR: relation "doesnotexist" does not exist +create table testgen.t1 (a int); +alter table testgen.t1 alter column doesnotexist + add generated always as (bar * 2) stored; +ERROR: column "doesnotexist" of relation "t1" does not exist +alter table testgen.t1 add column b int; +alter table testgen.t1 alter column b + add generated always as (doesnotexist * 2) stored; +ERROR: column "doesnotexist" does not exist +-- invalid: only supports ALWAYS +alter table testgen.t1 alter column b + add generated by default as (a * 2) stored; +ERROR: for a generated column, GENERATED ALWAYS must be specified +LINE 2: add generated by default as (a * 2) stored; + ^ +-- invalid: only supports STORED +alter table testgen.t1 alter column b add generated always as (a * 2); +ERROR: syntax error at or near ";" +LINE 1: ...e testgen.t1 alter column b add generated always as (a * 2); + ^ +alter table testgen.t1 alter column b add generated always as (a * 2) virtual; +ERROR: syntax error at or near "virtual" +LINE 1: ...n.t1 alter column b add generated always as (a * 2) virtual; + ^ +-- invalid: b is already a generated column +create table testgen.t2 (a int, b int generated always as (a * 2) stored); +alter table testgen.t2 alter column b add generated always as (a * 2) stored; +ERROR: column "b" of relation "t2" is already a generated column +-- not supported: partitioned tables +create table testgen.tpart (a int, b int) partition by hash (a); +alter table testgen.tpart alter column b add generated always as (a * 2) stored; +ERROR: ALTER action ALTER COLUMN ... ADD GENERATED ALWAYS AS (...) STORED cannot be performed on relation "tpart" +DETAIL: This operation is not supported for partitioned tables. +drop schema testgen cascade; +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to table testgen.t3 +drop cascades to table testgen.t1 +drop cascades to table testgen.t2 +drop cascades to table testgen.tpart diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..d776595a6ed 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,72 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Tests for ALTER COLUMN ... ADD GENERATED ALWAYS as ( expr ) STORED +-- turning a regular column into a stored generated column +create schema testgen; + +create table testgen.t1 (a int, b int not null); +insert into testgen.t1 (a, b) + select x, x from generate_series(1, 10) x; +alter table testgen.t1 alter column b + add generated always as (a * 2) stored; +\d+ testgen.t1 +select a, b, a * 2 as expected, b = (a * 2) as correct + from testgen.t1 order by a; +insert into testgen.t1 (a, b) values (10, 20); +insert into testgen.t1 (a, b) values (10, 21); +drop table testgen.t1; + +-- turning a regular column into a stored generated column +-- fails when another constraint conflicts with the new expression +create table testgen.t2 (a int, b int not null); +insert into testgen.t2 (a, b) select x, x * 2 from generate_series(0, 5) x; +alter table testgen.t2 add constraint chk_gen_clause check (b = a * 2); +select pg_relation_filenode('testgen.t2') as t2_filenode_before \gset +alter table testgen.t2 alter column b add generated always as (a * 3) stored; +select pg_relation_filenode('testgen.t2') as t2_filenode_after \gset +select :t2_filenode_before = :t2_filenode_after as did_not_rewrite; +\d+ testgen.t2 +drop table testgen.t2; + +-- rewrite an indexed column +create table testgen.t3 (a int, b int); +create index idx_b on testgen.t3 (b); +insert into testgen.t3 (a, b) select x, x from generate_series(1, 10) x; +select pg_relation_filenode('testgen.idx_b') as idx_filenode_before \gset +alter table testgen.t3 alter column b add generated always as (a * 2) stored; +select pg_relation_filenode('testgen.idx_b') as idx_filenode_after \gset +select :idx_filenode_before != :idx_filenode_after as did_rewrite_idx; + +-- tests for invalid invocations +alter table doesnotexist alter column foo + add generated always as (bar * 2) stored; + +create table testgen.t1 (a int); + +alter table testgen.t1 alter column doesnotexist + add generated always as (bar * 2) stored; + +alter table testgen.t1 add column b int; + +alter table testgen.t1 alter column b + add generated always as (doesnotexist * 2) stored; + +-- invalid: only supports ALWAYS +alter table testgen.t1 alter column b + add generated by default as (a * 2) stored; + +-- invalid: only supports STORED +alter table testgen.t1 alter column b add generated always as (a * 2); +alter table testgen.t1 alter column b add generated always as (a * 2) virtual; + +-- invalid: b is already a generated column +create table testgen.t2 (a int, b int generated always as (a * 2) stored); +alter table testgen.t2 alter column b add generated always as (a * 2) stored; + +-- not supported: partitioned tables +create table testgen.tpart (a int, b int) partition by hash (a); +alter table testgen.tpart alter column b add generated always as (a * 2) stored; + +drop schema testgen cascade; -- 2.51.2 --24dzbv6kpqxe4yje Content-Type: text/x-patch; charset=utf-8 Content-Disposition: attachment; filename="v1-0002-Try-to-avoid-a-rewrite-when-adding-a-stored-gener.patch" ^ permalink raw reply [nested|flat] 14+ messages in thread
end of thread, other threads:[~2026-03-16 23:25 UTC | newest] Thread overview: 14+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH v4] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH v1] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH v2] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH v2] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH v3] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2022-09-14 09:10 [PATCH] Allow wildcard (%) in extension upgrade paths Sandro Santilli <strk@kbt.io> 2026-03-16 23:25 [PATCH v1 1/2] Support changing a column into a stored generated column Alberto Piai <alberto.piai@gmail.com>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox